Source code for chardet.sbcharsetprober

######################## BEGIN LICENSE BLOCK ########################
# The Original Code is Mozilla Universal charset detector code.
#
# The Initial Developer of the Original Code is
# Netscape Communications Corporation.
# Portions created by the Initial Developer are Copyright (C) 2001
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
#   Mark Pilgrim - port to Python
#   Shy Shalom - original C code
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see
# <https://www.gnu.org/licenses/>.
######################### END LICENSE BLOCK #########################

import logging
from collections.abc import Mapping
from typing import NamedTuple, Optional, Union

from .charsetprober import CharSetProber
from .enums import CharacterCategory, ProbingState, SequenceLikelihood


[docs] class SingleByteCharSetModel(NamedTuple): charset_name: str language: str char_to_order_map: Mapping[int, Union[CharacterCategory, int]] language_model: Mapping[int, Mapping[int, Union[SequenceLikelihood, int]]] typical_positive_ratio: float keep_ascii_letters: bool alphabet: str
[docs] class SingleByteCharSetProber(CharSetProber): SB_ENOUGH_REL_THRESHOLD = 1024 # 0.25 * SAMPLE_SIZE^2 (SAMPLE_SIZE was 64) POSITIVE_SHORTCUT_THRESHOLD = 0.95 NEGATIVE_SHORTCUT_THRESHOLD = 0.05 def __init__( self, model: SingleByteCharSetModel, is_reversed: bool = False, name_prober: Optional[CharSetProber] = None, ) -> None: super().__init__() self._model = model # TRUE if we need to reverse every pair in the model lookup self._reversed = is_reversed # Optional auxiliary prober for name decision self._name_prober = name_prober self._last_order = CharacterCategory.UNDEFINED self._seq_counters: list[int] = [] self._total_seqs = 0 self._total_char = 0 self._control_char = 0 self._freq_char = 0 self.logger = logging.getLogger(__name__) self.reset()
[docs] def reset(self) -> None: super().reset() # char order of last character self._last_order = CharacterCategory.UNDEFINED self._seq_counters = [0] * len(SequenceLikelihood) self._total_seqs = 0 self._total_char = 0 self._control_char = 0 # characters that fall in our sampling range self._freq_char = 0
@property def charset_name(self) -> Optional[str]: if self._name_prober: return self._name_prober.charset_name return self._model.charset_name @property def language(self) -> Optional[str]: if self._name_prober: return self._name_prober.language return self._model.language
[docs] def feed(self, byte_str: Union[bytes, bytearray]) -> ProbingState: if self._model.keep_ascii_letters: byte_str = self.remove_xml_tags(byte_str) else: byte_str = self.filter_international_words(byte_str) if not byte_str: return self.state char_to_order_map = self._model.char_to_order_map language_model = self._model.language_model for char in byte_str: order = char_to_order_map[char] if order < CharacterCategory.DIGIT: self._total_char += 1 elif order == CharacterCategory.UNDEFINED: # If we find a character that is undefined in the mapping, # this cannot be the right charset self._state = ProbingState.NOT_ME self._last_order = order break # TODO: Follow uchardet's lead and discount confidence for frequent # control characters. # See https://github.com/BYVoid/uchardet/commit/55b4f23971db61 elif order == CharacterCategory.CONTROL: self._control_char += 1 if 0 < order < CharacterCategory.DIGIT: self._freq_char += 1 if 0 < self._last_order < CharacterCategory.DIGIT: self._total_seqs += 1 if not self._reversed: lm_cat = language_model[self._last_order][order] else: lm_cat = language_model[order][self._last_order] self._seq_counters[lm_cat] += 1 self._last_order = order charset_name = self._model.charset_name if self.state == ProbingState.DETECTING: if self._total_seqs > self.SB_ENOUGH_REL_THRESHOLD: confidence = self.get_confidence() if confidence > self.POSITIVE_SHORTCUT_THRESHOLD: self.logger.debug( "%s confidence = %s, we have a winner", charset_name, confidence ) self._state = ProbingState.FOUND_IT elif confidence < self.NEGATIVE_SHORTCUT_THRESHOLD: self.logger.debug( "%s confidence = %s, below negative shortcut threshold %s", charset_name, confidence, self.NEGATIVE_SHORTCUT_THRESHOLD, ) self._state = ProbingState.NOT_ME # Early termination: if we have enough data and very low confidence, give up elif self._total_seqs > 512 and self._total_char > 1000: confidence = self.get_confidence() if confidence < 0.01: self.logger.debug( "%s confidence = %s, giving up early", charset_name, confidence ) self._state = ProbingState.NOT_ME return self.state
[docs] def get_confidence(self) -> float: r = 0.01 if self._total_seqs > 0 and self._total_char > 0: r = ( self._seq_counters[SequenceLikelihood.POSITIVE] / self._total_seqs / self._model.typical_positive_ratio ) # Multiply by ratio of positive sequences per character. # This helps distinguish close winners by penalizing models # that have very few positive sequences relative to the # number of characters. If you add a letter, you'd expect # the positive sequence count to increase proportionally. # If it doesn't, this new character may not have been a letter # but a symbol, making the model less confident. r *= ( self._seq_counters[SequenceLikelihood.POSITIVE] + self._seq_counters[SequenceLikelihood.LIKELY] / 4 ) / self._total_char # The more control characters (proportionnaly to the size # of the text), the less confident we become in the current # charset. r *= (self._total_char - self._control_char) / self._total_char # The more frequent characters (proportionnaly to the size # of the text), the more confident we become in the current # charset. r *= self._freq_char / self._total_char if r >= 1.0: r = 0.99 return r