¿La mejor forma de convertir una URL Unicode a ASCII (UTF-8 por ciento de escape) en Python?

Me pregunto cuál es la mejor manera, o si existe una forma simple con la biblioteca estándar, para convertir una URL con caracteres Unicode en el nombre de dominio y la ruta a la URL ASCII equivalente, codificada con el dominio como IDNA y la ruta% -codificado, según RFC 3986.

Obtengo del usuario una URL en UTF-8. Entonces, si han escrito http://➡.ws/♥ , obtengo 'http://\xe2\x9e\xa1.ws/\xe2\x99\xa5' en Python. Y lo que quiero es la versión ASCII: 'http://xn--hgi.ws/%E2%99%A5' .

Lo que hago en este momento es dividir la URL en partes a través de una expresión regular, y luego codificar manualmente el dominio con IDNA, y codificar por separado la ruta y la cadena de consulta con diferentes llamadas a urllib.quote() .

 # url is UTF-8 here, eg: url = u'http://➡.ws/㉌'.encode('utf-8') match = re.match(r'([az]{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' 

¿Es esto correcto? ¿Alguna sugerencia mejor? ¿Hay una función de biblioteca estándar simple para hacer esto?

Código:

 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') 

Salida:

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

Lee mas:

  • urllib.quote ()
  • urlparse.urlparse ()
  • urlparse.urlunparse ()
  • urlparse.urlsplit ()
  • urlparse.urlunsplit ()

Ediciones:

  • Se corrigió el caso de los caracteres ya citados en la cadena.
  • Se cambió urlparse / urlunparse a urlsplit / urlunsplit .
  • No codifique la información de usuario y puerto con el nombre de host. (Gracias Jehiah)
  • Cuando falta “@”, ¡no trate al host / puerto como usuario / pase! (Gracias hupf)

El código dado por MizardX no es 100% correcto. Este ejemplo no funciona:

example.com/folder/?page=2

echa un vistazo a django.utils.encoding.iri_to_uri () para convertir la URL Unicode a las URL ASCII.

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

hay un poco de trabajo de análisis de url RFC-3896 en curso (por ejemplo, como parte del Summer Of Code) pero nada en la biblioteca estándar todavía AFAIK, y tampoco mucho en el lado de encoding de uri de las cosas, nuevamente AFAIK. Así que también podrías ir con el enfoque elegante de MizardX.

De acuerdo, con estos comentarios y algunas correcciones de errores en mi propio código (no manejaba fragmentos en absoluto), se me ocurrió la siguiente función canonurl() : devuelve una forma ASCII canónica de la URL:

 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'[az][-+.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() 

Puede usar urlparse.urlsplit en urlparse.urlsplit lugar, pero por lo demás parece que tiene una solución muy sencilla.

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

(Puede acceder al dominio y al puerto por separado accediendo a las propiedades nombradas del valor devuelto, pero como la syntax del puerto está siempre en ASCII, no se ve afectado por el proceso de encoding IDNA).