UnicodeEncodeError

XMLを解析したい用事が出来たので、折角だからPythonでやってみたところ、
日本語を含むXMLだったのでUnicodeEncodeErrorが出てしまった。。。
Unicode文字列って何?」状態だったので、やったことを忘れないようにメモメモ。

XMLパーサー

代表的なパーサーは2つ(SAXとDOM)

SAXはイベント駆動型と呼ばれるXMLパーサー。
XML文章を読み込んで、タグやテキストなど読み込んだ文字列に応じてイベントを発生させ、
そのイベントに対応するハンドラを呼び出すことでXMLを解析する。

XMLパーサーにはDOMと呼ばれるものもあって、XML文章を一度すべて読み込んで解析し、
DOMツリーと呼ばれる形式でメモリ上に展開する。
メモリ上に一回全て展開するので、それなりにメモリを消費する。

SAX/DOMともに、Python2.0で追加されていて、処理の構造上、向いている処理が異なる。

  • SAXは逐次処理に向いている
  • DOMはランダム処理に向いている

他にもExpatやElementTreeというパーサーもPythonには存在する。
ExpatはSAXみたいにイベントに合わせて処理を進めていく感じ。
ElementTreeはDOMみたいに一度メモり上に展開するけど、形式がPythonのオブジェクト形式で展開されるので扱い易いらしい。
ElementTreeは2.5で追加されたモジュールだから、RHEL5系とか2.4のPythonだと使えないのがちょっと残念。

SAX

XMLを処理したことが無かったから、勉強がてらまずはSAXを使ってみることに。
SAXでXMLを解析するには、

  • xml.sax.make_parser()でSAX XMLReader オブジェクトを生成
  • 生成したSAX XMLReader オブジェクトに対して、setContentHandler()を呼び出してハンドラ(ContentHandlerクラスのインスタンス)を登録
  • SAX XMLReader オブジェクトのparse()を実行

setContentHandler()に登録するハンドラで、XML解析用の処理を実装(独自クラスを定義)することでXMLの解析を行う。
独自クラスを定義する場合は、必ずContentHandlerクラスを継承する。
個々の解析処理は、それぞれのイベントに応じたメソッドを実装することで実現する。

イベント メソッド名
タグの開始() startElement(name, attrs)
タグの() endElement(name)
テキスト character(data)

どうもテキストイベントは、タグ以外の要素が出現した時に発生するらしく、
スペースやタブ、改行コードが来た時にも発生するので、こいつらは無視するような処理を追加しなきゃいけない。

以下のような処理をする独自クラスは次のようになる。

  • タグの開始時にタグの名前とタグの中に指定された属性を出力
  • タグの終了時にタグの名前を出力
  • テキスト要素(スペース/タブ/改行コードだけのものは除く)を出力
[kobakoba0723@fedora13-intel64 xml_parse]$ cat sax_parser.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import re
import sys
from xml.sax.handler import ContentHandler
import xml.sax

class grpParseHandler(ContentHandler):
  def __init__(self, writer=sys.stdout):
    ContentHandler.__init__(self)
    self._writer = writer
  def startElement(self, name, attrs):
    self._writer.write(u"Start Element: %s\n".encode("utf-8") % name.encode("utf-8"))
    for (key, value) in attrs.items():
      self._writer.write(u"(key, value): (%s, %s)\n".encode("utf-8") % (key.encode("utf-8"), value.encode("utf-8")))
  def endElement(self, name):
    self._writer.write(u"End Element: %s\n".encode("utf-8") % name.encode("utf-8"))
  def characters(self, data):
    if not re.match(r"\s+", data):
      self._writer.write(u"char: %s\n".encode("utf-8") % data.encode("utf-8"))

if __name__ == '__main__':
  if len(sys.argv) < 2:
    print >> sys.stderr, "xml file name is not specified"
    sys.exit(1)
  file_name = sys.argv[1]
  
  parser = xml.sax.make_parser()
  parser.setContentHandler(grpParseHandler())
  parser.parse(file_name)

[kobakoba0723@fedora13-intel64 xml_parse]$

ここで、name/key/value/dataの後ろにencode("utf-8")を毎度つけているのは、name等々がunicode文字列だから。

unicode文字列

通常の文字列 『"ほげほげ"』 とかは、生成した段階で文字コードが決まっている。
上のコードの場合はUTF-8(# -*- coding: utf-8 -*- が付いているから、EmacsUTF-8文字コードで保存してくれる)
それに対してunicode文字列は 『u"ほげほげ"』 は、生成した段階では文字コードは決まっておらず、
unicode文字列型という特殊な形式で保存されている。
文字列の頭に "u" をつけることでunicode文字列として生成される。

演算(出力、加算、比較など)する場合に、特定の文字コードのオブジェクトに変換する。
この時に使うのがencode()で、引数として変換する文字コードを指定する。
なので、『name.encode("utf-8")』というのは、『nameに保存された内容をUTF-8で読める形に変換しろ』という意味になる。

UnicodeEncodeError

日本語などの2バイト(以上の)文字が格納されたunicode文字列をasciiで変換しようとして出力されるエラー。
PythonRecipeのサイトにあったコードをそのままコピペして実行したら出力されて困ってしまったエラー
その時に書いてたコードは

self._writer.write(u"char: %s\n" % data)

まず、unicode文字列のdataをwriteで扱えるstr文字列に変換しないといけない。
ここで、Pythonは親切なので、『data』を『data.encode()』として扱ってくれる。
この時にエンコード使用される文字コードは、sys.getdefaultencoding()にて取得できる値が使われる。

>>> import sys
>>> sys.getdefaultencoding()
'ascii'
>>> 

通常この値は『ascii』が入っているので、asciiにエンコードしようとする。
でもdataに入っている内容は2バイト以上の文字なので、asciiで取り扱えない範囲となってエラーが発生する。

なので、dataをエンコードする時に、"utf-8"を指定すれば解決するはずなので、

self._writer.write(u"char: %s\n" % data.encode("utf-8"))

と修正して実行してみると、相変わらずUnicodeEncodeError。
それもそのはず、『u"char: %s\n"』とやってるから、この部分がそもそもUnicode文字列。
自動的にasciiでエンコードしようとしてUnicodeEncodeErrorが発生してたわけ。
なので、ここも"utf-8"でエンコードするようにしてやれば良いので、

self._writer.write(u"char: %s\n".encode("utf-8") % data.encode("utf-8"))

という上で書いたコードになった。

UnicodeEncodeErrorであうあうしてる中で試したコードに↓のがある。

self._writer.write("char: %s\n" % data.encode("utf-8"))

UnicodeEncodeErrorが出るだろうなぁと思いながら実行してみると、
エラーが出ず、しかも日本語が化けずにちゃんと表示される。
なんでだろうか。。。

追記

UnicodeEncodeErrorが出なくて、しかも日本語がちゃんと表示できるのは、

# -*- coding: utf-8 -*-

のおかげ。

これを書いてるおかげで、PythonUTF-8の文字列として出力できる。

[kobakoba0723@fedora13-intel64 xml_parse]$ cat print_ja_wo-coding.py 
#!/usr/bin/env python

import sys
sys.stdout.write("ほげほげ\n")
[kobakoba0723@fedora13-intel64 xml_parse]$ python print_ja_wo-coding.py 
  File "print_ja_wo-coding.py", line 4
SyntaxError: Non-ASCII character '\xe3' in file print_ja_wo-coding.py on line 4, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details
[kobakoba0723@fedora13-intel64 xml_parse]$ cat print_ja.py 
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
sys.stdout.write("ほげほげ\n")
[kobakoba0723@fedora13-intel64 xml_parse]$ python print_ja.py 
ほげほげ
[kobakoba0723@fedora13-intel64 xml_parse]$ 

理由は判ったけど、今度は新しい疑問が。。。
なぜにPythonRecipeではwrite()の引数に"u"をつけたんだろうか。