【问题标题】:Best way to convert a Unicode URL to ASCII (UTF-8 percent-escaped) in Python?在 Python 中将 Unicode URL 转换为 ASCII(UTF-8 百分比转义)的最佳方法?
【发布时间】:2010-10-22 16:55:21
【问题描述】:

我想知道最好的方法是什么——或者如果标准库有一个简单的方法——将域名和路径中带有 Unicode 字符的 URL 转换为等效的 ASCII URL,用域编码为 IDNA 和根据 RFC 3986 编码的路径。

我从用户那里得到一个 UTF-8 格式的 URL。因此,如果他们输入了http://➡.ws/♥,我会在 Python 中得到'http://\xe2\x9e\xa1.ws/\xe2\x99\xa5'。我想要的是 ASCII 版本:'http://xn--hgi.ws/%E2%99%A5'

我目前所做的是通过正则表达式将 URL 拆分为多个部分,然后手动对域进行 IDNA 编码,并使用不同的 urllib.quote() 调用分别对路径和查询字符串进行编码。

# url is UTF-8 here, eg: url = u'http://➡.ws/㉌'.encode('utf-8')
match = re.match(r'([a-z]{3,5})://(.+\.[a-z0-9]{1,6})'
                 r'(:\d{1,5})?(/.*?)(\?.*)?$', url, flags=re.I)
if not match:
    raise BadURLException(url)
protocol, domain, port, path, query = match.groups()

try:
    domain = unicode(domain, 'utf-8')
except UnicodeDecodeError:
    return ''  # bad UTF-8 chars in domain
domain = domain.encode('idna')

if port is None:
    port = ''

path = urllib.quote(path)

if query is None:
    query = ''
else:
    query = urllib.quote(query, safe='=&?/')

url = protocol + '://' + domain + port + path + query
# url is ASCII here, eg: url = 'http://xn--hgi.ws/%E3%89%8C'

这是正确的吗?有更好的建议吗?是否有一个简单的标准库函数可以做到这一点?

【问题讨论】:

    标签: python url unicode utf-8


    【解决方案1】:

    代码:

    import urlparse, urllib
    
    def fixurl(url):
        # turn string into unicode
        if not isinstance(url,unicode):
            url = url.decode('utf8')
    
        # parse it
        parsed = urlparse.urlsplit(url)
    
        # divide the netloc further
        userpass,at,hostport = parsed.netloc.rpartition('@')
        user,colon1,pass_ = userpass.partition(':')
        host,colon2,port = hostport.partition(':')
    
        # encode each component
        scheme = parsed.scheme.encode('utf8')
        user = urllib.quote(user.encode('utf8'))
        colon1 = colon1.encode('utf8')
        pass_ = urllib.quote(pass_.encode('utf8'))
        at = at.encode('utf8')
        host = host.encode('idna')
        colon2 = colon2.encode('utf8')
        port = port.encode('utf8')
        path = '/'.join(  # could be encoded slashes!
            urllib.quote(urllib.unquote(pce).encode('utf8'),'')
            for pce in parsed.path.split('/')
        )
        query = urllib.quote(urllib.unquote(parsed.query).encode('utf8'),'=&?/')
        fragment = urllib.quote(urllib.unquote(parsed.fragment).encode('utf8'))
    
        # put it back together
        netloc = ''.join((user,colon1,pass_,at,host,colon2,port))
        return urlparse.urlunsplit((scheme,netloc,path,query,fragment))
    
    print fixurl('http://\xe2\x9e\xa1.ws/\xe2\x99\xa5')
    print fixurl('http://\xe2\x9e\xa1.ws/\xe2\x99\xa5/%2F')
    print fixurl(u'http://Åsa:abc123@➡.ws:81/admin')
    print fixurl(u'http://➡.ws/admin')
    

    输出:

    http://xn--hgi.ws/%E2%99%A5
    http://xn--hgi.ws/%E2%99%A5/%2F
    http://%C3%85sa:abc123@xn--hgi.ws:81/admin
    http://xn--hgi.ws/admin

    阅读更多:

    编辑:

    • 修复了字符串中已引用字符的大小写问题。
    • 已将urlparse/urlunparse 更改为urlsplit/urlunsplit
    • 不要使用主机名对用户和端口信息进行编码。 (感谢耶希亚)
    • 当缺少“@”时,不要将主机/端口视为用户/密码! (感谢 hupf)

    【讨论】:

    • 很好的解决方案,谢谢。关于使用 urlparse/unparse 的好电话,并注意输入中已引用字符的情况。但我不确定你为什么需要 split('/') 逻辑,因为 urllib.quote() 已经认为斜线是安全的。另请参阅下面我的新的、经过文档测试的、更完整的解决方案。
    • 问题是 '/' 被认为是路径分隔符,而 '%2F' 不是。如果我只是取消引用字符串,它们就会变成一个并且相同。也许永远不要取消引用路径会更好,并将所有现有的 '%' 编码为 '%25'..?
    • netloc != 域,所以你应该先从user:pass@domain:port 解析出域,然后转换为idna
    • 具有用户/端口信息支持的编辑版本不再适用于没有用户或端口信息的 URL。用户名、密码和端口应该有条件地解析出来。我使用以下正则表达式来执行此操作:(?:(?P<user>[^:@]+)(?::(?P<password>[^:@]+))?@)?(?P<host>[^:]+)(?::(?P<port>[0-9]+))?,然后使用 groupdict 访问值:p.match(parsed.netloc).groupdict()
    • 值得注意的是,urllib/2/requests 不支持嵌入在“netloc”中的凭据,因为它不是 http url 的标准。因此,它们必须被删除并在使用时作为单独的参数传递。
    【解决方案2】:

    您可以改用urlparse.urlsplit,否则您似乎有一个非常简单的解决方案。

    protocol, domain, path, query, fragment = urlparse.urlsplit(url)
    

    (您可以通过访问返回值的命名属性来分别访问域和端口,但由于端口语法始终采用 ASCII 格式,因此不受 IDNA 编码过程的影响。)

    【讨论】:

      【解决方案3】:

      有一些 RFC-3896 url 解析 工作正在进行中(例如,作为代码之夏的一部分),但在标准库中还没有 AFAIK —— uri 编码方面也没什么 事情的另一面,再次 AFAIK。所以你不妨采用 MizardX 的优雅方法。

      【讨论】:

        【解决方案4】:

        好的,有了这些 cmets 和我自己的代码中的一些错误修复(它根本不处理片段),我想出了以下 canonurl() 函数——返回一个规范的 ASCII 形式的网址:

        import re
        import urllib
        import urlparse
        
        def canonurl(url):
            r"""Return the canonical, ASCII-encoded form of a UTF-8 encoded URL, or ''
            if the URL looks invalid.
        
            >>> canonurl('    ')
            ''
            >>> canonurl('www.google.com')
            'http://www.google.com/'
            >>> canonurl('bad-utf8.com/path\xff/file')
            ''
            >>> canonurl('svn://blah.com/path/file')
            'svn://blah.com/path/file'
            >>> canonurl('1234://badscheme.com')
            ''
            >>> canonurl('bad$scheme://google.com')
            ''
            >>> canonurl('site.badtopleveldomain')
            ''
            >>> canonurl('site.com:badport')
            ''
            >>> canonurl('http://123.24.8.240/blah')
            'http://123.24.8.240/blah'
            >>> canonurl('http://123.24.8.240:1234/blah?q#f')
            'http://123.24.8.240:1234/blah?q#f'
            >>> canonurl('\xe2\x9e\xa1.ws')  # tinyarro.ws
            'http://xn--hgi.ws/'
            >>> canonurl('  http://www.google.com:80/path/file;params?query#fragment  ')
            'http://www.google.com:80/path/file;params?query#fragment'
            >>> canonurl('http://\xe2\x9e\xa1.ws/\xe2\x99\xa5')
            'http://xn--hgi.ws/%E2%99%A5'
            >>> canonurl('http://\xe2\x9e\xa1.ws/\xe2\x99\xa5/pa%2Fth')
            'http://xn--hgi.ws/%E2%99%A5/pa/th'
            >>> canonurl('http://\xe2\x9e\xa1.ws/\xe2\x99\xa5/pa%2Fth;par%2Fams?que%2Fry=a&b=c')
            'http://xn--hgi.ws/%E2%99%A5/pa/th;par/ams?que/ry=a&b=c'
            >>> canonurl('http://\xe2\x9e\xa1.ws/\xe2\x99\xa5?\xe2\x99\xa5#\xe2\x99\xa5')
            'http://xn--hgi.ws/%E2%99%A5?%E2%99%A5#%E2%99%A5'
            >>> canonurl('http://\xe2\x9e\xa1.ws/%e2%99%a5?%E2%99%A5#%E2%99%A5')
            'http://xn--hgi.ws/%E2%99%A5?%E2%99%A5#%E2%99%A5'
            >>> canonurl('http://badutf8pcokay.com/%FF?%FE#%FF')
            'http://badutf8pcokay.com/%FF?%FE#%FF'
            >>> len(canonurl('google.com/' + 'a' * 16384))
            4096
            """
            # strip spaces at the ends and ensure it's prefixed with 'scheme://'
            url = url.strip()
            if not url:
                return ''
            if not urlparse.urlsplit(url).scheme:
                url = 'http://' + url
        
            # turn it into Unicode
            try:
                url = unicode(url, 'utf-8')
            except UnicodeDecodeError:
                return ''  # bad UTF-8 chars in URL
        
            # parse the URL into its components
            parsed = urlparse.urlsplit(url)
            scheme, netloc, path, query, fragment = parsed
        
            # ensure scheme is a letter followed by letters, digits, and '+-.' chars
            if not re.match(r'[a-z][-+.a-z0-9]*$', scheme, flags=re.I):
                return ''
            scheme = str(scheme)
        
            # ensure domain and port are valid, eg: sub.domain.<1-to-6-TLD-chars>[:port]
            match = re.match(r'(.+\.[a-z0-9]{1,6})(:\d{1,5})?$', netloc, flags=re.I)
            if not match:
                return ''
            domain, port = match.groups()
            netloc = domain + (port if port else '')
            netloc = netloc.encode('idna')
        
            # ensure path is valid and convert Unicode chars to %-encoded
            if not path:
                path = '/'  # eg: 'http://google.com' -> 'http://google.com/'
            path = urllib.quote(urllib.unquote(path.encode('utf-8')), safe='/;')
        
            # ensure query is valid
            query = urllib.quote(urllib.unquote(query.encode('utf-8')), safe='=&?/')
        
            # ensure fragment is valid
            fragment = urllib.quote(urllib.unquote(fragment.encode('utf-8')))
        
            # piece it all back together, truncating it to a maximum of 4KB
            url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
            return url[:4096]
        
        if __name__ == '__main__':
            import doctest
            doctest.testmod()
        

        【讨论】:

        • 仅在 4096 个字符处将其截断可能会留下部分引用的字符。您可以使用正则表达式 r'%.?$' 来匹配任何尾随的部分转义。
        【解决方案5】:

        MizardX 给出的代码不是 100% 正确的。这个例子不起作用:

        example.com/folder/?page=2

        查看 django.utils.encoding.iri_to_uri() 将 unicode URL 转换为 ASCII url。

        http://docs.djangoproject.com/en/dev/ref/unicode/

        【讨论】:

        • 问题和答案已经快 10 年了,这仍然有效,并且仍然是我看到的最佳答案。如果您查看 Django 2.0 代码,它会导入 from urllib.parse import quote 并返回 quote(iri, safe="/#%[]=:;$&amp;()+,!?*@'~"),因此这可能是任何现代旁观者可能想要进入的方向。
        猜你喜欢
        • 1970-01-01
        • 2018-02-15
        • 1970-01-01
        • 2016-07-08
        • 1970-01-01
        • 2019-10-31
        • 2020-06-07
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多