56

I need to fetch data from a URL with non-ascii characters but urllib2.urlopen refuses to open the resource and raises:

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

I know the URL is not standards compliant but I have no chance to change it.

What is the way to access a resource pointed by a URL containing non-ascii characters using Python?

edit: In other words, can / how urlopen open a URL like:

http://example.org/Ñöñ-ÅŞÇİİ/

10 Answers 10

59

Strictly speaking URIs can't contain non-ASCII characters; what you have there is an IRI.

To convert an IRI to a plain ASCII URI:

  • non-ASCII characters in the hostname part of the address have to be encoded using the Punycode-based IDNA algorithm;

  • non-ASCII characters in the path, and most of the other parts of the address have to be encoded using UTF-8 and %-encoding, as per Ignacio's answer.

So:

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'

(Technically this still isn't quite good enough in the general case because urlparse doesn't split away any user:pass@ prefix or :port suffix on the hostname. Only the hostname part should be IDNA encoded. It's easier to encode using normal urllib.quote and .encode('idna') at the time you're constructing a URL than to have to pull an IRI apart.)

Sign up to request clarification or add additional context in comments.

7 Comments

Although this seems to be a very niche problem, it's sure resolved a very specific issue of my own. Great answer.
How to handle this elegantly in Python 3? Any suggestions?
This actually works great for serving files where the name may contain non-american characters such as kanji symbols!
in python 3 you import urllib.parse instead of urlparse, decode b in urlEncodeNonAscii: b.decode('utf-8') and leave the idna part out of the iriToUri: return urllib.parse.urlunparse([url_encode_non_ascii(part.encode('utf-8')) for part in parts])
Using UTF-8 for query is not always correct; details are in my answer. Web is a weird place.
|
51

In python3, use the urllib.parse.quote function on the non-ascii string:

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

6 Comments

Simple and effective! :D
Much better than the other answers.
This is a wonderful solution. Solved my issue when using Kanji with urls, works with Japanese character sets.
WOW This is underrated
Note this doesn't handle the hostname (IDNA) correctly.
|
27

Python 3 has libraries to handle this situation. Use urllib.parse.urlsplit to split the URL into its components, and urllib.parse.quote to properly quote/escape the unicode characters and urllib.parse.urlunsplit to join it back together.

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

3 Comments

@user230137 What do you mean it doesn't work? Works perfectly for me.
Note this doesn't handle the hostname (IDNA) correctly.
urllib.parse.quote(url, safe=':/')
7

Based on @darkfeline answer:

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 Comment

This has some issues 1) scheme does not support percentage encoded chars, 2) path and query have different chars that are safe than path and those should not be percent-encoded, the default safe chars for quote is for the path component - werkzeug has a better iri2uri implementation [ref].
7

It is more complex than the accepted @bobince's answer suggests:

  • netloc should be encoded using IDNA;
  • non-ascii URL path should be encoded to UTF-8 and then percent-escaped;
  • non-ascii query parameters should be encoded to the encoding of a page URL was extracted from (or to the encoding server uses), then percent-escaped.

This is how all browsers work; it is specified in https://url.spec.whatwg.org/ - see this example. A Python implementation can be found in w3lib (this is the library Scrapy is using); see w3lib.url.safe_url_string:

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

An easy way to check if a URL escaping implementation is incorrect/incomplete is to check if it provides 'page encoding' argument or not.

Comments

5

For those not depending strictly on urllib, one practical alternative is requests, which handles IRIs "out of the box".

For example, with http://bücher.ch:

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

1 Comment

Amazing! Thanks! In my case I was trying to download a file (.png) in bytes format. My original code: urllib.request.urlopen(url).read() The code after the change to requests: requests.get(url).content
4

Encode the unicode to UTF-8, then URL-encode.

4 Comments

thanks for the response. can you be more specific please? unicode(url, 'utf-8') raises TypeError: decoding Unicode is not supported. also which function do you suggest for encoding url? urlencode for example is for building query string. but mine is only a path on the server.
For the first part, you want url.encode('utf-8') (assuming url is a unicode object).
@ignacio: thanks. i still think the problem is with the urlopen not accepting non-ascii characters as a URL (which it is right in a way, as they are not standard). please see my update in question.
4

Use iri2uri method of httplib2. It makes the same thing as by bobin (is he/she the author of that?)

1 Comment

The proposed solution doesn't work for non-ASCII domain names (IRI). urllib2.urlopen(httplib2.iri2uri("http://домены.рф"), timeout=15) returns urlopen error [Errno -2] Name or service not known
1

Another option to convert an IRI to an ASCII URI is to use furl package:

gruns/furl: 🌐 URL parsing and manipulation made easy. - https://github.com/gruns/furl

Python's standard urllib and urlparse modules provide a number of URL related functions, but using these functions to perform common URL operations proves tedious. Furl makes parsing and manipulating URLs easy.

Examples

Non-ASCII domain

http://国立極地研究所.jp/english/ (Japanese National Institute of Polar Research website)

import furl

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

Non-ASCII path

https://ja.wikipedia.org/wiki/日本語 ("Japanese" article in Wikipedia)

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'

Comments

0

works! finally

I could not avoid from this strange characters, but at the end I come through it.

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.