"html.parser" --- 簡單的 HTML 和 XHTML 剖析器
*********************************************

**原始碼：**Lib/html/parser.py

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

該模組定義了一個類別 "HTMLParser"，是剖析 (parse) HTML（HyperText
Mark-up Language、超文本標記語言）和 XHTML 格式文本檔案的基礎。

class html.parser.HTMLParser(*, convert_charrefs=True)

   建立一個能夠剖析無效標記的剖析器實例。

   如果 *convert_charrefs* 為 "True" （預設值），所有字元參照
   (reference)（ "script"/"style" 元素中的參照除外）將自動轉換為相應的
   Unicode 字元。

   "HTMLParser" 實例被提供 HTML 資料，並在遇到開始標籤、結束標籤、文本
   、註解和其他標記元素時呼叫處理程式 (handler) 方法。使用者應該繼承
   "HTMLParser" 並覆蓋其方法以實作所需的行為。

   此剖析器不檢查結束標籤是否與開始標籤匹配，也不會為透過結束外部元素
   來隱晦地被結束的元素呼叫結束標籤處理程式。

   在 3.4 版的變更: 新增關鍵字引數 *convert_charrefs*。

   在 3.5 版的變更: 引數 *convert_charrefs* 的預設值現在是 "True"。


HTML 剖析器應用程式範例
=======================

以下的基礎範例是一個簡單的 HTML 剖析器，它使用 "HTMLParser" 類別，當遇
到開始標籤、結束標籤和資料時將它們印出：

   from html.parser import HTMLParser

   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)

   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


"HTMLParser" 方法
=================

"HTMLParser" 實例具有以下方法：

HTMLParser.feed(data)

   向剖析器提供一些文本。只要它由完整的元素組成，它就會被處理；不完整
   的資料會被緩衝，直到輸入更多資料或呼叫 "close()"。 *data* 必須是
   "str"。

HTMLParser.close()

   強制處理所有緩衝資料，如同它後面跟有文件結束標籤一樣。此方法可能有
   被衍生類別重新定義，以在輸入末尾定義額外的處理，但重新定義的版本仍
   應要呼叫 "HTMLParser" 基底類別方法 "close()"。

HTMLParser.reset()

   重置實例。丟棄所有未處理的資料。這在實例化時被會隱晦地呼叫。

HTMLParser.getpos()

   回傳當前列號 (line number) 和偏移量 (offset)。

HTMLParser.get_starttag_text()

   回傳最近開啟 (open) 的開始標籤的文本。這對於結構化處理通常不必要，
   但在處理「已部署」的 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/')])" 的形式被
   呼叫。

   在屬性值中來自 "html.entities" 的所有實體參照都會被替換。

HTMLParser.handle_endtag(tag)

   呼叫此方法來處理元素的結束標籤（例如 "</div>"）。

   *tag* 引數是轉換為小寫的標籤名稱。

HTMLParser.handle_startendtag(tag, attrs)

   與 "handle_starttag()" 類似，但在剖析器遇到 XHTML 樣式的空標籤
   ("<img ... />") 時呼叫。這個方法可能被需要這個特定詞彙資訊 (lexical
   information) 的子類別覆蓋；預設實作只是呼叫 "handle_starttag()" 和
   "handle_endtag()"。

HTMLParser.handle_data(data)

   呼叫此方法來處理任意資料（例如文本節點與 "<script>...</script>" 和
   "<style>...</style>" 的內容）。

HTMLParser.handle_entityref(name)

   呼叫此方法來處理形式為 "&name;" （例如 "&gt;"）的附名字元參照，其中
   *name* 是一般實體參照（例如 "'gt'"）。如果 *convert_charrefs* 為
   "True"，則永遠不會呼叫此方法。

HTMLParser.handle_charref(name)

   This method is called to process decimal and hexadecimal numeric
   character references of the form "&#*NNN*;" and "&#x*NNN*;".  For
   example, the decimal equivalent for "&gt;" is "&#62;", whereas the
   hexadecimal is "&#x3E;"; in this case the method will receive
   "'62'" or "'x3E'".  This method is never called if
   *convert_charrefs* is "True".

HTMLParser.handle_comment(data)

   當遇到註解時呼叫此方法（例如 "<!--comment-->"）。

   舉例來說，註解 "<!-- comment -->" 會使得此方法被以引數 "' comment
   '" 來呼叫。

   Internet Explorer 條件式註解（conditional comments, condcoms）的內
   容也會被發送到這個方法，故以 "<!--[if IE 9]>IE9-specific
   content<![endif]-->" 為例，這個方法將會收到 "'[if IE
   9]>IE9-specific content<![endif]'"。

HTMLParser.handle_decl(decl)

   呼叫此方法來處理 HTML 文件類型聲明 (doctype declaration)（例如
   "<!DOCTYPE html>"）。

   *decl* 參數將是 "<!...>" 標記內聲明部分的全部內容（例如 "'DOCTYPE
   html'"）。

HTMLParser.handle_pi(data)

   遇到處理指示 (processing instruction) 時會呼叫的方法。 *data* 參數
   將包含整個處理指示。例如，對於處理指示 "<?proc color='red'>"，這個
   方法將以  "handle_pi("proc color='red'")" 形式被呼叫。它旨在被衍生
   類別覆蓋；基底類別實作中什麼都不做。

   備註:

     "HTMLParser" 類別使用 SGML 語法規則來處理指示。使用有 "?" 跟隨在
     後面的 XHTML 處理指示將導致 "?" 被包含在 *data* 中。

HTMLParser.unknown_decl(data)

   當剖析器讀取無法識別的聲明時會呼叫此方法。

   *data* 參數將是 "<![...]>" 標記內聲明的全部內容。有時被衍生類別被覆
   蓋會是好用的。在基底類別實作中什麼都不做。


範例
====

以下類別實作了一個剖析器，將用於解說更多範例：

   from html.parser import HTMLParser
   from html.entities 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 = chr(name2codepoint[name])
           print("Named ent:", c)

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

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

   parser = MyHTMLParser()

剖析文件類型：

   >>> 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]

剖析附名 (named) 且為數值的 (numeric) 字元參照，並將它們轉換為正確的字
元（注意：這 3 個參照都等同於 "'>'"）：

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

將不完整的區塊提供給 "feed()" 是可行的，但是 "handle_data()" 可能會被
多次呼叫（除非 *convert_charrefs* 設定為 "True"）：

   >>> 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
