19.1. "HTMLParser" --- HTML および XHTML のシンプルなパーサー
*************************************************************

注釈: "HTMLParser" モジュールは Python 3 で "html.parser" に改名され
  ました 。ソースを 3 用に移行する際には *2to3* がインポートを自動的に
  直して くれます。

バージョン 2.2 で追加.

**ソースコード:** Lib/HTMLParser.py

======================================================================

このモジュールでは "HTMLParser" クラスを定義します。このクラスは HTML
(ハイパーテキスト記述言語、 HyperText Mark-up Language) および XHTML
で書式化されているテキストファイルを解釈するための基礎となります。
"htmllib" にあるパーサーと違って、このパーサーは "sgmllib" の SGML パ
ーサーに基づいてはいません。

class HTMLParser.HTMLParser

   "HTMLParser" インスタンスは、HTML データが入力されると、開始タグ、
   終了タグ、およびその他の要素が見つかる度にハンドラーメソッドを呼び
   出します。各メソッドの挙動を実装するには "HTMLParser" サブクラスを
   使ってそれぞれを上書きして行います。

   "HTMLParser" クラスは引数なしでインスタンス化します。

   "htmllib" のパーサーと違い、このパーサーは終了タグが開始タグと一致
   しているか調べたり、外側のタグ要素が閉じるときに内側で明示的に閉じ
   られていないタグ要素のタグ終了ハンドラを呼び出したりはしません。

例外も定義されています:

exception HTMLParser.HTMLParseError

   "HTMLParser" は正しくないマークアップを処理することが出来ますが、あ
   るケースにおいてはこの例外が発生しえます。これは、パース中にエラー
   に遭遇した場合の例外です。この例外は三つの属性を提供しています:
   "msg" はエラーの内容を説明する簡単なメッセージ、"lineno" は壊れたマ
   ークアップ構造を検出した場所の行番号、"offset" は問題のマークアップ
   構造の行内での開始位置を示す文字数です。


19.1.1. HTML パーサーアプリケーションの例
=========================================

基礎的な例として、"HTMLParser" クラスを使い、発見した開始タグ、終了タ
グ、およびデータを出力する、シンプルな HTML パーサーを以下に示します:

   from HTMLParser import HTMLParser

   # create a subclass and override the handler methods
   class MyHTMLParser(HTMLParser):
       def handle_starttag(self, tag, attrs):
           print "Encountered a start tag:", tag

       def handle_endtag(self, tag):
           print "Encountered an end tag :", tag

       def handle_data(self, data):
           print "Encountered some data  :", data

   # instantiate the parser and fed it some HTML
   parser = MyHTMLParser()
   parser.feed('<html><head><title>Test</title></head>'
               '<body><h1>Parse me!</h1></body></html>')

出力は以下のようになります:

   Encountered a start tag: html
   Encountered a start tag: head
   Encountered a start tag: title
   Encountered some data  : Test
   Encountered an end tag : title
   Encountered an end tag : head
   Encountered a start tag: body
   Encountered a start tag: h1
   Encountered some data  : Parse me!
   Encountered an end tag : h1
   Encountered an end tag : body
   Encountered an end tag : html


19.1.2. "HTMLParser" メソッド
=============================

"HTMLParser" インスタンスは以下のメソッドを提供します:

HTMLParser.feed(data)

   パーサーにテキストを入力します。入力が完全なタグ要素で構成されてい
   る場合に限り処理が行われます; 不完全なデータであった場合、新たにデ
   ータが入力されるか、"close()" が呼び出されるまでバッファーされます
   。 *data* は "unicode" でも "str" でも構いませんが、 "unicode" の方
   が良いです。

HTMLParser.close()

   全てのバッファされているデータについて、その後にファイル終了マーク
   が続いているとみなして強制的に処理を行います。このメソッドは入力デ
   ータの終端で行うべき追加処理を定義するために派生クラスで上書きする
   ことができますが、再定義を行ったクラスでは常に、 "HTMLParser" 基底
   クラスのメソッド "close()" を呼び出さなくてはなりません。

HTMLParser.reset()

   インスタンスをリセットします。未処理のデータはすべて失われます。イ
   ンスタンス化の際に暗黙的に呼び出されます。

HTMLParser.getpos()

   現在の行番号およびオフセット値を返します。

HTMLParser.get_starttag_text()

   最も最近開かれた開始タグのテキスト部分を返します。このテキストは必
   ずしも元データを構造化する上で必須ではありませんが、 "広く知られて
   いる (as deployed)" HTML を扱ったり、入力を最小限の変更で再生成 (属
   性間の空白をそのままにする、など) したりする場合に便利なことがあり
   ます。

以下のメソッドはデータまたはマークアップ要素が見つかる度に呼び出されま
す。これらはサブクラスで上書きされることを想定されています。基底クラス
の実装は ("handle_startendtag()" を除き) 何もしません:

HTMLParser.handle_starttag(tag, attrs)

   このメソッドは開始タグを扱うために呼び出されます (例: "<div
   id="main">")。

   引数 *tag* はタグの名前で、小文字に変換されます。引数 *attrs* は
   "(name, value)" のペアからなるリストで、タグの "<>" 括弧内にある属
   性が収められています。*name* は小文字に変換され、*value* 内の引用符
   は取り除かれ、文字参照と実態参照は置き換えられます。

   例えば、タグ "<A HREF="https://www.cwi.nl/">" を処理する場合、この
   メソッドは "handle_starttag('a', [('href',
   'https://www.cwi.nl/')])" として呼び出されます。

   バージョン 2.6 で変更: 属性中の "htmlentitydefs" の全てのエンティテ
   ィ参照が置換されるようになりました。

HTMLParser.handle_endtag(tag)

   このメソッドは要素の終了タグを扱うために呼び出されます (例:
   "</div>")。

   引数 *tag* はタグの名前で、小文字に変換されます。

HTMLParser.handle_startendtag(tag, attrs)

   "handle_starttag()" と似ていますが、パーサーが XHTML 形式の空タグ
   ("<img ... />") を見つける度に呼び出されます。この特定の字句情報が
   必要な場合にこのメソッドをサブクラスで上書きすることができます; 既
   定の実装では、単に "handle_starttag()" および "handle_endtag()" を
   呼び出します。

HTMLParser.handle_data(data)

   このメソッドは任意のデータを処理するために呼び出されます (例: テキ
   ストノードおよび "<script>...</script>" aや "<style>...</style>" の
   内容)。

HTMLParser.handle_entityref(name)

   このメソッドは "&name;" 形式の名前指定文字参照 (例: "&gt;") を処理
   するために呼び出されます。*name* は一般実体参照になります (例:
   "'gt'")。

HTMLParser.handle_charref(name)

   このメソッドは "&#NNN;" あるいは "&#xNNN;" 形式の 10進および 16 進
   数値文字参照を処理するために呼び出されます。例えば、"&gt;" と等価な
   10 進数は "&#62;" で、16進数は "&#x3E;" になります。この場合、メソ
   ッドは "'62'" あるいは "'x3E'" を受け取ります。

HTMLParser.handle_comment(data)

   このメソッドはコメントが見つかった場合に呼び出されます (例: "<!--
   comment-->")。

   例えば、コメント "<!-- comment -->" があると。このメソッドを引数 "'
   comment '" で呼び出されます。

   Internet Explorer 独自拡張の条件付きコメント (condcoms) はこのメソ
   ッドに送ることができます。"<!--[if IE 9]>IE9-specific
   content<![endif]-->" の場合、このメソッドは "'[if IE
   9]>IE9-specific content<![endif]'" を受け取ります。

HTMLParser.handle_decl(decl)

   このメソッドは HTML doctype 宣言を扱うために呼び出されます (例:
   "<!DOCTYPE html>")。

   引数 *decl* は "<!...>" マークアップ内の宣言の内容全体になります (
   例: "'DOCTYPE html'")。

HTMLParser.handle_pi(data)

   処理指令 (processing instruction) が見つかった場合に呼び出されます
   。*data* には、処理指令全体が含まれ、例えば "<?proc color='red'>"
   という処理指令の場合、"handle_pi("proc color='red'")" のように呼び
   出されます。

   注釈: The "HTMLParser" クラスでは、処理指令に SGML の構文を使用し
     ます。 末尾に "'?'" がある XHTML の処理指令では、 "'?'" が *data*
     に含ま れることになります。

HTMLParser.unknown_decl(data)

   このメソッドはパーサーが未知の宣言を読み込んだ時に呼び出されます。

   パラメーター *data* は "<![...]>" マークアップ内の宣言の内容全体に
   なります。これは派生クラスで上書きする時に役立つ場合があります。


19.1.3. 例
==========

以下のクラスは、より多くの例を示すのに用いられるパーサーの実装です:

   from HTMLParser import HTMLParser
   from htmlentitydefs import name2codepoint

   class MyHTMLParser(HTMLParser):
       def handle_starttag(self, tag, attrs):
           print "Start tag:", tag
           for attr in attrs:
               print "     attr:", attr

       def handle_endtag(self, tag):
           print "End tag  :", tag

       def handle_data(self, data):
           print "Data     :", data

       def handle_comment(self, data):
           print "Comment  :", data

       def handle_entityref(self, name):
           c = unichr(name2codepoint[name])
           print "Named ent:", c

       def handle_charref(self, name):
           if name.startswith('x'):
               c = unichr(int(name[1:], 16))
           else:
               c = unichr(int(name))
           print "Num ent  :", c

       def handle_decl(self, data):
           print "Decl     :", data

   parser = MyHTMLParser()

doctype をパースします:

   >>> parser.feed('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" '
   ...             '"http://www.w3.org/TR/html4/strict.dtd">')
   Decl     : DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"

要素のタイトルと一部属性をパースします:

   >>> parser.feed('<img src="python-logo.png" alt="The Python logo">')
   Start tag: img
        attr: ('src', 'python-logo.png')
        attr: ('alt', 'The Python logo')
   >>>
   >>> parser.feed('<h1>Python</h1>')
   Start tag: h1
   Data     : Python
   End tag  : h1

それ以上のパースを行わずに、"script" と "style" 要素の内容をそのまま返
します:

   >>> parser.feed('<style type="text/css">#python { color: green }</style>')
   Start tag: style
        attr: ('type', 'text/css')
   Data     : #python { color: green }
   End tag  : style

   >>> parser.feed('<script type="text/javascript">'
   ...             'alert("<strong>hello!</strong>");</script>')
   Start tag: script
        attr: ('type', 'text/javascript')
   Data     : alert("<strong>hello!</strong>");
   End tag  : script

コメントをパースします:

   >>> parser.feed('<!-- a comment -->'
   ...             '<!--[if IE 9]>IE-specific content<![endif]-->')
   Comment  :  a comment
   Comment  : [if IE 9]>IE-specific content<![endif]

名前指定および数値文字参照をパースし、正しい文字に変換します (注: これ
ら 3 個の参照はすべて "'>'" と等価です):

   >>> parser.feed('&gt;&#62;&#x3E;')
   Named ent: >
   Num ent  : >
   Num ent  : >

不完全なチャンクを "feed()" に処理させても、"handle_data()" は 1 回以
上呼び出される場合があります:

   >>> for chunk in ['<sp', 'an>buff', 'ered ', 'text</s', 'pan>']:
   ...     parser.feed(chunk)
   ...
   Start tag: span
   Data     : buff
   Data     : ered
   Data     : text
   End tag  : span

不正な HTML (例えば属性が引用符で括られていない) のパースも動作します:

   >>> parser.feed('<p><a class=link href=#main>tag soup</p ></a>')
   Start tag: p
   Start tag: a
        attr: ('class', 'link')
        attr: ('href', '#main')
   Data     : tag soup
   End tag  : p
   End tag  : a
