Web スクレイピングとは、プログラムによって自動的に Web から情報を取得する技術のことを指します。

本稿では、Python によって Web スクレイピングをする際の注意点、およびその方法について記載します。

まず、Web スクレイピングについて Wikipedia から引用します。

ウェブスクレイピング(英: Web scraping)とは、ウェブサイトから情報を抽出するコンピュータソフトウェア技術のこと。ウェブ・クローラー[1]あるいはウェブ・スパイダー[2]とも呼ばれる。 通常このようなソフトウェアプログラムは低レベルのHTTPを実装することで、もしくはウェブブラウザを埋め込むことによって、WWWのコンテンツを取得する。

ウェブスクレイピングは多くの検索エンジンによって採用されている、ボットを利用してウェブ上の情報にインデックス付けを行うウェブインデクシングと密接な関係がある。ウェブスクレイピングではウェブ上の非構造化データの変換、一般的にはHTMLフォーマットからデータベースやスプレッドシートに格納・分析可能な構造化データへの変換に、より焦点が当てられている。また、コンピュータソフトウェアを利用して人間のブラウジングをシミュレートするウェブオートメーションとも関係が深い。ウェブスクレイピングの用途は、オンラインでの価格比較、気象データ監視、ウェブサイトの変更検出、研究、ウェブマッシュアップやウェブデータの統合等である。

法律的な問題

いきなりややこしいところからですが、大事な話ですので最初に記しておきます。

まず、やり方によっては、Web スクレイピングは法的に問題となります。 後述する岡崎市立中央図書館事件などはその最たる例で、不起訴となったものの実際に逮捕された方がいらっしゃいます。

ただし「Web ブラウザ (およびそれに類するもの?) を手動で操作する以外の方法でアクセスするのは全て違法だ」というわけでは当然ないでしょう。 現に、Google, Yahoo! のような検索サービスを提供するためには Web スクレイピング (クローリング) が不可欠です。

関連しそうな法律が文化庁の Web ページ (著作物が自由に使える場合) にまとめられていましたので一部を引用しておきます。

私的使用のための複製 (第30条) 家庭内で仕事以外の目的のために使用するために,著作物を複製することができる。同様の目的であれば,翻訳,編曲,変形,翻案もできる。 ...

送信可能化された情報の送信元識別符号の検索等のための複製等 (第47条の6) インターネット情報の検索サービスを業として行う者(一定の方法で情報検索サービス事業者による収集を禁止する措置がとられた情報の収集を行わないことなど、政令(施行令第7条の5)で定める基準を満たす者に限る。)は、違法に送信可能化されていた著作物であることを知ったときはそれを用いないこと等の条件の下で、サービスを提供するために必要と認められる限度で、著作物の複製・翻案・自動公衆送信を行うことができる。

情報解析のための複製等 (第47条の7) コンピュータ等を用いて情報解析(※)を行うことを目的とする場合には,必要と認められる限度において記録媒体に著作物を複製・翻案することができる。  ただし,情報解析用に広く提供されているデータベースの著作物については,この制限規定は適用されない。 ※情報解析とは,大量の情報から言語,音,映像等を抽出し,比較,分類等の統計的な解析を行うことをいう。

特に関連しそうなところを記載していますが、Web スクレイピングをするにあたって、上記の内容は頭に入れておいた方がよさそうです。

なお、私は法律の専門家ではありませんのでここに記載していることの保証はできません。実際に何かする際には自己責任でお願いします。 Web スクレイピングと法律についてはネット上に色々と情報があがっていますので調べてみられることをおすすめします。

岡崎市立中央図書館事件

2010 年に、岡崎市立中央図書館の蔵書検索システムに対してスクレイピングしていた方が逮捕されるという事件がありました。

その方が作られていた Web スクレイパー (クローラー) は Web サーバへの不可も考慮した上で、

  • 同時には一回しかリクエストを送らない
  • 応答受信後に間隔をおいてから次のリクエストを送信する(1 秒に 1 アクセス程度に調整)

という動作となるように作られていたらしいのですが、図書館の Web サイト側に不具合があったことが原因でアクセス障害が発生する (HTTP 500 (Internal Server Error) が発生する) ようになる場合があったそうです (後に不具合改修されて、この程度ではアクセス障害が発生するようなことはなくなったそうです)。

これにより、図書館側から警察に被害届が提出され、逮捕に至ったということです。

後にその方は不起訴 (起訴猶予処分) となっており罪に問われたわけではありませんが、状況によっては逮捕されるようなこともあり得るということは認識しておいた方がよさそうです。

なお、その方が事件後に作られたまとめサイト (Librahack) によると

  • 限られた予算(税金)で運営されている公共施設のサービスに、大手企業のサービスと同じような感覚でアクセスしてしまった、
  • 相手サーバのパフォーマンスを自分勝手に予測し、レスポンスに気を配らなかった、
  • 相手サーバからのレスポンスHTTP500を細かくハンドリングせず単にスキップだけしていた、
  • 固定IPのレンタルサーバ(3月14日から3月31日、cronで起動)と、プロバイダより割り振られるIPの自宅や実家(4月2日から4月15日、Thinkpadを持ち運んで自宅と実家から手動で、日毎にどちらか一方から)、合計3カ所からアクセスしていた、

といったことが反省点として挙げられています。

Web スクレイピングにおいて注意したいこと

上記のことを踏まえた上で、Web スクレイピングにおいて注意したいことを記載しておきます。

Web サーバに対して同時に複数のリクエストをしない、リクエストの間隔を空ける

図書館の事件でも注意されていた点ですが、Web サーバに対して過度の負荷をかけないという点については十分に注意しておく必要がありそうです。

Web サーバからの応答によっては処理を中断するようにしておく

  • HTTP 500 が返ってくる
  • 極端に応答が遅くなった

といった場合に、処理間隔を長めにする、処理を中断するといった措置を講じることを検討した方がよさそうです。

Web API が提供されている場合はその使用を検討する

Web API が提供されている場合、Web スクレイピングをするような向きにはそちらを使うべきと思われます。

ただし、

  • データが不十分
  • データフォーマットが扱いづらい

などの事情がある場合は Web スクレイピングの使用も検討した方がよいかもしれません。

認証が必要な Web ページにアクセスする場合

上記文化庁の Web ページには何故か書かれていないのですが、著作権法第47条の6 には次の記載が含まれています。

当該著作物に係る自動公衆送信について受信者を識別するための情報の入力を求めることその他の受信を制限するための手段が講じられている場合にあつては、当該自動公衆送信の受信について当該手段を講じた者の承諾を得たものに限る。

要は、認証が必要な情報については承諾を得た上でアクセスしなければならないということです。ただし、第47条の6 は「インターネット情報の検索サービスを業として行う者」についての条文ですので、それ以外の場合に上記の制約が適用されるわけではなさそうです。

robots.txt の内容に注意する

robots.txt とは、Web クローラーに対して Web サイトのどこにアクセスしてよいか、またはしてはならないかを指定するためのファイルです。 Web サイトのルートフォルダに配置されます (例: example.com/robots.txt)。

次のような構文で記載されます。

User-agent: *
Disallow: *

User-agent: Googlebot
Allow: *
Disallow: /private

上記の例は、Google のクローラーは /private 以外のどこでもアクセスできますが、それ以外のクローラーは全てアクセスを禁止するというものです。

Python による Web スクレイピング

それでは、Web スクレイピングについての注意点がわかったところで、実際に Python で Web スクレイピングを行う方法について説明していきます。

なお、本稿のプログラムは Python 3.5.2 で動作確認しています。

Web スクレイピングに役立つ Python のライブラリ

Web スクレイピングに役立てられるライブラリは数多くありますが、本稿では次のライブラリを使用することにします。

  • requests (2.11.1)
  • Beautiful Soup (4.5.1)
  • Selenium (2.53.6) + PhantomJS (2.1.1)

カッコ内は本稿のプログラムの動作確認に使用した各ライブラリのバージョンです。

まずは requests, Beautiful Soup について説明します。

requests

"HTTP for Humans", 「人間のための HTTP ライブラリ」です。GET, POST リクエストを送信したり、HTTP ヘッダ・クッキーを簡単に取り扱うことができます。 標準ライブラリとして urllib というライブラリがありますが、requests はより簡単にコーディングできるように作られています。

Beautiful Soup

Beautiful Soup は、HTML, XML の Python 向けパーサです。requests で GET してきた Web ページを解析するために使います。

HTML パーサについては lxml やそれをラップした pyquery といったものもありますが、lxml は C で書かれており、高速な代わりに Pure Python な環境 (iPhone 上の Pythonista で動作させる場合とか) では動作しません。

Beautiful Soup は 3 までは Pure Python でしたが、現行の 4 からはパーサとして lxml も使用できるようになっており、環境次第では高速に処理させられます。

ちなみに、Beautiful Soup という名前は「ふしぎの国のアリス」に登場する同名の詩に由来するそうです。

requests, BeautifulSoup の使用法

例として、弊社の技術記事一覧を表示するプログラムを書いてみます。

まず、技術記事一覧ページの一部は次のような HTML となっています。

  :
  :
  <div class="article">
    <h2 class="article-title">
      <a href="/rambo/articles/4248a2654c9ac277f174">Webアプリケーションのセキュリティ</a>
    </h2>
    <ul class="article-meta">
      <li class="meta-item"><span class="article-date">2016-08-22</span></li>
      <li class="meta-item"><span class="author"><a href="/rambo/author/nishikawa">nishikawa</a></span></li>
      <li class="meta-item"><span class="category"><a href="/rambo/category/engineering">技術</a></span></li>
    </ul>
  </div>

  <div class="article">
    <h2 class="article-title">
      <a href="/rambo/articles/a3318ec14374931651c6">BASIC処理系の製作</a>
    </h2>
    <ul class="article-meta">
      ...
    </ul>
  </div>
  :
  :

上記の HTML を解析するプログラムは次の通りです。

import requests
from bs4 import BeautifulSoup

r = requests.get('https://creasys.org/rambo/category/engineering')
soup = BeautifulSoup(r.text, 'html.parser')
titleTags = soup.select('.article-title a')
for titleTag in titleTags:
    print(titleTag.text.strip())

requests によって技術記事一覧ページを GET した後、BeautifulSoup オブジェクトを生成しています。ここでは html.parser というパーサを使用するように指定しましたが、lxml を指定することもできます。

次に、select() メソッドを使用して CSS セレクタ記法で記事タイトル要素のリストを取得しています (ここでは、article-title というクラスが適用された要素の子孫に含まれる a 要素を取得しています)。この部分については別のメソッド・プロパティを使用する事もできます (soup.body.div...soup.find_all('h2', 'article-title') など) が、CSS セレクタ記法の方が簡単、かつ CSS, jQuery と同じ記法で書けますので私は専らこれを使用しています。

上記のプログラムを実行すると次のように出力されます。

Webアプリケーションのセキュリティ
BASIC処理系の製作
PL/SQL 入門
初めての NoSQL - Redis
FFTの概要
倍長整数クラスの実装(今更ながらのJava入門)
Ansible の基本
SQL - グルーピング, 集約関数
計算幾何の基礎と領域探索
CentOS 6.6 に Python 2.7 と mod_wsgi 3 系をインストールする (Ansible もあるよ)
REST, JAX-RS 入門
CDI 1.0 事始め
Java 8 - Lambda / Stream API
WebSocket 入門

POST (入力フォームの送信)

requests を使用すれば入力フォームの POST も簡単です。

POST リクエストを送りつけてよい適当な Web サイトがないためただの例となりますが、例えば次のような入力フォームを含む Web ページがあったとします。

<form method="post" action="search">
    query: <input type="text" name="query" /><br />
    exclude: <input type="text" name="exclude" />
    <br />
    <input type="checkbox" name="target" value="1" />A
    <input type="checkbox" name="target" value="2" />B
    <input type="checkbox" name="target" value="3" />C
    <br />
    <input type="submit" value="Submit" id="submit" />
</form>

このフォームを、

項目
query foo
exclude bar
target A, B をチェック

という入力内容で POST する場合のプログラムは次のようになります。

import requests
from bs4 import BeautifulSoup

s = requests.Session()
r = s.post('https://example.com/search', data = {
    'query': 'foo',
    'exclude': 'bar',
    'target': ['1', '2'],
})
soup = BeautifulSoup(r.text, 'html.parser')
itemTags = soup.select('.item-caption a')
for itemTag in itemTags:
    r = s.get(itemTag['href'])
    soup = BeautifulSoup(r.text, 'html.parser')
    itemDetailTag = soup.select_one('.item-detail')
    print('{}: {}'.format(itemTag.text.strip(), itemDetailTag.text.strip()))

この例では、まず Session オブジェクトを生成しています。これを使って GET, POST することによりクッキーや認証状態を維持してリクエストすることができます。

次に、生成した Session オブジェクトを使用して example.com/search という URL にフォームの入力内容を指定して POST しています。 フォームは、name 属性の値をキーに入力値 (value) がマッピングされたものが送信されますので、HTML の name 属性の通りに data を指定する必要があります。 なお、checkbox, select といった、同一 name で複数の値を送信する項目については、上記の例の通りリストで値を指定すればよいです。

その後、検索結果?ページの各項目のページを順に GET して詳細情報を表示しています。

User-Agent について

requests は、デフォルトでは次のような HTTP ヘッダでリクエストを送信するようです。

{
    'Accept-Encoding': 'identity, deflate, compress, gzip',
    'Accept': '*/*',
    'User-Agent': 'python-requests/1.2.0'
}

Web サーバに上記の User-Agent でリクエストを送信した場合、多くの場合は「デスクトップブラウザ向け」の Web ページが返されます。 しかし、近年、スマートフォン・タブレットのモバイルブラウザで Web ページを閲覧する場合、多くの場合は「モバイルブラウザ向け」の Web ページが表示されます。 HTML を解析することを考えた場合、ページの構成要素が少ない「モバイルバージョン」の Web ページの方が解析しやすい場合がほとんどと思われます。

モバイル端末のブラウザと同じ User-Agent でリクエストを送ることにより、requests でモバイルバージョンの Web ページを受け取ることができます。 例えば次の通りです。

h = {
    'User-Agent': 
        'Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_1 like Mac OS X) '
        'AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/14A403 Safari/602.1',
}
r = requests.get(url, headers=h)

上記の例では、iOS 10 上で動作する Safari が送信する User-Agent と同じものを使ってリクエストを送信しています。

応答ステータスコードについて

「Web スクレイピングにおいて注意したいこと」として「Web サーバからの応答によっては処理を中断するようにしておく」ということを挙げました。 requests では raise_for_status() という Response オブジェクトのメソッドを使うことによって、ステータスコードが 4XX, 5XX だった場合に例外を発生させられます。

r = requests.get('https://example.com/')
r.raise_for_status()

Selenium, PhantomJS

Selenium は、Web ブラウザの操作を自動化するためのソフトウェアです。Python 以外にも Java やその他多くの言語で実装されています。 Chrome, IE といった主要な Web ブラウザがサポートされています。

PhantomJS は、Selenium でもサポートされている WebKit ベースの「ヘッドレスブラウザ」です。画面上に何も表示することなく Web ブラウザの動作をさせられます。スクリーンショットを撮ることも可能です。

近年の Web ページは、Ajax によって動的に Web ページの内容を読み込むものが少なくありません。 このようなページは、実際に Web ブラウザにロードして Javascript を動作させなければ本来の内容が表示されません。

Selenium と PhantomJS によって、このような Web ページに対してもスクレイピングすることが可能となります。

Selenium の使用法

http://pythonscraping.com/pages/files/form.html という、入力 Form を備えた Web ページに対して POST するプログラムを Selenium を使用して書いてみます。

上記 Web ページのソースは次の通りです。

<h2>Tell me your name!</h2>
<form method="post" action="processing.php">
First name: <input type="text" name="firstname"><br>
Last name: <input type="text" name="lastname"><br>
<input type="submit" value="Submit" id="submit">
</form>

上記の Web ページに対して、

項目
firstname Taro
lastname Yamada

という内容を、User-Agent モバイル Safari で POST するプログラムは次のように書けます。

import time
import requests
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

dcap = dict(DesiredCapabilities.PHANTOMJS)
dcap['phantomjs.page.settings.userAgent'] = (
    'Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_1 like Mac OS X) '
    'AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/14A403 Safari/602.1'
)
driver = webdriver.PhantomJS(desired_capabilities=dcap)
driver.get('http://pythonscraping.com/pages/files/form.html')
time.sleep(1)
textFields = driver.find_elements_by_css_selector('input[type=text]')
vs = ['Taro', 'Yamada']
for i, textField in enumerate(textFields):
    textField.send_keys(vs[i])
submitButton = driver.find_element_by_css_selector('input[type=submit]')
submitButton.click()
time.sleep(1)
soup = BeautifulSoup(driver.page_source, 'html.parser')
print(soup.body.text)

WebDriver オブジェクトを得るまでの数行は User-Agent を設定するためのものです。

WebDriver を得た後は requests, Beautiful Soup を使った場合と大きくは異なりません。CSS セレクタ記法を使って要素を取り出すところまではほぼ同じですが、Selenium ではそれを操作することができます。上記の例では send_keys() でテキストフィールドに文字を入力し、click() で送信ボタンをクリックしています。

上記のプログラムを実行すると次のように出力されます。

Hello there, Taro Yamada!

テストの自動化

Web ブラウザの操作を自動化できる Selenium ですが、これを使うことにより Web アプリケーションのテストを自動化することもできます。元々、このライブラリは Web アプリケーションのテストを自動化するために作られたものだったりします。

参考 URL