TorasenLab@はてな このページをアンテナに追加 RSSフィード

2011-04-02

不正な(malformed)HTMLテキストを修正する

| 00:36 | 不正な(malformed)HTMLテキストを修正するを含むブックマーク 不正な(malformed)HTMLテキストを修正するのブックマークコメント

Web上のHTMLテキストには不正な(malformed)形式を持っている場合があります。

malformedなHTMLテキストにに対してはBeautifulSoupやHTMLParserなどがうまく機能しません。

例えば以下の3つのパターンはBeautifulSoupでは例外が生じます。

1. タグの閉じ>が無い

<div class="invalid_start_tag" 
  <a src="http://...">something</a>
</div>

2. 閉じタグに属性がある

</div class="invalid_end_tag">

3. scriptタグ中に閉じタグがある

<script language="javascript"><!--
  function splitTag() { return '</scr' + 'ipt>'; }
  //-->
</script>

Pythonの勉強ついでにパターン1,2を修正する関数formalize_htmlを作成しました。

パターン3については修正できていません。ただしHTMLテキストからのテキスト抽出などが目的であれば、scriptタグやコメントタグごと削除すれば問題は無いと思います。

勉強のために、doctestというものを試しに使用しています。

import re
from sgmllib import SGMLParser


_RE_SCRIPT = re.compile(r'<script.*?>.*?</script>',
                       re.MULTILINE | re.DOTALL | re.IGNORECASE)
def _remove_script_tags(htmltext):
    return _RE_SCRIPT.sub('', htmltext)


_RE_COMMENT = re.compile(r'<!--.*?-->', re.MULTILINE | re.DOTALL)
def _remove_comment_tags(htmltext):
    return _RE_COMMENT.sub('', htmltext)


_RE_STYLE = re.compile(r'<style.*?>.*?</style>',
                      re.MULTILINE | re.DOTALL | re.IGNORECASE)
def _remove_style_tags(htmltext):
    return _RE_STYLE.sub('', htmltext)


def _escape_htmltext(htmltext):
    escape_patterns = (('<', '&lt;'), ('>', '&gt;'),
                       ('\'', '&#39;'), ('"', '&quot;'))
    escaped_text = htmltext
    for from_text, to_text in escape_patterns:
        escaped_text = escaped_text.replace(from_text, to_text)
    return escaped_text


class _HTMLFormalizer(SGMLParser):
    def __init__(self):
        self._data = []
        SGMLParser.__init__(self)
    def unknown_starttag(self, tag, attributes):
        self._data.append('<{0}'.format(tag))
        for key, value in attributes:
            self._data.append(' {0}="{1}"'.format(key, _escape_htmltext(value)))
        self._data.append('>')
    def unknown_endtag(self, tag):
        tag = tag.split()[0].lower()
        self._data.append('</{0}>'.format(tag))
    def handle_data(self, data):
        self._data.append(_escape_htmltext(data))
    def append_data(self, data):
        self._data.append(data)
    def get_data(self):
        return self._data


def formalize_html(htmltext, 
                   keep_newlines=True,
                   remove_scripts=True,
                   remove_comments=True,
                   remove_styles=True):
    '''
    >>> malformed_htmltext = """
    ... <html>
    ...   <meta>
    ...   <head>
    ...     <title>TITLE</title>
    ...     <script language="javascript"><!--
    ...       function splitTag() { return '</scr' + 'ipt>'; }
    ...       function max(a, b) { return a > b ? a : b; }
    ...       //-->
    ...     </script>
    ...   </head>
    ...   <body class="invalid_start_tag"
    ...       id="id_invalid"
    ...     <img src="http://...">
    ...     <div>
    ...       a > b ? a : b;
    ...     </div class="invalid_end_tag">
    ...   </body>
    ... </html>"""
    >>> print formalize_html(malformed_htmltext)
    <BLANKLINE>
    <html>
      <meta>
      <head>
        <title>TITLE</title>
    <BLANKLINE>
      </head>
    <BLANKLINE>
    <BLANKLINE>
    <body class="invalid_start_tag" id="id_invalid"><img src="http://...">
        <div>
          a &gt; b ? a : b;
        </div>
      </body>
    </html>
    <BLANKLINE>
    '''

    text = htmltext
    if remove_scripts:
        text = _remove_script_tags(text)
    if remove_comments:
        text = _remove_comment_tags(text)
    if remove_styles:
        text = _remove_style_tags(text)
    formalizer = _HTMLFormalizer()
    for line in text.splitlines():
        formalizer.feed(line)
        if keep_newlines:
            formalizer.append_data('\n')
    formalizer.close()
    return ''.join(formalizer.get_data())


def _test():
    import doctest
    doctest.testmod()
    

if __name__ == '__main__':
    _test()
トラックバック - http://d.hatena.ne.jp/torasenriwohashiru/20110402/1301672215