Press HTMLの技術解説

〜Press HTMLの技術解説〜

最終更新日 2002.4.20

 ここでは、PressHTMLについての技術的解説を行います。一応、Perlがある程度判っている人向けの解説ですが、プログラム言語を知らなくても「雰囲気」はつかめると思います。
 ま、白砂がいかに苦労したかという自慢話ですから、気軽に読んで下さい(笑)。


■ログデータの構成

 これは実際に見ていただいた方が早いんですが、

連番<>日付<>題名<>記事<>見出しの書き出し位置<>見出しのみサイン<>

 となっています。

 連番は毎回書くごとに+1され、途中で削除があってもマイナスはされません。ですから、削除が多ければ、データの連番は12469なんていう感じで続いていきます。このデータを12345と整列させ直すこともありませんし、次に新規データを書いた時に間の3を連番にする、なんてこともありません。

 見出しの書き出し位置というのは、もう読んで字の如くなんですが(笑)、目次のページ(index.html)での見出しの書き出し位置を調節するものです。デフォルトでは0〜7となっています。環境設定で変更可能ですが、これを0〜99とかして99を指定すると、多分見出しはすっごい右側の方に表示されて見えなくなります(笑)。

 見出しのみサインは、これもまた読んで字の如くなんですが(笑)、見出しだけで記事はないよ、というサインです。このサインが立っていると、ページを作成しません。目次のページの見栄えを整える方策として作成した項目です。見出しのみにしたい場合に1が入り、記事もあるという時は何も入りません(0とかスペースですらありません)

 ログデータは古いものから順に並んでいます。ColumnHTMLの方は新しいものから順に並んでいますので注意して下さい。ですから、PressHTMLでは、最新のデータは配列の一番後ろです。
 別にColumnHTMLの並びを踏襲してもよかったんですが、目次のページでは見出しは古い方から並びますよね。そのために古い順にしたんです。こうすれば、

foreach(@logs)
{
  処理
}

 とすれば古い順に見出しが作成できますから。もちろん配列を逆順に処理することも可能なんですが、ソースは書けても内部では手間をかけるんだろうなと思い、スピードを考えてこちらにしました。


■ページとデータの関係

 非常に単純です。
 データの連番が、ページになってます。ですから、連番の10は10.htmlというページに書かれます。
 ColumnHTMLでは、わざわざ10だったら0010.htmlという風に4桁の連番にしていたんですが、それはやめました。別にsortをするわけではないので(sortをするのであれば、連番は揃えないとダメです。1、11、12、13……2となっちゃいますから)。

 ページを書く、という処理はサプルーチン化し、そのサブルーチンにはデータ全部ではなく、書きたいデータの配列位置だけを渡しています。連番ではなく配列位置を渡していますので、ページ名を得るために連番を得る処理が必要になります。イメージとしてはこんな感じです。

($no)=split(/<>/,$logs[配列位置]);
&wr_open("./$no.html","HTML");

 わざわざデータをsplit命令で刻んで連番を得て、「./$no.html」というページを作成しています。例えば14だったら、14.htmlというhtmlファイルが作成されます。


■「自動ページリンク機能」とは?

 すいません。実はそんな大仰なものじゃないです(笑)。
 PressHTMLでは、各ページに[前頁][目次][次頁]というリンクが張られます。このリンクは最初と最後のページではそれぞれ[前頁][次頁]がなかったり、間の記事を削除するとそれを飛ばしてリンクを張ったりと、どんな場合でもきちんとリンクできるようになっています。
 簡単に見えるこの「自動ページリンク機能」ですが、実は非常に面倒な仕組みを使っています。そのカラクリを解説しましょう。


■そのページの前後ページを探す1.単純な方法

 [前頁][次頁]というリンクを張る、ということは、言い換えると「前のデータ、後のデータが何かを知る」ということになります。前のデータ、後のデータというのはデータの配列の前後ですから、単純に考えれば「今のデータ」が配列のどの位置にあるのかが判れば、それを+1、−1することによって前後のデータが判ります。
 また、Perlでは、例えば@dataという配列のN番目のデータは「$data[N]」と書くことによって得られますから、より好都合です。

 さて、具体的な探し方なんですが、白砂はPerlの文法に精通していないので「一発で得る」方法が判りませんでした。そこで、非常に原始的ですが、「配列を頭っから読んで、読みながらカウントを取る」という方法を採用しました。ソースはこんな感じです。

$num=現在のデータの連番;
$i=0;
foreach(@logs)
{
  ($no)=split(/<>/);
  if($num==$no)
  {
    $logs[$i-1]; #今の1つ前が前ページ
    $logs[$i+1];#今の1つ後が次ページ
  }
  $i++;
}

 というアルゴリズムです。ホントに原始的ですが(笑)。
 先にも説明しましたが、連番=配列位置というわけではない、というところに注意して下さい。配列位置は10でも連番は15かもしれません。また、ページはあくまでも連番と連動しているので、リンクを張る際には配列位置ではなく連番が必要になります。
 ところが、これだけではうまくいきません。


■そのページの前後ページを探す2.見出しのみサインを考える

 PressHTMLでは「見出しのみ」というデータを持っています。このデータは文字通り見出ししか持っていないわけで、記事なんてありませんからページが作成されません。

 例えば、現在データがこんな感じになっているとします。

1<>日付<>題名<><>0<>1<>
2<>日付<>題名<>記事<>1<><>
3<>日付<>題名<><>1<>1<>

 この時、新たにデータを作成した場合、つまり連番4のデータを作る場合ですね。この場合の、連番4のページの[前頁]のリンク先は、2.htmlになっていないといけません。1つ前のデータである連番3のデータに見出しサインの1がついているためです。判りますか?
 しかし、先程のアルゴリズムを採用すると、[前頁]のリンク先は「今の1つ前」となっているために3.htmlとなってしまいます。これは見出しで、実際はこんなページは存在しません。見出しのみのデータは飛ばして考えたいのです。
 そのためには、先の判定法をもう少し拡張する必要があります。まずはソースをご覧下さい。

$num=現在のデータの連番;
$i=0;
foreach(@logs)
{
  ($no)=split(/<>/);
  if($num==$no)
  {
    for($j=$i-1;$j>=0;$j--)
    {
      ($num,$d,$d,$d,$d,$mo)=split(/<>/,$logs[$j]);
      if($mo!=1){#前ページと考える}
    }
    for($j=$i+1;$j<=$#logs;$j++)
    {
      ($num,$d,$d,$d,$d,$mo)=split(/<>/,$logs[$j]);
      if($mo!=1){#次ページと考える}
    }
  }
  $i++;
}

 さぁて、何がなんだかさっぱり判りませんね(笑)。

 基本的な構造は変わっていません。
 foreach文で配列を頭から見ていき、splitでデータを刻んで「そのデータの連番」を得、「そのデータの連番」と「現在のデータの連番」がイコールかどうかを判定し、一致した場合、カウント数$iが配列位置です。ここまではいいんです。
 問題はこの後。
 例えば前ページを探す場合で言えば、現在のデータより前のデータが見出しのみかどうかを判断し、見出しのみでない「ほんもんデータ」を探す必要があるわけです。
 それをやっているのが、foreach文の中に入ったfor文です。

for($j=$i-1;$j>=0;$j--)
{
  ($num,$d,$d,$d,$d,$mo)=split(/<>/,$logs[$j]);
  if($mo!=1){#前ページと考える}
}

 ですね。
 forの開始は現在の1コ前。そこから1つずつカウントをマイナスしていって、1件ずつデータを読みます。そしてそのデータをsplitで刻んで、見出しのみサインを調べます。で、見出しのみサインが1でないものが「ほんもんデータ」というわけです。
 先のデータの例で言うと、まず連番3のデータを読みます。で、見出しのみサインを調べると1。これは違うというわけで次のデータを読みます。連番2のデータですね。連番2のデータは見出しサインは1でないので、あ、これが「ほんもんデータ」だ、と判ります。
 ここまでやんないと判んないのかよ〜と思うかもしれませんが、実は白砂もそう思います(笑)。だってPerl判んないんだも〜ん(爆)。
 もっとうまい方法があったら教えて下さい。すぐ変えます(笑)

 あ、ちなみに、次ページの方のループ、

for($j=$i+1;$j<=$#logs;$j++)
{
  ($num,$d,$d,$d,$d,$mo)=split(/<>/,$logs[$j]);
  if($mo!=1){#次ページ}
}

 の$#logs。これは「配列の数」を表します。配列の中にいくつデータがあるか判りませんが、とにかく「配列の最後まで処理したい」って時に使えます。全件だったらforeach文でいいんですけどね。
 Perl使いであれば常識でしょうが、白砂はつい最近知りました(泣)。


■これだけでは終わらない

 ここで終われればいいんです、まだ。
 しかし、まだまだ難関があります。

データを削除した場合です

 判る人はこれだけで「あぁ」となるでしょうし、もっとプログラマーな人(笑)は「そうそう、そう思ってたんだ」となるでしょうが、実際にデータを示しながら説明しましょう。
 こんなデータがあるとします。さっきの続きみたいなデータですね。

1<>日付<>題名<><>0<>1<>
2<>日付<>題名<>記事<>1<><>[なし] [4]
3<>日付<>題名<><>1<>1<>
4<>日付<>題名<>記事<>1<><>[2]  [5]
5<>日付<>題名<>記事<>2<><>[4]  [6]
6<>日付<>題名<>記事<>2<><>[5]  [8]
7<>日付<>題名<><>1<>1<>
8<>日付<>題名<>記事<>1<><>[6]  [なし]

 右側に書いてあるのはリンクの状態です。[前頁][次頁]のリンクです。
 さて。
 このようなデータで、連番6のデータを削除したとします。この場合、単純に連番6を削除しただけですと、

1<>日付<>題名<><>0<>1<>
2<>日付<>題名<>記事<>1<><>[なし] [4]
3<>日付<>題名<><>1<>1<>
4<>日付<>題名<>記事<>1<><>[2]  [5]
5<>日付<>題名<>記事<>2<><>[4]  [6]
7<>日付<>題名<><>1<>1<>
8<>日付<>題名<>記事<>1<><>[6]  [なし]

 という状態になります。
 リンクがつながってませんよね。連番5のページと連番8のページが連番6のページにリンクしちゃって、リンク切れを起こしています。
 これを解決するためにはどうすればいいんでしょうか?
 これまたヘタレの白砂にはうまい解決方法が見つかりませんでしたので、超力技の解決方法を用いています(<超って言うな)。それは、

前後のページを呼び出し、もう一度書き直す

 という方法です。
 先程までのアルゴリズムで「そのページの前後ページを探す」という処理はできます。それを利用して、削除したデータの前後のページを呼び出して、それぞれのページをきちんと書き直せばリンクがちゃんと張り直せる筈です。
「そうか。じゃあ、連番から配列位置を割り出して、その前後だから+1と−1して……」
 違います(笑)。
 もう判りますよね。じゃあ、解答は事項で。


■削除対応アルゴリズム〜白砂考案版〜

 まず、削除したデータの連番から、配列位置を割り出します。これはもうできますね(ソースは後でまとめて出しますんで、ちょっと待ってて下さい)。
 この時、配列位置を割り出すためには削除前のデータ群を使わないといけません。削除した後のではダメです。あ、当然か(笑)。

1<>日付<>題名<><>0<>1<>
2<>日付<>題名<>記事<>1<><>[なし] [4]
3<>日付<>題名<><>1<>1<>
4<>日付<>題名<>記事<>1<><>[2]  [5]
5<>日付<>題名<>記事<>2<><>[4]  [6]
6<>日付<>題名<>記事<>2<><>[5]  [8]  ■ここの配列位置を調べました
7<>日付<>題名<><>1<>1<>
8<>日付<>題名<>記事<>1<><>[6][なし]

 で、次に「前後の、見出しのみサインが立っていない「ほんもんデータ」」を割り出します。これも今までの解説で判りますよね。

1<>日付<>題名<><>0<>1<>
2<>日付<>題名<>記事<>1<><>[なし] [4]
3<>日付<>題名<><>1<>1<>
4<>日付<>題名<>記事<>1<><>[2]  [5]
5<>日付<>題名<>記事<>2<><>[4]  [6]  ■ここと
6<>日付<>題名<>記事<>2<><>[5]  [8]
7<>日付<>題名<><>1<>1<>
8<>日付<>題名<>記事<>1<><>[6]  [なし] ■ここが前後のページです

 さて。
 削除後の正しい状態のリンクを張りたいんで、ここでデータを削除します。

1<>日付<>題名<><>0<>1<>
2<>日付<>題名<>記事<>1<><>[なし] [4]
3<>日付<>題名<><>1<>1<>
4<>日付<>題名<>記事<>1<><>[2]  [5]
5<>日付<>題名<>記事<>2<><>[4]  [6]
7<>日付<>題名<><>1<>1<>
8<>日付<>題名<>記事<>1<><>[6]  [なし]

 このデータを使って、先程調べておいた「前後のページ」のデータの前後のページのリンクを作成します。なんだか判りにくいなぁ表現が(笑)。
 この場合で言うと、さっき調べていた連番は5と8でしたよね。ですから、「現在のページ」を5にして前後のリンクを、次に「現在のページ」を8にして前後のリンクを完成させると、こういうわけです。
 この結果、

1<>日付<>題名<><>0<>1<>
2<>日付<>題名<>記事<>1<><>[なし] [4]
3<>日付<>題名<><>1<>1<>
4<>日付<>題名<>記事<>1<><>[2]  [5]
5<>日付<>題名<>記事<>2<><>[4]  [8]  ■このページと
7<>日付<>題名<><>1<>1<>
8<>日付<>題名<>記事<>1<><>[5]  [なし] ■このページを処理しました

 という正しいリンクが張られたページが完成します。

 このアルゴリズムを考えるのは本当に苦労して、というか考えること自体はいいんですがループの渦になるんで頭ん中がこんがらがっちゃうんですよ(笑)。いやホントに。自分の中でこうやって整理して、やっと「あぁ、これならできるか」と認識できたわけです。
 その証拠に、というわけではないですが、PressHTMLのソースを見て下さい。コメントに混乱がにじみ出てます(笑)。


■削除対応アルゴリズム〜ソース(いめえじ(笑))〜

 さて、具体的なソースですが、実際に動くソースはCGIを参照して下さい。ここでは適度にはしょって紹介することにします。#でコメントアウトしていない日本語がはしょった処理です。

$num=現在のデータの連番;
$i=0;
foreach(@logs)
{
  ($no)=split(/<>/);
  if($num==$no)
  {
    $prevpage=$i-1;  #1つ前の配列位置を記憶
    $nextpage=$i+1;  #1つ後の配列位置を記憶
    同時に新データ作成
  }
  $i++;
}

#前ページのリンク作成処理
for($i=$prevpage;$i>=0;$i--)
{
  ($num,$date,$sub,$com,$mt,$mo)=split(/<>/,$logs[$i]);
  if($mo!=1)
  {
    $i=0;
    foreach(@lines)
    {
      ($no)=split(/<>/);
      if($num==$no)
      {
        $iを「現在のページ」として、リンクを作成;
        last;
      }
      $i++;
    }
    last; #HTML作成完了のため処理修了
  }
}

次ページのリンクについても同様に処理

 という感じです。
 ……ちゃんと解説しないと判んないですよね。

 まず、最初のforeachはもういいですよね?削除するつもりのデータ、その連番から配列位置を割り出してます。+1とか−1をしていますが、それはこれを実際の位置として使うというんではなくて、ループがややこしくなるんで別の場所で使うために書き出しておいたいわば「メモ書き」みたいなもんです。
 実際の「削除したページの有効な前後ページ探し」は、次のかたまりのfor文でやっています。

for($i=$prevpage;$i>=0;$i--)
{
  ($num,$date,$sub,$com,$mt,$mo)=split(/<>/,$logs[$i]);
  if($mo!=1)
  {
    ★
  }
}

 という部分ですね。

 ここで有効ページを探した後(上のソースでは「前のページ」です)、そのページが「データを削除し終わった後のデータ郡のどの位置にあるか」を調べます。それが★の部分に書いてあったプログラム

$i=0;
foreach(@lines)
{
  ($no)=split(/<>/);
  if($num==$no)
  {
    $iを「現在のページ」として、リンクを作成;
    last;
  }
  $i++;
}

 です。
 もうこの書き方は説明不要ですよね。「今持っている連番の、そのデータ群の中での配列位置」を調べるプログラムです。
 で、ここで得られた配列位置を使って、「その配列位置のページを実際に書く」という処理を行っています。その中でも、先程の「■そのページの前後ページを探す」で紹介したアルゴリズムが動いています。ホントにループの嵐でしょ(笑)。


■終わりに

 これで大体のアルゴリズムは説明しました。
 特にPressHTMLで苦心したのがリンクの張り方だったので、そこについてはエラく筆を割いちゃいましたが、みなさん理解していただけたでしょうか?

 実際のところ、配列の位置を簡単に求める方法があればこんなループまみれにはならないでしょうし、例えば連想配列を使うなどとすれば配列位置を求める必要すらないのかもしれません。しかし連想配列だとデータがちゃんと並んでる保証がないんで「前後」というのを判断するのはちょっと不安ですし、となるとこの方法くらいしかないのでは……なんて思ってます。

 半分は自慢話だったんですが(笑)、もう半分は今まで白砂が勉強させていただいたHP、書籍の作者さん達への恩返しです。こうやって自分の考えたこと実現したことを判りやすく公開し解説することによって、もっとCGIを作ってくれる人、Perlを勉強してくれる人、プログラムを作ってくれる人が増えるかもしれません。
 たいした内容ではありませんが、しかし、これを見てくれた誰かが何かの役に立てくれれば、それ以上の喜びはありません。

白砂青松 

トップへ戻る