python, Beautifulsoupでサイトの構造変化に強いscraping
よくWEBから情報抽出するのですがHTMLをパースする時にはpython のBeautifulsoupライブラリ(BS4)を利用しています。 BS4は公式の解説がわかりやすいですし、有名ライブラリなので日本語記事もたくさん見つかります。
BS4ではfind('div', id="main")
などとして簡単にhtml上の必要なDOM情報にアクセスでき、簡単な処理ならこれで十分ですがもう少し複雑にscraipingしたいことがあります。
例えばHタグの後ろにあるテキスト情報のみを取りたい。特定のdiv要素の子情報を取りたい。その際DOMtreeの木構造は保持したい。などです。
そのような時にはBS4のnext_element
(html上でタグ開始位置を基準に現在見ているタグの次のタグを見つける)parent, children, descendants
(現在見ているタグの親・子・子孫要素を返す。) などが便利です。全て豊富な例と一緒に公式ドキュメントに説明があります。
便利と感じている書き方
私の場合、next_element
などを使ってHTML内をさまようと読みにくいコードになる傾向にあったのですが、最近以下のような書き方をすることで改善されたような気がしています。
html_doc = """ <html><head><title>The Dormouse's story</title></head> <body> <h1> Title</h1> <div id="main"> <p class="title"><b>The Dormouse's story</b></p> <p class="story">Once upon a time there were three little sisters; and their names were <h3>names</h3> <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>, <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>; and they lived at the bottom of a well.</p> <h3>other names</h3> <a href="http://example.com/others" id="link4">AAA</a>; <a href="http://example.com/others2" id="link5">BBB</a>; <p class="story">...</p> </div> <div id="footer"> <p>aaa</p> </div> </body> """ from bs4 import BeautifulSoup from bs4.element import NavigableString, Tag def gen_next_element(process): def _next_element(current_element, status): next_element = current_element.next_element if isinstance(current_element, NavigableString): return next_element, status return process(current_element, status) return _next_element def read_page(text, status, function): soup = BeautifulSoup(text, 'html.parser') current_dom = soup.find("div", id="main") if not current_dom: return status current_dom = current_dom.extract() next_element = gen_next_element(function) while True: if not current_dom: break current_dom, status = next_element(current_dom, status) return status def extract_links(current_element, status): next_element = current_element.next_element if current_element.name == "a": href = current_element.get("href") status["links"].append((current_element.text, href, status["previous_header"])) elif current_element.name == "h3": status["previous_header"] = current_element.text return next_element, status if __name__ == '__main__': status = read_page(html_doc, {'links' : [], "previous_header":""}, extract_links) for url, text, title in status["links"]: print("url : {}, text : {}, title: {}".format(url, text, title))
この例ではHTML内のリンク情報を見出しとともに列挙しています。このコードにはgen_next_element, read_page, extract_linksと3つの関数がありますが、前半の2つは前から順にタグを見ていくという処理を表しています。extract_linksは具体的な処理を表す関数です。この関数は現在のタグをみて何か処理(今回はリンク収集)を行い情報を更新し、次のタグと更新された情報を返します。 このような構成にしてから巡回のことは意識せずに1つのタグについての取り扱いだけを考えるのが容易になった気がしています。 それでもやはり解りにくいコードになりやすいという感覚があるため、ScrapingのGood Practice があれば知りたいところです。