目次


魅力的な Python

mechanize と Beautiful Soup を使って Web データの収集を簡単に行う

Web サイトからデータを抽出し、そのデータを編成する作業を容易に行える Python のツール

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: 魅力的な Python

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:魅力的な Python

このシリーズの続きに乞うご期待。

Web サイトと対話するためのスクリプトは、基本的な Python モジュールを使えば作成できますが、その必要がないとしたら、わざわざ作成しようとは思わないでしょう。Python 2.x に組み込まれている urllib および urllib2 モジュールは、Python 3.0 で統一された urllib.* サブパッケージとともに、URL の先にあるリソースを取得する上である程度の役目を果たします。けれども、Web ページで見つけたコンテンツと適度に洗練されたあらゆる種類の対話を行うとなると、mechanize ライブラリーが欠かせません (「参考文献」にダウンロード・リンクを記載しています)。

Web スクレイピングを自動化したり、ユーザーと Web サイトとの対話のシミュレートを自動化したりする上での大きな問題は、サーバーが cookie を使ってセッションの進行状況を追跡することです。当然のことながら、cookie は HTTP ヘッダーに組み込まれていて、urllib がリソースをオープンするときには本質的に可視になっています。しかも、標準モジュールの Cookie (Python 3 では http.cookie) と cookielib (Python 3 では http.cookiejar) は、直接 HTTP ヘッダーのテキストを処理するのではなく、もっと高いレベルで行われる HTTP ヘッダーの処理の手助けをします。しかしそれでも HTTP ヘッダーの処理を行うのは必要以上に厄介なことです。mechanize ライブラリーはこの処理をさらに高いレベルの抽象化に引き上げ、スクリプト (またはインタラクティブな Python シェル) が実際の Web ブラウザーのごとく振る舞えるようにします。

Python の mechanize は Perl の WWW:Mechanize から発想を得ています。mechanize と WWW:Mechanize の機能の範囲はほとんど共通していますが、Pythonista として既に長い私としては、Python と Perl 両方の言語の汎用的なパターンに従っていると思われる mechanize のほうが WWW:Mechanize よりも堅牢であると感じます。

mechanize と相性の良いツールは、同じく優れたライブラリーである Beautiful Soup です (「参考文献」にダウンロード・リンクを記載しています)。この見事で「寛容なパーサー」は、実際の Web ページでよく見かける、完全ではないけれどもほぼ妥当な HTML にも対応します。Beautiful Soup は必ずしも mechanize と一緒に使用しなければならないわけではありません。また、その逆も然りですが、「実在する Web」と対話する際には大抵この 2 つのツールがセットで必要になります。

実際の例

私はこれまで、いくつかのプログラミング・プロジェクトで mechanize を使ってきました。前回 mechanize を使用したのは、人気の高い Web サイトから一定の基準と一致する名前のリストを収集するというプロジェクトです。このサイトには検索機能が備わっていたものの、このような検索を目的とした正式な API はありませんでした。読者の皆さんは、私が具体的にどのようなことを行っていたのか想像できると思いますが、これから記載するコードは、スクレイピング対象のサイトや私のクライアントについてあからさまにならないよう、具体的な詳細を変えています。これと同様のタスクには、ここに記載するようなコードが一般的な形となります。

ツールの紹介

Web スクレイピング/分析用のコードを実際に開発する過程で、関連する Web ページで実際に何が行われているのかを知るために、Web ページの内部をインタラクティブに表示させたり、探索したり、実行したりできると非常に有益です。関連する Web ページとは、大抵の場合、あるサイトに含まれるページの一式のことであり、これらのページはクエリーによって動的に生成されるか (ただし、クエリーによって生成されることから一貫したパターンがあります)、または極めて厳格なテンプレートに従って事前に生成されるものです。

このインタラクティブな実験的作業を行う上で役立つ 1 つの方法は、mechanize 自体を Python シェル内、特に IPython (「参考文献」にリンクを記載) のような拡張シェルのなかで使用することです。このようにして Web ページの探査を行えば、本番環境で必要な対話を行うための最終的なスクリプトを作成する前に、リンクされた各種リソースを要求したり、フォームを送信したり、さらにサイトの cookie を管理、操作したりするなどの作業が可能になります。

ただし、この Web サイトとの実験的対話のほとんどは、実際の最新の Web ブラウザーで行ったほうがパフォーマンスに優れています。状況に応じてページをレンダリングして確認すれば、特定のページやフォームで何が行われているかが遥かに素早く見えてきます。問題は、ページをレンダリングするだけでは話の半分、あるいはそれ以下しかわからないことです。「ページのソース」があれば、もう少し詳しいことがわかってきますが、特定の Web ページまたは Web サーバーとの一連の対話の背後に何があるのかを十分に理解するためには、それだけでは足りません。

背後にある核心に達するために私が通常使用しているのは、Firebug (「参考文献」にリンクを記載)、または Firefox の Web Developer プラグインです (最近の Safari バージョンでオプションとして用意されている組み込みの Develop メニューも使いますが、この記事の読者向けではありません)。これらのツールにはいずれもフォーム・フィールドを明らかにする機能や、パスワードを表示する機能、ページの DOM を検査する機能、Javascript を表示させたり実行したりする機能、Ajax トラフィックを観察する機能などがあります。ツールそれぞれの利点と特徴を比較するとなると、それだけで 1 つの記事が書けてしまうほどですが、Web 指向のプログラミングを実践している場合には、これらのツールを十分に理解しておいてください。

対話を自動化しようとしている Web サイトの実験にどのツールを使用するかに関わらず、タスクを実行するために必要な mechanize を利用したコードは驚くほど簡潔なものになるため、短時間で作成できますが、サイトが実際に行っている内容を突き止めるには、それよりも遥かに長い時間がかかるはずです。

検索結果スクレイパー

上述のプロジェクトのために、私は 100 行からなるスクリプトを以下の 2 つの機能に分けました。

  • 対象とするすべての結果を取得する機能
  • 取得したページから興味のある情報を抽出する機能

以上のようにスクリプトを編成し、開発しやすいようにしました。したがって、タスクを開始する時点ではっきりしていたのは、この 2 つの機能を実行するそれぞれの方法を見つける必要があるということです。必要な情報はサイト全体から集めたページの中にあることはわかっていましたが、これらのページに特有のレイアウトについてはまだ調べていませんでした。

最初に一連のページを取得してディスクに保存すれば、保存されたファイルから興味のある情報を抽出するというタスクに取り掛かれます。もちろん、取得した情報を使って同じセッション内で新たな対話を行うという作業を伴うタスクの場合は、これとは多少異なる開発手順を踏まなければなりません。

まずは、fetch() 関数から見て行きます。

リスト 1. ページのコンテンツを取得する
import sys, time, os
from mechanize import Browser

LOGIN_URL = 'http://www.example.com/login'
USERNAME = 'DavidMertz'
PASSWORD = 'TheSpanishInquisition'
SEARCH_URL = 'http://www.example.com/search?'
FIXED_QUERY = 'food=spam&' 'utensil=spork&' 'date=the_future&'
VARIABLE_QUERY = ['actor=%s' % actor for actor in
        ('Graham Chapman',
         'John Cleese',
         'Terry Gilliam',
         'Eric Idle',
         'Terry Jones',
         'Michael Palin')]

def fetch():
    result_no = 0                 # Number the output files
    br = Browser()                # Create a browser
    br.open(LOGIN_URL)            # Open the login page
    br.select_form(name="login")  # Find the login form
    br['username'] = USERNAME     # Set the form values
    br['password'] = PASSWORD
    resp = br.submit()            # Submit the form

    # Automatic redirect sometimes fails, follow manually when needed
    if 'Redirecting' in br.title():
        resp = br.follow_link(text_regex='click here')

    # Loop through the searches, keeping fixed query parameters
    for actor in in VARIABLE_QUERY:
        # I like to watch what's happening in the console
        print >> sys.stderr, '***', actor
        # Lets do the actual query now
        br.open(SEARCH_URL + FIXED_QUERY + actor)
        # The query actually gives us links to the content pages we like,
        # but there are some other links on the page that we ignore
        nice_links = [l for l in br.links()
                        if 'good_path' in l.url
                        and 'credential' in l.url]
        if not nice_links:        # Maybe the relevant results are empty
            break
        for link in nice_links:
            try:
                response = br.follow_link(link)
                # More console reporting on title of followed link page
                print >> sys.stderr, br.title()
                # Increment output filenames, open and write the file
                result_no += 1
                out = open(result_%04d' % result_no, 'w')
                print >> out, response.read()
                out.close()
            # Nothing ever goes perfectly, ignore if we do not get page
            except mechanize._response.httperror_seek_wrapper:
                print >> sys.stderr, "Response error (probably 404)"
            # Let's not hammer the site too much between fetches
            time.sleep(1)

対象のサイトをインタラクティブに探査したことによって、私が実行したいと思っているクエリーは、いくつかの固定要素と可変要素で構成されることがわかりました。そこで、この 2 種類の要素を連結して大きな GET リクエストにまとめ、このリクエストに対する「結果」ページを調べます。結果の一覧には実際に必要なリソースへのリンクが含まれるので、これらのリンクを辿り (途中で何かが上手く行かなかった場合に備え、いくつかの try/except ブロックを追加します)、リンク先のコンテンツ・ページで見つけたすべての情報を保存します。

このように、コードの内容は単純明快です。もちろん mechanize の機能はこれだけではありませんが、上記のような簡単な例が、このライブラリーが持つ機能の概要を明らかにします。

結果の処理

これで、mechanize を使った処理は完了したので、後は fetch() ループの処理中に保存したあの大量の HTML ファイルを解析するだけです。このプロセスは本来バッチ処理であることから、結果を取得する処理 (fetch()) と情報を抽出する処理 (process()) を明確に分けることができますが、別のプログラムでは明らかに fetch()process() がより密接に影響し合う場合も考えられます。Beautiful Soup はこの事後処理を、最初の取得処理よりさらに容易にします。

このバッチ・タスクでは、取得した各種の Web ページにある情報から、表形式のカンマ区切り値 (CSV) データを生成する必要があります。

リスト 2. Beautiful Soup によって、雑多な情報から整然としたデータを生成する
from glob import glob
from BeautifulSoup import BeautifulSoup

def process():
    print "!MOVIE,DIRECTOR,KEY_GRIP,THE_MOOSE"
    for fname in glob('result_*'):
        # Put that sloppy HTML into the soup
        soup = BeautifulSoup(open(fname))

        # Try to find the fields we want, but default to unknown values
        try:
            movie = soup.findAll('span', {'class':'movie_title'})[1].contents[0]
        except IndexError:
            fname = "UNKNOWN"

        try:
            director = soup.findAll('div', {'class':'director'})[1].contents[0]
        except IndexError:
            lname = "UNKNOWN"

        try:
            # Maybe multiple grips listed, key one should be in there
            grips = soup.findAll('p', {'id':'grip'})[0]
            grips = " ".join(grips.split())   # Normalize extra spaces
        except IndexError:
            title = "UNKNOWN"

        try:
            # Hide some stuff in the HTML <meta> tags
            moose = soup.findAll('meta', {'name':'shibboleth'})[0]['content']
        except IndexError:
            moose = "UNKNOWN"

        print '"%s","%s","%s","%s"' % (movie, director, grips, moose)

上記の process() には、Beautiful Soup の第一印象を印象的なものにするコードが含まれています。このモジュールの詳細については、ドキュメントを読んでください。ただし、全体的な雰囲気はこのスニペットから十分つかめるはずです。Beautiful Soup コードの大部分は、HTML として完全な整形式ではない可能性のあるページに対する .findAll() 呼び出しで構成されます。こうしたページでは、DOM 風の .parentnextSiblingpreviousSibling 属性が使われており、これらの属性は Web ブラウザーの Quirks モードに相当します。このように、Beautiful Soup は構文解析ツリーというよりも、いわばスープの具材となるかもしれない野菜が詰め込まれた袋のようなものです。

まとめ

私のような時代遅れの人間だけでなく、若い読者のなかにも、TCL の Expect (または Python やその他の言語で作成された、これとそっくりのライブラリー) を使ってスクリプトを作成するときの大きな喜びを覚えているプログラマーはいることでしょう。telnet や ftp、そして ssh などのリモートからの対話を含め、シェルによる対話は、セッションにすべての内容が表示されるため、自動化するのは比較的簡単です。しかし Web での対話となると、情報がヘッダーと本体とで分割されるという点、そして各種の従属リソースが href リンクやフレーム、Ajax などとバンドルされることも珍しくないという点で、多少扱いが難しくなります。けれども原則としては、wget のようなツールを使いさえすれば、Web サーバーが提供するすべての情報を取得して、他の接続プロトコルでの場合とまったく同じスタイルの Expect スクリプトを実行することができます。

実際には、私が言う wget + Expect 手法のような昔懐かしい手法にこだわっているプログラマーはほとんどいません。mechanize には Expect スクリプトと同じ懐かしさと使いやすさの多くが残されていて、Expect スクリプトより簡単とは言わないまでも、同じように簡単に作成することができます。.select_form().submit().follow_link() などの Browser() オブジェクト・コマンドは、「これを検索して、あれを送信する」ことを指示する実に単純極まりなく最も明らかな方法であると同時に、Web 自動化フレームワークに期待される高度な状態およびセッション処理の長所をすべて備えています。


ダウンロード可能なリソース


関連トピック

  • Linux で Web スパイダーをビルドする」(developerWorks、2006年11月) では、Web スパイダーおよびスクレイパーについて説明し、Ruby を使用して単純なスクレイパーをビルドする方法を紹介しています。
  • Firebug を使ってオンザフライでアプリケーションをデバッグ、調整する」(developerWorks、2008年5月) では、Firebug を使って Web アプリケーションや Ajax アプリケーションのページ・ソースを表示するだけでなく、さらに高度な操作を行う方法を説明しています。
  • Using Net-SNMP and IPython」(developerWorks、2007年12月) では、IPython と Net-SNMP を組み合わせてインタラクティブな Python ベースのネットワーク管理を行う方法を詳しく解説しています。
  • developerWorks Linux ゾーンに豊富に揃った Linux 開発者向けの資料を調べてください。記事とチュートリアルの人気ランキングも要チェックです。
  • developerWorks に掲載されているすべての「Linux のヒント」シリーズの記事と Linux チュートリアルを参照してください。
  • mechanize とその資料をダウンロードしてください。
  • Beautiful Soup とその資料をダウンロードしてください。
  • IPython は、計算の並列化支援などの高度な機能を備えた Python のネイティブ・インタラクティブ・シェルの見事な拡張バージョンです。私は主に、コードの色分け、コマンドライン再呼び出しの改善、タブの補完、マクロ機能、インタラクティブ・ヘルプの改善など、対話性を支援するために IPython を使用しています。
  • Firebug をインストールすることによって、ブラウズしながらいつでも簡単に、Firefox 3.0+ の「Tools/Add-ons」メニューから豊富な編集、デバッグ、およびモニタリング用の Web 開発ツールを使用できるようになります。これと同じく、Web Developer 拡張機能を追加すると、ブラウザーにさまざまな Web 開発者向けツールのメニューとツールバーが追加されます。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux, Web development, Open source
ArticleID=458790
ArticleTitle=魅力的な Python: mechanize と Beautiful Soup を使って Web データの収集を簡単に行う
publish-date=11242009