はじめに
Python3 で chardet を使った文字コードの判定で UTF-8 になってほしいところが別な文字コード(Windows-1254など)で判定されることがあり、どういう判定をしているのかを調べてみました。
動かしたりドキュメントを読みながら調べる
以下のようなコードであるとします。
import chardet filepath = "test.csv" with open(filepath, 'rb') as f: c = f.read() result = chardet.detect(c) print(result)
chardet.detect(c) の部分で __init__.py に detect() メソッドがあり、バイト列として処理しているようです。
- https://github.com/chardet/chardet/blob/5.2.0/chardet/init.py#L30-L50
def detect( byte_str: Union[bytes, bytearray], should_rename_legacy: bool = False ) -> ResultDict: """ Detect the encoding of the given byte string. :param byte_str: The byte sequence to examine. :type byte_str: ``bytes`` or ``bytearray`` :param should_rename_legacy: Should we rename legacy encodings to their more modern equivalents? :type should_rename_legacy: ``bool`` """ if not isinstance(byte_str, bytearray): if not isinstance(byte_str, bytes): raise TypeError( f"Expected object of type bytes or bytearray, got: {type(byte_str)}" ) byte_str = bytearray(byte_str) detector = UniversalDetector(should_rename_legacy=should_rename_legacy) detector.feed(byte_str) return detector.close()
ちなみにPythonのバイト列は以下のようなものです。
>>> content='あ'.encode('utf-8') >>> print(content) b'\xe3\x81\x82'
detector.feed(byte_str) で chardet/universaldetector.py の feed() メソッドが呼ばれているようで、ここで文字列の解析が行われているようです。
InputState.HIGH_BYTE (127文字以上のマルチバイト文字) の場合 MBCSGroupProber, SBCSGroupProber, Latin1Prober, MacRomanProber が使われるようです。
class UniversalDetector: : # If we've seen high bytes (i.e., those with values greater than 127), # we need to do more complicated checks using all our multi-byte and # single-byte probers that are left. The single-byte probers # use character bigram distributions to determine the encoding, whereas # the multi-byte probers use a combination of character unigram and # bigram distributions. elif self._input_state == InputState.HIGH_BYTE: if not self._charset_probers: self._charset_probers = [MBCSGroupProber(self.lang_filter)] # If we're checking non-CJK encodings, use single-byte prober if self.lang_filter & LanguageFilter.NON_CJK: self._charset_probers.append(SBCSGroupProber()) self._charset_probers.append(Latin1Prober()) self._charset_probers.append(MacRomanProber()) for prober in self._charset_probers: if prober.feed(byte_str) == ProbingState.FOUND_IT: self.result = { "encoding": prober.charset_name, "confidence": prober.get_confidence(), "language": prober.language, } self.done = True break if self.WIN_BYTE_DETECTOR.search(byte_str): self._has_win_bytes = True :
https://github.com/chardet/chardet/blob/98b2acd6216e9a0fa4f47940b9f8adabdfd8aa8a/chardet/universaldetector.py#L266-L271
charset_probers にProberを格納し、それぞれ対応する chardet/*prober.py の feed() メソッドが呼ばれて解析が行われるようです。
処理の内容に関してはドキュメントを見ると以下のような記載がありました。 1文字だけでなく、連続した2文字の出現頻度分布からも評価している場合もあるようです。
MultiByteCharSetProber runs the text through the encoding-specific state machine, one byte at a time, to look for byte sequences that would indicate a conclusive positive or negative result. At the same time, MultiByteCharSetProber feeds the text to an encoding-specific distribution analyzer.
The distribution analyzers (each defined in chardistribution.py) use language-specific models of which characters are used most frequently. Once MultiByteCharSetProber has fed enough text to the distribution analyzer, it calculates a confidence rating based on the number of frequently-used characters, the total number of characters, and a language-specific distribution ratio. If the confidence is high enough, MultiByteCharSetProber returns the result to MBCSGroupProber, which returns it to UniversalDetector, which returns it to the caller.
The case of Japanese is more difficult. Single-character distribution analysis is not always sufficient to distinguish between EUC-JP and SHIFT_JIS, so the SJISProber (defined in sjisprober.py) also uses 2-character distribution analysis. SJISContextAnalysis and EUCJPContextAnalysis (both defined in jpcntx.py and both inheriting from a common JapaneseContextAnalysis class) check the frequency of Hiragana syllabary characters within the text. Once enough text has been processed, they return a confidence level to SJISProber, which checks both analyzers and returns the higher confidence level to MBCSGroupProber.
SBCSGroupProber feeds the text to each of these encoding+language-specific probers and checks the results. These probers are all implemented as a single class, SingleByteCharSetProber (defined in sbcharsetprober.py), which takes a language model as an argument. The language model defines how frequently different 2-character sequences appear in typical text. SingleByteCharSetProber processes the text and tallies the most frequently used 2-character sequences. Once enough text has been processed, it calculates a confidence level based on the number of frequently-used sequences, the total number of characters, and a language-specific distribution ratio.
バイト列に応じて各 chardet/*prober.py の get_confidence() メソッドで評価値を算出して、それぞれの戻り値を比較して、最大値を返す prober が判定した文字コードを採用しているようです。
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 return unlike
シングルバイト文字の場合は以下のようです。
def get_confidence(self) -> float: r = 0.01 if self._total_seqs > 0: r = ( ( self._seq_counters[SequenceLikelihood.POSITIVE] + 0.25 * self._seq_counters[SequenceLikelihood.LIKELY] ) / self._total_seqs / self._model.typical_positive_ratio ) # The more control characters (proportionnaly to the size # of the text), the less confident we become in the current # charset. r = r * (self._total_char - self._control_char) / self._total_char r = r * self._freq_char / self._total_char if r >= 1.0: r = 0.99 return r
>>> import chardet >>> chardet.__version__ '5.2.0' >>> u = chardet.UniversalDetector() >>> u.feed("あ".encode("utf-8")) >>> u._charset_probers[0].get_confidence() 0.505 >>> u._charset_probers[1].get_confidence() 0.01 >>> u._charset_probers[2].get_confidence() 0.01
以下のように、バイト列によっては異なる文字コードと判定されてしまうこともあるようです。
>>> import chardet >>> chardet.__version__ '5.2.0' >>> u = chardet.UniversalDetector() >>> u.feed("testあ".encode("utf-8")) >>> u._charset_probers[0].get_confidence() 0.505 >>> u._charset_probers[1].get_confidence() 0.5889255495043456
終わりに
ざっと見た限り以下であるように思いました。
- chardetは対象の文字をバイト列として読み込んで処理しているようである
- バイト長に応じていくつかの文字コード判定用の Prober (判定処理)を通して、 confidence 値を算出し、それが一番高いものを採用しているようである
- 1文字だけでなく連続した2文字の出現頻度の分布も評価・比較している