2016/05/30

PythonのurllibでWebページの内容とヘッダを取得

Pythonの “urllib.request” (Python 2では “urllib2”) というモジュールを使用すると、サーバとの通信の仕方やその仕様が分からなくても、オブジェクトの操作という形で簡単にWebページの内容を取得してその内容やレスポンスヘッダを扱うことができる。同モジュールにはファイルをローカルに保存する機能もあるが、ここでは扱わない。

本記事では、HTTPサーバからWebページの内容を取得して、これをレスポンスヘッダとともに出力するという操作を行う。

以前、同様の内容を2008年5月に扱っているが、新しいurllibの使い方やPython 3での変更点などの関係で大部分の内容を書き直し、更にプロキシサーバ関係の内容を追加している。

  1. 扱い方
  2. 簡単な例
  3. ユーザエージェントの扱いと変更方法
  4. プロキシサーバの扱い
  5. ユーザエージェントやプロキシの処理を含む例
  6. 他記事における使用例(追記)

扱い方

  • urlopen()関数を前述のモジュールからインポートし、 “http://” もしくは “https://” で始まるURL文字列を引数に指定して呼び出し、戻り値としてオブジェクトを得る
    • このオブジェクトはファイルオブジェクトに近い扱い方が可能で、readline()などのメンバ関数やfor文[1]でページの本文の内容が得られ、close()で閉じる(後始末処理を行う)ことができる
    • オブジェクトのメンバ関数info()を呼ぶと戻り値としてレスポンスヘッダの情報が得られる
      • ヘッダ情報のオブジェクトのメンバ関数get()を呼ぶことにより、特定の名前のヘッダ(例:Content-Type)の値を取り出すことができる[2]
  • Python 3では本文の内容はバイト型で得られ、文字列として扱うにはページのエンコーディングに合わせてstr()を “encoding=” 付きで呼ぶ
  • Python 3とPython 2の両方で同じように動くものが作りたい場合は “urllib.request” からのインポートを試みてImportError例外が発生するかどうかなどでバージョン判別を行い、動作の違いを吸収するための同名の関数をそれぞれに対して用意するなどする

簡単な例

UTF-8エンコーディングのWebページのURL文字列を引数に指定して下のスクリプトを実行すると、レスポンスヘッダとページ本文の内容が標準出力に出力される。

[任意]ファイル名:retrieve-webpage.py
#! /usr/bin/python
# -*- coding: utf-8 -*-

from __future__ import print_function

# Python 3では、Python 2で "urllib2" とされていたものをもとに
# 細かく分割されており、 "urllib2" の名前では存在しない
try:
  from urllib.request import urlopen  # Python 3
  # 取得したテキストを表示するための処理がPython 3とPython 2とで異なるため
  # 表示用の関数をそれぞれ定義しておくことにする
  # 入力は改行文字を含むため、print()のend引数を空文字列にする
  def print_line (l):
    print (str (l, encoding = 'utf8'), end = '')
except ImportError:
  from urllib2 import urlopen         # Python 2
  def print_line (l):
    print (l, end = '')
import sys


if len (sys.argv) < 2:
  sys.exit ('USAGE: {0} [URL]'.format (__file__))
# "http://www.example.com/" などのURL文字列を受け付ける
# HTTP,HTTPSのいずれのURLスキームも指定されていない場合("www.example.com" など)
# HTTPと解釈することにする
url = sys.argv[1]
if not url.startswith ('http://') and not url.startswith ('https://'):
  url = 'http://' + url

try:
  # URLを開く
  f = urlopen (url)

  # HTTPヘッダの各行の内容を表示
  for l in str (f.info ()).rstrip ().splitlines ():
    print (l)

  print ('-' * 80)

  # Content-Typeを取得し、テキスト系の形式(text/*)でのみ内容を表示する
  if f.info ().get ('Content-Type').startswith ('text/'):
    try:
      for l in f:
        # Python 3では行ごとのデータはバイト列として得られるので
        # エンコーディングを指定してデコードする必要がある
        print_line (l)
    except IOError as e:
      sys.exit ('Could not read from URL "{0}": {1}'.format (url, e))
    finally:
      f.close ()
  else:
    print ('(Binary data)')
    f.close ()

except IOError as e:
  sys.exit ('Could not open URL "{0}": {1}'.format (url, e))
実行例
$ /path/to/retrieve-webpage.py http://www.example.com/
Accept-Ranges: bytes
...
Connection: close
--------------------------------------------------------------------------------
<!doctype html>
<html>
...
</html>

ユーザエージェントの扱いと変更方法

既定では “urllib.request” (旧urllib2) を用いてHTTPサーバに要求を送信する際に伝えるユーザエージェント文字列は “Python-urllib/X.Y” [3]となっている。

これを変更するには、事前にRequestクラスをインポートしておき、urlopen()の引数に文字列ではなく同クラスのオブジェクトを事前に作成して入れ、そのオブジェクト作成時の最初の引数にURL文字列を指定したうえで辞書型の引数headersの “User-Agent” に対する値として別のユーザエージェント文字列を指定する。

もう1つの方法はbuild_opener()で作成したOpenerDirectorクラスのオブジェクトのメンバaddheadersに “[('User-agent', 'CUSTOM USER AGENT')]” 形式の値[4]を代入してメンバ関数open()urlopen()の代わりに呼び出すものとなる。

プロキシサーバの扱い

GNU/Linuxでurllibを使用する場合、プロキシサーバについては実行時の環境変数により自動的に処理される。

プロトコル環境変数名
HTTPhttp_proxy
HTTPShttps_proxy

値には “http://[プロキシサーバ]:[ポート番号]” 形式の文字列を指定する。

(HTTPとHTTPSの両方でローカルプロキシの8080番を一時的に指定する例)
$ http_proxy='http://localhost:8080' https_proxy='http://localhost:8080' /path/to/retrieve-webpage.py https://www.example.com/

常にこの設定が必要な場合は使用している端末シェルの初期化設定ファイル(Bashの場合は[ホームディレクトリ]/.bashrc)やグラフィカルログイン時の自動実行スクリプト([ホームディレクトリ]/.xprofileなど・GUI環境依存)やOSごとの環境変数設定ファイル(ディストリ依存)などで環境変数を設定しておく。

環境変数による処理を行わずにプログラム内で設定を行うことも可能で、build_opener()ProxyHandlerクラスのオブジェクト[5]を渡してメンバ関数open()でURLを開く。

ユーザエージェントやプロキシの処理を含む例

下は見出し:簡単な例のサンプルをもとにしてユーザエージェントを指定したものとなる。使い方や出力は基本的に同じ。プロキシ関係の設定をプログラムで行う場合の処理についても、コメントアウトされた形で入れている(使用する場合は環境に合わせた編集が必要)。

[任意]ファイル名:retrieve-webpage-custom-ua.py
#! /usr/bin/python
# -*- coding: utf-8 -*-

from __future__ import print_function

try:
  from urllib.request import Request, urlopen  # Python 3
##from urllib.request import ProxyHandler, build_opener
  def print_line (l):
    print (str (l, encoding = 'utf8'), end = '')
except ImportError:
  from urllib2 import Request, urlopen         # Python 2
##from urllib2 import ProxyHandler, build_opener
  def print_line (l):
    print (l, end = '')
import sys


if len (sys.argv) < 2:
  sys.exit ('USAGE: {0} [URL]'.format (__file__))
url = sys.argv[1]
if not url.startswith ('http://') and not url.startswith ('https://'):
  url = 'http://' + url

try:
  # 辞書型の引数headersにて "User-Agent" に対する値として文字列を記述した
  # RequestクラスのオブジェクトをURL文字列の代わりに渡すと
  # 任意の文字列がユーザエージェントとして使用できる
  f = urlopen (Request (url, headers = {'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240'}))

  # build_opener()で作成したOpenerDirectorクラスのオブジェクトの
  # メンバ関数open()を *urlopen()の代わりに* 用いることもできる
  # (別途import文でbuild_openerもインポートしておく)
##od = build_opener ()
  # プロキシの設定を環境変数からではなくプログラム内で行う場合は
  # build_opener()の引数にProxyHandlerクラスのオブジェクトを渡す
  # (別途import文でProxyHandlerもインポートしておく)
##od = build_opener (ProxyHandler ({'http' : 'http://localhost:8080', 'https' : 'http://localhost:8080'}))
  # メンバaddheadersでユーザエージェントを書き換えることができる("a" は小文字)
##od.addheaders = [('User-agent', 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240')]
  # 下のコメントを解除する場合は上にあるurlopen()の行をコメントにするか消す
##f = od.open (url)

  for l in str (f.info ()).rstrip ().splitlines ():
    print (l)

  print ('-' * 80)

  if f.info ().get ('Content-Type').startswith ('text/'):
    try:
      for l in f:
        print_line (l)
    except IOError as e:
      sys.exit ('Could not read from URL "{0}": {1}'.format (url, e))
    finally:
      f.close ()
  else:
    print ('(Binary data)')
    f.close ()

except IOError as e:
  sys.exit ('Could not open URL "{0}": {1}'.format (url, e))

他記事における使用例(追記)

使用したバージョン:
  • Python 2.7.11, 3.5.1
[1]: “for [行データを入れる変数名] in [urlopen()の戻り値]:” の形で各行の内容を繰り返し得る
[2]: 引数に指定した名前のヘッダがなければ “None” が返る
[3]: XとYはPythonのバージョンを示す数字
[4]: “CUSTOM USER AGENT” とした部分にユーザエージェント文字列を入れる
[5]: 作成時に辞書型の引数でプロキシ設定を記述する