BeautifulSoupオブジェクトの生成
オリジナルドキュメントの文字コードを調べる
ドキュメントの整形
コンテンツの出力
オブジェクトを文字列に変換
findAllメソッド(条件にマッチするすべてのTagオブジェクトとオブジェクトを検索)
nameを使った検索
関数を使った検索条件の指定
正規表現を使った検索条件の指定
キーワード引数を使った検索条件の指定
CSSクラスの検索
test(NavigableStringオブジェクトの検索)
recursive(再帰的な検索)
limit(マッチの上限を設定)
findメソッド(条件にマッチする最初のTagオブジェクトとNavigableStringオブジェクトを検索)
Tagオブジェクト
Tagの呼び出しとfindAllの呼び出しの類似性
HTMLドキュメントをパースするにはHTMLのソースからBeautifulSoupオブジェクトを生成して、BeautifulSoupオブジェクトのメソッドを使う必要があります。
BeautifulSoupObject = BeautifulSoup( htmlSource [, fromEncoding=encoding])
htmlSourceにはHTMLソースを格納する文字列またはUnicode文字列を指定します。
#HTMLソース
doc = '<html><head><title>ねこや書店</title></head><body>ねこや書店へようこそ</body></html>'
#HTMLソースからBeautifulSoupオブジェクトを作る
soup = BeautifulSoup(doc)
fromEncodingキーワード引数は読み込むドキュメントで使われている文字コードを明示的に指定するためのものです。Beautifl Soupは自動的に文字コードを判別する処理を行いますが、これがうまくいかないことも少なくありません。そのような場合に、この引数を使って明示的に文字コードを指定します。引数の値encodingには以下のような値を指定することができます。
Beautiful Soupは与えられたドキュメントをUnicode文字列に変換して内部のデータ構造に保持しますが、BeautifulSoupのoriginalEncodingメソッドは変換される前のオリジナルのドキュメントの文字コードを文字列で返します。このメソッドは引数を持ちません。
object.originalEncoding()
このメソッドは以下のような文字列を返します。
utf-8 |
euc-jp |
shift-jis |
ascii |
windows-1252 |
BeautifulSoupオブジェクトのprettifyメソッドはHTMLを整形した結果を返します。
object.prettify( [ encode ] )
encodeに'utf-8'のようなエンコード名を指定することで、出力のエンコーディングを指定することができます。prettifyメソッドはドキュメントに以下のような加工を行います。
prettifyメソッドはドキュメントの構造を分かりやすく見せるために、インデントのためのスペースや改行を追加します。
ホワイトスペースのみからなるテキストノードを取り除きます。これによってXMLドキュメントの意味が変わるかもしれません。これに対し、str関数とunicoed関数はこれらのホワイトスペースからなるノードを削除しません
以下のスクリプトでは、prettifyメソッドの出力をprintでコンソールに出力します。
#coding: UTF-8
#HTMLパースのためのインポート
from BeautifulSoup import BeautifulSoup
#HTMLソース
doc = '<html><head><title>ねこや書店</title></head><body>ねこや書店へようこそ</body></html>'
#HTMLソースからBeautifulSoupオブジェクトを作る
soup = BeautifulSoup(doc)
#整形してコンソールに出力
print soup.prettify()
このスクリプトによって、HTMLは次のように整形されます。
<html>
<head>
<title>
ねこや書店
</title>
</head>
<body>
ねこや書店へようこそ
</body>
</html>
ドキュメント全体に対する操作では、renderContentsメソッドとstr関数の違いはありませんが、ドキュメント内の特定のタグに対する操作では、str関数がタグも含めて出力することに対し、renderContentsメソッドはそのタグを出力しません(タグの中身のみを出力します)。
renderContents( [ encode ] )
encodeに'utf-8'のようなエンコード名を指定することで、出力のエンコーディングを指定することができます。
#coding: UTF-8
from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup( \"<html><h1>Heading</h1><p>ねこや書店</p></html>\" )
print \"print(1) =\", str(soup)
print \"print(2) =\", soup.renderContents()
h1 = soup.h1
print \"print(3) =\", str(h1)
print \"print(4) =\", h1.renderContents()
上記のスクリプトを実行すると、以下のような結果が得られます。
print(1) = <html><h1>Heading</h1><p>ねこや書店</p></html>
print(2) = <html><h1>Heading</h1><p>ねこや書店</p></html>
print(3) = <h1>Heading</h1>
print(4) = Heading
1行目と2行目はドキュメント全体に対する操作でstr関数とrenderContentsメソッドに違いはありません。3行目は<h1>タグに対してstr関数で出力したもの、4行目は<h1>タグに対してrenderContentsメソッドで出力したものです。renderContentsメソッドではタグの内容のみが出力されています。
__str__メソッドは組み込み関数のstr関数に似ていますが、引数でエンコードを指定することができます。
__str__( [ encode ] )
Beautify Soupのドキュメントで詳細されているサンプルでは、encodeには、"ISO-8859-1"や"UTF-16"、"EUC-JP"のようなエンコード名が指定されています。これらは、Pythonのドキュメントの「Standard Encodings」のリストに含まれず、気になりますが'utf-8'や、'euc-jp'を指定しても期待通りに動作するようなので、あまり気にしなくてもいいかもしれません。
#coding: UTF-8
from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup( "<html><h1>Heading</h1><p>ねこや書店</p></html>" )
print soup.__str__( 'euc-jp' )
findAllメソッドはツリーの与えられた位置からツリーをたどり、与えられた条件にマッチするすべてのTagオブジェクトとNavigableStringオブジェクトを見つけます。findAll書式は以下のとおりです。
findAll( name=None, attrs={}, recursive=True, text=None, limit=None, **kwargs )
これらの引数はBeautifle SourpのAPIでよく用いられる引数です。このなかで最も重要な引数なnameとキーワード引数です。
name引数を使うと、名前によってタグの条件を指定することができます。条件を指定するにはいくつかの方法があります。最も簡単な方法は、単にタグ名を渡す方法です。
#coding: UTF-8
from BeautifulSoup import BeautifulSoup
doc = ['<html><head><title>Page title</title></head>',
'<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',
'<p id="secondpara" align="blah">This is paragraph <b>two</b>.',
'<div class=cls1>cls1</div>'
'<div class=cls2>cls2<b>test</b><div>sub</div></div>'
'<div class=cls3>cls3</div>'
'</html>']
soup = BeautifulSoup(''.join(doc))
print soup.findAll('div')
上記のスクリプトは以下のような構造のHTMLドキュメントからすべての<div>タグを表示します。
<html>
<head>
<title>
Page title
</title>
</head>
<body>
<p id="firstpara" align="center">
This is paragraph
<b>
one
</b>
.
</p>
<p id="secondpara" align="blah">
This is paragraph
<b>
two
</b>
.
<div class="cls1">
cls1
</div>
<div class="cls2">
cls2
<b>
test
</b>
<div>
sub
</div>
</div>
<div class="cls3">
cls3
</div>
</p>
</body>
</html>
上記のスクリプトによって、以下のようにすべての</div>タグが表示されます。
[<div class="cls1">cls1</div>, <div class="cls2">cls2<b>test</b><div>sub</div></div>, <div>sub</div>, <div class="cls3">cls3</div>]
nameオプションを使うと、findAllは条件に一致したタグ(開始タグから終了タグまで)のリストを返します。
#coding: UTF-8
from BeautifulSoup import BeautifulSoup
doc = ['<html><head><title>Page title</title></head>',
'<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',
'<p id="secondpara" align="blah">This is paragraph <b>two</b>.',
'<div class=cls1>cls1</div>'
'<div class=cls2>cls2<b>test</b><div>sub</div></div>'
'</html>']
soup = BeautifulSoup(''.join(doc))
print soup.findAll('div')
上記のスクリプトを実行すると、以下のように条件にマッチするすべてのタグの開始タグから終了タグまでの内容がリストで返されます。
[<div class="cls1">cls1</div>, <div class="cls2">cls2<b>test</b><div>sub</div></div>, <div>sub</div>]
nameオプションを使ったときに、findAllが返すのは文字列のリストなので、リストの特定の要素を指定することもできます。上記のスクリプトの10行目を次のように書き換えると、、
print soup.findAll('div')[1]
次のように指定された要素だけを取得することができます。
<div class="cls2">cls2<b>test</b><div>sub</div></div>
検索条件にマッチするタグが、同じように検索条件にマッチするタグを含む場合、両方のタグはそれぞれ検索結果のリストに加えられます。たとえば、以下のようにネストした<div>タグを持つHTMLドキュメントから<div>タグを検索した場合について考えてみます。
...
<div class="cls1">
cls1
<b>
test
</b>
<div class="cls2">
cls2
</div>
</div>
...
このドキュメントからfindAllで<div>タグを検索すると、以下のような2つの要素を持つリストが得られます。最初の要素(<div class=cls1>)には<div class=cls2>が含まれ、<div class=cls2>はこれとは別の要素としてもリストに存在します。
[<div class="cls1">cls1<b>test</b><div class="cls2">cls2</div></div>, <div class="cls2">cls2</div>]
リストを使って、出力するタグの種類を指定することができます。
from BeautifulSoup import BeautifulSoup
doc = ['<HTML><head><TITLE>Page title</title></head>',
'<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',
'<p id="secondpara" align="blah">This is paragraph <b>two</b>.',
'<div class=cls1>cls1<b>test</b><div class=cls2>class2 div</div></div>'
'</html>']
soup = BeautifulSoup(''.join(doc))
print soup.findAll(['title', 'div'])
この例では、<title>タグと<div>タグを出力します。出力の形式はこれまでと同じで、文字列のリストとなります。
[<title>Page title</title>, <div class="cls1">cls1<b>test</b><div class="cls2">class2 div</div></div>, <div class="cls2">class2 div</div>]
リストと同様にディクショナリを使ってタグを指定することもできます。上記のリスト指定による方法の例をディクショナリ指定による方法にするには、8行目を次のように修正します。
print soup.findAll({'title' : True, 'div' : True})
findAllメソッドにTrueを渡すとすべてのタグにマッチするようになります。
Tagオブジェクトを唯一の引数とし、ブール値を返す関数をfindAllメソッドに渡すこともできます。findAllが遭遇するすべてのTagオブジェクトはこの関数に渡され、関数がTrueを返す場合タグがマッチしたとみなされます。
以下のスクリプトは2つの属性を持つタグを検索します。
from BeautifulSoup import BeautifulSoup
doc = ['<HTML><head><TITLE>Page title</title></head>',
'<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',
'<p id="secondpara" align="blah">This is paragraph <b>two</b>.',
'<div class=cls1>cls1<b>test</b><div class=cls2>class2 div</div></div>'
'</html>']
soup = BeautifulSoup(''.join(doc))
print soup.findAll(lambda tag: len(tag.attrs) == 2)
上記のスクリプトを実行すると、以下が出力されます。
[<p id="firstpara" align="center">This is paragraph <b>one</b>.</p>, <p id="secondpara" align="blah">This is paragraph <b>two</b>.<div class="cls1">cls1<b>test</b><div class="cls2">class2 div</div></div></p>]
reモジュールを使って正規表現をfindAllに渡すこともできます。この方法はreモジュールのcompileメソッドの結果をfindAllに渡します。
import re
var = object.findAll( re.compile( exp ) )
print [tag.name for tag in var]
reモジュールをインポートします(1行目)。expには正規表現を記述します。objectは BeautifulSoupオブジェクトまたは BeautifulStoneSoupオブジェクトです。varにはfindAllメソッドの結果を格納します。通常、findAllは検索結果を文字列のリストで返しますが、reモジュールのcompileメソッドの結果を渡した場合、findAll返すのは文字列のリストではないようです。そのため3行目のような処理(リストの内包表記?)を使って結果を取り出します。
from BeautifulSoup import BeautifulSoup
doc = ['<HTML><head><TITLE>Page title</title></head>',
'<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',
'<p id="secondpara" align="blah">This is paragraph <b>two</b>.',
'<div class=cls1>cls1<b>test</b><div class=cls2>class2 div</div></div>'
'</html>']
soup = BeautifulSoup(''.join(doc))
import re
divTag = soup.findAll( re.compile('div') )
print [tag.name for tag in divTag]
上記のスクリプトを実行すると、以下のような出力が得られます。nameオプションを使った場合は、開始タグから終了タグまでの内容がリストの一つの要素となります。
[u'div', u'div']
キーワード引数はタグの属性条件を指定します。以下のスクリプトはalign属性が"center"のタグを表示します。
from BeautifulSoup import BeautifulSoup
doc = ['<HTML><head><TITLE>Page title</title></head>',
'<body><p id="firstpara" align="left">paragraph-1',
'<p id="secondpara" align="center">paragraph-2',
'<div class = cls1>cls1<b>test</b><div class=cls2>class2 div</div></div>'
'</html>']
soup = BeautifulSoup(''.join(doc))
print soup.findAll(align="center")
キーワード検索でも、nameオプションを使った検索のように正規表現をやリストや関数を使って条件を指定することができます。
import re
#属性値が'para'で始まるタグを表示
print soup.findAll( id=re.compile( "^para" ) )
#属性値が'center'または'left'のタグを表示
print soup.findAll(align=["center", "left"])
#属性値が7文字より少ないタグを表示
print soup.findAll(align=lambda(value): value and len(value) < 7)
キーワード検索で属性の値にTrueやNoneを指定すると特別な意味になります。Trueが指定された属性を持つタグは、属性の値に関わらずすべてマッチするようになります。Noneが指定された場合、その属性の値を持たないタグがマッチします。ドキュメントがnameとう名前の属性を持つ場合、この属性に対してキーワード検索を行うことはできません。なぜなら、findAllメソッドはnameキーワード引数を定義しているからです(name=と書くとname引数として処理される)。このような状況のために、Beautifl Soupはattrs引数を用意しています。attrs引数はキーワード引数のように働くディクショナリです。次の例はid属性が'para'で始まるタグを取得します。
soup.findAll( attrs={ 'id' : re.compile( "^para" ) } )
sttrs引数CSSクラスを含むタグの検索に便利ですが、CSS属性の名前はPythonによって予約されています。以下のようにディクショナリを使うことで、CSSクラスを検索することができます。
object.findAll("tagName", { "class" : "cssClass" })
しかしこの方法は多くのコードを必要とします。この代わりに、以下の書式を使うことでディクショナリではなく文字列を渡せるようになります。
object.findAll("tagName", "cssClass" )
以下のスクリプトはclass=cls1の<div>タグ(5行目で定義)を表示します。
#coding: UTF-8
from BeautifulSoup import BeautifulSoup
doc = ['<HTML><head><TITLE>Page title</title></head>',
'<body><p id="para1" align="left">paragraph-1',
'<p id="para2" align="center">paragraph-2',
'<div class = cls1>cls1<b>test</b><div class=cls2>class2 div</div></div>'
'</html>']
soup = BeautifulSoup(''.join(doc))
print soup.find("div", "cls1")
NavigableStringオブジェクトとは、明確な説明をまだ見つけていませんが、開始タグと終了タグを除いたタグの内容部分を格納したオブジェクトのようです。これにはそのタグの下にある子タグは含まれません。たとえば、次のようなHTMLドキュメントについて考えてみます。
<html>
<head>
<title>
Page title
</title>
</head>
<body>
<p id="para1" align="left">
paragraph-1
</p>
<p id="para2" align="center">
paragraph-2
<div class="cls1">
cls1
<b>
test
</b>
<div class="cls2">
class2 div
</div>
</div>
</p>
</body>
</html>
このHTMLドキュメントには、次のようなNavigableStringオブジェクトが存在することになります。
[u'Page title',
u'paragraph-1',
u'paragraph-2',
u'cls1',
u'test',
u'class2 div']
testキーワード引数はTagオブジェクトではなく、NavigableStringオブジェクトの検索に用いられます。この引数の値には文字列、正規表現、リスト、ディクショナリ、True、None、NavigableStringオブジェクトを引数に持つ関数が使えます。
#coding: UTF-8
from BeautifulSoup import BeautifulSoup
doc = ['<HTML><head><TITLE>Page title</title></head>',
'<body><p id="para1" align="left">paragraph-1',
'<p id="para2" align="center">paragraph-2',
'<div class = cls1>cls1<b>test</b><div class=cls2>class2 div</div></div>'
'<p><p><p></p>ppp</p></p>'
'</html>']
soup = BeautifulSoup(''.join(doc))
print soup.findAll(text="class2 div")
print soup.findAll(text=["paragraph-1", "paragraph-2"])
print soup.findAll(text=True)
print soup.findAll(text=None)
print soup.findAll( text=lambda(x): len(x) > 6 )
import re
print soup.findAll( text=re.compile('^cls') )
12行目は単純にタグの内容が"class2 div"であるものを探しています。これは7行目で定義されている<div class=cls2>class2 div</div>のタグの中身にマッチし、以下が出力されます。
[u'class2 div']
13行目はリストによる指定です。以下が出力されます。
[u'paragraph-1', u'paragraph-2']
14行目はTrueを指定していますが、これはname引数の場合と少し意味が違って、タグの中身(子タグは除く)が存在するタグにマッチします。
[u'Page title', u'paragraph-1', u'paragraph-2', u'cls1', u'test', u'class2 div']
15行目はNoneを指定していますが、これはすべてのタグにマッチするようです。以下が出力されます。この場合は、タグの中身だけでなく開始タグ、終了タグ、中に含まれるタグも出力されます。
[<html><head><title>Page title</title></head><body><p id="para1" align="left">paragraph-1</p><p id="para2" align="center">paragraph-2<div class="cls1">cls1<b>test</b><div class="cls2">class2 div</div></div></p></body></html>, <head><title>Page title</title></head>, <title>Page title</title>, <body><p id="para1" align="left">paragraph-1</p><p id="para2" align="center">paragraph-2<div class="cls1">cls1<b>test</b><div class="cls2">class2 div</div></div></p></body>, <p id="para1" align="left">paragraph-1</p>, <p id="para2" align="center">paragraph-2<div class="cls1">cls1<b>test</b><div class="cls2">class2 div</div></div></p>, <div class="cls1">cls1<b>test</b><div class="cls2">class2 div</div></div>, <b>test</b>, <div class="cls2">class2 div</div>]
16行目はラムダ関数を定義してタグの内容が6文字より大きなものを出力します。19行目は正規表現を使って、タグの内容が'cls'で始まるものを指定しています。正規表現による指定はname引数の場合と挙動が異なるようです。name引数の場合、findAllの結果をいったん変数に格納して、それをリストの内部表記のような操作でprintしなければ直接printできませんでしたが、text引数で正規表現を使う場合はそのままprintできます。
recursive引数によってドキュメントを再帰的に検索するかどうかを設定することができます。Trueに設定すると再帰的な検索が有効になり、Falseに設定すると再帰的な検索が無効になります。デフォルトはTrueになります。再帰検索を無効にするとドキュメントの現在のタグの階層から下には降りません。
limit 引数によって、、マッチの上限を設定することができます。たとえばlimit=1の場合、指定された条件にマッチするものが1件見つかったところで検索を終了します。
findAllが条件にマッチするすべてのオブジェクトを返すことに対して、findメソッドは最初に見つかったオブジェクトのみを返します。それ以外はfindメソッドはfindAllと同じ動作をします。
find( name=None, attrs={}, recursive=True, text=None, limit=None, **kwargs )
findメソッドの引数の意味はfindAllメソッドと同じです。
Beautify SoupではタグをTagオブジェクトによって表現します。Tagオブジェクトについてはまとまったドキュメントが見つかりませんが、Beautify Soupのドキュメントで断片的に説明されています。それらから推測したTagオブジェクトの仕様です。
Beautify Soupのドキュメントのサンプルではtagというオブジェクトがしばしば登場しますが、これについては理解がまだ不十分です。おそらく、暗黙のうちに作られるTagオブジェクトのインスタンスだと思われます。
以下のスクリプトは与えられたHTMLドキュメントのすべてのタグとその内容を出力します。
#coding: UTF-8
from BeautifulSoup import BeautifulSoup
doc = ['<HTML><head><TITLE>Page title</title></head>',
'<body><p id="para1" align="left">paragraph-1',
'<p id="para2" align="center">paragraph-2',
'<div class = cls1>cls1<b>test</b><div class=cls2>class2 div</div></div>'
'</html>']
soup = BeautifulSoup(''.join(doc))
print soup.findAll(text=None)
以下がこのスクリプトの出力です。
[<html><head><title>Page title</title></head><body><p id="para1" align="left">paragraph-1</p><p id="para2" align="center">paragraph-2<div class="cls1">cls1<b>test</b><div class="cls2">class2 div</div></div></p></body></html>, <head><title>Page title</title></head>, <title>Page title</title>, <body><p id="para1" align="left">paragraph-1</p><p id="para2" align="center">paragraph-2<div class="cls1">cls1<b>test</b><div class="cls2">class2 div</div></div></p></body>, <p id="para1" align="left">paragraph-1</p>, <p id="para2" align="center">paragraph-2<div class="cls1">cls1<b>test</b><div class="cls2">class2 div</div></div></p>, <div class="cls1">cls1<b>test</b><div class="cls2">class2 div</div></div>, <b>test</b>, <div class="cls2">class2 div</div>]
上記のスクリプトの9行目をtagオブジェクトを使って以下のように書き換えることでタグの名前だけを出力することができます。
9: print [tag.name for tag in soup.html.findAll(text=None)]
以下が修正後のスクリプトの結果です。
[u'head', u'title', u'body', u'p', u'p', u'div', u'b', u'div']
'title'や'div'などのタグ名が格納されます。
タグの属性が格納されます。これは以下のように使えることから、リスト型と思われます。
soup.findAll( lambda tag: len( tag.attrs ) == 2 )
開始タグと終了タグと内包するタグを除いた部分が格納されます。
パーサーオブジェクトやTagライクな関数の呼び出しでも、findAllのような操作を行うことができます。
#coding: UTF-8
from BeautifulSoup import BeautifulSoup
doc = "<html><head><title>Page title</title></head><body><div>__div1__</div><div>__div2__</div></body></html>"
soup = BeautifulSoup( doc )
print soup( text=lambda(x): len(x) < 9 )
print soup.body( 'div', limit=1 )
上記のスクリプトを実行すると、以下が出力されます。
[u'__div1__', u'__div2__']
[<div>__div1__</div>]