自動巡回エージェントを作る(『スタパライフ』 Ver 1.01)

 Chapter.17 目次ページの解析

 さてさて、お楽しみのページ解析。AirWebは、果たしてどんな仕組みでページの解析を行っているのでしょうか? 具体例を元に検証してみましょう。
 まず前提条件として、生のhtmlを見ておきましょう。スタパライフの目次ページのhtmlをこちらに掲載しています
 次に、今度はいきなり、AirWebがこのページを解析するときどのように見えているのかを確認します。このため、筆者の作った『HTMLアナライザ』というエージェントで、上記htmlを処理した結果をこちらに掲載しました。

  ・AirWebから見たhtmlソース
 まずは、airWebがページ解析する場合のルールを理解する必要があります。筆者が大事だと思っているのは、だいたいこんなところです。

  1. タグは全て連番を振られる
  2. ただし</table>,</a>,</font>等の『閉じタグ』は一個のタグとしては数えられない
  3. タグ外のテキストも、タグ=""(space)のタグの一つとして列挙される
  4. 閉じタグを考慮した入れ子の状態も保存される
 具体的にどういうことかといいますと。まず、先にあげた『HTMLアナライザ』の処理結果を見てください。Countが、airWebがページの先頭から終わりまでをチェックして、タグ毎に振っていった連番です。0番がドキュメントタイプ。そして1番がhtmlタグです。お分かりですね? htmlという見出しの下には、その番号のタグから閉じたタグまでのhtmlが全て書き出されています。ドキュメントタイプは閉じタグを持っていないので、ドキュメントタイプ自身しか書かれていません。しかし、htmlタグは、その内容として<html>から</html>までの全てのテキストが参照されるのです。

 続いて見てみましょう。2番目のタグはheadタグです。もちろん、これは対応する閉じタグを持ちますので、内容として<head>〜</head>の全てのテキストが参照されます。つまり、headタグの内容として、開始タグから閉じタグまでの全てのテキストが参照され、さらにhtmlタグの内容としては、headもbodyも含めた全てのテキストが参照されているのです。

 3番目と4番目はmetaタグですね。これは閉じタグを持ちませんので、metaタグ自身しか参照できません。

 5番目はtitleタグです。これは、<title>〜</title>に挟まれたテキストを、その文書のタイトルとして識別するタグです。
 6番目のタグは空白になっています。内容は『スタパライフ』というテキストのみ。つまり、テキストのみの部分は、タグ無しのテキストとして、他のタグからは独立して識別されているわけです。

 これで一応、上にあげた4つの特性を全て確認できたことになります。airWebのページ解析関数群は、タグの番号に対応するテキストの内容を様々な形で取り出すことによって処理を行います。特定のタグ番号のリストを取り出す時に、正規表現集合演算が利用できるため、柔軟な解析処理を実現することができるのです。

 ちなみに、解析結果はタグを含めた形や、テキストのみ、あるいはタグ内のオプションというように、いろいろな形で取り出すことができます。たとえば、htmlソースではなくテキストのみ取り出して列挙すると、こんなふうになります。
 ……おもしろいでしょう? ドキュメントタイプやmetaタグ、imgタグはテキストを持たないので、何も表示されません。しかし、titleタグやアンカータグは、間に挟むテキストを持っているので、その内容が表示されています。5番目のtitleタグと6番目のタグ無しの場合は、一見同じテキストが表示されていますが、実はhtmlソースの状態で見ると、5番目のほうはtitleタグで挟まれているわけです。こういう特性を考慮しながら、プログラムを書かなければいけないわけですね。


・目次ページの解析(checkmessages)
 それでは、具体的に自動巡回エージェントと照らし合わせてみましょう。

 137:    page=page_create(buf->text,buf->esize);
 138:    ea=page_elements(page);
 139:    el=page_find_elements(page,NULL,"TABLE");
 140:
 141:    for(scan=0;scancount;scan++) {
 142:        index=el->items[scan].number+1;
 143:        if((index+5)>=ea->count) break;
 144:        if( (ea->items[index].tag=="TR")&&
 145:            (ea->items[index+1].tag=="TD")&&
 146:            (ea->items[index+2].tag=="FONT")&&
 147:            (ea->items[index+3].tag=="")&&
 148:            (ea->items[index+4].tag=="TD")&&
 149:            (ea->items[index+5].tag=="")){
 150:            count=checknewmessage(qd,inet,dirpath+page_element_att(page,&ea->items[index+7],"href"),
 151:                page_element_text(page,&ea->items[index+3]));
 152:            if(!count) break;
 153:            result += count;
 154:        }
 155:    }
 156:    page_free(page);

 これが目次ページの解析部分です。137行目はpage_create関数により、buf->textに格納されたhtmlテキストが、PAGE型構造体の変数pageにごっそりコピーされ、ページ解析関数による処理が可能なように加工されます。
 次に、138行目のpage_elements関数により、page内のタグのリストがELEMENTS型構造体の変数eaに書き込まれます。ea内の全てのタグ番号とその内容を書き出すと、先ほどのhtmlソース付きの一覧テキストのみの一覧になるわけです。
 139行目では、eaに含まれるタグのリストのうち、テーブルタグのみを抽出したリストをelに作成しています。htmlソースを見ると分かりますが、目次のタイトル部は、一つずつテーブルタグになっています。つまり、テーブルタグを頭出ししてチェックしていけば、全てのタグを順番に調べるより効率的に処理ができるのです。

 141行目からはfor句によりループに入ります。139行目でテーブルタグのみ抜き出していますので、この個数だけループして、目次書式に合っているかチェックし、マッチした場合はcheknewmessage関数を起動します。
 ここで、テーブルタグによる分割したリストを見てみましょう。これは正確には、テーブルタグにより頭出しを行ったリストですので、テーブルタグ以降のタグも含まれています。htmlソースの一覧と、テキストの一覧があります。テーブルタグで分割すると、実際には一つのタイトルは2つのテーブルの入れ子の中にあるため、2回ずつ登場してしまうことがわかります。
 ではどうすれば良いかというと、マッチングの条件を厳しくして、一つのタイトルのうち一回だけマッチングが成立するようにすれば良いわけですね。実際には、このようなパートが144〜149行目の条件式に当てはまることになります。

***** <TABLE> tag Partition #1 *****
   19         0      TABLE 23703/02 04:00真夜中不毛化現象
   20         1         TR 23703/02 04:00真夜中不毛化現象
   21         2         TD 237
   22         3       FONT 237
   23         4            237
   24         5         TD 03/02 04:00
   25         6            03/02 04:00
   26         7         TD 真夜中不毛化現象
   27         8          A 真夜中不毛化現象
   28         9            真夜中不毛化現象

***** <TABLE> tag Partition #3 *****
   32         0      TABLE 23603/02 03:39眠くなったら喋っていきたい!!
   33         1         TR 23603/02 03:39眠くなったら喋っていきたい!!
   34         2         TD 236
   35         3       FONT 236
   36         4            236
   37         5         TD 03/02 03:39
   38         6            03/02 03:39
   39         7         TD 眠くなったら喋っていきたい!!
   40         8          A 眠くなったら喋っていきたい!!
   41         9            眠くなったら喋っていきたい!!

***** <TABLE> tag Partition #5 *****
   45         0      TABLE 23503/02 03:23俺のNTTディジタルでんわS-1000
   46         1         TR 23503/02 03:23俺のNTTディジタルでんわS-1000
   47         2         TD 235
   48         3       FONT 235
   49         4            235
   50         5         TD 03/02 03:23
   51         6            03/02 03:23
   52         7         TD 俺のNTTディジタルでんわS-1000
   53         8          A 俺のNTTディジタルでんわS-1000
   54         9            俺のNTTディジタルでんわS-1000

 例えば、『ea->items[index].tag=="TR"』は、index番のタグが"TR"かどうか、という意味です。indexには、見つかったテーブルタグの番号+1が転記されていますから、実際にはテーブルタグから数えて一番目ですね。つまり、ifの内容は、

 テーブルタグから1番目のタグ="TR"
 テーブルタグから2番目のタグ="TD"
 テーブルタグから3番目のタグ="FONT"
 テーブルタグから4番目のタグ=""
 テーブルタグから5番目のタグ="TD"
 テーブルタグから6番目のタグ=""

 この全ての条件を満たしているかどうかを判定しているわけです。上に抜き出したパートが、この条件を満たしていることが分かると思います。

 あとは、page_element_att(指定タグ番号のタグ内の指定オプションを抜き出す)やpage_element_text(指定タグ番号のhtmlに含まれるテキストを抜き出す)を使ってログの要素であるデータを抜き出し、本文ページの処理を起動すれば良いわけです。

 言葉で説明するのはとても難しいでのすが、実際の解析結果と見比べればその特性を理解できることと思います。これらのルールをしっかり習得できれば、自動巡回エージェントの作成は難しくありません。ここが一番のハードルだからです。とにかくいろんなページを解析してみて、どういう条件を当てはめれば『Tea-Cup』や『スタパライフ』エージェントのように処理ができるか考えてみてください。実際、この条件式以外の部分は、ほぼ流用が可能です。一度理解すれば、どんどんエージェントを量産できるよになりますので、がんばってください!