When it’s ready.

出来るまで出来ない

Pythonでスクレイピングに最適なライブラリはlxmlな気がした。時間的な意味で

ここ数日でHTMLからTagを除去する方法を、色々知った。とても勉強になりました。教えてくれた人ありがとうです。

具体的には、BeautifulSoupとHTMLParserとlxmlという3つのライブラリでそれぞれTag除去が可能な事が分かった。実際どれも満足な挙動で、じゃあどれを使えばいいのさ!と、迷ったので実行速度を適当に測ってみた。

時間を計るところのコードが激しく恥ずかしい。ホントは、3つのファンクションを配列に入れて、forで回したかったけど、配列に入れる時に評価されてしまってNG、map関数で、関数と関数(計測したい関数と、計測する関数)を2つ渡すやり方がわかんなかったので、同じ事を3回書く事にした。マジ恥ずかしいがこれしか思いつかなかった。

計測用のHTMLには、はてダのトップページとした、コメント、Style、Script、htmlがそこそこのボリュームで入っていた為

計測環境

  • python 2.6
  • HTMLParser python付属
  • BeautifulSoup 3.0.7a
  • lxml 2.1.2
  • MBP 2GHz 2GB
  • OSX 10.5.5
# coding:utf-8
from urllib import urlopen
from BeautifulSoup import BeautifulSoup
from lxml.html import fromstring
from HTMLParser import HTMLParser
from timeit import Timer
from time import time

class TagStrip(HTMLParser):
  # id:aodag兄さん提供
  def __init__(self):
    HTMLParser.__init__(self)
    self.datum = []
    self.instyle = False
  def handle_data(self, data):
    if data.strip() and not self.instyle:
      self.datum.append(data)
  def getString(self):
    return "".join(self.datum)
  def handle_starttag(self, tag, attrs):
    if tag == 'style' or tag == 'script':
      self.instyle = True
  def handle_endtag(self, tag):
    if tag == 'style' or tag == 'script':
      self.instyle = False

def getHtml(url):
  return urlopen(url).read()

def useBS(html):
  # id:y_yanbe さん提供モノ
  # http://python.g.hatena.ne.jp/y_yanbe/20081025/1224910392
  soup = BeautifulSoup(html)
  text = '\n'.join([e.string for e in soup.findAll() 
    if e.string!=None and e.name not in ('script','style')])
  return text

def useLXML(html):
  # id:Alexandre さん提供モノ
  # http://d.hatena.ne.jp/a2c/20081025/1224924646#c1225076104
  et = fromstring(html)
  xpath = r'//text()[name(..)!="script"][name(..)!="style"]'
  text = ''.join([text for text in et.xpath(xpath) if text.strip()])
  return text

def useHP(html):
    p = TagStrip()
    p.feed(html)
    return  p.getString()

if __name__ == '__main__':
  url = 'http://www.hatena.ne.jp/'
  repeatCnt = 30
  htmlSource = getHtml(url)
  tmpDelta, timeList = [],{}

  print 'BS start!'
  for i in range(repeatCnt):
    start = time()
    useBS(htmlSource)
    tmpDelta.append( time() - start)
  timeList['BS'] = sum(tmpDelta)/repeatCnt

  print 'LXML start!'
  tmpDelta = []
  for i in range(repeatCnt):
    start = time()
    useLXML(htmlSource)
    tmpDelta.append( time() - start)
  timeList['LXML'] = sum(tmpDelta)/repeatCnt

  print 'HP start!'
  tmpDelta = []
  for i in range(repeatCnt):
    start = time()
    useHP(htmlSource)
    tmpDelta.append( time() - start)
  timeList['HP'] = sum(tmpDelta)/repeatCnt

  print timeList

とりあえづ、30回くらいの平均でやってみた。何回かやったけど、それほどばらつき無く同じような結果が出たので、信じる事にした。以下結果(改行たしてあります)

{
  'BF':   0.58448076248168945,
  'LXML': 0.01511224110921224,
  'HP':   0.03491028149922689
}

PySpaで作ってた時にBSつかってて、何となく遅いなぁって思ってたけど、ひょっとしたら、BSが引っかかってたのかも知れないと思った。100個くらいのHTMLからTag除去したら1分(BS)と1秒(lxml)くらいの差が出来ると思うと、lxmlしか選択肢は無いなぁと思った。HTMLParseもまぁまぁ速いけど、クラスをオーバライドしなくちゃscriptとかstyleに対応できないのがめんどくさい。

それぞれ適材適所が有るかと思いますが、大量のhtmlファイルからTagを除去するには、lxmlが向いていると思いました。

id:aodag兄さん、id:y_yanbeさん、id:Alexandreさん 教えてくれて有り難うございました。大変勉強になりました。


宿題として、華麗にTimerを使いこなせるようになる事が追加されました。

追記

タグを除去したテキストファイルがなにげに各ライブラリで差があるなぁと感じた。テキストをまんま載せるのは丸ごと引用になりそうだったので、載せるのを躊躇った。代わりにwcの結果

% cat lxml_log.txt|wc
      35     168    7586
% cat BeautifulSoup_log.txt|wc
      24     121    6464
% cat HTMLParce_log.txt|wc
      34     166    7582

短かけりゃ or 長けりゃ 良いってモンでもないがタグを完全に除去してル事を考えたら、誤判定除去が少ない方が長いので、ここでもlxmlが優秀なきがした。BSだと、nbsp とかがそのまま出てしまっている。それでこれだけ短いという事は他になにかが消えてるって事か。