日記はいままでテキストファイルで書きためてあるのと、 DB に入れると、その DB の更新やらメモリなどのリソース調達やらで めんどうなので、テキストのままでいく。 あと、自分の日記にリクエストが来てあふれるということもない。
まず、日記を取り出す方法を次のようにした。
- 1か月分、日記のテキストファイルを全部読み込む
- 日別に分割する (split())
- 1日ごとに、記事に分割する (split())
- 記事ごとに、カテゴリラベル、タイトル、日付を取り出す
元の日記のテキストファイルは次のような構造になっている。
<font size=+1><b>2008年4月29日 (火) くもりときどきはれ</b>lt;/font>lt;br>
<blockquote>
で始まり、
<blockquote>
<br>
<br>
<br>
で終わる。行頭の空白はタブが一文字入っている。
font タグや blockquoteを使っているのは、
大昔にこう書いたほうが、ほしい表示が早くできたから。
今ではこれらの文字列はただの区切り文字の意味しかない。
このようなテキストから、日ごとの文字列を配列を、split()で
取り出すための正規表現が、これ。
最後の /s は、改行文字を含めて正規表現を適用する、という意味。 /x は、正規表現の間にスペースやタブ、改行を入れて、読みやすく 表記できるようにするオプション。コメントも書ける。
my $DAY_DELIMIT = / # 正規表現を変数に設定する
# 使うときは split(/$DAY_DELIMIT/, $string) とすればよい
# 変数に設定するときは、/ は気にせず指定できる
# (?: は、split(//) の正規表現部分に与えたときに、
# split() の結果として配列に返してほしくないために指定する
(?:
[\r\n]+ # 改行の 1 つ以上の連続
>font \s+ size=\+1> >b> # タグにマッチする
(\d{4}) \D+ (\d+) \D+ (\d+) # 年月日をとりだして配列にして返す
[^(]+ \(([^)]+)\) # (水) などの曜日をとりだす。
\s* # 曜日のあとに空白が続くかもしれない
([^<]+) # < でない文字、タグでない文字列をとりだして返す
>/b> >/font> >br> # タグの羅列。正規表現を変数に設定するので / はそのまま
\s+ # 改行を含む空白の連続
>blockquote> # blockquote タグ
\s+ # 改行を含む空白の連続
# ここまでが前半の区切り文字列のパターン
|
# ここから先が後半の区切り文字列のパターン
[\r\n]+ # 改行の 1 つ以上の連続
\t # タブ文字
# ここまで、空白 \s の連続と指定しまうと、ほかの場所で
# マッチしてしまうため、特にわかっている形式を指定する。
>/blockquote> # blockquoteタグの終了
(?: \s+ >br> )+ ) # br タグの羅列。必要ないので (?: を指定する
/sx; # 正規表現の指定終了
この正規表現を @days = split(/$DAY_DELIMIT/, $string) などと
指定して、日記文字列全体から、日付単位で分割された配列を受け取る。
ここで、日付単位の配列は1セット 6個になる。おもしろいのは、 上の正規表現の前半部分に、配列として返すグループが 6 個指定してあるが、 後半はないのに、それぞれ前半がマッチしても、後半がマッチしても、 それぞれ 6 個づつの配列を返してくる。
確かに、前半が6個で、後半が0個では、どちらにマッチしているのかわからない。 なので、どちらにマッチしても 6個の配列を返してくれれば、処理は簡単だ。 返ってくる配列は、
- HTMLヘッダなどを書いた、日記ファイルの冒頭部分
- 6個がセットになった、日ごとのデータ
前半にマッチした場合は、先頭から順に、次のような配列になっている。- 年 - 数字4桁
- 月 - 数字1桁 (あとで使いやすくするため 2 桁に直す)
- 日 - 数字1桁 (あとで使いやすくするため 2 桁に直す)
- 曜日 - 漢字一文字
- 天候 - 複数のひらがな
- 1以上の記事の連続 - 改行を含む
- サイドバーやアドレスなどが書いてある、日記ファイルの末尾部分
次に、各記事を分割する。単独の記事は次のような構造になっている。
<img src="img/news-icon.png" align=right> Network: Web: 会社で14:00ごろからgoogle.co.jpがつながらなくなっていた。 google.com も gmail も youtube.com もつながらなかった。 ...最初に、画像表示などのため、HTMLタグが入ることがある。 次にカテゴリを示すラベルが ": " で区切られている。 ラベルは一行で書ききることにしている。そのあと、HTMLを含む 本文が続く。本文の冒頭の一文は、記事のタイトルになる。 最初の一文とは、本文の先頭から、最初の 。 までの区間になる。
これを正規表現で書くとこうなる。
/ (?: \s* < [^>]+ >)* \s* ( (?: [^:\r\n]+? [^\s:] : \s+ )* ) \s* ( [^。]+ )。(.*) /sx;
上と同じ正規表現をコメントを入れて説明しておく。 こちらは split() ではなく、すでに取り出した文字列に 正規表現を適用して (=~)、括弧のグループ指定を使って、 特定の部分だけを配列としてとりだす形になっている。 そのため、括弧のグループで指定されていない部分は、 読み捨てる形になる。
my @article = ($_artString =~ / # $_artStringから、記事を分割する
(?: \s* < [^>]+ >)* # 記事冒頭にある HTML タグの連続を削除
# カテゴリのラベル文字列の連続をかたまりで取り出す
\s*
( # 空白文字列の連続があるかもしれない
# ひとつの文字列で返るので、あとで split() すること
(?: ### (?: ...)* という書き方をする
[^:\r\n]+? # : でも改行文字でもない連続 (1行内だけ注目する)
# 次の、直前に空白がない、を検査するため、+? にする
[^\s:] # : の直前は空白ではない ... カテゴリ文字列は2文字以上
# "C" などのカテゴリのラベル文字列は使えない
: # コロン文字1文字
\s+ # 空白文字が 1 文字以上 (: ) のようになっているはず
)* ### (?: ...) は配列として返らないが、この複数マッチをまとめて次の括弧で返す
) ### この括弧で、カテゴリ文字列の組の複数マッチを一度に返す
# タイトル文字列の取り出し
\s* ( [^。]+ )。 # タイトル文字列は、。までの文字
(.*) # それ以後は本文
/sx);
けっこう長くて気持ちが良い。
ここで、正規表現のグループの繰り返しについて。
()* というように、グループを繰り返し指定すると、
これにマッチしたときには、マッチしたときの最後のマッチ文字列ひとつだけが返る。
そのため、"Network: IPv6: " のような文字列があると、
最後の IPv6: しか返ってこない。
そのため、マッチするための文字列は (?:...) で囲み、
複数マッチする、(?:...)* をさらに普通の括弧で囲んで、
( (?:...)* ) としている。
こういう、正規表現のどの位置で何がマッチしたので、どの変数に
積み込むか、という作業を簡単にできそうなのが、
Perlの(... (?{code}))。

