¿La mejor manera de convertir una URL Unicode a ASCII (UTF-8 por ciento escapado) en Python?
Pregunta
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 dominio como IDNA y la ruta codificada en%, según RFC 3986.
Recibo del usuario una URL en UTF-8. Entonces, si escribieron http: // & # 10145; .ws / & # 9829;
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 IDNA manualmente el dominio, y codificar por separado la ruta y la cadena de consulta con diferentes urllib.quote ()
llamadas.
# 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})?(/.*?)(\?.*)?
¿Es esto correcto? ¿Alguna sugerencia mejor? ¿Existe una función simple de biblioteca estándar para hacer esto?
, 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? ¿Existe una función simple de biblioteca estándar para hacer esto?
Solución
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
Leer más:
- urllib.quote ()
- urlparse.urlparse ()
- urlparse.urlunparse ()
- urlparse.urlsplit ()
- urlparse.urlunsplit ()
Ediciones:
- Se corrigió el caso de los caracteres ya citados en la cadena.
- Cambió
urlparse
/urlunparse
aurlsplit
/urlunsplit
. - No codifique la información de usuario y puerto con el nombre de host. (Gracias Jehiah)
- Cuando " @ " falta, no trate el host / puerto como usuario / pase! (Gracias hupf)
Otros consejos
el código proporcionado por MizardX no es 100% correcto. Este ejemplo no funcionará:
example.com/folder/?page=2
consulte django.utils.encoding.iri_to_uri () para convertir la URL unicode en URL ASCII.
hay algún RFC-3896 url parsing trabajo en curso (por ejemplo, como parte del Summer Of Code) pero nada en la biblioteca estándar todavía AFAIK - y nada más en la codificación uri lado de las cosas tampoco, de nuevo AFAIK. Por lo tanto, también puede ir con el enfoque elegante de MizardX.
De acuerdo, con estos comentarios y algunas correcciones de errores en mi propio código (no manejó 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'[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()
Puede usar urlparse.urlsplit
en cambio, pero por lo demás parece que tienes una solución muy sencilla, allí.
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 sintaxis del puerto está siempre en ASCII, no se ve afectado por el proceso de codificación IDNA).