Gentleちゃれんじ


Python(lxml)でhtmlを処理する まとめ

Pythonにはxml/htmlを取り扱うためのlxmlという便利なサードパーティモジュールがあります。 ここでは、lxmlを使ってhtmlを処理する際に、使えるメソッドなどを簡単にまとめています。 (例がHTML5を使ったものなので気をつけてください。)
初歩的なものを細かくまとめたものは、 「lxmlでhtmlを処理する」にあります。 また、 lxml にある全てのメソッドを紹介しているわけではありませんのでご注意を。

1. htmlの読み込み

>>> dom = lxml.html.fromstring(html)
>>> dom
<Element html at 1967ed8>
>>> dom2 = lxml.html.parse('python_tips_003.html')
>>> dom2
<lxml.etree._ElementTree object at 0x1981530>

htmlの読み込みは主に fromstring、 parse メソッドを使います。 fromstring は、文字列から HtmlElement 型で、 parse はファイルオブジェクト等から _ElementTree 型でDOMツリーのルートを返します。 自分は、htmlを扱う場合は、HtmlElement 型の方が便利なメソッドを使えるので、 fromstring をおすすめします。

特定のタグの取得

body、headタグ

>>> dom.body
<Element body at 1c24998>

body タグ以下の内容だけを見たい場合は、html タグにあたる HtmlElement から body だけ指定すれば取り出せます(headタグの場合も同様)。

子要素

>>> list(dom)
[<Element head at 1c24dc0>, <Element body at 1c24df8>]
>>> dom[0]
<Element head at 1c24dc0>

子要素は HtmlElement に list 関数を使うことで取り出せます。 getchildren メソッドもありますが非推奨なので使わないで下さい。
配列のように、インデックス i を指定してやると i 番目の子要素が返されます。 (インデックスの範囲を超えると IndexError を返します。)

親要素

>>> dom.body.getparent()
<Element html at 1c24d50>

親要素は getparent メソッドで取り出せます。 ルートに getparent メソッドを使うと None が返ります。

前後の要素

>>> list(dom.body)
[<Element header at 1c24e68>, <Element article at 1c24dc0>, <Element footer at 1c24fb8>, ...]
>>> dom.body[1].getprevious()
<Element header at 1c24e68>
>>> dom.body[1].getnext()
<Element footer at 1c24fb8>

getnext メソッドで次の following sibling にあたる要素が、 getprevious メソッドで前の preceding sibling にあたる要素が返ります。

ルート

>>> dom.body.getroottree()
<lxml.etree._ElementTree object at 0x1c00af8>
>>> dom.body.xpath('/*')
[<Element html at 1c24d50>]

ルートを得るには、 getroottree メソッドを使うんですが、 HtmlElement の html タグが返ってこないので、 xpath メソッドでルートを返すようにした方が混乱がなくていいとおもいます。

指定した名前のタグ

>>> dom.find('body/article/section')
<Element section at 1c24ed8>
>>> dom.findall('body/article/section')
[<Element section at 1c24ed8>, <Element section at 1c24e30>, <Element section at 1c24f48>]

読み込んだ html のツリー構造がわかっていれば、 find メソッドを使って指定したパスの要素を取り出すことが出来ます。 find メソッドでは、条件を満たす最初のノードのみを返しますが、 findall メソッドだと条件を満たす全てのノードを返してくれます。

指定したIDのタグ

>>> dom.get_element_by_id('whats_new')
<Element section at 1a05d18>

欲しい要素の id が分かっているのであれば、 get_element_by_id メソッドを使います。 メソッド名が長いのが玉に瑕です。

あらゆるタグ

>>> dom.xpath('//section')
[<Element section at 1c24ed8>, <Element section at 1c24dc0>, <Element section at 1c249d0>]
>>> dom.cssselect('section')
[<Element section at 1c24ed8>, <Element section at 1c24dc0>, <Element section at 1c249d0>]

xpath 式が分かる方は、 xpath メソッドを使うのが楽です。 css セレクタに馴染みが深い方は、 cssselect メソッドがあります。 xpath 式および css セレクタを記述すれば、対応するタグを取り出せます。 xpath についての、その他の利用方法は 「関数を利用したXPath式」で触れています。

特定の属性、値、テキストの取得

要素の名前

>>> input = dom.xpath('//input')[0]
>>> input.tag
'input'

要素の名前は、 tag で取り出します。 getnext メソッドで得られた要素等、タグの名前が分からない時に使います。

属性と属性の値

>>> input = dom.xpath('//input')[0]
>>> input.attrib
{'size': '20', 'type': 'text', 'name': 'name', 'value': '', 'id': 'name'}
>>> input.attrib['size']
'20'
>>> input.get('size')
'20'

ある要素(HtmlElement)の属性を得たい場合は、 attrib か、 get メソッドを使います。 attrib は、ある要素の全ての属性を取り出したいときに便利です。 get メソッドは、特定の属性を取り出したいときに便利です。 get メソッドは指定した属性がなかった場合、デフォルトで None を返します。 (dom.get('size', default='0')で変更可能)

テキストノード

>>> p = dom.xpath('//p')[1]
>>> print p.text
Diaryは不定期更新のため省略しています。

ある要素(HtmlElement)のテキストノードを得たい場合は、 text を使って下さい。 (日本語で描かれたページなら Unicode 文字列が返されます。)
ここで、注意して欲しいのは、 「ある要素以下に含まれる全てのテキスト」ではなく、 「ある要素の直接の子にあたるテキスト」だということです。 (<div><p>hoge</p></div> は、 p.text == hoge ですが、 div.text != hoge (is None)です。)

ある要素以下のテキスト全て

>>> section = dom.xpath('//section')[0]
>>> print section.text_content()
What’s New?
      Diaryは不定期更新のため省略しています。
      2010-01-16
        
          /Gallery/Original/Gentleメンバー/
          『兼田將太瓏・ばにモ 2010』を追加
        ...
>>> type(section.text_content())
<type 'lxml.etree._ElementUnicodeResult'>
>>> lxml.html.tostring(section, method='text', encoding='utf-8')
"What’s New?\n      Diary\xe3\x81\xaf\xe4\xb8
    ..."

ある要素以下のテキスト全てを取り出したい場合は、 text_content メソッドを使います。 注意しないといけないのは、単に print するだけならば、 何の問題も無いのですが、type が str や unicode 文字列ではなく、 lxml.etree._ElementUnicodeResult なので、 文字列だと思って他の関数に渡すとエラーが発生します。
純粋に文字列型で結果が欲しいのであれば lxml.html.tostring 関数を使う必要があります。 tostring はデフォルトでは html をそのまま返すのでテキストだけ欲しい場合は、 method で text と指定し、日本語であればエンコードするために encoding で文字コードを指定する必要があります。

タグの後ろにあるテキスト

>>> a = dom.xpath('//a')[10]
>>> print a.tail
を追加

XMLと違い、HTMLでは、「<b>text</b>tail」のように、 タグの後ろにもテキストが来る場合があります。 このタグの後ろにあるテキストを取り出したい場合は tail を使います。 (tail はありますが、タグの前を取り出すものはおそらくありません。)

html の編集

要素の作成

>>> new_tag = lxml.html.Element('p', {'id': 'new'})
>>> lxml.html.tostring(new_tag)
'<p id="new"></p>'

新しく要素を作るには、 lxml.html.Element 関数を使います。 作りたいタグの名前の第一引数に、 もし、属性も一緒に指定したいのであれば第二引数として 属性と値を組にしたディクショナリを入力してやれば、 属性も付加された要素が返されます。

タグ名の変更

>>> lxml.html.tostring(new_tag)
'<p id="new"></p>'
>>> new_tag.tag = 'div'
>>> lxml.html.tostring(new_tag)
'<div id="new"></div>'

タグを名前だけ置き変えたい場合(そんな場合があるかは不明ですが)は、 tag に直接変更後の値を入れてやれば変更出来ます。

属性の値の変更・追加

>>> new_tag.attrib['id']
'new'
>>> new_tag.attrib['id'] = 'old'
>>> new_tag.attrib['id']
'old'
>>> new_tag.set('id', 'new')
>>> new_tag.attrib['id']
'new'

属性の値の変更には、二種類あります。 一つは、 attrib にアクセスし直接値を変える方法、 二つ目は、 get と対になるメソッド set を使う方法です。 この二つは、今ある属性だけでなく新しい属性であっても使うことができるので、 この二つの方法で属性の追加も可能です。

テキストの変更・追加

>>> lxml.html.tostring(new_tag)
'<p id="new"></p>'
>>> new_tag.text = 'hoge'
>>> lxml.html.tostring(new_tag)
'<p id="new">hoge</p>'

ある要素にテキストノードを追加したい場合は、 text にそのまま値を入れてください。 None を代入すれば消去も出来ますし、 既に存在するの所に他の値を代入すれば置換も出来ます。

子要素の追加

>>> b = lxml.html.Element('b')
>>> new_tag.append(b)
>>> lxml.html.tostring(new_tag)
'<p id="new"><b></b></p>'
>>> b2 = lxml.html.Element('b')
>>> b3 = lxml.html.Element('b')
>>> new_tag.extend([b2, b3])
>>> lxml.html.tostring(new_tag)
'<p id="new"><b></b><b></b><b></b></p>'

ある要素に子要素を追加したい場合は、 リストと同じように append、 extend を使います。 append は一つの要素、 extend は複数の要素を子要素として追加出来ます。 子要素を得る方法は list() 関数だったのでわかりやすいですね。 注意しなければいけないのは、 extend にリスト型ではなく HtmlElement型の変数を引数にした場合、引数にした要素の子要素全てが追加されます。 これは、extend がリスト型を引数にとるので、 HtmlElement 型の要素に list() 関数をしようしてから代入するためと思われます。

タグの消去

>>> div = lxml.html.Element('div')
>>> p = lxml.html.Element('p')
>>> p.text = 'hoge'
>>> div.append(p)
>>> lxml.html.tostring(div)
'<div><p>hoge</p></div>'
>>> p.drop_tag()
>>> lxml.html.tostring(div)
'<div>hoge</div>'

ある要素の子要素やテキストノードを残して、 タグだけ削除したい場合は、drop_tag メソッドを使います。 drop_tag メソッドは、親要素がある要素でしか使えません。

要素全体の消去

>>> div = lxml.html.Element('div')
>>> p = lxml.html.Element('p')
>>> p.text = 'hoge'
>>> div.append(p)
>>> lxml.html.tostring(div)
'<div><p>hoge</p></div>'
>>> p.drop_tree()
>>> lxml.html.tostring(div)
'<div></div>'

ある要素の子要素やテキストノードを含めて削除したい場合は、 drop_tree メソッドを使います。 drop_tree をしてしまうと、その要素以下の全ての要素は完全に消えてしまうので、 誤って使わないように注意して下さい。

>>> dom.xpath('//@href')
['css/home.css', 'material/favicon.ico', 'index.html', 'index.html', 'about.html', ...]
>>> dom.make_links_absolute('http://www.cafe-gentle.jp')
>>> dom.xpath('//@href')
['http://www.cafe-gentle.jp/css/home.css', 'http://www.cafe-gentle.jp/material/favicon.ico',
 'http://www.cafe-gentle.jp/index.html', 'http://www.cafe-gentle.jp/index.html',
 'http://www.cafe-gentle.jp/about.html', ...]

不特定多数のウェブページを解析やローカル保存する時は、 相対パスを絶対パスにしておいた方が良いこともあります。 そんな時は、 make_link_absolute メソッドを使って下さい。 ベースとなる URL を引数にしただけで、相対パスをベース URL を 基にした絶対パスに置き換えてくれます。

htmlの書き出し

>>> lxml.html.tostring(dom)

htmlの書き出しは tostring を使います。 tostring は、いろいろと複雑なので説明します。 第一引数には、書き出したい HtmlElement 型の要素を入れてください。 第一引数だけだと、「meta 要素で Content-Type が記述されたタグ」が除かれた、 「html ソース」が返され、もし、ASCII 以外の文字があった場合は、 文字参照で置き換えられます。

  • ASCII 以外の文字を置き換えたくない場合(日本語などを扱いたい場合)は、 encoding で文字コードを指定して下さい (lxml.html.tostring(dom, encoding="utf-8"))。
  • html を少しでも整形して出力して欲しい場合は、 pretty_print を True にして下さい。
  • html ではなく、中に含まれるテキストのみ、 もしくは xml として出力したい場合には、 method に text または、 xml として下さい。
  • 「meta 要素で Content-Type が記述されたタグ」を除きたくない場合は、 include_meta_content_type を True にして下さい。

tostring に関しては help() 関数で、マニュアル読んだ方が速いです。

まとめ

ここでは、Python(lxml) で html 取り扱う際の便利なメソッドや、 その使い方について簡単にまとめました。 Python はすごく素敵でパワフルな言語ですが、 日本語の解説サイトが少ないのが難です(lxmlについては特にそうです)。
ここに書かれた情報が少しでも皆さんの役に立てば幸いです。