寛容な HTML 構文解析器

ネット上にある HTML ファイルを無作為に拾って眺めてみるといい。 とんでもなく壊れた HTML ばかりだ。 そんな壊れまくった HTML 文書をウェブブラウザは何事もなかったかのように解釈してくれる。 さすがにこれはもうエラーにしてよいのじゃないかと思うようなひどいものも解釈できてしまうのである。 少々のミスでいちいちエラーにされても困るが、あまりにも許容範囲が広すぎるために壊れた HTML 文書が蔓延する状況を生んでしまっているようにも思う。

そういったグダグダな HTML 文書をスクリプト言語で簡単に処理しようとすると、結局は正規表現を使うことになる。 なんという馬鹿馬鹿しさ!

さて、私がそういったことを感じたのは Gaucheスクレイピングしていたときのことだ。 Scheme での HTML パーサの実装として最も有名なのは htmlprag であり Gauche でも使える。 eggs にある html-parser は chicken 用ではあるものの容易に Gauche へ移植可能だ。 しかし、これらが主要なウェブブラウザと同程度の能力があるかというとそこまでは期待できない。 かなり多くの場合は問題なく処理できるのだけれど、やっぱり時々おかしなことになるのである。 内部文字コードとして UTF-8 を採用していながらバイト単位で切ってしまうようなクソなウェブシステムはそこらじゅうにあるのである。

主要なウェブブラウザと同じくらい寛容な HTML パーサが欲しいと考えたとき、目の前にあるじゃないかと気付いた。 Windows では HTML パーサは OLE オートメーション対応のコンポーネントとして公開されている。 折角だから OLE オートメーションへのインターフェイスを作ればそれだけで多くのライブラリを手にすることが出来るというのが Gauche 用の OLE オートメーションライブラリを作ろうとした動機だ。

実際に HTML を構文解析してみよう。 解析した結果として生成される DOM を直接には見ることが出来ないので DOM を SXML に変換するコードをまず示す。

; -*- mode: gauche -*-
(define-library (dom-tool)
  (export dom->sxml)
  (import (scheme base)
          (only (srfi 13) string-downcase))

  (begin
    (define DOM_ELEMENT_NODE 1)
    (define DOM_ATTRIBUTE_NODE 2)
    (define DOM_TEXT_NODE 3)
    (define DOM_CDATA_SECTION_NODE 4)
    (define DOM_COMMENT_NODE 8)
    (define DOM_DOCUMENT_NODE 9)
    (define DOM_DOCUMENT_FRAGMENT_NODE 11)

    (define (dom-map-filter proc node)
      (let ((count (node 'length)))
        (do ((i 0 (+ i 1))
             (r '() (let ((item (proc (node 'Item i))))
                      (if item (cons item r) r))))
            ((= i count) (reverse r)))))

    (define (dom-element-node->sxml node)
      `(,(string->symbol (string-downcase (node 'nodeName)))
        ,@(let ((attributes
                 (dom-map-filter
                  (lambda(x)
                    (let ((name (x 'nodename))
                          (value (x 'nodeValue)))
                      (if (or (null? name)
                              (null? value)
                              (equal? "contentEditable" name)
                              (equal? "" value)
                              (equal? 0 value)
                              (equal? #f value))
                          #f
                          (list (string->symbol name) value))))
                  (node 'attributes))))
            (if (null? attributes) '() (list (cons '|@| attributes))))
        ,@(dom-map-filter dom->sxml (node 'childNodes))))

    (define (dom->sxml node)
      (let ((type (node 'nodeType)))
        (cond ((or (equal? type DOM_DOCUMENT_NODE)
                   (equal? type DOM_DOCUMENT_FRAGMENT_NODE))
               (cons '*TOP* (dom-map-filter dom->sxml (node 'childNodes))))
              ((or (equal? type DOM_TEXT_NODE)
                   (equal? type DOM_CDATA_SECTION_NODE))
               (node 'nodeValue))
              ((equal? type DOM_ELEMENT_NODE)
               (dom-element-node->sxml node))
              ((equal? type DOM_COMMENT_NODE)
               (list '*COMMENT* (node 'nodeValue)))
              (else (error "Unknown dom type" type)))))
    ))

そして htmlfile コンポーネントを使って実際に構文解析すると…。

#!/usr/bin/env gosh
; -*- mode: gauche -*-
(use win.ole)
(use dom-tool)

(define parser (make-ole "htmlfile"))

(parser 'open)
(parser 'write
        "<html><head><title></title><title>testcase</title></head><body>
         <a href=\"invalid-url\">リンクタイトル</a><p align=left>
         <ul compact style=\"aa\">
         <p>クソッタレ! <!-- comment <comment> -->
         <i> italic <b> bold <tt> ened </i>
         まだ &lt; ボールドだよ </b></body><P> まだまだ続くんじゃ...")
(parser 'close)
(write (dom->sxml parser))
(newline)
(ole-release!)

こんな結果になる。 (実際にはインデントは付かないが、ここではわかりやすさのため整形してある。)

(*TOP*
 (html (head (title))
       (body (|@| (bottomMargin "15")
                  (leftMargin "10")
                  (rightMargin "10")
                  (topMargin "15"))
             (a (|@| (href "about:invalid-url")) "リンクタイトル")
             (p (|@| (align "left")))
             (ul (|@| (compact #t))
                 (p "クソッタレ! "
                    (*COMMENT* " comment <comment> ")
                    (i "italic "
                       (b "bold "
                          (tt "ened " "まだ < ボールドだよ "
                              (p "まだまだ続くんじゃ..."))))
                    "まだ < ボールドだよ ")
                 (p "まだまだ続くんじゃ...")))))

どういうわけだか body にマージンの属性が勝手に付けられるし、ノードが重複して現れるという明らかにおかしな結果となってしまった。 htmlfile では少し意地の悪い例でもうまく解釈できないようだ。

InternetExplorer.Application ならかなり上手く解釈できる上に JavaScript で動的に生成されるようなサイトでも扱えるはずだが、それはそれで起動に時間がかかるという欠点もあり、これがあれば大丈夫と言えるようなものにはならなかった。 なかなか上手くいかないものであることだなぁ。

Document ID: cea30177fa5e62c904d86027a95cdde3