試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

Pythonにおける日本語のエンコーディングの検出について

(2015/1/29)一部のリンク先を修正し、更にサンプルコードもPython 3で動作することなどを目的に一部修正した。

  1. エンコーディングの簡易検出
    1. ASCIIとISO-2022-JPの区別が重要でない場合のデコード
  2. 実用的なエンコーディング判別パッケージ

エンコーディングの簡易検出

Pythonにおけるエンコーディングの扱いとエンコーディングの変換について」の最後で、特定のエンコーディングエンコードされた文字列をUnicode文字列にデコードする際に実際のエンコーディングに合っていなければUnicodeDecodeErrorが出ることを書いたが、言い換えると、一部の例外を除いて正しいエンコーディング指定と文字列オブジェクトとの組み合わせでのみUnicodeDecodeErrorは発生しない。
これを利用して、エンコーディングが不明な文字列オブジェクトに対して、エンコーディング名の幾つかの候補に対してdecode()を試みて、UnicodeDecodeErrorが出ないものがあればそれが正しいエンコーディングであると推測することができる。ただし、7bitのエンコーディングであるISO-2022-JPの文字列に対して「decode('ascii')」*1を実行しても例外は発生しないため、ASCIIコードの範囲内のコードのみを用いたテキストとISO-2022-JPエンコーディングのテキストのエンコーディングを判別することはできない。また、これ以外にも判別に問題がある可能性もある。

下のコードは引数に指定した日本語テキストファイルのエンコーディングを検出し、端末に結果を表示する。
[任意]ファイル名: simplechardet.py ライセンス: CC0

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

# 簡易エンコーディング判別スクリプト
# 日本語関係のエンコーディングのみチェックする
# 7bitどうしの判別(ASCIIとISO-2022-JPなど)はできない仕様

# CC0

from __future__ import print_function

import locale
import sys
import os

def simple_chardet (b_str):
  """
  簡易的なエンコーディング判別
  各コーデックに対してデコードを試みる
  成功したものが正しいエンコーディングとなる
  """
  try:
    b_str.decode ('iso-2022-jp')
    return '7bit (ascii, iso-2022-jp, etc...)'
  except UnicodeDecodeError:
    try:
      b_str.decode ('utf-8')
      return 'utf-8'
    except UnicodeDecodeError:
      try:
        b_str.decode ('cp932')
        return 'cp932'
      except UnicodeDecodeError:
        try:
          b_str.decode ('euc-jp')
          return 'euc-jp'
        except UnicodeDecodeError:
          return None

if len (sys.argv) != 2:
  sys.exit ('USAGE: {0} [FILE]'.format (sys.argv[0]))

locale.setlocale (locale.LC_ALL, '')
maxsize = 1 * 1024 * 1024  # 最大で1MiBまで読み込むことにする
infile = sys.argv[1]

# ファイルを開く
try:
  with open (infile, 'rb') as f_in:
    # 内容を読み込む
    try:
      b_text = f_in.read (maxsize)
    except IOError as e:
      sys.exit ('Error: cannot read from file "{0}" [{1}]: {2}'.format (infile, e.errno, e.strerror))
except IOError as e:
  sys.exit ('Error: cannot open file "{0}" [{1}]: {2}'.format (infile, e.errno, e.strerror))

# エンコーディング検出を実行
encoding = simple_chardet (b_text)

# 結果の表示
# encodingがNoneでなければencodingの内容を表示し
# Noneであれば「other encoding」と表示
print (encoding if encoding else 'other encoding')

ASCIIとISO-2022-JPの区別が重要でない場合のデコード
日本語の文字列のデコード*2だけを考えるのであれば、場合によってはISO-2022-JPのコーデックでデコードしてUnicodeDecodeErrorが発生しなければISO-2022-JPエンコーディングとみなして処理してもよいかもしれない。
[任意]ファイル名: simplechardet2.py ライセンス: CC0

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

# 簡易エンコーディング判別スクリプト
# 日本語関係のエンコーディングのみチェックする
# 7bitどうしの判別(ASCIIとISO-2022-JPなど)はできない仕様

# CC0

from __future__ import print_function

import locale
import sys
import os

def simple_chardet_2 (b_str):
  """
  簡易的なエンコーディング判別
  エンコーディング名と文字列(Python 2ではUnicode文字列)を返すバージョン
  """
  try:
    return ('iso-2022-jp', b_str.decode ('iso-2022-jp'))
  except UnicodeDecodeError:
    try:
      return ('utf-8', b_str.decode ('utf-8'))
    except UnicodeDecodeError:
      try:
        return ('cp932', b_str.decode ('cp932'))
      except UnicodeDecodeError:
        try:
          return ('euc-jp', b_str.decode ('euc-jp'))
        except UnicodeDecodeError:
          return (None, None)

if len (sys.argv) != 2:
  sys.exit ('USAGE: {0} [FILE]'.format (sys.argv[0]))

locale.setlocale (locale.LC_ALL, '')
maxsize = 1 * 1024 * 1024  # 最大で1MiBまで読み込むことにする
infile = sys.argv[1]

# ファイルを開く
try:
  with open (infile, 'rb') as f_in:
    # 内容を読み込む
    try:
      b_text = f_in.read (maxsize)
    except IOError as e:
      sys.exit ('Error: cannot read from file "{0}" [{1}]: {2}'.format (infile, e.errno, e.strerror))
except IOError as e:
  sys.exit ('Error: cannot open file "{0}" [{1}]: {2}'.format (infile, e.errno, e.strerror))

# エンコーディング検出を実行
encoding, text = simple_chardet_2 (b_text)

# 結果の表示
# エンコーディングが判別できた場合は文字列も表示
if sys.version_info.major == 2:
  # Python 2ではUnicodeからUTF-8にエンコードして表示
  print ('encoding: {0}\ntext: {1}'.format (encoding, text.encode ('utf-8')) if encoding else 'other encoding')
else:
  print ('encoding: {0}\ntext: {1}'.format (encoding, text) if encoding else 'other encoding')

実用的なエンコーディング判別パッケージ

上の方法はISO-2022-JPとASCIIとの区別ができない制約があるなどの使いにくい面があるが、Universal Encoding Detector(chardet.feedparser.org https://github.com/chardet/chardet)という追加モジュール(モジュール名chardet)を用いるとエンコーディング判別が簡単に行える。これは「python-chardet」もしくは「chardet」というパッケージ名でディストリのパッケージになっていることも多い。Pythonのバージョン2系と3系向けそれぞれのバージョンがある。
これはMozilla製品のエンコーディング判別機能を移植したもの*3で、自動判別としての性能は良い*4が、短い文字列では判別に失敗することもあるようだ。
下は使用例。chardet.detect()という関数を呼び出して判別を行う。

(注意:UTF-8エンコーディングの端末におけるテスト)
>>> import chardet
>>> chardet.detect('日本語')
{'confidence': 0.87624999999999997, 'encoding': 'utf-8'}
>>> chardet.detect('日本語')['encoding']
'utf-8'
>>> chardet.detect('日本語'.decode('utf-8').encode('iso-2022-jp'))
{'confidence': 0.98999999999999999, 'encoding': 'ISO-2022-JP'}
>>> chardet.detect('日本語'.decode('utf-8').encode('iso-2022-jp'))['encoding']
'ISO-2022-JP'
>>> chardet.detect('english')
{'confidence': 1.0, 'encoding': 'ascii'}
>>> chardet.detect('english')['encoding']
'ascii'

結果は辞書で返され、confidenceの値は判別の信頼度を0から1の範囲で表したもの,encodingの値は判別されたエンコーディング名となる。
これ以外にも追加モジュールの形をしたパッケージが幾つかあるようだが、標準でディストリのパッケージになっていることが多いものはない。
その中で、pykf(http://sourceforge.jp/projects/pykf/)というものは短い文字列の判別に強いとのことなので、使い分けるといいかもしれない。もちろん、日本語のエンコーディングだけ処理できれば十分というのであれば上のUnicodeDecodeErrorを用いた方法を用いる手もある。
(2015/1/28)chardetとリファレンスのリンク先を修正

関連URL:

使用したバージョン:

*1:ASCIIも7bit

*2:特定のエンコーディングエンコードされた文字列からUnicode文字列を得る処理を指す

*3:chardet.feedparser.org/docs/faq.html http://chardet.readthedocs.org/en/latest/faq.html#who-wrote-this-detection-algorithmの「Who wrote this detection algorithm?」も参照

*4:Mozilla製品のWebブラウザ並みということになる