BSXPath.pyをいじっていて……
どうも、想像以上にパフォーマンスが悪いので、ボトルネックを調べていたのですが……。
ひとつには、Beautiful Soup にも原因がありそうだということが分かりました。
例えば、全ノード検索(soup.findAll())は結構早いにも関らず、属性指定検索(soup.findAll(attrs={'class':'slow'}))になると想像以上に時間がかかります(後者は前者の数倍からひどいときには十数倍時間がかかる)。
比較にかかる分を考慮しても、ちょっとかかりすぎな気が……。
で、調べていくうちに
Beautiful Soupでは、ノード(Tag)の属性への初回アクセス時に、node.attrs([(key1,val1),...,(keyN,valN)]のようなキーと値がペアになったtupleのリスト)をnode.attrMap({key1:val1,...,keyN:valN}のような辞書)に変換していることが分かりました。
■該当箇所
def _getAttrMap(self):
"""Initializes a map representation of this tag's attributes,
if not already initialized."""
if not getattr(self, 'attrMap'):
self.attrMap = {}
for (key, value) in self.attrs:
self.attrMap[key] = value
return self.attrMap
なので、初回アクセス時は遅く、2回目以降は速くなります。
従って、BSXPathのように、逐次属性を調べながら辿っていくような処理だと、かなりパフォーマンスに影響してしまいます。
対策
初期化直後に全ノードのattrMapを一括して作ってしまうことに。
soup=BeautifulSoup(html)
for node in soup.findAll(): node.attrMap=dict(node.attrs) # (key,val)のtupleリストなら、dict()で一括で辞書に変換可
初期化に多少時間を食うことにはなりますが、その後の処理が複雑であればあるほど、全体的なパフォーマンスは改善すると思います。
■サンプル
import sys
import datetime
from BeautifulSoup import *
MEASUREMENT=3
if __name__ == '__main__':
html=sys.stdin.read()
key='class'
val='comment-content'
for ci in range(1,1+MEASUREMENT):
print u'%d回目:' % (ci)
btime=datetime.datetime.now()
soup=BeautifulSoup(html)
nodes=soup.findAll(attrs={key:val})
atime=datetime.datetime.now()
stime=atime-btime
print u'original:%d.%06d sec. : <* %s="%s">=%d' % (stime.seconds,stime.microseconds,key,val,len(nodes))
btime=datetime.datetime.now()
soup=BeautifulSoup(html)
for node in soup.findAll(): node.attrMap=dict(node.attrs) # ◆ attrMapの初期化処理
nodes=soup.findAll(attrs={key:val})
atime=datetime.datetime.now()
stime=atime-btime
print u'patched :%d.%06d sec. : <* %s="%s">=%d' % (stime.seconds,stime.microseconds,key,val,len(nodes))
print u''
■結果
type test.html | python testbs.py
※test.htmlは自分のココログから適当にとってきたもの(202,986バイト)。
1回目: original:3.235000 sec. : <* class="comment-content">=108 patched :0.937000 sec. : <* class="comment-content">=108 2回目: original:3.203000 sec. : <* class="comment-content">=108 patched :0.954000 sec. : <* class="comment-content">=108 3回目: original:3.406000 sec. : <* class="comment-content">=108 patched :1.062000 sec. : <* class="comment-content">=108
patchedの方は、originalの約3倍強の速度(上記時間には、attrMapの初期化時間も含んでいます)。
attrMapの初期化にかかる時間を補ってあまりあるパフォーマンス向上だと思います*1。
*1:BSXPath.pyにも反映しました。かなり速くなったと思います(元が遅すぎたという説あり)