Pythonのurllibライブラリ徹底解説:HTTPリクエストからURL解析まで

Python

Python標準ライブラリでWebアクセスをマスターしよう!

Pythonには、ウェブサイトへのアクセスやデータの取得、URLの操作などを簡単に行うための強力な標準ライブラリ urllib が用意されています。標準ライブラリであるため、Pythonがインストールされていれば追加のインストールなしにすぐに利用を開始できるのが大きなメリットです。🎉

urllib は単一のモジュールではなく、いくつかのサブモジュールが集まったパッケージです。それぞれのサブモジュールが特定の機能を提供しています。主なサブモジュールは以下の通りです。

  • urllib.request: URLを開いて読み取るためのモジュール。HTTPリクエストの送信(GET, POSTなど)やレスポンスの取得、認証、リダイレクト、Cookieの処理などを担当します。
  • urllib.parse: URL文字列を解析(パース)したり、構成要素からURLを構築したり、URLエンコーディング・デコーディングを行ったりするためのモジュールです。
  • urllib.error: urllib.request がリクエスト処理中に発生させる例外クラス(URLError, HTTPError など)を含みます。
  • urllib.robotparser: robots.txt ファイルを解析するためのモジュールです。ウェブスクレイピングを行う際に、対象サイトのアクセスルールを確認するために使用します。

この記事では、これらのサブモジュール、特に urllib.requesturllib.parse を中心に、基本的な使い方から少し応用的な内容まで、具体的なコード例を交えながら詳しく解説していきます。Web APIからのデータ取得や簡単なWebスクレイピングなど、Pythonでインターネット上のリソースを扱いたいと考えている方にとって、urllib は避けて通れない重要なライブラリです。さっそくその世界を探検してみましょう!🚀

urllib.request: Webリソースへのアクセス

urllib.request は、URLを開いてデータを取得するための最も基本的なモジュールです。HTTP、HTTPS、FTPなどの様々なプロトコルに対応しています。

基本的なGETリクエスト

最も簡単な使い方は urlopen() 関数を使うことです。引数にアクセスしたいURLを文字列で指定します。


import urllib.request

url = 'https://www.example.com/'

try:
    # URLを開き、レスポンスオブジェクトを取得
    with urllib.request.urlopen(url) as response:
        # レスポンス情報を取得
        print(f"ステータスコード: {response.getcode()}") # HTTPステータスコード (例: 200)
        print(f"実際のURL: {response.geturl()}")      # リダイレクトされた場合の最終的なURL
        print("レスポンスヘッダー:")
        # info()メソッドはhttp.client.HTTPMessageオブジェクトを返す
        headers = response.info()
        for header, value in headers.items():
            print(f"  {header}: {value}")

        # レスポンスボディを読み込む (バイト列)
        body_bytes = response.read()

        # バイト列を適切なエンコーディングでデコードして文字列にする
        # Content-Typeヘッダーからエンコーディングを取得するのが一般的
        # ここでは例としてUTF-8を使用
        try:
            body_string = body_bytes.decode('utf-8')
            print("\nレスポンスボディ (最初の500文字):")
            print(body_string[:500]) # 全文表示は長くなるので一部のみ
        except UnicodeDecodeError:
            print("\nレスポンスボディのデコードに失敗しました。エンコーディングを確認してください。")

except urllib.error.URLError as e:
    print(f"URLエラーが発生しました: {e.reason}")
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}")
        

urlopen()http.client.HTTPResponse オブジェクトを返します。このオブジェクトには、read(), getcode(), info() (ヘッダー情報を取得), geturl() などのメソッドがあります。

read() メソッドはレスポンスボディをバイト列として返します。HTMLなどのテキストデータを扱う場合は、適切な文字エンコーディング(例: UTF-8)を指定して文字列にデコードする必要があります。エンコーディングはレスポンスヘッダーの Content-Type から取得するのが一般的です。

with 文を使うことで、処理終了後に自動的にレスポンスオブジェクトがクローズされるため、リソースリークを防ぐことができます。✅

POSTリクエスト

サーバーにデータを送信する場合はPOSTリクエストを使用します。フォームの送信やAPIへのデータ登録などで使われます。urlopen() 関数の data 引数に送信したいデータをバイト列で指定します。多くの場合、データは urllib.parse.urlencode() を使ってURLエンコードする必要があります。


import urllib.request
import urllib.parse

url = 'https://httpbin.org/post' # POSTリクエストを受け付けるテスト用URL
# 送信するデータ (辞書形式)
values = {
    'name': 'Taro Yamada',
    'email': 'taro@example.com',
    'query': 'Python urllib'
}

try:
    # データをURLエンコードし、バイト列に変換
    data = urllib.parse.urlencode(values).encode('utf-8')

    # data引数を指定すると、自動的にPOSTリクエストになる
    with urllib.request.urlopen(url, data=data) as response:
        print(f"ステータスコード: {response.getcode()}")
        body_bytes = response.read()
        body_string = body_bytes.decode('utf-8')
        print("\nレスポンスボディ:")
        # httpbin.org/postは送信されたデータをJSON形式で返す
        import json
        print(json.dumps(json.loads(body_string), indent=2, ensure_ascii=False))

except urllib.error.HTTPError as e:
    print(f"HTTPエラーが発生しました: {e.code} {e.reason}")
    # エラーレスポンスの内容も読み取れる場合がある
    error_body = e.read().decode('utf-8')
    print(f"エラーレスポンス: {error_body}")
except urllib.error.URLError as e:
    print(f"URLエラーが発生しました: {e.reason}")
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}")
        

data 引数にデータを渡すと、urlopen() は自動的にHTTPメソッドをPOSTと判断します。

リクエストヘッダーのカスタマイズ (Requestオブジェクト)

User-Agentを変更したり、特定のヘッダー(例: Accept, Authorization)を追加したい場合は、urllib.request.Request オブジェクトを使用します。


import urllib.request
import urllib.parse

url = 'https://httpbin.org/headers' # 送信したヘッダー情報を返すテスト用URL
headers = {
    'User-Agent': 'My Awesome Python Client/1.0',
    'Accept': 'application/json',
    'X-Custom-Header': 'MyValue'
}

# Requestオブジェクトを作成
# GETリクエストの場合はdata=None
req = urllib.request.Request(url, data=None, headers=headers)

# POSTリクエストの場合の例
# post_url = 'https://httpbin.org/post'
# post_values = {'key': 'value'}
# post_data = urllib.parse.urlencode(post_values).encode('utf-8')
# post_req = urllib.request.Request(post_url, data=post_data, headers=headers, method='POST')
# method引数で明示的にPOSTを指定することも可能

try:
    with urllib.request.urlopen(req) as response:
        print(f"ステータスコード: {response.getcode()}")
        body_string = response.read().decode('utf-8')
        print("\nレスポンスボディ (ヘッダー情報):")
        import json
        print(json.dumps(json.loads(body_string), indent=2))

except urllib.error.HTTPError as e:
    print(f"HTTPエラーが発生しました: {e.code} {e.reason}")
except urllib.error.URLError as e:
    print(f"URLエラーが発生しました: {e.reason}")
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}")
        

Request オブジェクトを作成し、そのインスタンスを urlopen() に渡すことで、より詳細なリクエスト制御が可能になります。

urllib.parse: URLを自在に操る

urllib.parse モジュールは、URL文字列の解析、構築、エンコード、デコードといった操作を行うための関数を提供します。

URLの解析 (urlparse)

urlparse() 関数は、URL文字列をその構成要素(スキーム、ネットロケーション、パス、パラメータ、クエリ、フラグメント)に分解します。


from urllib.parse import urlparse

url_string = 'https://user:pass@www.example.com:8080/path/to/page;params?query1=value1&query2=value2#fragment'

parsed_result = urlparse(url_string)

print(f"解析結果: {parsed_result}")
print(f"  スキーム (scheme):   {parsed_result.scheme}")   # 'https'
print(f"  ネット locatie (netloc): {parsed_result.netloc}")  # 'user:pass@www.example.com:8080'
print(f"  パス (path):       {parsed_result.path}")     # '/path/to/page'
print(f"  パラメータ (params):   {parsed_result.params}")   # 'params'
print(f"  クエリ (query):      {parsed_result.query}")    # 'query1=value1&query2=value2'
print(f"  フラグメント (fragment): {parsed_result.fragment}") # 'fragment'

# netlocからユーザー名、パスワード、ホスト名、ポートを取得することも可能
print(f"  ユーザー名 (username): {parsed_result.username}") # 'user'
print(f"  パスワード (password): {parsed_result.password}") # 'pass'
print(f"  ホスト名 (hostname): {parsed_result.hostname}") # 'www.example.com'
print(f"  ポート (port):     {parsed_result.port}")     # 8080
        

urlparse()ParseResult という名前付きタプルを返します。これにより、各要素に属性名でアクセスできます。

クエリ文字列の解析 (parse_qs, parse_qsl)

URLのクエリ部分 (? 以降の部分) をキーと値のペアとして解析するには parse_qs() または parse_qsl() を使います。


from urllib.parse import parse_qs, parse_qsl

query_string = 'name=Taro+Yamada&age=30&hobbies=reading&hobbies=programming'

# parse_qs: 値をリストとして持つ辞書を返す
query_dict = parse_qs(query_string)
print(f"parse_qsの結果 (辞書): {query_dict}")
# {'name': ['Taro Yamada'], 'age': ['30'], 'hobbies': ['reading', 'programming']}

# parse_qsl: (キー, 値) のタプルのリストを返す
query_list = parse_qsl(query_string)
print(f"parse_qslの結果 (リスト): {query_list}")
# [('name', 'Taro Yamada'), ('age', '30'), ('hobbies', 'reading'), ('hobbies', 'programming')]

# urlparseと組み合わせる例
parsed_url = urlparse('https://example.com/?' + query_string)
query_params = parse_qs(parsed_url.query)
print(f"URLから抽出したクエリパラメータ: {query_params}")
        

parse_qs() は同じキーが複数ある場合に値をリストに格納します。parse_qsl() は単純にキーと値のペアのリストを返します。

URLの構築 (urlunparse, urlencode)

分解された要素からURL文字列を再構築するには urlunparse() を使います。クエリパラメータを辞書やリストから生成するには urlencode() を使います。


from urllib.parse import urlunparse, urlencode

# urlunparse: 6要素のタプル/リストからURL文字列を構築
components = ('https', 'www.example.com', '/path', '', 'query=value', 'fragment')
reconstructed_url = urlunparse(components)
print(f"urlunparseで再構築: {reconstructed_url}")
# https://www.example.com/path?query=value#fragment

# urlencode: 辞書や(キー, 値)タプルのリストからクエリ文字列を生成
params_dict = {'q': 'Python urllib', 'lang': 'ja'}
query_string_dict = urlencode(params_dict)
print(f"urlencode (辞書から): {query_string_dict}") # q=Python+urllib⟨=ja

params_list = [('category', 'programming'), ('sort', 'popular')]
query_string_list = urlencode(params_list)
print(f"urlencode (リストから): {query_string_list}") # category=programming&sort=popular

# urlunparseとurlencodeを組み合わせてURL全体を構築
final_url_components = ('https', 'example.com', '/search', '', urlencode(params_dict), '')
final_url = urlunparse(final_url_components)
print(f"組み合わせた最終URL: {final_url}") # https://example.com/search?q=Python+urllib⟨=ja
        

URLエンコードとデコード (quote, quote_plus, unquote, unquote_plus)

URL内で特別な意味を持つ文字(/, ?, &, =, スペースなど)や、ASCII以外の文字(日本語など)を安全に含めるためには、パーセントエンコーディング(URLエンコード)が必要です。

  • quote(string, safe='/'): 文字列をエンコードします。デフォルトでは / はエンコードしません。safe 引数でエンコードしない文字を指定できます。スペースは %20 になります。
  • quote_plus(string, safe=''): quote() と似ていますが、スペースを + にエンコードします。HTMLフォームのエンコード形式 (application/x-www-form-urlencoded) でよく使われます。デフォルトでは / もエンコードします。
  • unquote(string): パーセントエンコードされた文字列をデコードします (%20 はスペースになります)。
  • unquote_plus(string): unquote() と同様ですが、+ もスペースにデコードします。

from urllib.parse import quote, quote_plus, unquote, unquote_plus

original_string = 'こんにちは 世界 / Hello World'

# quote: / はエンコードされず、スペースは %20
encoded_quote = quote(original_string)
print(f"quote:        {encoded_quote}")
# こんにちは%20世界%20/%20Hello%20World

# quote (safe=''): / もエンコード
encoded_quote_safe = quote(original_string, safe='')
print(f"quote (safe=''): {encoded_quote_safe}")
# %E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF%20%E4%B8%96%E7%95%8C%20%2F%20Hello%20World

# quote_plus: / はエンコードされ、スペースは +
encoded_quote_plus = quote_plus(original_string)
print(f"quote_plus:   {encoded_quote_plus}")
# %E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF+%E4%B8%96%E7%95%8C+%2F+Hello+World

# デコード
decoded_unquote = unquote(encoded_quote)
print(f"unquote:      {decoded_unquote}")
# こんにちは 世界 / Hello World

decoded_unquote_plus = unquote_plus(encoded_quote_plus)
print(f"unquote_plus: {decoded_unquote_plus}")
# こんにちは 世界 / Hello World

# quote_plusでエンコードしたものをunquoteでデコードすると '+' が残る
print(f"unquote(quote_plus): {unquote(encoded_quote_plus)}")
# こんにちは+世界+/+Hello+World
        

どのエンコード/デコード関数を使うかは、対象となるURLのどの部分を扱っているか(パス部分か、クエリパラメータかなど)や、通信先のサーバーが期待する形式によって異なります。🤔

urllib.error: エラーハンドリング

Webリソースへのアクセスには様々なエラーが伴います。サーバーが見つからない、アクセス権がない、サーバー内部でエラーが発生したなど。urllib.error モジュールは、これらのエラーに対応するための例外クラスを提供します。

  • URLError: URLアクセスに関する一般的なエラーの基底クラスです。ネットワーク接続の問題(DNS解決失敗、接続拒否など)で発生します。これは OSError のサブクラスです。
    • reason 属性でエラーの原因(メッセージ文字列または別の例外インスタンス)を確認できます。
  • HTTPError: URLError のサブクラスで、HTTPエラー(4xx系のクライアントエラーや5xx系のサーバーエラーなど)が発生した場合に送出されます。
    • code 属性でHTTPステータスコード(例: 404, 500)を取得できます。
    • reason 属性でステータスコードに対応する理由フレーズ(例: ‘Not Found’, ‘Internal Server Error’)を取得できます。
    • headers 属性でエラーレスポンスのヘッダーを取得できます。
    • read() メソッドなどでエラーレスポンスのボディを読むことも可能です。
  • ContentTooShortError: urllib.request.urlretrieve() 関数(ここでは詳しく解説しませんが、URLからファイルを直接ダウンロードする関数)が、ダウンロードしたデータ量がヘッダーで示されたサイズより小さい場合に発生します。

エラーハンドリングの例:


import urllib.request
import urllib.error

urls_to_check = [
    'https://www.google.com/',         # 存在するはず
    'https://httpbin.org/status/404', # 404 Not Foundエラーを返す
    'https://httpbin.org/status/500', # 500 Internal Server Errorを返す
    'https://invalid-domain-name-that-does-not-exist.xyz/' # DNS解決エラー
]

for url in urls_to_check:
    print(f"\n--- Checking: {url} ---")
    try:
        with urllib.request.urlopen(url, timeout=10) as response: # timeout設定
            print(f"成功: ステータスコード {response.getcode()}")
            # 必要ならレスポンスを読む
            # print(response.read()[:100])

    except urllib.error.HTTPError as e:
        print(f"HTTPエラー 발생: コード={e.code}, 理由={e.reason}")
        print(f"  ヘッダー:\n{e.headers}")
        try:
            # エラーレスポンスのボディを読んでみる
            error_body = e.read().decode('utf-8')
            print(f"  エラーボディ (一部): {error_body[:200]}")
        except Exception as read_err:
            print(f"  エラーボディの読み込み中にエラー: {read_err}")

    except urllib.error.URLError as e:
        # HTTPErrorはURLErrorのサブクラスなので、先にHTTPErrorをキャッチする
        print(f"URLエラー 발생: 理由={e.reason}")
        if isinstance(e.reason, ConnectionRefusedError):
            print("  (接続が拒否されました)")
        elif isinstance(e.reason, TimeoutError):
             print("  (タイムアウトしました)")
        # 他にもsocket.gaierrorなど、reasonが例外インスタンスの場合がある

    except Exception as e:
        # 予期しないその他のエラー
        print(f"予期せぬエラー発生: {type(e).__name__} - {e}")

        

HTTPErrorURLError のサブクラスなので、except 節では HTTPError を先に書くことが重要です。⚠️ そうしないと、HTTPエラーも URLError としてキャッチされてしまい、ステータスコードなどの詳細情報にアクセスしにくくなります。

urllib.robotparser: robots.txt の遵守

Webスクレイピングやクローリングを行う際には、対象ウェブサイトの robots.txt ファイルを確認し、その指示に従うことがマナーであり、法的にも重要です。robots.txt は、ウェブサイトのルートディレクトリに置かれ、どのユーザーエージェント(クローラーの識別名)がどのパスへのアクセスを許可または禁止されているかを定義します。

urllib.robotparser モジュールは、この robots.txt ファイルを解析し、特定のURLへのアクセスが許可されているかどうかを簡単に確認する機能を提供します。


import urllib.robotparser
import urllib.request
from urllib.parse import urljoin

# 対象サイトのベースURL
base_url = 'https://www.python.org/'
# robots.txtのURL
robots_url = urljoin(base_url, 'robots.txt')

# 自分のクローラーのUser-Agent名 (適切に設定する)
user_agent = 'MyFriendlyBot/1.0 (+http://mybotwebsite.example.com/info)'
# user_agent = '*' # すべてのボットに適用されるルールを見る場合

# チェックしたいパス
paths_to_check = ['/', '/about/', '/psf-landing/', '/static/', '/secret-area/']

rp = urllib.robotparser.RobotFileParser()
rp.set_url(robots_url)

try:
    print(f"'{robots_url}' を読み込んで解析します...")
    rp.read()
    print("解析完了。")

    # robots.txtの内容を表示 (デバッグ用)
    # try:
    #     with urllib.request.urlopen(robots_url) as f:
    #         print("\n--- robots.txtの内容 ---")
    #         print(f.read().decode('utf-8'))
    #         print("------------------------\n")
    # except Exception as e:
    #     print(f"robots.txtの取得に失敗: {e}")

    # アクセス許可をチェック
    for path in paths_to_check:
        full_url = urljoin(base_url, path)
        can_access = rp.can_fetch(user_agent, full_url)
        print(f"'{user_agent}' は '{full_url}' にアクセス可能か? -> {can_access}")

    # クロール遅延 (Crawl-delay) の取得 (あれば)
    delay = rp.crawl_delay(user_agent)
    if delay:
        print(f"\n推奨されるクロール遅延 (秒): {delay}")
    else:
        print("\nクロール遅延の指定はありません。")

    # サイトマップ (Sitemap) の取得 (あれば)
    sitemaps = rp.sitemaps()
    if sitemaps:
        print("\nサイトマップ:")
        for sitemap in sitemaps:
            print(f"  {sitemap}")
    else:
        print("\nサイトマップの指定はありません。")


except urllib.error.URLError as e:
    print(f"robots.txtの読み込み中にURLエラーが発生しました: {e.reason}")
except Exception as e:
    print(f"robots.txtの処理中に予期せぬエラーが発生しました: {e}")

        
  1. RobotFileParser インスタンスを作成します。
  2. set_url()robots.txt のURLを指定します。
  3. read() でURLからファイルを読み込み、解析します。
  4. can_fetch(user_agent, url) で、指定したユーザーエージェントが特定のURLにアクセス可能かどうかを True/False で返します。
  5. crawl_delay(user_agent) で指定されたクロール遅延(秒)を取得できます。
  6. sitemaps() で指定されたサイトマップのURLリストを取得できます。

丁寧なクローラーを実装する上で、urllib.robotparser の利用は不可欠です。👍

urllib vs requests: どちらを使うべき?

PythonでHTTPリクエストを扱う際、urllib と並んでよく名前が挙がるのがサードパーティ製の requests ライブラリです。requests は「人間のためのHTTP」を標榜しており、urllib よりもシンプルで直感的なAPIを提供することを目指しています。

特徴urllibrequests
種類Python標準ライブラリサードパーティライブラリ (要 pip install requests)
依存関係なし (Python本体のみ)urllib3, chardet などに依存 (内部でurllib3を使用)
基本的な使い方 (GET)urllib.request.urlopen(url).read() (バイト列)requests.get(url).text (自動デコードされた文字列)
POSTデータ送信urlencode + encode() + urlopen(data=...)requests.post(url, data=payload) (辞書を直接渡せる)
JSONレスポンス処理json.loads(response.read().decode())response.json() (組み込みメソッド)
ヘッダー設定Request オブジェクトを使用requests.get(url, headers=...) (引数で簡単)
Cookie処理http.cookiejar との連携が必要自動で処理、Session オブジェクトで容易に管理
リダイレクト自動で処理される (ハンドラで制御可能)自動で処理される (制御可能)
タイムアウトurlopen(timeout=...)requests.get(timeout=...)
URL解析urllib.parse が提供直接的なURL解析機能は提供しない (urllib.parse を使う)
学習コストやや高め (バイト列の扱い、Requestオブジェクトなど)低め (より直感的)

どちらを選ぶべきか?

  • urllib を選ぶ場合:
    • 外部ライブラリへの依存を避けたい場合(標準ライブラリのみで完結させたい)。
    • AWS Lambdaのような環境で、デプロイパッケージを最小限に抑えたい場合。
    • HTTP通信の低レベルな詳細を学びたい、または細かく制御したい場合。
    • URLの解析やエンコード機能 (urllib.parse) が主目的の場合。
  • requests を選ぶ場合:
    • よりシンプルで直感的なコードを書きたい場合。
    • JSONの扱いやCookie管理、セッション維持などを簡単に行いたい場合。
    • 一般的なWeb APIの利用やWebスクレイピングタスク。
    • 開発効率を重視する場合。
    • Python公式ドキュメントでも高レベルなHTTPクライアントインターフェースとして推奨されています。

多くの場合、特にアプリケーション開発においては requests の方が書きやすく、生産性が高いとされています。しかし、urllib は標準ライブラリであり、基本的な機能を網羅しているため、その仕組みを理解しておくことはPythonプログラマーにとって依然として価値があります。💡

なお、urllib3 という名前のライブラリもありますが、これは urllib とは別のサードパーティライブラリです。requests は内部で urllib3 を利用しています。

まとめ

Pythonの標準ライブラリ urllib は、WebリソースへのアクセスやURL操作のための基本的な機能を提供する強力なパッケージです。

  • urllib.request でGETやPOSTリクエストを送信し、レスポンスを取得できます。ヘッダーのカスタマイズには Request オブジェクトを使います。
  • urllib.parse でURLの解析、構築、クエリパラメータの操作、エンコード/デコードが可能です。
  • urllib.errorURLErrorHTTPError を捕捉し、適切なエラーハンドリングを行います。
  • urllib.robotparserrobots.txt を解析し、クローリングのルールを遵守します。

サードパーティの requests ライブラリは多くの場合より簡潔にコードを書けますが、urllib は標準ライブラリであるという利点があり、基本的なHTTP通信の理解を深めるためにも有用です。

この記事が、PythonでWebの世界と対話するための一助となれば幸いです。Happy coding! 😊

コメント

タイトルとURLをコピーしました