PythonでHTMLのテーブルを解析した


# 動機

wikiのテーブルにまとめられたデータを読み込んで、正規化したり、コードにを生成したりしたかった。
URLコピペとかが理想だが、解析対象のTableの特定など困難なので、「検証」タブなどからtable部分のHTMLをコピペして、それを解析することにした。

Python苦手なのであしからず。

# tdにネストしたタグ

HTMLparserをそのまま使うと、タグごとに `handle_data()` が呼ばれてちょっと不便なので、tdの中にネストしたタグがあったら全部くっつけて、tdが終わるタイミングで独自のコールバックを呼び出すことにした。

# rowspan, colspan

HTMLのテーブルは、縦横につなげる事ができる。
今回の用途では、これらは同じ値が入っているものとして扱いたかったので、同じ値で埋めることにした。

そのために、解析中にrowspanやcolspanが1より大きいものがあれば、仮想的な欄を生成してバッファに保持していき、必要なタイミングで取り出してコールバックを呼ぶことで対応した。

# 成果物

※何も考えずにline, rowで作ったら、HTMLではrow, columnで、かなり混乱を生むコードになってしまった

```python
#coding: utf-8
from html.parser import HTMLParser

class Error(Exception):
    pass

class HTMLTableParser(HTMLParser):
    """Class for getting data from HTML tableself. Please override `handle_td` to parse."""
    def __init__(self):
        super().__init__()
        self.is_inside_td = False
        self.line = 0
        self.row = 0
        self.buffer = ''
        self.current_rowspan = 1
        self.current_colspan = 1
        self.virtual_rows = []

    def before_handle_td(self):
        for i in range(0, self.current_rowspan):
            # Append virtual_rows except (0, 0)
            if i > 0:
                self.virtual_rows.append((self.line+i, self.row, self.buffer))
            for j in range(1, self.current_colspan):
                self.virtual_rows.append((self.line+i, self.row+j, self.buffer))
        self.current_rowspan = 1
        self.current_colspan = 1
        matchVirtualRow = next(filter(lambda x: x[0] == self.line and x[1] == self.row, self.virtual_rows), None)
        if matchVirtualRow is not None:
            self.handle_td(matchVirtualRow[0], matchVirtualRow[1], matchVirtualRow[2])
            self.virtual_rows.remove(matchVirtualRow)
            self.row += 1
            self.before_handle_td()

    def handle_td(self, line, row, buffer):
        """Please override this function to handle table cell data"""
        print(self.line, self.row, self.buffer)

    def handle_starttag(self, tag, attrs):
        if tag == 'td':
            if self.is_inside_td:
                print("[td] tag must not be nested.")
                raise Error()
            self.is_inside_td = True
            self.current_rowspan = next(map(lambda x: int(x[1]), filter(lambda x:x[0] == 'rowspan', attrs)), 1)
            self.current_colspan = next(map(lambda x: int(x[1]), filter(lambda x:x[0] == 'colspan', attrs)), 1)
        elif tag == 'br':
            if self.is_inside_td:
                self.buffer += '\n'
        elif tag == 'ul':
            if self.is_inside_td:
                self.buffer += '('

    def handle_endtag(self, tag):
        if tag == 'td':
            if not(self.is_inside_td):
                print("[td] tag must not be nested.")
                raise Error()
            self.before_handle_td()
            self.handle_td(self.line, self.row, self.buffer)
            self.is_inside_td = False
            self.buffer = ''
            self.row += 1
        elif tag == 'tr':
            # process trailing virtual rows
            for virtual_row in sorted(filter(lambda x: x[0] == self.line, self.virtual_rows), key=lambda x:x[1]):
                self.handle_td(virtual_row[0], virtual_row[1], virtual_row[2])
                self.virtual_rows.remove(virtual_row)
            self.line += 1
            self.row = 0
        elif tag == 'p':
            if self.is_inside_td:
                self.buffer += '\n'
        elif tag == 'li':
            if self.is_inside_td:
                self.buffer += ', '
        elif tag == 'ul':
            if self.is_inside_td:
                self.buffer += ')'

    def handle_data(self, data):
        if self.is_inside_td:
            self.buffer += data
```

# 使い方

継承して、`handle_td`を実装して、使うだけです！
以下は、第3カラムの内容を取得して、uniqしてアルファベット順に並べたデータを取得する例です。

```python
class Column2Parser(HTMLTableParser):
    def __init__(self):
        super().__init__()
        self.values = []

    def handle_td(self, line, row, buffer):
        if row == 2:
            self.values.append(buffer)
            
def valueList():
    parser = Column2Parser()
    with open('table.html') as f:
        parser.feed(f.read())
    values_uniq = sorted(set(parser.values), key=lambda v: v.upper())
    print(values_uniq)
    
valueList()
```

# 他の言語のメタプロ

Python、型がなくてツライけど、気軽にメタプロ（他の言語のコードを吐き出す）できるから楽しい。
KVOなコードは自動生成していきたい。