Gentleちゃれんじ


lxmlでhtmlを処理する

Pythonでhtmlを取り扱う際は、「htmllib(標準モジュール)」や 「Beautiful Soup」 といったモジュールがあります。 しかし、高速で柔軟な操作がしたい場合は、 「lxml」がいいということなので、 今回はlxmlでhtmlを操作する方法をメモしたいと思います。

lxmlは、Beautiful Soupより高速で、htmllibより柔軟なhtml(xml)操作を可能にするのですが、 日本語資料が少ないと言うのが難点でした。最低限のことならば、ちょっと英語を読めば、 何とかなりますが、ちょっと凝ったことをしようと思うと英語力がネックでつまづいてしまいました…。 そこで、今回は、自分がつまづいた所を中心に紹介したいと思います。

目次

  1. htmlから情報を抽出する
  2. htmlソースを改変する
  3. まとめと補足

1. htmlから情報を抽出する

htmlを操作する時に、一番多い要求は、html中の一部分の情報を抽出するということでしょう。 というわけで、まずは、htmlからどうやって情報を抽出するかを書いていきます。

>>> import urllib2
>>> import lxml.html
>>> html = urllib2.urlopen('http://www.cafe-gentle.jp/').read() # html 取得
>>> root = lxml.html.fromstring(html)
or
>>> from lxml import etree
>>> root = etree.fromstring(html, etree.HTMLParser()) 

とりあえず、最低限絶対しないといけない所からです。 lxmlには、htmlを扱う際に2つの方法があります。 上にあるような「lxml.html.fromstring()」を使う方法と、 「lxml.etree.fromstring()」を使う方法です。一長一短ありますが、 自分は、「lxml.html.fromstring()」をお勧めします。 というのも、lxmk.htmlの方がhtmlを操作する上で便利なメソッドがあるからです。

上の例のrootには、HTMLElementクラスのインスタンスが生成されます。 rootが作れれば後は楽です。ページ中のアンカー(a要素)を取得したい場合は、 こうします。

>>> anchors = root.xpath('//a')
>>> for anchor in anchors:
...     print anchor.text
...

        
Top
About
Diary
Gallery
Story
 | (中略
ページ先頭に戻る

XPath を知らない人がいると説明しづらいですが、 htmlから情報収集したいならXPathを覚えておくことをお勧めします。 この例では、 「//a : // = root(htmlタグ)以下の入れ子上の、 a = a要素全て」 とい解釈します。 例えば、全てのp要素をとりたいならば、「//p」になります。

xpathメソッドは見つかった要素のHTMLElementクラスのリストを返してくれます。

>>> anchors[1].text # 要素の中のテキストの取得
'Top'
>>> anchors[1].attrib # 要素が持つ属性のディクショナリの取得
{'href': './index.html'}
>>> anchors[1].attrib['href'] # リンク先URL(href)の値の取得
'./index.html'
>>> anchors[1].tag # 要素名の取得
'a'

HTMLから情報収集をする時は、基本的には、 xpath → (text, attrib[key], tag)を知ってれば大体出来ます。 ただ、textはタグに含まれる入れ子構造を除くテキスト部分しか 取り出せません。そこで、text_contentメソッドを使います。

>>> p = lxml.html.fromstring(u'<p>はじめ<strong>注意</strong>おわり</p>')
>>> print p.text
はじめ
>>> print p.text_content()
はじめ注意おわり

text_contentメソッドは、「lxml.html」でしか使えない(と思う)ので、 「lxml.html」の方がhtmlを扱う上では楽になると思います。 以上でhtmlからの情報抽出は終わりです。

2. htmlソースを改変する

あまり需要はありませんが、htmlをプログラムで改変したり、修正したり、 ってことをしたい人は少なからずいます。(自分もその部類です。) しかし、需要が無い為に(かつpythonの総人口が少ない為に)htmlの改変でつまづくと、 なかなかどうすればいいのか分かりません。 というわけで、ここではhtmlの改変方法についてさらっと紹介します。

追加

それでは、まず、追加をしてみましょう。

>>> # 要素を文字から作る場合、fromstringメソッドを使う
>>> div = lxml.html.fromstring('<div></div>')
>>> print lxml.html.tostring(div)
<div></div>
>>> # もうすこし楽に要素を作りたい場合Elementを使う
>>> p = lxml.html.Element('p')
>>> print lxml.html.tostring(p)
<p></p>
>>> div.append(p) # 要素を子に追加する場合はappendメソッドを使う
>>> print lxml.html.tostring(div)
<div><p></p></div>
>>> div.addnext(p) # 要素をその要素の後ろに追加する場合は、addnextメソッド
>>> print lxml.html.tostring(div.getparent())
<body><div></div><p></p></body>
>>> # 一気に追加したい場合は、extendメソッドを使う
>>> p1 = etree.Element('p')
>>> p2 = etree.Element('p')
>>> div.extend([p1, p2])
>>> print lxml.html.tostring(div)
<div><p></p><p></p></div>
>>> div.set('class', 'sample') # 属性の追加にはsetメソッド
>>> print lxml.html.tostring(div)
<div class="sample"><p></p><p></p></div>

「lxml.html.tostring()」は、fromstringの逆でHTMLElementクラスから、 HTMLソースの文字列を返してくれる関数です。 ここでは、指定していませんが、 日本語等のマルチバイト文字が含まれている場合、 「lxml.html.tostring(div, encoding="文字コード")」としないと、エンコードエラー が起こります。また、「include_meta_content_type=True」も指定しておかないと、 meta要素を省略した形のHTMLソースが出力されてしまうので注意してください。

追加は、要素の追加には、append、addprevious、addnext、 属性の追加には、setを使うと覚えていれば問題無く追加できると思います。 注意すべきなのは、「appendメソッドは指定した、HTMLElementクラスを子に 追加しますが、extendメソッドに、HTMLElementクラスを引数にした場合、 引数にしたHTMLElementクラスの子の要素全てがappendされる」 と言うことです。

>>> # divの子要素としてpを追加する
>>> div.append(p)
>>> # divの子要素としてpを追加する([]で囲んでいるのに注意)
>>> div.extend([p])
>>> # divの子要素としてpの子要素を追加する
>>> div.extend(p)

上の例のように、extendメソッドには、HTMLElementクラスが格納されたリストを引数にするか、 HTMLElementクラスを引数にするかで大きくやっていることが変わるので注意してください。

>>> # テキストを要素中に挿入したい場合は、単純に代入する
>>> div.text = 'aaa'
>>> print lxml.html.tostring(div)
<div class="sample">aaa<p></p><p></p></div>
>>> # 指定した要素の後ろに挿入したい場合は、tailに代入する
>>> div.tail = 'bbb'
>>> print lxml.html.tostring(div)
<div class="sample">aaa<p></p><p></p></div>bbb

後、テキストの追加には、textかtailに直接代入するのが楽です。 これで、追加についての説明は終わりです。

消去

次に消去を見ていきましょう。

>>> div.drop_tree()
>>> print lxml.html.tostring(div)
<div class="sample">aaa</div>bbb
>>> del div # そのタグを消してしまう。

消去は、drop_treeメソッドや、del文を使います。 delは要素を完全に消してしまいます。 drop_treeメソッドは、その要素のタグ以下を消してます。 (今回の場合、p要素以下が2つ消えてしまいました。) 他に、drop_tagメソッドがあり、これは、タグだけを消して、 子要素は消さないというものです。
注意すべきなのは、「消去は全てのデータに影響します。」 要するに、Pythonの配列と同じような扱い(参照渡し)ということです。rootからxpathで、 HTMLElementを返してもらいますが、それは参照渡しなため、divを消してしまうと、 rootの中のdivに当たる部分も消えてしまうというわけです。 追加の所で出た、例を見ても分かるんですが、ある要素を他の部分にappendすると、 引数に指定した要素はコピーされず、移動してしまいます。もし、これが嫌な場合、 copyモジュールを使って、コピーしたHTMLElementクラスをappendしてください。

テキストを消したい場合は、textに空文字を代入してやれば消えます。 属性を消したい場合は、attribで呼び出したものをdelで消してやります。

>>> div.text = ''
>>> print lxml.html.tostring(div)
<div class="sample"></div>bbb
>>> del div.attrib['class']
>>> print lxml.html.tostring(div)
<div></div>bbb

この辺りは、ディクショナリや、文字列を扱うのと全く同じですね。 以上で消去の説明を終わります。

置換

最後に応答として置換のやり方を紹介します。
>>> p = lxml.html.fromstring('<p class="classA">aaa</p>')
>>> root = p.getroottree()
>>> print lxml.html.tostring(root)
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 (ry
<html><body><p class="classA">aaa</p></body></html>

こういったhtmlソースがあるとします。(getroottreeメソッドは、要素のルート(一番上の親要素)を返します。) このソースのpタグのclassを「classA」から「classB」にしたい場合は、どうしたらいいでしょうか? こういう時に、XPathの力が発揮されます。

>>> # class="classA"となっているpタグを取得
>>> p_classA_list = root.xpath('//p[@class="classA"]')
>>> # setメソッドでclassの値をclassBに上書き
>>> for p_classA in p_classA_list:
...     p_classA.set('class', 'classB')
...
>>> print lxml.html.tostring(root)
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 (ry
<html><body><p class="classB">aaa</p></body></html>

結果を見るとpのclassの値がclassBに置き換わっています。 これは、classの値がclassAとなっているpタグが複数あると全て置き換えられます。

XPathで「要素名[@属性名="値"] とすると、指定した属性名が指定した値を持つ要素」を取り出します。 例えば、「//a[@href="."]」ならば、href属性の値が「.」であるa要素全てになります。 「//a[@href]」ならば、href属性を持つa要素全てと言った風になります。 XPathが分かれば、後は追加と消去で自由な操作が可能になります。

まとめと補足

今回の例を、ちょっと改変し、XPathをちょこっと勉強すればHTMLをlxmlで処理することは、 そこまで難しくなくなると思います。ここまで、調べるのが非常に面倒だったので…。 このページを見た人の役に立てたらいいなぁと思います。

lxmlに限らず、言語処理をプログラムで行おうとするとつきまとう問題に 「文字コードの問題」があります。 lxmlは、fromstringの際に、decodeされたユニコード文字列を、 tostringの際に、encoding="文字コード"を指定してやれば大抵文字化けしませんが、 依存文字が入ってしまうと、encodingが出来ない為に、tostringの時にエンコードエラーになってしまいます。 これを改善する方法で自分が知っているのは、依存文字を予め他の文字で置き換えておく方法だけです。 HTMLはとりわけ、文字コードがカオスなことになっているので、処理する際は、 文字コードに注意していてください。 Tipsでも文字コードについていつかは取り上げたいと思います。

(追記):lxmlと日本語の文字コード周りについての補足を追加しました。

それでは、今回のTipsはこれまで!

ページのトップへ戻る