48800


# 環境

  * Python3.6.5 
    * chardet 3.0.4
  * Windows10

# 背景

Pythonのchardetモジュールで、文字コードの判定結果を確認しています。

UTF-8の"あ"という文字列の判定結果は"UTF-8"でしたが、"testあ"という文字列の判定結果は"Windows-1254"でした。これはWindowsのトルコ語の文字コードです。  
<https://en.wikipedia.org/wiki/Windows-1254>

[code]

    In [163]: chardet.detect( "あ".encode("utf-8"))                                                                            
    Out[163]: {'encoding': 'utf-8', 'confidence': 0.505, 'language': ''}                                                      
    
    In [164]: chardet.detect( "testあ".encode("utf-8"))                                                                        
    Out[164]:                                                                                                                 
    {'encoding': 'Windows-1254',                                                                                              
     'confidence': 0.5889255495043456,                                                                                        
     'language': 'Turkish'}                                                                                                   
    
    In [165]: chardet.detect( "tあ".encode("utf-8"))                                                                           
    Out[165]: {'encoding': 'utf-8', 'confidence': 0.505, 'language': ''}                                                      
    
    In [166]: chardet.detect( "stあ".encode("utf-8"))                                                                          
    Out[166]: {'encoding': 'utf-8', 'confidence': 0.505, 'language': ''}                                                      
    
    In [167]: chardet.detect( "estあ".encode("utf-8"))                                                                         
    Out[167]:                                                                                                                 
    {'encoding': 'Windows-1254',                                                                                              
     'confidence': 0.5153098558163024,                                                                                        
     'language': 'Turkish'}                                                                                                   
    
[/code]

# 質問

  1. "testあ"というUTF-8の文字列は、なぜ"Windows-1254"と判断されたのでしょうか？

  2. chardetが文字列を"UTF-8"と判断するには、どのような文字があればよいのでしょうか？

  3. chardetのSupport Encodingsというページに、Windows-1254は載っていませんでした。何か理由はあるのでしょうか？  
<https://chardet.readthedocs.io/en/latest/supported-encodings.html>

単純な興味として質問しました。特に困ってはいません。


CC BY-SA 4.0
2018-09-28T11:18:43.003
48793
2018-09-30T19:45:16.500
19524
question
python 文字コード
Pythonのchardetモジュールが、"testあ"という文字列（UTF-8）の文字コードを"Windows-1254"だと判定する


`"testあ"`のUTF-8表現は、74 65 73 74 e3 81 82 (1バイトデータの表記は全部16進、以下同様,
python3風に書くと`b'\x74\x65\x73\x74\xe3\x81\x82'`)で、chardetが判定するのは「文字列」ではなく、このバイト列です。

ちなみにこのバイト列をUTF-8, Shift_JIS, EUC-JP, ISO-8859-1, Code Page 437,
Windows-1254で解釈すると、以下のようになります。

[code]

    UTF-8     testあ (まぁ、当たり前)
    Shift_JIS (不正)
    EUC-JP    (不正)
    ISO-8859-1 testã (81 82 は制御コードにあたるので見えないが不正ではない)
    CP437     testπüé
    Win1254   testã‚ (81は未定義なので本来は不正、chardetは未定義にあたるバイトが現れても気にしない実装になっている模様)
    
[/code]

つまり明らかに不正なバイト列とみなされる、Shift_JIS, EUC-
JPを除くと、(制御文字や未定義文字をありにすれば(*))このバイト列を解釈することのできる文字エンコーディングはたくさんあるわけです。

文字エンコーディングの判定というのは、ここからいろんな経験則なんかを使って、「どれが一番それっぽいか」を調べること、と言うことになります。

(*) 滅多に使われない制御コードや未定義コードを減点してやれば、もうちょっと精度の高い判定ができるはずなんで、「あり」にすべきじゃないんですが…。

* * *

**_"testあ"というUTF-8の文字列は、なぜ"Windows-1254"と判断されたのでしょうか？_**

繰り返しますが、判定されたのは`b'\x74\x65\x73\x74\xe3\x81\x82'`と言うバイト列です。ま、それは読み替えるとして。

### chardetの現在の実装がそうなっているからです。

としか言いようがないんですが、ソースコードまで探し出して解析するのも大変なので、リンク先から辿れる[How it
works](https://chardet.readthedocs.io/en/latest/how-it-
works.html)を読むと、ざっくりこんなことが書いてあります。

**UTF-n with a BOM**

BOMが現れたらそれに応じて、UTF-8, UTF-16, UTF-32のどれかと判定する。

**Escaped encodings**

文字コード切り替えのエスケープシーケンスが現れるなら、そのエスケープシーケンスを使用する文字コードと判定する。(ESC
`$B`が現れたら、ISO-2022-JPと判定する、など)

**Multi-byte encodings**

UTF-8やSJISなど1〜2バイト以上になるエンコーディングの場合、まずはそのエンコーディングで解釈できるバイト列になっているかどうかを確認して、ダメならすぐに候補から外す。

バイト列が文字列として解釈できたら、1文字単位での出現頻度分布の解析器(distribution
analyzer)を使って評価、確信度(confidence)って評価を出す。

日本語の場合、SJISとEUCの判別は難しいので、2文字連の出現頻度分布も併用。

**Single-byte encodings**

常に1バイトが1文字を表すシングルバイトのエンコーディングの場合、2文字連の出現頻度分布で確信度を出す。

ヘブライ語の場合は大変らしいが、省略。

最後の砦として、 **Windows-1252** が挙げてあります。

と言うわけで、 **UTF-8として解釈した`('t', 'e', 's', 't',
'あ')`を1文字単位の出現頻度分布から算出した確信度(`0.505`)よりも、Windows-1254として解釈した`('te', 'es', 'st',
'tã')`のトルコ語での2文字連出現頻度分布から出した確信度(`0.5153098558163024`)の方がでかくなっちゃったんで、そう判定した**と言うことになりますね。

分析に使う言語ごとの出現頻度表(やそれを含む言語ごとの解析器)がバージョンアップなんかで更新されたら、違う結果になるかもしれません。

ちなみに、GithubのIssuesにはすでに[incorrect detection of windows-1254 instead of
utf-8](https://github.com/chardet/chardet/issues/148)なんてのが今年の2月に挙がっていて、まだオープンなままのようです。

そちらにも「Windows-1254としては未定義の8fを含むバイト列がWindows-1254として判定されるのはおかしい」なんて書かれています。

* * *

**_chardetが文字列を"UTF-8"と判断するには、どのような文字があればよいのでしょうか？_**

  * 他のエンコーディングで解釈した時に、何かの言語でありそうなバイト並びを避ける
  * UTF-8でよく出て来る文字(のうち上記に該当しないもの)をたくさん入れる

すでにやってみたと思いますが、`"testあいうえお"`なら、ほぼ確実(`'confidence':
0.9690625`)にUTF-8だと判定されます。一般的には、出現頻度分布なんて統計的情報を利用しているんだから、できるだけ標本数を多くする、つまり長めのバイト列を与えるべき、ってことになります。

処理の性格上「どのような文字」を「何か特定の文字を何文字か以上」みたいに規定することはできないです。( **追記**
)と書いたんですが、metropolis さんの解析によると、UTF-8用の場合 distribution analyzer
なんて言葉から想像されるほど複雑な統計処理なんかはしていないので、ひらがな(あるいは他の多バイト文字)を6文字ほど入れておけば、大抵の場合、他のエンコーディングに勝てそうです。

* * *

**_chardetのSupport Encodingsというページに、Windows-1254は載っていませんでした。何か理由はあるのでしょうか？_**

有償のフレームワークでさえ、ドキュメント中の一覧に漏れがあるのはしょっちゅうなので、オープンソースのライブラリのドキュメントに漏れがあっても、詮索する程の理由はないことが多いですが。

もちろんバグと思えるほど誤検出が多いので載せていない、と言う可能性もあります。

ドキュメントバグとして報告を上げれば何か反応が返って来るかもしれません。

* * *


CC BY-SA 4.0
2018-09-28T18:33:36.933
48800
2018-09-30T19:45:16.500
2018-09-30T19:45:16.500
13972
13972
48793
answer


chardet.detect() は
[chardet/__init__.py](https://github.com/chardet/chardet/blob/stable/chardet/__init__.py#L24)
で定義されています。内部的には以下の処理を行っています。

[code]

    detector = UniversalDetector()
    detector.feed(byte_str)
    return detector.close()
    
[/code]

ステップ実行してみます。

[code]

    $ python3
    Python 3.6.6
    
    >>> import chardet
    >>> chardet.__version__
    '3.0.4'
    >>> u = chardet.UniversalDetector()
    >>> u.feed("testあ".encode("utf-8"))
    >>> u._charset_probers
    [<chardet.mbcsgroupprober.MBCSGroupProber object at 0xb6a773ac>, <chardet.sbcsgroupprober.SBCSGroupProber object at 0xb6a79e4c>, <chardet.latin1prober.Latin1Prober object at 0xb6a8508c>]
    
    >>> u._charset_probers[0].charset_name  ## MBCSGroupProber
    'utf-8'
    >>> u._charset_probers[1].charset_name  ## SBCSGroupProber
    'ISO-8859-9'
    >>> u.ISO_WIN_MAP[u._charset_probers[1].charset_name.lower()]
    'Windows-1254'
    >>> u._charset_probers[2].charset_name  ## Latin1Prober
    'iso-8859-1'
    >>> u.ISO_WIN_MAP[u._charset_probers[2].charset_name.lower()]
    'Windows-1252'
    
    >>> u._charset_probers[0].get_confidence() ## MBCSGroupProber
    0.505
    >>> u._charset_probers[1].get_confidence() ## SBCSGroupProber
    0.5889255495043456
    >>> u._charset_probers[2].get_confidence() ## Latin1Prober
    0.01
    
[/code]

最終的に
[chardet/universaldetector.py](https://github.com/chardet/chardet/blob/stable/chardet/universaldetector.py#L243)
で、各 prober の get_confidence() メソッドの戻り値を比較して、最大値を返す prober
が判定した文字コードを採用しています。文字列 "testあ" の場合、SBCSGroupProber の confidence
値が最も高いので、文字コードは Windows-1254 と判定される事になります。

ここで、get_confidence() メソッドの中身はどの様になっているのかと言うと、UTF-8 の場合(MBCSGroupProber)は
[chardet/utf8prober.py](https://github.com/chardet/chardet/blob/stable/chardet/utf8prober.py#L76)
になります。

[code]

    def get_confidence(self):
        unlike = 0.99
        if self._num_mb_chars < 6:
            unlike *= self.ONE_CHAR_PROB ** self._num_mb_chars
            return 1.0 - unlike
        else:
            return unlike
    
[/code]

_num_mb_chars はマルチバイト文字の個数で、文字列 "testあ" では 1 になります。

[code]

    1.0 - (0.99 * 0.5 ** 1) => 0.505
    
[/code]

これは先程の値と一致します。ところで、`if self._num_mb_chars < 6:` の部分を見ると、文字列に含まれるマルチバイト文字が 6
文字以上であれば 0.99 が返る事が分かります。

[code]

    >>> u.feed("testあ1い2う3え4お5漢".encode("utf-8"))
    >>> u._charset_probers[0].probers[0]._num_mb_chars
    6
    >>> u._charset_probers[0].get_confidence()
    0.99
    >>> u.close()
    {'encoding': 'utf-8', 'confidence': 0.99, 'language': ''}
    
[/code]


CC BY-SA 4.0
2018-09-28T18:50:31.283
48801
2018-09-28T18:50:31.283
48793
answer