Stimulator

機械学習とか好きな技術話とかエンジニア的な話とかを書く

Webスクレイピングする際のルールとPythonによる規約の読み込み

- はじめに -

この記事は Webスクレイピング Advent Calendar 2017 - Adventar の1日目の記事です。

近年では、Pythonが様々な場面で使われるようになりました。
Webからデータを取ってくる際のスクリプトとして利用し、そのままデータを機械学習における学習データとするといった案件も多く見るようになっています。

ありがたい事に本年度書きました以下の記事は、はてなブログに投稿されたPython関連の記事の中で歴代はてブ数1位だそうです。

Webスクレイピングも日に日に情報が増え、様々なパッケージやフレームワークによって手軽になっています。

本記事は、スクレイピングやクローラを記述する際に抜けがちな、「規約」について記載するものです。

スクレイピングの間隔はどうすればいい?規約は?違法でないの?という人のために法律等もまとめています。

アジェンダ


 

- Webスクレイピングにおけるルール -

Webスクレイピングまとめ記事の後半で記載しているが、ネット上のデータを収集して取り扱う上での著作権法対象のWebサーバに適切にアクセスし身元の開示や負荷を考える上での動産不法侵入の2つを特に気にすべきである。

 
前者の著作権法は、スクレイピングしたデータでデータセットを作って公開したりしたらダメ、データ分析に使用する場合は他法律に触れなければOK等といった内容で、SNS等でもよく議論に上がる知的財産権に属する内容である。

日本におけるスクレイピングでの著作権の取扱は「つまるところ、データ分析や教育、引用等の認められた利用の範囲内であれば、スクレイピング行為自体は著作権法上認められた行為」となっている。これについては、以下記事が最も参考になる。
「日本は機械学習パラダイス」 その理由は著作権法にあり - ITmedia NEWS

公開されている情報の利用、機械学習におけるデータの取得等は、著作権法においては問題ない(当然、各サービスの規約や後述する動産不法侵入、robot.txtによって制限されている場合はある)。

ちなみに著作物が自由に使える場合については文化庁のWebサイトにある以下を見ておくと良い。

 
後者の動産不法侵入は、スクレイピングするコードを書いたのだから意図性がある、相手のサーバへの負荷を考慮する、Webページが提示する条項を守るといった内容である。
著作権等で認められた場合でも高負荷なbotを作成した場合、例えツールを利用しただけの場合でも意図性が優先され裁判となる可能性がある。

 
本記事で取り扱う規約保持のスクリプトは、主に後者の動産不法侵入の内容に含まれるWebページが提示する条項を守る「同意の欠如」から身を守るためのものである。

主に以下の内容をPythonスクリプトで実行していくための記事である。

  • aタグにrel="nofollow"が設定されていたら辿らない
  • robots metaタグの属性記載に従う
  • HTTP ヘッダーのX-Robots-Tagに従う
  • robots.txtに従う
  • User-agentなどを正しく設定する


   

- robot.txt -

metatagやHTTPヘッダーの確認も重要であり、後述するが、まず前提としてrobot.txt周りの話を記述する。

robot.txtとは

robot.txtとはクローラーに対する指示書である。

1994年に採用されたMK1994、および拡張のMK1996Google, Yahoo and Microsoftが2008年に出してきたGYM2008、2007年のACAP等が存在し、各検索サービス等が対応したりしていなかったりというのが現状である。

そもそもは検索エンジンのクローラに対する指示書。
これ以上の歴史的経緯は割愛するが、気になる場合は以下追っていくと良。
Nikita The Spider * Articles and News
Robots Exclusion Standard - WikipediaAutomated Content Access Protocol - Wikipedia

 
Webスクレイピング、クローリングをする上では、HTACCESS等より拘束力はないが、まずはじめにrobot.txtの準拠をしておくと良い。

robot.txtの拘束力については、「法として定義されたルールではない」「検索エンジンに向けたものでありスクレイピングにおいて準拠すべきか」などの議論が幾度となく繰り返されているが、そちらについてはここでは強く言及しないものとする。

拘束力についての議論もありながら、現実robot.txtの準拠については事件の判例にも関わっている。
検索エンジンと著作権 - 三浦基/小林憲一
フィールド対Google事件 - Wikipedia
Google、新聞記事掲載が著作権侵害とするベルギーの判決にコメント

また、以下日本における「1回URL叩いたら1秒Sleepしましょう」という言説の元となっている、国立国会図書館法に関連する資料にもrobot.txtに関する記載がある。
国立国会図書館法によるインターネット資料の収集について

この国立国会図書館の資料については、言及しておく。
岡崎市立中央図書館事件について調べ、容疑者のブログ等をよく読むと理解できるはずだが、「1アクセスごとに1秒の間隔」という基準に意味は何の意味もなく、これらの言説は無視すべきである(筆者はそういった基準を国立国会図書館が公開している事自体どうかと思う)。この岡崎市中央図書館の件は、スクレイピング時に間隔を開けていたにも関わらず、サーバ側の不具合によって高負荷と判断され、逮捕、起訴猶予となった。高木浩光氏のブログにもあるように、明らかな誤認逮捕であり、警察、検察ならびに岡崎市の技術理解が乏しい事が分かったというだけの事件ではあったものの、この事件のように「相手のサーバにとっての高負荷」が発生してしまう事が罪となる可能性があり、その事について我々は肝に命じておくべきである。

 
要は「節度」となるのだが、実際の判例を見るに準拠しておいて損はないだろう。


これらが気になる場合は、以下の書籍に歴史的経緯や判例の項目があるため、一読すべきである。

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術

 

robot.txtフォーマット

  
robot.txtはドメイン直下に配置されるテキストファイルであり、Webスクレイピングやクローリングを行う際、以下のように読みに行く事になる。

https://hogehoge.com/robot.txt

以下にrobot.txtのsampleを示す。

User-agent: *
Disallow: *

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

上記は「Googleクローラーは/private以外どこでもアクセスしていいけど、それ以外のbotは全て禁止する」という指示を記したものである。

基本的にMK1994のSyntaxに従っていれば、python内のurlib.robotparserでparseする事が出来る。
後述するように、自作パーサーを作る他、Robotexclusionrulesparser、reppyといったパーサーパッケージも存在する。

また、一部Webスクレイピングまとめ記事でも紹介したようなmechanizeパッケージ等、自動でrobot.txtを読み込んでキャッシュしておき、規約に応じたスクレイピングが可能なものもある。

# mechanize
# set_handle_robots(False)しなければ以下エラーになる
httperror_seek_wrapper: HTTP Error 403: request disallowed by robots.txt

http://wwwsearch.sourceforge.net/mechanize/

後述するようにスクレイピングのためのフレームワークとしても有名なScrapyにもそれらを適応するパラメータが存在する。


 

- Pythonスクリプトでの規約の読み込み -

上記の内容を踏まえ、Pythonで各規約情報を読み込むスクリプトを示す。
ここでは「aタグのrelチェック」「robot metaの検出」「HTTPヘッダーのチェック」に加えて、既存のPython謹製パーサーによるrobot.txtのパースについて記述する。

aタグのrelチェック

aタグにはrel属性でnofollowが指定されている場合がある。
rel="nofollow"はスパムコメントに対する防御策として提案され、コメント欄等にスパムURLが投稿される可能性のある箇所に自動的にnofollowを付ける事で、GoogleやYahooの検索エンジンにおけるリンク先考慮のPageRankを下げないように設置するものである。
nofollow - Wikipedia
RFC 3667 - IETF Rights in Contributions
https://tools.ietf.org/html/rfc3668

近年ではSEO対策に加え、スクレイピング負荷対策等に使われる場合がある。

robot.txtに比べスクレイピング、クローリングに対する拘束範囲は小さいが、rel="nofollow"されたリンクはWebサイト作者の意図しないリンクである可能性が高いため、処理しておくと良い。


BeautifulSoupならfindAllの引数としてlambdaや正規表現が渡せるので、それらを利用する。

from bs4 import BeautifulSoup
import re

html = """
<html><body>
<a href="sample.com" />
<a rel="nofollow" href="badsample.com" />
<a rel="nofollow" href="badsample.com" />
<a href="sample.jp" />
<a href="sample.ne.jp" />
</body></html>
"""

soup = BeautifulSoup(html, "html5lib")
# hrefが設定されておりrelにnofollowが設定されてないaタグを探す
links = soup.findAll('a', href=True, rel=lambda x: "nofollow" not in str(x).lower())
for link in links:
    print(link['href'])


lxmlを使って解析している場合はrel属性のついたlinkを探せるfind_rel_linksがある。

import lxml.html

h = lxml.html.fromstring(html)
for x in h.find_rel_links("nofollow"):
    print(lxml.html.tostring(x))


 

robot metaの検出

クローリングされないため、Webサイトにmetatagとしてnofollow、noarchive、noindexを設置する方法がある(他にもnocacheや特定サービス拒否のcontent属性がある)。noarchiveであればそのページを保存しないし、nofollowがついていれば以降リンクを走査しないようにしておくと安全。
http://www.robotstxt.org/meta.html
HTML 4.01仕様における記載

BeautifulSupの解析器にLambdaを投げるのが吉。

from bs4 import BeautifulSoup
html = """
<html><body>
<meta content="nofollow" name="robots"/>
<meta content="noarchive" name="robots"/>
<meta content="noindex" name="robots"/>
<a href="sample.com" />
</body></html>
"""

soup = BeautifulSoup(html, "html5lib")
meta = soup.find_all('meta',
                     attrs={"name":"robots"},
                     content=lambda x: "nofollow" in str(x).lower() or "noarchive" in str(x).lower())
print(meta)
print(len(meta) > 0)


 

HTTPヘッダーにおけるX-Robots-Tagのチェック

Googleによると、HTTPヘッダーでも上記metatag設定と同義の事ができるらしい。
Robots meta tag and X-Robots-Tag HTTP header specifications  |  Search  |  Google Developers

一応チェックできる。例えばrequestsなら下記。

import requests
r = requests.get('https://hogehoge.com')
print( "nofollow" not in str(r.headers.get("X-Robots-Tag")) )
print( "noarchive" not in str(r.headers.get("X-Robots-Tag")) )

設定してるWebページがそこまで多い訳じゃないが、見ておくと良さげ。

 

urllib.robotparserを使ったrobot.txtのparse

21.10. urllib.robotparser — robots.txt のためのパーザ — Python 3.6.5 ドキュメント

Pythonデフォルトで使えるパーサ。

Googleのrobot.txtをparseしてみる。

[https://www.google.com/robots.txt]

以下のように読み込んでcan_fetchしていく

import urllib.robotparser
rp = urllib.robotparser.RobotFileParser()
rp.set_url("https://www.google.com/robots.txt")
rp.read()

# エージェントがURLを見れるか
if rp.can_fetch("*", "https://www.google.com/search"):
    print("OK")
else:
    print("NG")

# クローラの遅延時間指定パラメータの取得
# なければNone
print(rp.crawl_delay("*"))
print(rp.request_rate("*"))

参考:Parsing Robots.txt in python - Stack Overflow

 

Robotexclusionrulesparserを使ったrobot.txtのparse

Robotexclusionrulesparserはurllib.robotparserのBSD License代替スクリプト
Nikita The Spider * A Robot Exclusion Rules Parser for Python

前述したGYM2008とMK1994/96との構文の差で発生する問題(ワイルドカード*をどう読むか)等について触れられており、両者をparseできるようになっている。non-ASCIIやBOM等にも対応。
RobotExclusionRulesParser and RobotFileParserLookalikeの2つのクラスを提供していて前者がwrapperになっている。
BSDライセンスなのでGithubで検索すると導入しているところがちらほら。

以下URLのスクリプトを利用するか、pipで導入する。
http://nikitathespider.com/python/rerp/robotexclusionrulesparser-1.7.1.py

pip install robotexclusionrulesparser

使い方はほぼurllibと同じ。

import robotexclusionrulesparser

rerp = robotexclusionrulesparser.RobotExclusionRulesParser()
rerp.fetch('https://www.google.com/robots.txt')
if rerp.is_allowed('*', '/search'):
    print("OK")
else:
    print("NG")

get_crawl_delayやparseもあるので結構使える。
複数のWebサイトを取って回る、urllibじゃparseできない時に有用。

 

reppyを使ったrobot.txtのparse

robot.txtのparseパッケージ
クロール遅延やサイトマップの読み込み、robots.txtをキャッシュしておく機能が備わっている。
GitHub - seomoz/reppy: Modern robots.txt Parser for Python

導入はpipで

pip install reppy

使い方もほぼ上記と同じ。
sitemapが見れるだけでなくHeaderWithDefaultPolicyでこちらのPolicyに合わせてbool値が出せたりする。

from reppy.robots import Robots

# This utility uses `requests` to fetch the content
robots = Robots.fetch('https://www.google.com/robots.txt')
if robots.allowed('https://www.google.com/search', 'robot'):
    print("OK")
else:
    print("NG")
    
print(robots.agent('*').delay)
print(list(robots.sitemaps))

reppyの強みはcacheにある。

from reppy.cache import RobotsCache
robots = RobotsCache(capacity=100)
robots.allowed('https://www.google.com/search', 'robot')
print(robots.cache)

これによりrobotsを使いまわしながら、複数のサイトのrobot.txtを採用しながらスクレイピングを行うことができる。

 

scrapyではどうすればいいの?

scrapyでは、以上のrobot.txtパーサを利用しなくてもurllibを利用してくれるパラメータが存在する。

ご存知の通り、scrapyのデフォルト設定はスクレイピング先のサーバに負荷をかけまくる極悪設定になっているので、必ず以下くらいを設定しておくと良い。

DOWNLOAD_DELAY = 5
ROBOTSTXT_OBEY = True 

負荷を気にするなら、そもそもScrapyを使わないという選択肢も大いにあるが…


 

- おわりに -

robots.txt、Metatags辺りに対応する方法についてまとめておきました。

最近、辻さんがTwitterで仰っていましたが、robot.txtとnoindexを情報隠ぺいのように使う企業も存在します(下記はrobot.txtとタグの優先順序により失敗していましたが…)。


直近だと、Ads.txtなるものも出てきました。
Industry Aligns to Adopt ads.txt Specification – IAB Tech Lab

今後クローラ界隈がどうなっていくかは未知ですが、する側もされる側も良識を持って対応しなければいけないというのが現状で、こうしていれば間違いない、批判されないという方法はありません。

日本では解析のためのWebスクレイピングが認められている訳ですが、その中でも全員が節度を持って行動する事が全員のためになると私は思います。
「日本は機械学習パラダイス」 その理由は著作権法にあり - ITmedia NEWS


最後になりましたが、上記に加えてWebスクレイピングを行う際はUser-Agentを適切に設定しましょう。

明日はanoChickさん!期待です!
adventar.org


Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術