花拾录
← 返回知识库

抓取中文域名抛 UnicodeEncodeError:IDNA 与 Punycode 入门

编程语言导入2026/09/220 阅读0 评论

你写了个脚本去抓一批站点,源清单里混进了几个中文域名。程序跑到这几个就抛异常退出,报的还是个跟网络八竿子打不着的编码错误。更坑的是,这个错误把真正的网络问题也一起盖住了,你对着 UnicodeEncodeError 排查了半天网络,方向全错。

现象

抛出的异常类似:

UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3:
ordinal not in range(128)

或:

UnicodeEncodeError: 'idna' codec can't encode character ...

值得注意的是:这不是「显示」问题,是「请求发不出去」问题。 报错发生在构造 HTTP 请求的那一刻,连接还没建立。

还有一种更隐蔽的情况——如果代码里用 except UnicodeEncodeError 之外的方式处理,真正的网络异常(比如超时、连接被拒)会被这个编码错误顶掉,你看到的永远是编码错误,查不出网络到底通不通。

根因

要理解这个,得先知道域名在网络上是怎么传输的。

HTTP 请求行里出现的域名,规范上必须是 ASCII。但世界上有大量非拉丁字符的域名(中文、日文、阿拉伯文等)。为了兼顾「人能看懂」和「协议只认 ASCII」,就设计了一套转换机制:

IDNA(Internationalized Domain Names in Applications,国际化域名)——它规定,非 ASCII 域名在发送前要转成 Punycode 形式。

Punycode 是一种编码方案,能把任意 Unicode 字符串表示成纯 ASCII 的字符串,并且带有固定前缀 xn--。比如「中国」这样的域名片段会被编码成 xn--fiqs8s 之类。

所以问题就清楚了:当你直接把中文域名丢给 HTTP 库去请求时,请求行里就是非 ASCII 字节,底层要按 ASCII 编码发送,编不出来,于是抛 UnicodeEncodeError。

关键是,这跟网络通不通完全没有关系。请求根本还没发出去。

解决

请求之前,先对主机名做 IDNA 编码,把它转成 Punycode:

import urllib.request
from urllib.parse import urlsplit, urlunsplit

def to_punycode(url: str) -> str:
    parts = urlsplit(url)
    host = parts.hostname
    # 把 Unicode 主机名编码成 IDNA(Punycode)形式
    ascii_host = host.encode('idna').decode('ascii')
    # 重新拼回 URL,端口等信息保持原样
    netloc = ascii_host
    if parts.port:
        netloc = f'{ascii_host}:{parts.port}'
    return urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))

url = 'http://某域名.example/path'
req = urllib.request.Request(to_punycode(url))
resp = urllib.request.urlopen(req, timeout=10)

核心就一句:host.encode('idna')。它把 Unicode 主机名按 IDNA 规则编码成 ASCII,含 xn-- 前缀。

如果是用 requests 这类库,它通常会在内部自动做 IDNA 处理,所以你可能在 requests 上没遇到过——这也是为什么这个问题容易在「手写 urllib」或「自定义 socket」的场景里冒出来。

延伸与预防

这条坑有两层收获。

第一层,具体的:任何要发到网络上的主机名,都先确认它是 ASCII。 遇到非 ASCII 域名的场景不只是中文——表情符号域名、日文域名、带变音符的欧洲域名,都走同一套 IDNA 机制。处理方法统一:encode('idna')。

第二层,更重要的排查原则:别让一个异常盖住另一个异常。

这里真正的教训是——编码错误和网络错误是两个独立的失败模式,但前者抢先抛出了,把后者藏了起来。你以为在查网络,其实在查编码;等你终于「修好」编码,才发现网络还有问题。

所以写这类代码时:

  • 把「构造请求」和「发送请求」分开处理,各自捕获自己的异常;
  • 捕获异常时,打印异常类型和完整的 traceback,不要只打印 message——类型是 UnicodeEncodeError 还是 ConnectionError,直接决定了你该往哪个方向查;
  • 当你在一个次要环节看到异常时,先问一句:「这是根因,还是它挡住了一个更深的根因?」

先分清是「编码不对」还是「网络不通」,否则会朝错误的方向调半天。 这句话适用于所有把多个子系统串起来的流程:第一个报错的,未必是真正坏掉的那个。

评论(0)

  • 还没有评论,来抢沙发~

相关文章