urllib.requestを使って日本語ドメインを含むURLを取得しようとするとエラーになる

# Python 3.6.1
import urllib.request
res = urllib.request.urlopen('http://日本語.jp/about/')
# UnicodeEncodeError: 'latin-1' codec can't encode characters in position 0-2: ordinal not in range(256)

urllibの手足である http.clientsocket は既に国際化ドメインに対応してます。

しかし、urllib が punycode で符号化せずに Host ヘッダーをくっつけているので、エラーになっている模様。

回避策① 手動でHostヘッダーを設定する

import urllib.request

req = urllib.request.Request('http://日本語.jp/about/')
req.add_header('Host', req.host.encode('idna'))  # これ!
res = urllib.request.urlopen(req)

# あとは通常どおり
print(res.status, res.reason)
content = res.read()
print(repr(content[:1000]))

回避策② 他のライブラリを使う

たまに使われているのを見かけるrequests、こちらは国際化ドメインに対応してました。

しかし、標準ライブラリに入っていないので別途インストールが必要です。依存ライブラリが多いのでWindowsでもpipを使ってインストールした方がいいでしょう。コマンドラインから python -m pip install requests みたにやると一発で入ります。

import requests
res = requests.get('http://日本語.jp/about/')
print(res.status_code, res.reason)
print(repr(res.content[:1000]))

punycodeとidnaエンコーディングの違い

文字列をpunycode化するには、こうしますが、

print('abc日本語def'.encode('punycode').decode('ascii'))
# abcdef-rl2mm8fl32k

ドメインの場合は各階層ごとにpunyる必要があるので、idnaを使います。

print('あいう.abc.日本語.jp'.encode('idna').decode('ascii'))
# xn--l8jeg.abc.xn--wgv71a119e.jp

以上です。

参考

encodings.idna — アプリケーションにおける国際化ドメイン名 (IDNA)
idnaコーデックの説明。使うべき場所やドメインの正規化について軽く触れている。
Requests: HTTP for Humans
requestsの使い方。ドキュメントが充実している。やや古いけど日本語の翻訳もある。