UnicodeEncodeError: に悩まされない。Python2.x から Python3.x への乗り換え



Python2.x から Python3.x への乗り換えで日本語処理でハマったのでメモ。

スポンサードリンク

Python3.xのインストール

レンタルサーバなどで試す場合には、自分のローカル環境にインストールします。

mkdir -p $HOME/local/src/
cd $HOME/local/src/
wget http://python.org/ftp/python/3.2.2/Python-3.2.2.tgz
tar zxvf Python-3.2.2.tgz
cd Python-3.2.2
./configure --help
./configure --prefix=$HOME/local/
make
make install

Python2のソースコードをPython3 に移植

Python3をインストールすると「2to3」という実行ファイルもインストールされるのが分かると思います。
これはPython2のソースコードをPython 3でも動くように可能な限り自動変換してくれるツールです。また「2to3」は関数の呼び出しも修正してくれます。

例えば、次のような変換を自動的に行なってくれます。

【参考】http://diveintopython3-ja.rdy.jp/porting-code-to-python-3-with-2to3.html

Python 2 Python 3
import urllib import urllib.request, urllib.parse, urllib.error
import urllib2 import urllib.request, urllib.error
import urlparse import urllib.parse
import robotparser import urllib.robotparser
from urllib import FancyURLopener
from urllib import urlencode
from urllib.request import FancyURLopener
from urllib.parse import urlencode
from urllib2 import Request
from urllib2 import HTTPError
from urllib.request import Request
from urllib.error import HTTPError

Python 3では、print()は関数となり、print()に引数として渡す必要があるなど修正箇所が多いので「2to3」は強力です。

urllibを利用する日本語処理において Python3への移植

「2to3」の利用方法

Python2で書かれたソースコードです。urllib2を用いてURL先のソースを取得しています。

#!/local/bin/python
# -*- coding: utf-8 -*-

import sys
import threading, urllib2

f = open('hoge.txt', 'a')

opener  = urllib2.build_opener()
httpres = opener.open('http://headlines.yahoo.co.jp/cm/personal')
html    = httpres.read()

print html
f.write(html)
f.close()

実行結果は次のようになります。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ja">
...(省略)

これを「2to3」を利用すると差分パッチが作成されます。

%2to3 sample.py
RefactoringTool: Skipping implicit fixer: buffer
RefactoringTool: Skipping implicit fixer: idioms
RefactoringTool: Skipping implicit fixer: set_literal
RefactoringTool: Skipping implicit fixer: ws_comma
--- sample.py (original)
+++ sample.py (refactored)
@@ -2,14 +2,14 @@
 # -*- coding: utf-8 -*-

 import sys
-import threading, urllib2
+import threading, urllib.request, urllib.error, urllib.parse

 f = open('hoge.txt', 'a')

-opener    = urllib2.build_opener()
+opener    = urllib.request.build_opener()
 httpres = opener.open('http://headlines.yahoo.co.jp/cm/personal')
 html = httpres.read()

-print html
+print(html)
 f.write(html)
 f.close()
RefactoringTool: Files that need to be modified:
RefactoringTool: sample.py

「2to3」後の手動修正(デコード・エンコード処理の追加)

「2to3」で修正された内容で実際に動作させてみましょう。

#!/local/bin/python
# -*- coding: utf-8 -*-

import sys
import threading, urllib.request, urllib.error

f = open('hoge.txt', 'a')

opener  = urllib.request.build_opener()
httpres = opener.open('http://headlines.yahoo.co.jp/cm/personal')
html    = httpres.read()

print(html)
f.write(html)
f.close()

実行すると、次のようなエラーが出ます。

%$HOME/local/bin/python3 sample.py > hage.txt
Traceback (most recent call last):
  File "sample.py", line 14, in 
    f.write(html)
TypeError: must be str, not bytes

また print() 関数で出力した結果にも「b'」が付き、「\xa5\xb2\xa5\xb9\xa5\xc8\xa4\xb5\xa4\xf3\」と日本語部分が文字化けしています。

b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> \n<html lang="ja">\n

これは Python3より、取得したHTMLデータはバイナリとなったためです。
このため、print すると、b'..........' という bytes 表現となってしまいます。

対応方法として、デコード・エンコード処理が必要です。

文字列   → バイナリ  bytes = str.encode('utf-8など')
バイナリ → 文字列    str = bytes.decode('utf-8など') 

「'utf-8'」以外には「'shift-jis'」「'euc-jp'」「'iso2022-jp'」などがあります。

ここで、試しに「'utf_8'」としてみます。

【修正前】html    = httpres.read()
【修正後】html    = httpres.read().decode('utf-8')

実行結果は次のようなエラーが出ます。

%$HOME/local/bin/python3 sample.py > hage.txt
Traceback (most recent call last):
  File "sample.py", line 11, in 
    html    = httpres.read().decode('utf-8')
UnicodeDecodeError: 'utf8' codec can't decode byte 0xa5 in position 328: invalid start byte

指定したURLのサイトは「'euc-jp'」で記載されているので、正しいコーデックを指定する必要があります。

「2to3」後の手動修正(デコード・エンコード処理の追加)

このソースコードをPython3上で実行してみます。

#!/local/bin/python
# -*- coding: utf-8 -*-

import sys
import threading, urllib.request, urllib.error

f = open('hoge.txt', 'a')

opener  = urllib.request.build_opener()
httpres = opener.open('http://headlines.yahoo.co.jp/cm/personal')
html    = httpres.read().decode('euc-jp')

print(html)
f.write(html)
f.close()

実行結果は次のようなエラーが出力されます。

%$HOME/local/bin/python3 sample.py > hage.txt
Traceback (most recent call last):
  File "sample.py", line 13, in 
    print(html)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 328-339: ordinal not in range(128)

これは、デフォルトで設定されてる文字コードを変えると解決します。

%$HOME/local/bin/python3
Python 3.2.2 (default, Sep 26 2011, 17:16:07)
[GCC 4.2.1 20070719  [FreeBSD]] on freebsd7
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> print(sys.stdout.encoding)
US-ASCII

確認すると「US-ASCII」のようなので次のように修正

#!/local/bin/python
# -*- coding: utf-8 -*-

import sys
import threading, urllib.request, urllib.error
import io # 追加
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') # 追加

f = open('hoge.txt', 'a')

opener  = urllib.request.build_opener()
httpres = opener.open('http://headlines.yahoo.co.jp/cm/personal')
html    = httpres.read().decode('euc-jp')

print(html)
f.write(html)
f.close()

これで UTF-8 形式で hage.txt ファイルが出力されました。

「2to3」後の手動修正(デコード・エンコード処理の追加)

上記のソースコードでは、ファイルへの書き出しで次のようなエラーが出ます。

%$HOME/local/bin/python3 sample.py > hage.txt
Traceback (most recent call last):
  File "sample.py", line 16, in 
    f.write(html)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 328-339: ordinal not in range(128)

Python3では、デフォルトがunicode形式のため、ファイル作成時に「'ecu-jp'」を指定してあげる必要があります。

スポンサードリンク

#!/local/bin/python
# -*- coding: utf-8 -*-

import sys
import threading, urllib.request, urllib.error
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

f = open('hoge.txt', 'a', encoding='euc-jp')

opener  = urllib.request.build_opener()
httpres = opener.open('http://headlines.yahoo.co.jp/cm/personal')
html    = httpres.read().decode('euc-jp')

print(html)
f.write(html)
f.close()

これで、エラーが出力される事なく処理が完了しました。

その他のエラー一覧

TypeError: expected an object with the buffer interface

decode("utf-8") などの処理が必要です。bytes コードを文字列として処理しようとしているのが原因です。

html.parser.HTMLParseError: bad end tag: "</scr');\ndocument.write('ipt>", at line 84, column 17

HTMLソースの中に、javascriptで</scrとipt>をくっつけて</script>のようにする箇所があるのが原因。

document.write('<scr');
document.write('ipt src="' + impAserver + '/bservers/AAMALL/ACC_RANDOM=' + impArnd + '/PAGEID=' + impApid + impAtarget + '">');
document.write('</scr');
document.write('ipt>');

これは、BeautifulSoupやHTMLParserを利用していると発生します。
以前は、SGMLParserを利用して回避することができましたが、Pythob3ではSGMLParserは無くなりました。

この対策として、 Python-3/Lib/html/parser.py のソースコードを overrideする事が考えられます。

import html.parser  
HTMLParser.endtagfind = re.compile("</\s*([a-zA-Z][-.a-zA-Z0-9:_+'\" ]*)\s*>")

スポンサードリンク