如何使用urlopen获取非ASCII字符的URL?

56

我需要从一个包含非ASCII字符的URL中提取数据,但是urllib2.urlopen拒绝打开该资源并引发以下错误:

UnicodeEncodeError: 'ascii' codec can't encode character u'\u0131' in position 26: ordinal not in range(128)

我知道这个URL不符合标准,但我没有机会去改变它。
那么,如何使用Python访问一个包含非ASCII字符的URL指向的资源呢?
编辑:换句话说,urlopen能否打开像这样的URL:
http://example.org/Ñöñ-ÅŞÇİİ/
10个回答

57

严格来说,URI不能包含非ASCII字符;你在那里使用的是IRI

要将IRI转换为纯ASCII URI:

  • 地址中主机名部分的非ASCII字符必须使用基于Punycode的IDNA算法进行编码;

  • 路径和大多数其他地址部分中的非ASCII字符必须使用UTF-8和%-编码进行编码,如Ignacio的答案所述。

所以:

import re, urlparse

def urlEncodeNonAscii(b):
    return re.sub('[\x80-\xFF]', lambda c: '%%%02x' % ord(c.group(0)), b)

def iriToUri(iri):
    parts= urlparse.urlparse(iri)
    return urlparse.urlunparse(
        part.encode('idna') if parti==1 else urlEncodeNonAscii(part.encode('utf-8'))
        for parti, part in enumerate(parts)
    )

>>> iriToUri(u'http://www.a\u0131b.com/a\u0131b')
'http://www.xn--ab-hpa.com/a%c4%b1b'

严格来讲,这在一般情况下还不够好,因为urlparse并没有将主机名上的user:pass@前缀或:port后缀分离出来。只有主机名部分应该被IDNA编码。在构造URL时,使用普通的urllib.quote.encode('idna')进行编码比拆解IRI要容易得多。


1
尽管这似乎是一个非常小众的问题,但它确实解决了我自己的一个非常具体的问题。非常好的答案。 - Llanilek
1
如何在Python 3中优雅地处理这个问题?有什么建议吗? - zeekvfu
1
这对于提供文件服务非常有效,其中文件名可能包含非美国字符,例如汉字符号! - Mike McMahon
2
在Python 3中,您需要使用import urllib.parse而不是urlparse。在urlEncodeNonAscii中解码b:b.decode('utf-8'),并将IRI的idna部分排除在iriToUri之外:return urllib.parse.urlunparse([url_encode_non_ascii(part.encode('utf-8')) for part in parts]) - RvdBerg
属性错误:模块“urllib”没有属性“unparse”。 - Mona Jalal
显示剩余2条评论

50

在Python 3中,对于非ASCII字符串,请使用urllib.parse.quote函数:

>>> from urllib.request import urlopen                                                                                                                                                            
>>> from urllib.parse import quote                                                                                                                                                                
>>> chinese_wikipedia = 'http://zh.wikipedia.org/wiki/Wikipedia:' + quote('首页')
>>> urlopen(chinese_wikipedia)

5
简单而有效! :D - bodruk
5
比其他答案要好得多。 - jigglypuff
2
这是一个很棒的解决方案。在使用带有url的Kanji时解决了我的问题,可以处理日文字集。 - Programming_Learner_DK
2
哇,这个被低估了。 - Polamin Singhasuwich
请注意,此代码无法正确处理主机名(IDNA)。 - Alex Shpilkin
对于希腊字母和像德语中的带有分音符号的字母(如ö)这样的奇怪字符,这对我很有效。 - Thalis

27

Python 3有用于处理此情况的库。使用urllib.parse.urlsplit将URL拆分为其组成部分, 并使用urllib.parse.quote正确引用/转义Unicode字符,并使用urllib.parse.urlunsplit将其重新连接在一起。

>>> import urllib.parse
>>> url = 'http://example.com/unicodè'
>>> url = urllib.parse.urlsplit(url)
>>> url = list(url)
>>> url[2] = urllib.parse.quote(url[2])
>>> url = urllib.parse.urlunsplit(url)
>>> print(url)
http://example.com/unicod%C3%A8

1
@user230137 你说它不工作是什么意思?对我来说完美无缺。 - darkfeline
请注意,此代码无法正确处理主机名(IDNA)。 - Alex Shpilkin
1
urllib.parse.quote(url, safe=':/') - dr.Pep

7

与@bobince提供的答案所示比起来,这更加复杂:

  • netloc应使用IDNA进行编码;
  • 非ASCII URL路径应先编码为UTF-8,然后百分号转义;
  • 非ASCII查询参数应编码为从页面URL中提取的编码(或服务器使用的编码),然后百分号转义。

所有浏览器都是这样工作的;在https://url.spec.whatwg.org/中有详细规定 - 可以查看此示例。Python实现可以在w3lib中找到(这是Scrapy正在使用的库);请参阅w3lib.url.safe_url_string

from w3lib.url import safe_url_string
url = safe_url_string(u'http://example.org/Ñöñ-ÅŞÇİİ/', encoding="<page encoding>")

检查URL转义实现是否不正确/不完整的简单方法是检查它是否提供“页面编码”参数。


7

根据@darkfeline的回答:

from urllib.parse import urlsplit, urlunsplit, quote

def iri2uri(iri):
    """
    Convert an IRI to a URI (Python 3).
    """
    uri = ''
    if isinstance(iri, str):
        (scheme, netloc, path, query, fragment) = urlsplit(iri)
        scheme = quote(scheme)
        netloc = netloc.encode('idna').decode('utf-8')
        path = quote(path)
        query = quote(query)
        fragment = quote(fragment)
        uri = urlunsplit((scheme, netloc, path, query, fragment))

    return uri

这里有一些问题:1)scheme不支持百分号编码的字符,2)路径和查询中有不同的字符是安全的,而不是路径中的那些应该被百分号编码,引用的默认安全字符是针对路径组件的 - Werkzeug 有一个更好的 iri2uri 实现[ref]。 - Iwan Aucamp

5

对于那些不仅仅依靠urllib的人,一个实用的替代方案是requests,它可以直接处理IRI。

例如,对于 http://bücher.ch

>>> import requests
>>> r = requests.get(u'http://b\u00DCcher.ch')
>>> r.status_code
200

太棒了!谢谢!在我的情况下,我试图以字节格式下载一个文件(.png)。 我的原始代码: urllib.request.urlopen(url).read()更改为使用requests后的代码: requests.get(url).content - AngryCoder

4

unicode 编码为 UTF-8,然后进行 URL 编码。


谢谢您的回复。您能具体说明一下吗?unicode(url, 'utf-8')会引发TypeError: decoding Unicode is not supported错误。另外,您建议使用哪个函数来编码URL?例如urlencode是用于构建查询字符串的,但我的URL只是服务器上的路径。 - onurmatik
2
http://farmdev.com/talks/unicode/ http://docs.python.org/library/urllib.html#urllib.quote - Ignacio Vazquez-Abrams
2
对于第一部分,您需要使用 url.encode('utf-8')(假设 url 是一个 unicode 对象)。 - Karl Knechtel
@ignacio:谢谢。我仍然认为问题在于urlopen不接受非ASCII字符作为URL(从某种程度上来说,这是正确的,因为它们不是标准的)。请看我在问题中的更新。 - onurmatik

4

使用httplib2iri2uri方法。它可以完成与bobin(他/她是那个作者吗?)相同的事情。


1
提出的解决方案不适用于非ASCII域名(IRI)。urllib2.urlopen(httplib2.iri2uri("http://домены.рф"), timeout=15)返回urlopen error [Errno -2] Name or service not known - maxkoryukov

1

将IRI转换为ASCII URI的另一种选项是使用furl包:

gruns/furl:轻松解析和操作URL。-https://github.com/gruns/furl

Python的标准urllib和urlparse模块提供了许多与URL相关的函数,但使用这些函数执行常见的URL操作变得很繁琐。Furl使解析和操作URL变得容易。

示例

非ASCII域名

http://国立極地研究所.jp/english/(日本极地研究所网站)

import furl

url = 'http://国立極地研究所.jp/english/'
furl.furl(url).tostr()

'http://xn--vcsoey76a2hh0vtuid5qa.jp/english/'

非ASCII路径

https://ja.wikipedia.org/wiki/日本語(维基百科中的“日语”文章)

import furl

url = 'https://ja.wikipedia.org/wiki/日本語'
furl.furl(url).tostr()

'https://ja.wikipedia.org/wiki/%E6%97%A5%E6%9C%AC%E8%AA%9E'

0

成功了!终于

我无法避免这些奇怪的字符,但最终我克服了它。

import urllib.request
import os


url = "http://www.fourtourismblog.it/le-nuove-tendenze-del-marketing-tenere-docchio/"
with urllib.request.urlopen(url) as file:
    html = file.read()
with open("marketingturismo.html", "w", encoding='utf-8') as file:
    file.write(str(html.decode('utf-8')))
os.system("marketingturismo.html")

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接