1. ホーム
  2. パイソン

Pythonウェブクローラー学習ノート

2022-02-22 12:12:15

Pythonウェブクローラー学習ノート

By  中環(ちゅうかん

 2014年9月4日 更新:2014年9月4日

プレゼンテーションの様子。

蜘蛛は餌を得るために巣を作りますが、私たちのクローラーも、ウェブ上の資源を得るためのものです。このブログは、私の学習の過程を記録したものです。Python2.7にはurllibとurllib2という2つのモジュールがあり、ウェブへのアクセスを提供しています。以下では、それらについて、より良い感触を得ることができるでしょう。python3では、urllibとurllib2の2つのモジュールが1つのurllibに統合されていることを述べておきます。 こちら

urllib と urllib2 は python で Web を扱うための強力なライブラリで、Web にファイルのようにアクセスすることができます (例えば、ファイルアクセスのためにはまずファイルを open() しなければなりませんが、この例で後で説明するように、同様のことをします)。なぜこのように便利になるかというと、これらのモジュールはこれらの機能を実現するために、内部で異なるネットワークプロトコルをうまく使っているからです。(ネットワークについて勉強したことがある人なら、Web ページにアクセスするという単純な処理が、実際には http や dns などの多くのネットワークプロトコルを含んでいることを理解できるでしょう。urllib と urllib2 はこれらのプロトコルをカプセル化して、それらを扱う必要がないようにして、単にこれら (のモジュールのメソッドを呼び出して必要なことを実行させる) を呼び出すだけにしています。また、これらのモジュールは、ユーザ認証、クッキー、プロキシなどの状況を処理するために、少し複雑な言い訳を提供します。では、それらについて勉強を始めましょう。


簡単な文章から始める。

先に述べたように、2つのモジュールを使うことで、Webページへのアクセスはファイルへのアクセスと同じくらい簡単になります。一般的には urllib2 によるアクセスの方が優れています (効率という点では urllib を使う必要がありますし、後述する urllib の使い方もあります)。そこで、urllib2 を使った最も簡単な例を見てみましょう。


import urllib2;

response = urllib2.urlopen("http://www.zhonghuan.info");
html = response.read();
print html;



ターミナルでコマンドライン python test.py > zhonghuan.html をタイプしてください。
ファイルが開き、私の個人ブログのトップページのhtmlコードが表示されます。

これは、urllib2 を使って Web ページにアクセスする最も単純な例です。urllib2 は、URL の : の前の部分から、Web ページにアクセスするためにどのプロトコルが使われるかを判断します。例えば、上の例ではhttpを使っていますが、ここではftp:、file:などに置き換えることも可能です。これらのプロトコルをどのようにカプセル化しているのかを理解しなくても使うことができます。これらのネットワークプロトコルを内部でどのようにカプセル化しているかを理解する必要はないのです。

urllib2では、Request Objectを使用して、私たちのhttpアクセスを表現することができます。これは、アクセスしたいURLを示しているので、次の例を見てみましょう。

import urllib2

req = urllib2.Request('http://www.zhonghuan.info')
response = urllib2.urlopen(req)
the_page = response.read()
print(the_page)



req変数はRequestオブジェクトです。これは、あなたがアクセスしたい正確なURLアドレスをマークします。(ここでは http://www.zhonghuan.info)。ftpやfileなど他の形式のアクセスでは、[こちら][2]のように似たような形式があります。 ];

実は、Requestオブジェクトはさらに2つのことをすることができる。

  1. サーバーにデータを送ることができる。
  2. いくつかの追加情報(別名メタデータ、データを説明するデータ)を送ることができます。言語によっては、その中にクラスを生成するメタクラスがあり、pythonはそのような言語です。メタデータはその名の通り、データを記述するデータですが、ではこのデータは何を記述しているのでしょうか?上記では、リクエストオブジェクトの情報もありますが、これらの記述は、送出されるhttpヘッダーに置かれます。httpヘッダについては ここで );

サーバーへのデータ送信

時には、URLで表されるサーバー、通常はCGI(Common Gateway Interface)スクリプトやその他のウェブアプリケーションにデータを送信する必要があります。(CGIスクリプトについては こちら これは、単にデータのアップロードを処理するスクリプトである)。HTTPアクセスでは、どのPOSTメソッドを使用してデータを送信するのが一般的です。ちょうど、htmlでフォームに入力し終わったときに、フォーム内のデータを送信する必要がありますが、通常はここでpost requestを使用します。もちろん、フォームだけでなく、他にもpostが使われるケースはあります。

次のコードを見てみましょう。


import urllib
import urllib2

url = 'http://www.someserver.com/cgi-bin/register.cgi'
values = {'name' : 'Michael Foord',
      'location' : 'Northampton',
      'language' : 'Python' }
data = urllib.urlencode(values) # The data needs to be re-encoded into the proper format, which is done in urllib, since there is no encoding method in urllib2
req = urllib2.Request(url, data) # # Here the data to be uploaded is passed to the request object as its parameter
response = urllib2.urlopen(req)
the_page = response.read()



その他のデータアップロードの種類については こちら

データをアップロードするには、postの他にgetも使えます。getとpostの明らかな違いは、getでアップロードされたデータはURLの末尾に表示されることです。これは、以下のコードで確認できます。



import urllib
import urllib2

data = {}
data['name'] = 'Somebody Here'
data['location'] = 'Northampton'
data['language'] = 'Python'

url_values = urllib.urlencode(data)
print url_values # The order here is not necessarily

url = 'http://www.example.com/example.cgi'
full_url = url + '?' + url_values
data = urllib2.urlopen(full_url)



url_value フォームは静かにプリントアウトできます。


HTTPヘッダー - データを記述するデータ

では、HTTPのRequestオブジェクトにHTTPヘッダを追加する方法について説明します。

サイトには、より賢く、プログラムによるアクセスを好まないものがあります(人間以外のクリックはサーバーの負荷を増やすだけです)。あるいは、もう少し賢くて、ブラウザごとに異なるページデータを送信するサイトもあります。

しかし、デフォルトでは、urllib2 は自分自身をこのようにラベル付けしています。 Python-urllib/x.y (ここで、xとyはバージョン番号で、例えば、私が Python-urllib/2.7 このデータはサイトによっては混乱を招くかもしれませんし、プログラムによる訪問を好まないサイトに遭遇した場合、そのような訪問は単に無視されるかもしれません)。そこで、サイトが拒絶しないように、何らかのIDを構築することができます。次の例を見てください。


import urllib
import urllib2

url = 'http://www.someserver.com/cgi-bin/register.cgi'
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' #user_agent is used to mark your browser, which in this case is mozilla
values = {'name' : 'Michael Foord',
      'location' : 'Northampton',
      'language' : 'Python' }

headers = { 'User-Agent' : user_agent }
data = urllib.urlencode(values)

req = urllib2.Request(url, data, headers)

response = urllib2.urlopen(req)

the_page = response.read()    




例外事項

例外は常に起こるので、気をつけましょう ファイルを操作したときに何が起こるか考えてみてください。ファイルを開けない、パーミッションが足りない、ファイルが存在しない、などなど。URLへのアクセスも同様です。(ValueErrorやTypeErrorなど、python内部の例外も発生することがあります)


URLError

ネットワーク接続がない場合、またはアクセスしたサーバーのアドレスが存在しない場合、URLErrorがスローされます。このとき、URLError例外はquot;reason"属性を持ち、それはエラーコード( int )とテキストエラーメッセージ( string )から成るタプルであり、次のコードを参照してください。


import urllib
import urllib2


req = urllib2.Request('http://www.pretend_server.org')
try: urllib2.urlopen(req)
except urllib2.URLError as e:
    print e.reason



出力 [Errno 8] nodename nor servname provided, or not known 出力は、以下の理由です。


HTTPError

各HTTPアクセスは、"ステータスコード&quotを取得します。サーバーから、通常、これらのステータスコードは、サーバーがいくつかのアクセスを満たすことができないことを教えて(単刀直入に言うと、いくつかのデータが、現在のアクセスなどのステータスは、現在のアクセスが拒否された。ステータスコードはあなたの行動があまりにも多く、通常のうち、注、塩辛い手は、ああ〜〜を持っていないことができるかどうかを伝えることができます)。

しかし、urllib2 のデフォルトハンドラは、いくつかのサーバーレスポンスを処理するのに役立ちます。例えば、現在アクセスしている URL がサーバーによってリダイレクトされた場合、つまりサーバーが新しい URL を教えてくれた場合、ハンドラはあなたのために新しい URL に直接移動してくれます。

しかし、デフォルトの処理系には限界があり、例えば、アクセスしているサイトが存在しない場合(404エラーに相当、時々見かける)、アクセスが無効になっている場合(403エラーに相当、十分な権限がないなどの理由が考えられる)、認証が必要な場合(401に相当)などは、すべての問題を解決できるわけではありません。その他の具体的なエラーについては、今回は紹介しませんので、具体的な ここで

HTTPErrorが404エラー、つまりページが存在しない場合、どのような出力をするか、次のプログラムを見てみましょう。


import urllib
import urllib2


req = urllib2.Request('http://www.zhonghuan.info/no_way')
try: urllib2.urlopen(req)
except urllib2.HTTPError as e:
       print e.code;
     print e.read();    



出力してください。
404

<!DOCTYPE html>

...

<title>Page not found &middot; GitHub Pages</title>

...


例外処理

HTTPErrorとURLErrorをキャッチする場合、2つの基本的な方法があります。

1つ目は


from urllib2 import Request, urlopen, URLError, HTTPError


req = Request(http://zhonghuan.info)
try:
    response = urlopen(req)
except HTTPError as e:
    print 'The server couldn't fulfill the request.'
    print 'Error code: ', e.code
except URLError as e:
    print 'We failed to reach a server.'
    print 'Reason: ', e.reason
else:
    # everything is fine



最初のアプローチでは、HTTPErrorをURLErrorの前に置かなければなりません。多くの言語の例外処理機構と同様に、HTTPErrorはURLErrorのサブクラスであり、HTTPErrorが発生した場合、URLErrorとしてキャッチすることができるためです。

2つ目は

from urllib2 import Request, urlopen, URLError


req = Request(someurl)
try:
       response = urlopen(req)
except URLError as e:
    if hasattr(e, 'reason'):
        print 'We failed to reach a server.'
        print 'Reason: ', e.reason
    elif hasattr(e, 'code'):
        print 'The server couldn\'t fulfill the request.'
        print 'Error code: ', e.code
else:
    # everything is fine        




情報およびgeturl

ここでは、info()とgeturl()という2つのメソッドについて説明します。

geturl():このメソッドは、訪問したページの実際のURLを返します。訪問したページはリダイレクトされる可能性があり、結果として訪問したURLは入力したものと同じでない場合があるので、これは貴重なものです。次の例を見てください。


import urllib
import urllib2

url = 'http://weibo.com/u/2103243911';
req = urllib2.Request(url);
response = urllib2.urlopen(req)

print "URL:",url;
print "After redirection:",response.geturl();



私のTwitterの個人ページを例にとると、出力に見られるように、実際の訪問は実際のURLにリダイレクトされたのです。

URL: http://weibo.com/u/2103243911


After redirection: http://passport.weibo.com/visitor/visitor?a=enter&url=http%3A%2F%2Fweibo.com%2Fu%2F2103243911&_rand= 1409761358.1794


info():ページの説明情報を取得することができます。 httplib.HTTPMessage のインスタンスで、辞書のように出力されます。次のコードを見てください。


import urllib
import urllib2


url = 'http://zhonghuan.info';
req = urllib2.Request(url);
response = urllib2.urlopen(req);
print response.info();
print response.info(). __class__;



出力してください。


Server: GitHub.com
Content-Type: text/html; charset=utf-8
Last-Modified: Tue, 02 Sep 2014 17:01:39 GMT
Expires: Wed, 03 Sep 2014 15:23:02 GMT
Cache-Control: max-age=600
Content-Length: 4784
Accept-Ranges: bytes
Date: Wed, 03 Sep 2014 16:38:29 GMT
Via: 1.1 varnish
Age: 5127
Connection: close
X-Served-By: cache-lax1433-LAX
X-Cache: HIT
X-Cache-Hits: 1
X-Timer: S1409762309.465760,VS0,VE0
Vary: Accept-Encoding

Class: httplib.HTTPMessage







オープナーとハンドラ

ここでは、OpenerとHandlerについて紹介します。

Openerとは?実は、上記の例ではOpenerを使用していますが、これはurlopenです。これはデフォルトのオープナーであり、より適切なオープナーを作成できるネットワークアクセスのケースが多く存在します。

ハンドラとは?実はOpenerはHandlerを呼び出してアクセスの雑学を処理しています。Handlerは特定のプロトコル(例えばFTPやHTTP)に対して、アクセスをどう処理するか知っているので重要で、例えばリダイレクトの処理を助けてくれるでしょう。

アクセス時に、あなたは、例えば、Openerがクッキーを処理できるようにしたい、あるいは、Openerがリダイレクトの処理を手助けすることを望まない、Openerに対する要求を持っているかもしれません。

必要なOpenerをどのように生成するか?(余談ですが、ここでのOpenerの生成方法は、デザインパターンのBuilderパターンに似ていると思います。全く同じではありませんが、パターンを理解した上で読み進めると良いと思います。 ビルダーパターン );

オープナーを作成するには、以下のような OpenerDirector をインスタンス化します。
を実行し、 .add_handler(some_handler_instance) を呼び出します。

build_opener はデフォルトでいくつかのハンドラを追加しますが、デフォルトのハンドラを追加または更新する迅速な方法を提供します。
その他、プロキシ、バリデーションなど、一般的ではあるがやや特殊なケースもハンドラで扱いたいところである。

ハンドラがリダイレクトを処理してくれるということですが、リダイレクトを望まない場合はどうすればいいのでしょうか。ハンドラをカスタマイズしてください。


mport urllib
import urllib2


class RedirectHandler(urllib2.HTTPRedirectHandler):# This RedirectHandler inherits from HTTPRedirectHandler, however, it overrides the methods of the parent class, leaving it to do nothing and lose its redirect functionality.
    def http_error_301(self, req, fp, code, msg, headers):
        pass
    def http_error_302(self, req, fp, code, msg, headers):
        pass


webo = "http://weibo.com/u/2103243911"; # The visit is to my Twitter page, because normally a redirect occurs on the visit
opener = urllib2.build_opener(RedirectHandler) # Here, we customize an opener, adding a custom handler that handles redirects when they occur
response = opener.open(webo);# response = urllib2.urlopen(webo);
print response.geturl();
urllib2.install_opener(opener); # install the custom opener, which will be returned when urllib2 is called later.



と出力されます。
urllib2.HTTPError: HTTP Error 302: Moved Temporarily

HTTPエラー302が発生したのは、Twitterのプロフィールにアクセスしたときに、リダイレクトするはずなのに、自前のリダイレクトハンドラーが何もせず、結果的に例外が発生してしまったからです。

カスタムOpenerを作成するためのurllib2クラス図は以下のとおりです。




基本認証

ウェブサイトが登録やログイン機能を提供している場合、一般的にユーザー名/パスワードが必要です。システムがユーザー名/パスワードの入力を求めるページにアクセスした場合、この処理を「認証」と呼び、サーバー側で行われる操作となります。これは、一部のページにセキュリティを提供するものです。

基本的なAuthenticationの流れは、以下のようになります。

  1. クライアントが、あるページにアクセスするためのリクエストを行う。
  2. サーバーは認証を求めるエラーを返します。
  3. クライアントは、ユーザー名/パスワードをエンコードして(通常はこのように)サーバーに送信します。
  4. サーバーは、ユーザー名とパスワードのペアが正しいかどうかをチェックし、ユーザーが要求したページか何らかのエラーを返します。

上記の処理、そしてもしかしたら他の形態もあるかもしれませんが、ここではより一般的な例としてご紹介します。

通常、サーバーはアクセスされたページが許可されていないことを示す401エラーを返し、返ってきたレスポンスのヘッダには

WWW-Authenticate: SCHEME realm="REALM".

例えば、cPanelの管理者用アプリケーションにアクセスしたい場合、次のようなヘッダーを受け取ります。 WWW-Authenticate: Basic realm="cPanel" (cPanel は、ウェブホスティング業界で最も評判の高い商用ソフトウェアパッケージの一つで、Linux と BSD をベースに PHP で開発されたクローズドソースです。cPanel は主にクライアント指向のコントロールシステムです)

ページにアクセスすると、opener は様々な状況を処理するためにハンドラを呼び出します。Authentication を処理するハンドラは urllib2.HTTPBasicAuthHandler で、これにはユーザーパスワード管理用の urllib2.HTTPPasswordMgr も必要です。

残念ながら、HTTPPasswordMgrには、ページを取得する前にそのrealmを知る必要があるという小さな問題があります。幸い、これにはHTTPPasswordMgrWithDefaultRealmといういとこがあり、realmを事前に知らなくても、realmパラメータの位置に、Noneを渡すことができ、より使い勝手がよくなっています。

以下のコードを参照してください。


import urllib2


url = 'http://www.weibo.com'# corresponding to domain, account, password
username = 'zhonghuan'
password = 'forget_it'
passman = urllib2.HTTPPasswordMgrWithDefaultRealm() # Create a password manager
passman.add_password(None, url, username, password)# parameter form (realm, URL, UserName, Password)
authhandler = urllib2.HTTPBasicAuthHandler(passman)# create Authentication's handler
opener = urllib2.build_opener(authhandler)
urllib2.install_opener(opener) # Same as described above, after install_opener, every time urllib2's urlopen is called, it returns this opener
pagehandle = urllib2.urlopen(url)







プロキシ

urllib2 はこのセットアッププロキシをよくサポートしており、パラメータにマップ、キーにプロキシのアクセスプロトコル名、値にプロキシのアドレスを指定した ProxyHandler を直接インスタンス化することが可能です。以下のコードの実装を見てください。


import urllib2


enable_proxy = True
proxy_handler = urllib2.ProxyHandler({"http" : 'http://some-proxy.com:8080'})
null_proxy_handler = urllib2.
if enable_proxy:
    opener = urllib2.build_opener(proxy_handler)
else:
    opener = urllib2.build_opener(null_proxy_handler)
urllib2.install_opener(opener)







タイムアウトの設定

古いバージョンのPythonでは、urllib2 APIはTimeoutの設定を公開しておらず、Timeoutの値を設定するには、ソケットのグローバルなTimeoutの値を変更するしか方法がありません。


import urllib2
import socket


socket.setdefaulttimeout(10) # timeout after 10 seconds
urllib2.socket.setdefaulttimeout(10) # Another way



Python 2.6 以降では、タイムアウトは urllib2.urlopen() の timeout パラメータで直接設定することができます。


import urllib2

response = urllib2.urlopen('http://www.google.com', timeout=10)







クッキー

urllib2によるクッキーの扱いも自動化されています。もし、クッキーの項目の値を取得する必要がある場合は、それを行うことができます。


import urllib2
import cookielib


cookie = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
response = opener.open('http://www.google.com')
for item in cookie:
    if item.name == 'some_cookie_item_name':
        print item.value







デバッグログ

urllib2 を使用する場合、以下の方法でデバッグログをオンにすると、送受信されるパケットの内容が画面に表示され、デバッグが容易になり、パケットをキャプチャする必要がなくなる場合もあります。


import urllib2


httpHandler = urllib2.HTTPHandler(debuglevel=1)
httpsHandler = urllib2.HTTPSHandler(debuglevel=1)
opener = urllib2.build_opener(httpHandler, httpsHandler)
urllib2.install_opener(opener)
response = urllib2.urlopen('http://www.google.com')




参考

  1. python urllib2 の使用 (推奨)

  2. Pythonウェブクローラー入門チュートリアル (推奨)

  3. CGIスクリプト入門 <スパン (推奨)

  4. urllib2 ソースコードビニェット

  5. Python標準ライブラリurllib2使用時の詳細説明 (推奨)

  6. Pythonによる認証 (推奨)

  7. http://en.wikipedia.org/wiki/List_of_HTTP_header_fields