自転車プチ探訪: 早岐神社周辺

今は空家となっている母方の実家には、小学生の頃までしょっちゅう泊まりに行っていました。母方の祖父母は私が小学生の頃、亡くなってしまいましたが、早岐の田子ノ浦界隈は子供の頃に過ごした思い出でいっぱいです。スタジオ撮影の合間、人手が足りてそうだったので、四ヶ町アーケードから自転車で早岐方面に向かいました。(こういう時に限って、カメラを持参してないんですよね。以下、ミニスマホのカメラで撮影した写真です。)

■正一位稲荷大明神

自動車学校のすぐ側、車道から見上げる形で、コンクリート壁の崖の斜面にある小さな社です。近付いて見てはいませんが、朱い綺麗な鳥居があり、比較的整備されている感じでした。

■正位稲荷大明神

2013-05-05 16.21.49
上の社と名前が似ていますが、小高い丘の中腹にある社です。全体的に古い感じがしましたが、よく草が刈り込まれていて手入れが行き届いている様子。

2013-05-05 16.23.15

石燈籠の文字を見ると、明治34年の文字が見受けられたので、120年近い歴史があるようです。小さな鳥居と、小さな社が二つ。名前のとおり、お稲荷さまが祀ってあるようです。

■乙姫神社(下陣ノ内町公民館)

正位稲荷大明神の近くをうろうろしていると、ふと子供の頃、もっと別の場所に神社があったような気がして、周辺をふらふらしていました。平戸街道跡に差しかかる直前に、路地の先にある鳥居を発見。木が茂っているため、鳥居の奥は薄暗く、鳥居が目に入った瞬間、ドキッとしてしまいました。

2013-05-05 16.30.38
鳥居には「乙姫神社」の文字。

2013-05-05 16.36.12
鳥居の裏側には、竜宮伝説でお馴染の亀が掘ってありました。

2013-05-05 16.31.06
本来、本殿があるであろう位置には「下陣ノ内町公民館」という建物が建っています。神社らしからぬ建物なので、社殿ではなく名前のとおり公民館なのでしょう。

2013-05-05 16.31.28
敷地内の一角に、小さな社もありました。近くに石碑があったので読んでみると、賤津(しつ)の地主らが山林を売り公民館設立に貢献した事を讃える碑文がありました。賤津の名は、今でこそ残っていませんが、江戸時代の古地図を見ると、陣ノ内周辺にある地名だと言うことが分かります。

2013-05-05 16.32.41
結局、この神社は何が祀ってあるのか、そもそも本当に神社なのか、良く分かりませんでした。

■早岐神社

2013-05-05 16.42.02

平戸街道跡の石畳の上を走り、車道から長い坂道を上り、さらに長い階段を登ったところに早岐神社があります。早岐一帯では一番大きな神社です。
子供の頃の記憶はあまりなかったのですが、一通りお参りさせていただきました。

2013-05-05 16.47.09
早岐神社は別名「速来宮」とも言われており、奈良時代に編纂された「肥前国風土記(ヒゼンノクニフドキ)」に登場する速来津姫伝説に何か関係があるのかな、と思っていました。

プチ探訪の思い出ということで、交通安全のお守りを買う際、宮司さんとお話をさせていただきました。早岐神社は熊野の神様をお祀りしているらしく、速来津姫とは関係がないらしいです。明治期の一村一社政策の折に、早岐村にあった神社は早岐神社に習合されたらしいのですが、その中にも速来津姫を祀っていた神社はなかったとのこと。また、周辺にある小さな社でも速来津姫を祀ったものは知らないとおっしゃっていました。
早岐神社は、「速来」という名だけが地名として残り、そこに建てられた神社なので速来宮の名を冠しているようです。(ちなみに、速来宮のパンフレットもいただきました。)

直接お話を伺うと、由緒書きでは見えてこない事も分かるので面白いですね。

より大きな地図で 130505 を表示

その後、折り返して、パソコン工房、釣具のポイント、エレナに立ち寄って帰りました。

2013-05-05 18.57.18
自転車に取りつけたサイクルコンピュータで計測したところ、全行程で24km弱でした。出発時に満タンにしていたバッテリーも到着時には40%になっていました。始終ハイパワーモードで走っていましたが、計算だと40km近く走れそうですね。標準体重でメーカー公表値が37kmだったのでなかなか良い数字。さすがパナソニック。

実際に走っている時間も、ぐるりと巡って1時間45分だったので、早岐も十分に活動範囲内のようです。

プチ島探訪

長崎県は日本有数の島だらけ地帯であり、フェリーも出ていて手軽に島に行けるので、ゴールデンウィークを利用して島に行ってみたいなあ、と思っています。私が住んでいる佐世保市には、島への定期航路がある港は佐世保港と相浦(あいのうら)港の二つがあって、五島列島や平戸島へ行くことができます。同じ長崎県である壱岐や対馬のフェリーは、佐賀や福岡との航路しかないみたいです。(あまり詳しく調べたわけではないので、ひょっとしたらあるのかもしれませんが)

佐世保港、相浦港からは、五島、大島、宇久、黒島方面にフェリーがでているようです。私の場合、釣りはせず、カメラを持って自転車で観光するのが目的なので、手近な黒島を調べてみることにしました。

黒島は、隠れキリシタンで有名な島で、黒島天主堂がある島です。先日、たまたま読んでいた「聖おにいさん」にも聖地巡礼ネタとして、黒島の名前が出てきていました。宿は次の3つがあるようです。

民宿つるさき
喜久屋旅館
山した旅館

どこも、一泊二食付きで5500円から6000円より。素泊まりできるつるさきは3000円より、のようです。宿泊だけではなく、食事利用だけでもOKのところもあるので、私のような貧乏人旅行者でも助かりますね!

さて。同じ佐世保市の九十九島にあるとは言え、意図的に行こうとしない限り行かないもんですねぇ、島は。陸続きだったら、どこかの通り道に「通った事はある」程度の経験はあると思いますが、海の向こうだとなかなか目的がないと行きません。そういう意味では、近場にも関わらず、ちょっとした遠出気分を味わえる、絶好の場所なんだと思います。

Googleマップで黒島の地図を見ていても、思った以上に大きいようです。相浦から出ている黒島行きのフェリーは自転車も手荷物として300円で載せてもらえるらしいので、サイクリングにはぴったりかもしれません。フェリーは一日3往復あり、高島を経由しています。ゴールデンウィークなどの季節は、一日4往復に増便されているようです。佐世保市内から、十分に日帰り旅行もできるようなので、週末のプチ旅行でもいいのかもしれません。

ちなみに、自分が勤めている市内の職場に以前、黒島出身の方がいらっしゃって、ご不幸があって亡くなられたようですが、その方もクリスチャンで、黒島天主堂でご葬儀があったようです。自分が入社する直前だったようで、直接は面識がなかったのですが、私と同世代だったし、同じ職場の仲間になったあろう方なので残念でなりません。

松浦鉄道の自転車持ち込み(輪行)について

全国の自転車普及率を見てみると、長崎県はワースト2位らしいです。そういえば、大阪の平地に比べたら、自転車に乗っている人をほとんど見かけません。街角に駐輪しているのも自転車ではなくバイクが中心です。ほとんど平地がない上に小さな島が散らかっているので、あまり普及していないのかもしれません。

さて、長崎県の北松周辺に走る唯一の鉄道、松浦鉄道に自転車の持ち込み乗車(輪行)について問い合わせてみました。2013年4月時点では、持ち込み方法には二つのパターンがあるらしく、次のようになっています。

1) 土日祝日に運行している特定列車を利用する

… 乗車代金のみで自転車ごと乗車できる。自転車を折り畳まなくて可。収納袋ナシで可。一日に上り、下りともに4本運行している(下記参照)。

2) 平日や特定列車以外を利用する

… 全列車に持ち込みは可だが、乗車代金のほか260円の手回品代金が必要。自転車は折り畳み、収納袋へ入れて持ち込む。

どちらのパターンであっても、乗車前に松浦鉄道佐世保駅まで電話連絡(番号は下記)が必要らしいです。また、団体乗車やイベントなどの都合で、持ち込めない事もあるらしいです。
私の場合は、週末にたびら方面でサイクリングをしたいだけなので、特定列車が利用できるようです。

■ 土日祝日に運行している特定列車の時刻表

上り

  • 佐世保発 08:08 → たびら平戸口発 09:48 → 伊万里着 10:57
  • 佐世保発 09:27 → たびら平戸口発 10:48 → 伊万里着 11:55
  • 佐世保発 12:25 → たびら平戸口発 13:48 → 伊万里着 14:55
  • 佐世保発 14:25 → たびら平戸口発 15:50 → 伊万里着 16:55

下り

  • 伊万里発 08:43 → たびら平戸口発 09:48 → 佐世保着 11:09
  • 伊万里発 10:08 → たびら平戸口発 11:18 → 佐世保着 12:39
  • 伊万里発 12:40 → たびら平戸口発 13:48 → 佐世保着 15:09
  • 伊万里発 14:36 → たびら平戸口発 15:47 → 佐世保着 17:07

詳しくは、こちらの時刻表をご覧ください。

※ このページの情報は、あくまでも参考までにお使いください。
※ 最新の情報は、必ず松浦鉄道佐世保駅(TEL:0956-25-3900)までお問い合わせください。

世代や職業によって変化する”Hello World”プログラム

GNUのジョーク集「Hello World!」という面白い記事があったので、少しだけ邦訳して、コメントを添えて掲載します。

中高生

[vb]
10 PRINT "HELLO WORLD"
20 END
[/vb]

まずは基本のBASICです。

大学生(初年度)

[perl]
program Hello(input, output)
begin
writeln(‘Hello World’)
end.
[/perl]

Pascalを覚えたようです。

大学生(卒業年)

[perl]
(defun hello
(print
(cons ‘Hello (list ‘World))))
[/perl]

Lispを使えるようになりました。

新人プログラマー

[cpp]
#include <stdio.h>

void main(void)
{
char *message[] = {"Hello ", "World"};
int i;
for(i = 0; i < 2; ++i)
printf("%s", message[i]);
printf("\n");
}
[/cpp]

Cを覚えました。ポインタも配列も使えます。

ベテランプログラマー

[cpp]
#include <iostream.h>
#include <string.h>
class string
{
private:
int size;
char *ptr;
public:
string() : size(0), ptr(new char(‘\0’)) {}
string(const string &s) : size(s.size)
{
ptr = new char[size + 1];
strcpy(ptr, s.ptr);
}
~string()
{
delete [] ptr;
}
friend ostream &operator <<(ostream &, const string &);
string &operator=(const char *);
};

ostream &operator<<(ostream &stream, const string &s)
{
return(stream << s.ptr);
}
string &string::operator=(const char *chrs)
{
if (this != &chrs)
{
delete [] ptr;
size = strlen(chrs);
ptr = new char[size + 1];
strcpy(ptr, chrs);
}
return(*this);
}
int main()
{
string str;
str = "Hello World";
cout << str << endl;
return(0);
}
[/cpp]

クラスを用いて、C++を使いこなせていますね。

システム管理者

[cpp]
#include <stdio.h>
#include <stdlib.h>
main()
{
char *tmp;
int i=0;
/* on y va bourin */
tmp=(char *)malloc(1024*sizeof(char));
while (tmp[i]="Hello Wolrd"[i++]);
/* Ooopps y’a une infusion ! */
i=(int)tmp[8];
tmp[8]=tmp[9];
tmp[9]=(char)i;
printf("%s\n",tmp);
}
[/cpp]

Cですが、コメントを見るとフランス人のようです。
まわりくどいことをやっている例でしょうか。

見習いハッカー

[perl]
#!/usr/local/bin/perl
$msg="Hello, world.\n";
if ($#ARGV >= 0) {
while(defined($arg=shift(@ARGV))) {
$outfilename = $arg;
open(FILE, ">" . $outfilename) || die "Can’t write $arg: $!\n";
print (FILE $msg);
close(FILE) || die "Can’t close $arg: $!\n";
}
} else {
print ($msg);
}
1;
[/perl]

書き易くて読みにくいPerlは、ハッカーの基本言語ですね。

上級ハッカー

[cpp]
#include <stdio.h>
#include <string.h>
#define S "Hello, World\n"
main(){exit(printf(S) == strlen(S) ? 0 : 1);}
[/cpp]

CでPerlのような読みにくいコードをガリガリ書けるようになり、
書いた本人も空気のように読めるようになってきます。

熟練ハッカー

[shell]
% cc -o a.out ~/src/misc/hw/hw.c
% a.out
Hello, world.
[/shell]

熟練になると、よく使うコードはコンパクトに持ち歩けるようにしておき、
必要な時にコンパイルして動かします。コーディングはしません。

グル・ハッカー

[shell]
% cat
Hello, world.
[/shell]

プログラミングの神様は、コーディングどころかコンパイルすらしません。
(catコマンドは引数を与えないと、標準入力に入ったものを標準出力に吐きます)

新人マネージャ

[vb]
10 PRINT "HELLO WORLD"
20 END
[/vb]

上の覚えてます? ;-)

中間管理職マネージャ

[shell]
% mail -s "Hello, world." bob@b12
Bob, could you please write me a program that prints "Hello, world."?
I need it by tomorrow.
^D
[/shell]

「明日までに必要なんだ!」

上級マネージャ

[shell]
% zmail jim
I need a "Hello, world." program by this afternoon.
[/shell]

「今日の午後、必要なんだ!」

経営責任者

[shell]
% letter
letter: Command not found.
% mail
To: ^X ^F ^C
% help mail
help: Command not found.
% damn!
!: Event unrecognized
% logout
[/shell]

「使い方が分からん、畜生!」

科学研究者

[vb]
PROGRAM HELLO
PRINT *, ‘Hello World’
END
[/vb]

科学分野で人気のFortran90です。

熟練科学研究者

[vb]
WRITE (6, 100)
100 FORMAT (1H ,11HHELLO WORLD)
CALL EXIT
END
[/vb]

さらに古いFORTRAN77です。

これらはジョークなので、本当にこれらの人が上のようなコーディングをしているわけではありませんが、スキルや考え方、他業種から見たイメージなどを上手く皮肉っていますね。

tumblrの画像を一括ダウンロードするスクリプト

tumblrの画像を一括ダウンロードするbash向けシェルスクリプトを書いてみました。

あまり効率的ではないですが、投稿を1ページずつ取得して、含まれる画像を抜き出しています。また、ダウンロードが完了したらZIPファイルに固めてくれます。

[bash]

!/usr/bin/env bash

#

simple picture downloader for tumblr

#

written by dyama (https://dyama.org/), Feb 1 2013

#

usage:

gettumblr http://example.tumblr.com/

function gettumblr()
{
url=$1

# fix the url
url=${url#http://} # this pattern is not regex!
url=${url%%/*}

# check the url and get tumblr user acount
if [[ $url =~ ^([a-z0-9]+)\.tumblr\.com$ ]]; then
    USER=${BASH_REMATCH[1]}
    echo OK, Target user name is $USER. &gt;&amp;2
else
    echo Error: $url is not tumblr URL. &gt;&amp;2
    return 1
fi

# working directory
wdir=&quot;/tmp/gettumblr$$&quot;
if [ ! -d $wdir ]; then
    mkdir $wdir
    if [ $? -ne 0 ]; then
        echo Error: Cannot make working directory at $wdir. &gt;&amp;2
        return 1
    fi
fi

pushd $wdir

for i in {1..999};
do
    echo Getting posted page $i ... &gt;&amp;2
    wget -O - -q &quot;http://$url/page/$i&quot; | grep -oE 'http[^&quot;]+post[^&quot;]+' \
        | grep $USER &gt; list
    [ $? -ne 0 ] &amp;&amp; break;
    echo -e '\tGetting included image ...' &gt;&amp;2
    grep -v frame list | wget -q -i - -O - | grep 'og:image' \
        | grep -v facebook | grep -oE 'http[^&quot;]+' | wget -i - -nv
done

file=$wdir/tumblr_$USER.zip
zip $file *

popd

mv $file .
rm -rf $wdir

echo Done. &gt;&amp;2

}

gettumblr "$*"
exit $?
[/bash]

関数 gettumblr() の冒頭部分のパターンによる変数保持値の編集や正規表現パターンマッチ構文は、bash依存です。また、grep の -o オプション(パターンに一致した行の、パターンに一致した部分だけを標準出力に印字する)も多用しています。このオプションは古めの grep にはなかったはずなので、注意が必要です。

ターゲットのtumblrユーザが存在しない場合、ファイルが削除できなかった場合など、例外処理はまったく書いていなかったり、一回で済むような処理を分けて書いていたりと、トライアンドエラーの跡が残ったままですので、もし流用する場合には適宜書き換えてください。

SONY VAIO Z シリーズに Ubuntu 12.10 を

総選挙があって、円安にシフトし初めた2012年12月にソニーのVAIO Zシリーズを購入しました。Ubuntu 12.10 をインストールして1ヶ月ほど使ってみたので、そろそろレビューを書いておきます。

なぜVAIOなのか

2012年の初めの頃まで、NECのLaVie LightにUbuntuを入れて普段使いしていたのですが、さすがにスペックがキツくなってきたのを理由に、マウスコンピューターのBTOラップトップを購入しました。Intel Core i5、メモリ 8GB とそこそこのスペックのマシンでお値段は4万円程度したのですが、手にした際に色々と問題があった為、半年ほどで弟に譲りました。

そこからの教訓か、「少々値段が高くなってもちゃんと使えるマシンを」という考えが出てきて、国内メーカーのBTOできるラップトップを視野に入れて検討していました。

先日、英語配列キーボードに脳内ドライバを切り替えたこともあって、BTO項目に最初から英語配列キーボードが選択できるものに搾ると、ソニーのVAIOか、アップルのMac Book Pro /Air くらいしか選択肢がありませんでした。他のメーカーももっと気合を入れて探せば、英語配列キーボードを選択できるのかもしれませんが、VAIOは初めてじゃないし、Zシリーズの評判もなかなか良かったので、VAIOに決めました。

VAIO Z シリーズ自体の素晴しさについては、他のレビューサイトをご覧ください。薄いし高性能だし、サクサク動いて快適ですよ!

日本の大手メーカーという不安

Linuxユーザーにとって、日本の大手メーカー製のコンピュータを購入する際に悩ましいのが、ハードウェアに独自の機能を盛り込んでしまう点です。個人的な意見ですが、日本の大手メーカーは市場に媚びすぎている感じがします。高性能な良いハードウェアを作る力は持っていますので、ユーザに媚びるとハードウェアは余計にガラパゴス化してしまいます。結果、Windowsでしか使えないようなハードウェアを平気で作ってしまいます。まあ勝てば官軍、Windowsで動けば商売的にはそれで良いんでしょうが、日本のコンピュータ産業はMicrosoftと心中でもする気なのかなあ、と一抹の不安もあります。

ただ、この不安も杞憂だった事をこのレビューで書けそうです。

カスタマイズ・オーダー

前述の不安もあって、ハードウェアをカスタマイズする際には、半ば賭けのような気分でした。私が行なったカスタマイズは次のとおりです。

  • CPUを選択できる中で最も高性能な Intel® Core™ i7-3612QM CPU @ 2.10GHz ハイパースレッド 8 コアに変更
  • メモリを選択できる中で最大の 8 GB に変更
  • キーボードを日本語配列から英語配列に変更し、バックライトも追加で装備
  • 内蔵ディスクは標準の SSD 64 GB × 2基、ハードウェア RAID 0 構成のまま。ストレージはNASに8TBほど持っているので、端末側にはそんなに必要ありませんでした。
  • ディスプレイを 1920×1080 フル HD に変更。
  • WiFiデバイスをMIMO3x3に変更、無線WANはなし。
  • Bluetooth搭載、指紋認証はなし、Power Dockや拡張バッテリーはなし。

 

OpenCASCADE でソリッドのブーリアン演算(とボリューム計算)を行う

OpenCASCADE でソリッドモデル同士のブーリアン演算とボリューム計算を行うサンプルコードを書いてみました。
コードは次のとおり。

#include <stdio.h>

#include <TopoDS.hxx>
#include <gp_Pnt.hxx>
#include <TopoDS_Solid.hxx>
#include <BRepPrimAPI_MakeBox.hxx>
#include <BRepAlgoAPI_Common.hxx>
#include <BRepGProp.hxx>
#include <GProp_GProps.hxx>

int main(int c, char** v)
{
    // s1 というソリッドを作成
    gp_Pnt opos1(0.0, 0.0, 0.0);
    BRepPrimAPI_MakeBox box1(opos1, 10.0, 10.0, 10.0);
    TopoDS_Solid s1 = box1.Solid();

    // s2 というソリッドを作成
    gp_Pnt opos2(2.0, 3.0, 5.0);
    BRepPrimAPI_MakeBox box2(opos2, 10.0, 10.0, 10.0);
    TopoDS_Solid s2 = box2.Solid();

    // ブーリアン演算オブジェクトを s1 と s2 で作成
    BRepAlgoAPI_Common bo(s1, s2);

    // ブーリアン演算オブジェクトのオペレーションを指定
    // BOP_COMMON:  s1 と s2 の共通部分を得る
    // BOP_FUSE:    s1 と s2 を結合したものを得る
    // BOP_CUT:     s1 を s2 でカットしたものを得る
    // BOP_CUT21:   s1 で s1 をカットしたものを得る
    // BOP_SECTION: s1 と s2 のセクションを得る
    bo.SetOperation(BOP_CUT);

    // 計算の実行
    bo.Build();

    // エラーが発生していなかったら
    if (!bo.ErrorStatus()) {
        TopoDS_Shape S = bo.Shape();

        // GPropsオブジェクト(ボリュームや重心位置を得るツール)を作成
        GProp_GProps gprops;
        BRepGProp::VolumeProperties(S, gprops);
        Standard_Real vol = gprops.Mass(); // ボリュームを得る

        printf("volome: %f\n", vol);
    }

    return bo.ErrorStatus();
}

BRepAlgoAPI_Common クラスは、名前に Common の文字がありますが、上のとおり setOperation() で指定すれば、コモン演算(共通部分を得る)だけではなく、カットしたり結合したりすることもできます。

ビルド方法は次のとおり。

g++ -g -lTKernel -lTKMath -lTKBO -lTKTopAlgo -lTKPrim -L/usr/lib/opencas -I/usr/include/opencascade -o solid solid.cxx

ロブ・パイクの正規表現マッチャを拡張する

ブライアン・カーニハンとロブ・パイクによる The practice of programming (邦題:プログラミング作法) に掲載されている正規表現マッチャを拡張したいと思います。

※ O’Reilly から出版された Beautiful Code: Leading Programmers Explain How They Think (邦題: ビューティフル コード)にも掲載されており、該当部分の全文は公式サイトで読むことができます。

前提

元の正規表現マッチャは、以下の表現をサポートしています。

  • 任意文字指定子: .
  • くりかえし指定子: *
  • 位置指定子: ^$

まず、正規表現マッチャを評価する為に main() 関数を追加しました。

#include <stdio.h>

/* match: テキスト中の任意位置にある正規表現を探索 */
int match(char *regexp, char *text)
{
  if (regexp[0] == '^')
    return matchhere(regexp+1, text);
  do { /* 文字列が空の場合でも調べる必要あり */
    if (matchhere(regexp, text))
      return 1;
  } while (text++ != '\0');
  return 0;
}
/* matchhere: テキストの先頭位置にある正規表現のマッチ */
int matchhere(char *regexp, char *text)
{
  if (regexp[0] == '\0')
    return 1;
  if (regexp[1] == '')
    return matchstar(regexp[0], regexp+2, text);
  if (regexp[0] == '$' && regexp[1] == '\0')
    return text == '\0';
  if (text!='\0' && (regexp[0]=='.' || regexp[0]==text))
    return matchhere(regexp+1, text+1);
  return 0;
}
/* matchstar: 「c」型の正規表現をテキストの先頭位置からマッチ */
int matchstar(int c, char *regexp, char *text)
{
  do { /* 「」は「0回以上の繰り返し」であることに注意 */
    if (matchhere(regexp, text))
      return 1;
  } while (text != '\0' && (*text++ == c || c == '.'));
  return 0;
}

int main(int argc, char** argv)
{
  int r;
  if ( argc != 3 ) {
    printf("Too few or many arguments for regex.\n");
    return 2;
  }
  printf("%s\n", (r = match(argv[1], argv[2])) ? "Match" : "No match" );
  return !r;
}

これで、

gcc -o myregex myregex.c

すると、実行バイナリが作成され

./myregex regex-pattern source-string

とタイプする事により、挙動を確認することができます。引数は、シェルによる展開を避ける為、クォートで囲むことをオススメします。

拡張1: 「c+(直前のパターンが1回以上くりかえす)」表現を追加する。

上の37行目にあるコメント『「*」は「0回以上の繰り返し」であることに注意』が最大のヒントになります。「+」は1回以上のくりかえしですので、0回でもマッチする dowhile 文による検索ではなく、while 文で実現すれば良いはずです。

ということで、次の関数を追加しました。

/* matchcorss: 「c+」型の正規表現をテキストの先頭位置からマッチ */
int matchcross(int c, char *regexp, char *text)
{
  while (text != '\0' && (*text++ == c || c == '.')) {
    if (matchhere(regexp, text))
      return 1;
  }
  return 0;
}

1回もマッチすることがなければ、return 0をして検索を終了します。また、呼び出し元となる matchhere() 関数を次のように変更します。

/* matchhere: テキストの先頭位置にある正規表現のマッチ */
int matchhere(char *regexp, char *text)
{
  if (regexp[0] == '\0')
    return 1;
  if (regexp[1] == '')
    return matchstar(regexp[0], regexp+2, text);
  /* ここから */
  if (regexp[1] == '+')
    return matchcross(regexp[0], regexp+2, text);
  /* ここまで追加 */
  /* … */

matchstar() を呼び出しているところと同じように、regexp[1]+ だった場合、matchcross() を呼び出しています。

これを実行してみると

$ ./myregex '^co+l' 'cool'
Match

$ ./myregex '^c+l' 'cool'
No Match

ちゃんと動いているようです。

「c?(直前のパターンが0回もしくは1回)」表現を追加する。

直前のパターンが0回、もしくは1回マッチすればいいので、次のような関数を追加しました。

int matchquestion(int c, char regexp, char *text)
{
  if (text == c)
    text++;
  return matchhere(regexp, text, 0);
}

仮引数は、matchstar()matchcross() と同じです。ポインタ text が指している場所にある文字が c と同じなら、text をインクリメントします。 同じでない場合は、「0回マッチした」ということですので、ポインタ text はそのままで検索をすすめていきます。

呼び出し元となる matchhere() はこんな感じになりました。

/* matchhere: テキストの先頭位置にある正規表現のマッチ */
int matchhere(char *regexp, char *text)
{
  if (regexp[0] == '\0')
    return 1;
  if (regexp[1] == '')
    return matchstar(regexp[0], regexp+2, text);
  if (regexp[1] == '+')
    return matchcross(regexp[0], regexp+2, text);
  /* ここから */
  if (regexp[1] == '?')
    return matchquestion(regexp[0], regexp+2, text);
  /* ここまで追加 */
  /* … */

くりかえし指定子ですので、おんなじですね。

$ ./myregex 'colour' 'color'
No Match

$ ./myregex 'colou?r' 'color'
Match

$ ./myregex 'colou?r' 'colour'
Match

ちゃんと動いてます。

指定子のエスケープに対応する

正規表現では、文字 *. そのものを指定したい場合、それぞれ *. といったバックスラッシュを用いたエスケープを行います。

それに対応します。

考え方としては、エスケープ文字が指定子の前にあれば、その指定子を単なる文字として解釈するわけですが、まずは matchhere() を修正してみましょう。

int matchhere(char *regexp, char *text)
{
  if (regexp[0] == '\0')
    return 1;

  /* 直前がバックスラッシュじゃない場合のみ、指定子として処理する */
  if (regexp[0] != '\\') {
    if (regexp[1] == '*')
      return matchstar(regexp[0], regexp+2, text);
    if (regexp[1] == '+')
      return matchcross(regexp[0], regexp+2, text);
    if (regexp[1] == '?')
      return matchquestion(regexp[0], regexp+2, text);
  }
  else {
    /* バックスラッシュだった場合、regexp をインクリメントして検索を続ける。 */
    return matchhere(regexp+1, text);
  }
  /* ... */

これで、くりかえし指定子のエスケープができるようになりました。さて、問題は次の位置指定子と任意文字のマッチングの部分です。

if (regexp[0] == '$' && regexp[1] == '\0')
  return *text == '\0';

if (text!='\0' && (regexp[0]=='.' || regexp[0]==text))
  return matchhere(regexp+1, text+1);

くりかえし指定子の処理突入部の場合、「regexp[0]が直前のパターン、regexp[1]が指定子」というペアでしたが、これらの文では、regexp[0]が指定子であり、直前のパターンを判断する手立てがありません。

直前のパターンがエスケープ文字かどうか、つまり、matchehere() が呼び出された時点で、エスケープされているのかどうかを知ることができればいいわけです。

matchhere() を次のように変更しました。

/* int matchhere(char *regexp, char *text) */
int matchhere(char *regexp, char *text, int esc)
{
/* … */

第3引数の int esc は、直前でエスケープして呼び出された場合には 0 以外が入るようにします。既存の match()matchstar()matchcross()matchquestion() 内で呼んでいる matchhere() は、全て第3引数を 0 にして呼び出すように修正をします。

さらに、matchhere()

int matchhere(char *regexp, char *text, int esc)
{
  if (regexp[0] == '\0')
    return 1;

  if (regexp[0] != '\\') {
    if (regexp[1] == '*')
      return matchstar(regexp[0], regexp+2, text);
    if (regexp[1] == '+')
      return matchcross(regexp[0], regexp+2, text);
    if (regexp[1] == '?')
      return matchquestion(regexp[0], regexp+2, text);
  }
  else {
    return matchhere(regexp+1, text + esc, 1); /* エスケープされている事を教える */
  }

  if (!esc && regexp[0] == '$' && regexp[1] == '\n')
    return *text == '\n';

  if (*text!='\n' && ((!esc && regexp[0]=='.') || regexp[0]==*text))
    return matchhere(regexp+1, text+1, 0);

  return 0;
}

のように変更します。条件式のところどころに、esc を差し込んでいます。順に追っていってみます。

15行目の

return matchhere(regexp+1, text + esc, 1);

は、regexp[0]がエスケープ文字だった場合に、第3引数を 1 に設定して matchhere() を呼び出します。また、第2引数の text + esc は、「エスケープされていたら1文字進め、そうでなければ現在のポインタの指す位置から」という意味になります。エスケープされており、かつこの return 文のスコープに入ってきたという事は、パターン「\」が指定されたことになります。エスケープされておらず、この return 文が実行される場合は、「\」以外のパターン「」や「\w」などの場合であり、それぞれ、文字「」「w」として解釈されるようになります。

次に、18行目の

if (!esc && regexp[0] == '$' && regexp[1] == '\0')

ですが、ここは「エスケープされていない$があり、かつパターンの文末」を意味しています。エスケープされていたら、マッチしません。

最後の、21行目

if (text!='\0' && ((!esc && regexp[0]=='.') || regexp[0]==text))

では、パターン「.」を弾いています。

これを試してみると

$ ./myregex '.++.+=.+' '12+34=46′
Match

$ ./myregex '^\abc' '\abc'
No Match

$ ./myregex '^\abc' '\abc'
Match

しっかり動いているようです。

ここまでの機能を盛り込んだソースコード

2つのくりかえし指定子とエスケープを実装した後のコードです。

#include <stdio.h>

/* match: テキスト中の任意位置にある正規表現を探索 */
int match(char *regexp, char *text)
{
  if (regexp[0] == '^')
    return matchhere(regexp+1, text, 0);
  do { / 文字列が空の場合でも調べる必要あり /
    if (matchhere(regexp, text, 0))
      return 1;
  } while (text++ != '\0');
  return 0;
}
/* matchhere: テキストの先頭位置にある正規表現のマッチ */
int matchhere(char *regexp, char *text, int esc)
{
  if (regexp[0] == '\0')
    return 1;

  /* くりかえし */
  if (regexp[0] != '\\') {
    if (regexp[1] == '*')
      return matchstar(regexp[0], regexp+2, text);
    if (regexp[1] == '+')
      return matchcross(regexp[0], regexp+2, text);
    if (regexp[1] == '?')
      return matchquestion(regexp[0], regexp+2, text);
  }
  else {
    return matchhere(regexp+1, text + esc, 1);
  }

  /* 文末 */
  if (!esc && regexp[0] == '$' && regexp[1] == '\n')
    return *text == '\n';

  /* その他、任意文字 */
  if (*text!='\n' && ((!esc && regexp[0]=='.') || regexp[0]==*text))
    return matchhere(regexp+1, text+1, 0);

  return 0;

}
/* matchstar: 「c」型の正規表現をテキストの先頭位置からマッチ */
int matchstar(int c, char *regexp, char *text)
{
  do { /* 「」は「0回以上の繰り返し」であることに注意 */
    if (matchhere(regexp, text, 0))
      return 1;
  } while (text != '\0' && (text++ == c || c == '.'));
  return 0;
}
/* matchcorss: 「c+」型の正規表現をテキストの先頭位置からマッチ */
int matchcross(int c, char *regexp, char *text)
{
  while (text != '\0' && (text++ == c || c == '.')) {
    if (matchhere(regexp, text, 0))
      return 1;
  }
  return 0;
}
/* matchquestion: c? */
int matchquestion(int c, char *regexp, char *text)
{
  if (text == c)
    text++;
  return matchhere(regexp, text, 0);
}

int main(int argc, char** argv)
{
  int r;
  if ( argc != 3 ){
    printf("Too few or many arguments for regex.\n");
    return 2;
  }
  printf("%s\n", (r = match(argv[1], argv[2])) ? "Match" : "No match" );
  return !r;
}

次回は、範囲パターン([a-z])の実装と、パターンのコンパイルフェイズ・チェックフェイズの分離をやってみたいと思います。

PDFを連番画像ファイルを含むZIP圧縮ファイルに変換

安価な Android 端末が出回っています。
Amazon キンドルが発売されて、投げ売り状態だった楽天の Kobo が、新品でも3000円程度で流通するようになりました。
ようやく、電子書籍を読むというライフスタイルが日本にも定着するんでしょうか。今後が楽しみです。

さて、私が持っている Huawei の IDEOS のような低スペックな端末では、PDF の文書を読むのにもひと苦労です。
低スペックな環境では、ファイルサイズが小さく高圧縮&レンダリングにマシンパワーが必要なものより、ファイルサイズが大きく冗長でも負荷をかけずに表示できるものの方が重宝される場合があります。

特にほとんどが文字の場合、ページをめくるたびに何秒も待たされるより、少々画質が荒くても(もちろん、文字が読める程度に、です)サクサクとページをめくれる方が良い事もあります。

という事で、既存の PDF ファイルの各ページを連番の JPEG ファイルにして、ZIP 形式で圧縮するスクリプトを書いてみましょう。

1. まず基本的な事

ImageMagick の convert コマンドで、次のようにタイプすると PDF のページを JPEG ファイルとして保存することができます。

[bash]
convert src.pdf[1] dst1.jpg
[/bash]

鉤括弧の中は、ページ番号を指定します。0 ページ目から開始します。存在しないページを指定すると

[bash]
convert src.pdf[999] dst999.jpg

Requested FirstPage is greater than the number of pages in the file: 645
No pages will be processed (FirstPage > LastPage).
[/bash]

とエラーになります。

2. シェルのブラケット展開で繰りかえす

エラーが出るまで繰替えせばいいので、

[bash]
for n in {0..999}; do convert src.pdf[$n] dst$n.jpg || break; done
[/bash]

と書く事ができます。0から999ページまで指定して変換を試みて、失敗したら break で抜ける、という動作をします。

3. 圧縮ファイルにつっこむ

変換し終わったファイルから ZIP ファイルに追加していくには

[bash]
for n in {0..999}; do convert src.pdf[$n] dst$n.jpg || break; zip target.zip dst$n.jpg; done
[/bash]

という風に、そのまま zip コマンドに投げてやれば良いです。

[bash]
for n in {0..999}; do convert src.pdf[$n] dst$n.jpg || break; zip target.zip dst$n.jpg && rm dst$n.jpg; done
[/bash]

ZIP ファイルへの追加に成功した後、JPEG ファイルは不要ですので、削除しておきましょう。

4. 変換オプションを指定する

必須ではありませんが、convert のオプションを利用する事によって、画像サイズの指定、グレースケール化、フォントの指定など様々なことができます。詳しくは

[bash]convert –help[/bash]

をご覧ください。

5. スクリプト化

以上の事を踏まえて、スクリプトにしてみます。

[bash]

/!usr/bin/env bash

src=$1
dst=$2

opt=’-density 600 -resize 640 -font kochigothic’

dir=/tmp/$$

(test ! -d $dir && mkdir $dir) || exit 1
test ! -f $src && exit 1

tmp=basename $dst

for n in {0..999}
do
pict=printf '%04d.jpg' $n
convert $opt $src[$n] $dir/$pict 2>/dev/null || break
pushd $dir >/dev/null
zip $tmp $pict && rm $pict
popd >/dev/null
done

mv $dir/$tmp $dst
rmdir $dir
[/bash]

このスクリプトは、/tmp 以下に一時ディレクトリを作成し、そこに画像ファイルとZIPファイルを貯め込むようにしています。
実行は、以下のように使います。

[bash]
pdf2cbz src.pdf dst.zip
[/bash]

dst.zip の名前を自動生成するようにすれば、複数ファイルの一括処理や Nautilus などのファイラから右クリックで変換、など、さらに便利に使えそうですね。

OpenCASCADE で IGES ファイルを読み込む

OpenCASCADE で IGES ファイルを読み込むには、IGESControl_Reader クラスを用います。

[cpp]

include <stdio.h>

include <Standard_TypeDef.hxx> // Standard_Boolean

include <Standard_Macro.hxx> // Handle()

include <TopAbs_ShapeEnum.hxx> // ShapeType

include <TopoDS.hxx> // TopoDS::

include <TopoDS_Shape.hxx> // TopoDS_Shape

include <TopoDS_Face.hxx> // TopoDS_Face

include <IGESControl_Reader.hxx> // IGES Reader

int main(int c, char** v)
{
Standard_CString path = "sample28.igs";

// IGES制御リーダで読み込む
IGESControl_Reader reader;
if (reader.ReadFile(path) != IFSelect_RetDone) {
    // エラーが起きた
    reader.PrintCheckLoad(Standard_True, IFSelect_ItemsByEntity);
    return 1;
}

// 変換対象のルート数
printf(&quot;number of roots = %d\n&quot;, reader.NbRootsForTransfer());

// IGES内のルートを OCC で扱えるように変換
reader.TransferRoots();

// 得られたシェイプ数
printf(&quot;number of shapes = %d\n&quot;, reader.NbShapes());

// シェイプの取得
for (int i=1;i&lt;=reader.NbShapes(); i++) {
    TopoDS_Shape s = reader.Shape(i);
    if (s.ShapeType() == TopAbs_FACE) {
        TopoDS_Face f = TopoDS::Face(s);
        // 取得した Face で何かする
        printf(&quot;%04X\n&quot;, f.HashCode(0xFFFF));
    }
}

// 一つのシェイプにして返す場合
// TopoDS_Shape s = reader.OneShape();

return 0;

}
[/cpp]

次のようにビルドします。TKIGES にリンクするのを忘れないように注意してください。

[bash]
$ g++ -g -lTKernel -lTKMath -lTKBinL -lTKIGES -L/usr/lib/opencas -I/usr/include/opencascade -o occtest occtest.cxx
[/bash]

IGES関連のクラスについての解説は、$CASROOT/../doc/iges.pdf にドキュメンテーションが同梱されているので、そちらを参考にしてください。また、IGES ファイル自体の仕様(PDF)は、US Product Data Association で公開されています。