CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: chardet でテキストファイルの文字コードを検出する

マルチバイト文字の含まれたテキストファイルを扱おうとすると、文字コードがまず問題になる。 そのファイルがいったい何でエンコードされているか分からないと、それを適切に扱うことは到底できない。 そんなとき使うと便利なのが、今回紹介する chardet というサードパーティ製のパッケージ。

今回の検証環境には Mac OS X を使った。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.11.1
BuildVersion:   15B42

インストール

まずは pip を使って chardet をインストールする。

$ pip install chardet

下準備

次に、文字コードを検出するのに使うテキストファイルを用意しよう。

$ cat << EOF > helloworld.txt
こんにちは、世界
EOF

上記のコマンドで作ったテキストファイルが何でエンコードされるかはシェルと端末の設定に依存する。 手元の環境では UTF-8 を使っている。

$ echo $LANG
ja_JP.UTF-8
$ nkf --guess helloworld.txt
UTF-8 (LF)

chardet を使う

それでは準備が整ったので chardet を使ってみよう。 まずは先ほど作ったテキストファイルをバイナリモードで読み込む。 この時点では読み込んだ内容は謎のバイト列だ。

$ python
>>> with open('helloworld.txt', mode='rb') as f:
...     binary = f.read()
...
>>> binary
b'\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab\xe3\x81\xa1\xe3\x81\xaf\xe3\x80\x81\xe4\xb8\x96\xe7\x95\x8c\n'

次に chardet パッケージをインポートして detect() 関数に先ほど読み込んだバイト列を渡す。 これだけで chardet が何の文字コードを使ってエンコードされたテキストなのか推定してくれる。 結果は辞書で返ってきて、その中の 'encoding' が推定した文字コードで、'confidence' がその文字コードである確度を表している。

>>> import chardet
>>> chardet.detect(binary)
{'confidence': 0.99, 'encoding': 'utf-8'}

あとは推定された文字コードを使ってバイト列を Python のユニコード文字列にデコードすればいい。

>>> binary.decode('utf-8')
'こんにちは、世界\n'

念のため別の文字コードでも試しておこう。 nkf コマンドを使って EUC-JP でエンコードされたテキストファイルを作りなおす。

$ nkf -e helloworld.txt > helloworld_euc.txt
$ nkf --guess helloworld_euc.txt
EUC-JP (LF)

先ほどと同様に chardet で文字コードを推定する。

$ python
>>> with open('helloworld_euc.txt', mode='rb') as f:
...     binary = f.read()
...
>>> import chardet
>>> chardet.detect(binary)
{'encoding': 'EUC-JP', 'confidence': 0.99}

ばっちり。

バイナリファイルだった場合

読み込んでみたらテキストファイルではなく実はバイナリファイルでした、という場合もあるはず。 そのときの挙動も確認しておこう。 これはようするにテキストファイルでさえなければいいので '/dev/random' デバイスから適当にバイト列を読み込むことにする。

>>> with open('/dev/random', mode='rb') as f:
...     binary = f.read(32)
... 
>>> binary
b'm\x1b\x828k\xd2\xc6\xefl\xf0Np\x7f\xd9*H\xd2\xf3\xb4\xc9\xc1>g\xf8\x1eo\xfd\xbe\x18A\xd3$'

読み込んだバイト列の文字コードを chardet の detect() 関数で推定させる。 しかし、単なるランダムなバイト列なので文字コードが分かるはずもない。 その場合は 'encoding' が None になる。

>>> import chardet
>>> chardet.detect(binary)
{'confidence': 0.0, 'encoding': None}

つまり、読み込んだのがバイナリファイルっぽいか否かはここの値をチェックすれば分かりそうだ。

大きなファイルを扱う場合

先ほどの detect() 関数をとても大きなファイルに対して使うと推定に長い時間がかかるらしい。 そのため、少しずつバイト列を渡していってある程度確証が得られた時点で計算を打ち切ることができる API も用意されているようだ。

次のサンプルコードでは、その API を使っている。 UniversalDetector というクラスのインスタンスには feed() というメソッドがあり、これには複数回に分けてバイト列を渡すことができる。 十分な確度で推定が完了するとインスタンスのメンバ変数 done が真になるため、そこで計算を打ち切ることができる。 ただ、done フラグが立つには相当に大きなサイズが必要っぽいので、中途半端な大きさではふつうにファイルを最後まで読み終わって処理が完了するようだ。

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

from chardet.universaldetector import UniversalDetector


def main():
    detector = UniversalDetector()

    try:
        with open('helloworld.txt', mode='rb') as f:
            while True:
                binary = f.readline()
                if binary == b'':
                    # ファイルを最後まで読みきった
                    break

                detector.feed(binary)
                if detector.done:
                    # 十分な確度でエンコーディングが推定できた
                    break
    finally:
        detector.close()

    encoding_info = detector.result
    print(encoding_info)


if __name__ == '__main__':
    main()

上記を実行してみよう。 helloworld.txt はたった一行だけのテキストファイルなので当然 done は真にならずに終わる。

$ python detector.py 
{'encoding': 'utf-8', 'confidence': 0.99}

めでたしめでたし。