動機
wikiのテーブルにまとめられたデータを読み込んで、正規化したり、コードにを生成したりしたかった。
URLコピペとかが理想だが、解析対象のTableの特定など困難なので、「検証」タブなどからtable部分のHTMLをコピペして、それを解析することにした。
Python苦手なのであしからず。
tdにネストしたタグ
HTMLparserをそのまま使うと、タグごとに handle_data()
が呼ばれてちょっと不便なので、tdの中にネストしたタグがあったら全部くっつけて、tdが終わるタイミングで独自のコールバックを呼び出すことにした。
rowspan, colspan
HTMLのテーブルは、縦横につなげる事ができる。
今回の用途では、これらは同じ値が入っているものとして扱いたかったので、同じ値で埋めることにした。
そのために、解析中にrowspanやcolspanが1より大きいものがあれば、仮想的な欄を生成してバッファに保持していき、必要なタイミングで取り出してコールバックを呼ぶことで対応した。
成果物
※何も考えずにline, rowで作ったら、HTMLではrow, columnで、かなり混乱を生むコードになってしまった
# 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してアルファベット順に並べたデータを取得する例です。
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なコードは自動生成していきたい。