Ruby で HTML をパースしたいんですが。
今日は PHP の関数辞書を作ってみましょう。
http://www.php.net/download-docs.php
PHP のドキュメントは↑からダウンロードできます。本日の最新版は v5.1.3 となっております。 Single HTML は解凍すると 14MB になる巨大な HTML ファイルです。これを Ruby で切ったり焼いたりします。切ったり焼いたりすることを割烹というのでしたね。
はじめに、単純なテキストフィルタで関数名を拾えるかと思って試してみました。パッと見た感じ、どうやら関数名の部分は <B CLASS="methodname">〜</B> でマークアップされてるようです。そこで、 CLASS="methodname" を Grep してみます。そしたら 5226 個出てきました。ほんまかいな?
辞書である以上、関数名を過不足なく網羅していなければなりません。そこで、末尾に付いてる「関数一覧」も情報源として使うことにしました。関数一覧の範囲を切り出して CLASS="function" を Grep すると 3843 個でした。あれれ、だいぶ少ないぞ!
さらに、関数のなかには SWFTextField->moveTo() みたいなクラスメソッド(って PHP で呼ぶのかどうか知らんけど)があって、これらはそのまま書くものではないので除外します。その判定には、「関数一覧からリンクが張られてるかどうか」が情報源として使えそうです。これで関数は 3192 個になりました。
最終的に、「関数一覧からリンクが張られてるものを抽出 → リンク先のセクションから説明文を抽出 → 両者を合わせて辞書ファイルに出力」という手順を踏むことになりそうです。単純なテキストフィルタではとても歯が立ちませんね。
ドキュメントの配布形式は HTML です。 HTML を解析して必要な情報を抽出しなければなりません。ほんなら DOM ツリーっぽいもんを作ればいいんじゃないのという気がしてきますね。 Ruby で XML/HTML をパースするモジュールはいくつかあります*1 *2 *3 *4。 PHP のドキュメントは XHTML ではないので、そのままでは XML パーサは食べてくれそうもありません。それどころか、よーく見ると HTML としても全然なっちょらんじゃありませんか! おいおい、これじゃ HTML パーサだって食えねーよ!
こうなったら仕方ありません。かろうじてドキュメント構造を正しく反映してると思われる <H1> で全体をセクションに分割し、1セクションを1行にまとめて一時ファイルに書き出します。あとは一時ファイルを1行ずつ読み込みながら正規表現でマッチングして…という由緒正しい方法に持ち込みましょう。使ってるのは Ruby なのに、やってることは Perl くさいです。にんともかんとも。
#!/usr/bin/ruby -Ku # PHP リファレンス(単一 HTML 版)から CompleteX 用の辞書とヒントファイルを作る # 2006/05/03 $singlehtmlfile = "php5.1.3_manual_ja_single.html" # 勝手にリネーム $funcreffile = "funcref.html" $funcancfile = "funcanc.txt" $funclistfile = "sample.dic" $funchintfile = "sample.hint" $funcref_bgnmark = /VI\. 関数リファレンス/ $funcref_endmark = /VII\. PHP および Zend エンジンの内部構造/ $funclist_bgnmark = /付録 T\. 関数一覧/ $funclist_endmark = /<\/BODY/ # 巨大 HTML から関数リファレンス部分と関数一覧部分を切り出す def cutpart() $stderr.puts "cutting..." File.open($singlehtmlfile) do |file1| while sect = file1.gets("<H1") break if $funcref_bgnmark =~ sect end file2 = File.open($funcreffile, "w") while sect = file1.gets("<H1").chomp("<H1") break if sect =~ $funcref_endmark file2.puts "<H1" + sect.gsub(/\s+/, " ").gsub(/ >/, ">") end file2.close while sect = file1.gets("<H1") break if $funclist_bgnmark =~ sect end file2 = File.open($funcancfile, "w") file3 = File.open($funclistfile, "w") while sect = file1.gets("<DT").chomp("<DT") next if sect =~ /関数一覧/ line = "<DT" + sect.gsub(/\s+/, " ").gsub(/ >/, ">") %r!<A HREF="#(function\..*?)"! =~ line next if $1 == nil file2.puts $1 %r!<B CLASS="function">(.*?)</B>! =~ line file3.puts $1 break if sect =~ $funclist_endmark end file2.close file3.close end end # 関数一覧の各関数に対し、リファレンスから引数と返り値を抜粋 def compose() ancstr = IO.read($funcancfile) refstr = IO.read($funcreffile) desc = Hash.new("") # 先にリファレンスからハッシュを構築しておく $stderr.puts "building..." refstr.each do |line| %r!<A NAME="(function\..*?)"! =~ line next if $1 == nil anchor = $1 line.sub!(%r!<P>(手続き.*?)</P>(.*?)<P>(オブジェクト指向.*?)</P>!) { "#{$1} #{$2}\\n#{$3} " } %r!<DIV CLASS="refsect1">.*?</H2>(.*?)<P>(.*?)</P>! =~ line content = "" content << $1.html_to_plain if $1 content << '\n' content << $2.html_to_plain if $2 desc[anchor] = content end # 関数名と説明を書き出す $stderr.puts "writing..." file = File.open($funchintfile, "w") ancstr.each do |anchor| anchor.chomp! file.puts desc[anchor] if desc[anchor] == "" $stderr.puts "no description: #{name}\tfunction.#{anchor}" end end file.close end class String def html_to_plain self.gsub(/<.*?>/, "").gsub(/&#([0-9a-f]+);/i) { $1.to_i.chr }.gsub(/</, "<").gsub(/>/, ">").gsub(/"/, "\"").gsub(/\s+/, " ").strip.gsub(/ /, " ") end end cutpart compose