models.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. import warnings
  2. from encodings.aliases import aliases
  3. from hashlib import sha256
  4. from json import dumps
  5. from typing import Optional, List, Tuple, Set
  6. from collections import Counter
  7. from re import sub, compile as re_compile
  8. from charset_normalizer.constant import TOO_BIG_SEQUENCE
  9. from charset_normalizer.md import mess_ratio
  10. from charset_normalizer.utils import iana_name, is_multi_byte_encoding, unicode_range
  11. class CharsetMatch:
  12. def __init__(
  13. self,
  14. payload: bytes,
  15. guessed_encoding: str,
  16. mean_mess_ratio: float,
  17. has_sig_or_bom: bool,
  18. languages: "CoherenceMatches",
  19. decoded_payload: Optional[str] = None
  20. ):
  21. self._payload = payload # type: bytes
  22. self._encoding = guessed_encoding # type: str
  23. self._mean_mess_ratio = mean_mess_ratio # type: float
  24. self._languages = languages # type: CoherenceMatches
  25. self._has_sig_or_bom = has_sig_or_bom # type: bool
  26. self._unicode_ranges = None # type: Optional[List[str]]
  27. self._leaves = [] # type: List[CharsetMatch]
  28. self._mean_coherence_ratio = 0. # type: float
  29. self._output_payload = None # type: Optional[bytes]
  30. self._output_encoding = None # type: Optional[str]
  31. self._string = decoded_payload # type: Optional[str]
  32. def __eq__(self, other) -> bool:
  33. if not isinstance(other, CharsetMatch):
  34. raise TypeError('__eq__ cannot be invoked on {} and {}.'.format(str(other.__class__), str(self.__class__)))
  35. return self.encoding == other.encoding and self.fingerprint == other.fingerprint
  36. def __lt__(self, other) -> bool:
  37. """
  38. Implemented to make sorted available upon CharsetMatches items.
  39. """
  40. if not isinstance(other, CharsetMatch):
  41. raise ValueError
  42. chaos_difference = abs(self.chaos - other.chaos) # type: float
  43. # Bellow 1% difference --> Use Coherence
  44. if chaos_difference < 0.01:
  45. return self.coherence > other.coherence
  46. return self.chaos < other.chaos
  47. @property
  48. def chaos_secondary_pass(self) -> float:
  49. """
  50. Check once again chaos in decoded text, except this time, with full content.
  51. Use with caution, this can be very slow.
  52. Notice: Will be removed in 3.0
  53. """
  54. warnings.warn("chaos_secondary_pass is deprecated and will be removed in 3.0", DeprecationWarning)
  55. return mess_ratio(
  56. str(self),
  57. 1.
  58. )
  59. @property
  60. def coherence_non_latin(self) -> float:
  61. """
  62. Coherence ratio on the first non-latin language detected if ANY.
  63. Notice: Will be removed in 3.0
  64. """
  65. warnings.warn("coherence_non_latin is deprecated and will be removed in 3.0", DeprecationWarning)
  66. return 0.
  67. @property
  68. def w_counter(self) -> Counter:
  69. """
  70. Word counter instance on decoded text.
  71. Notice: Will be removed in 3.0
  72. """
  73. warnings.warn("w_counter is deprecated and will be removed in 3.0", DeprecationWarning)
  74. not_printable_pattern = re_compile(r'[0-9\W\n\r\t]+')
  75. string_printable_only = sub(not_printable_pattern, ' ', str(self).lower())
  76. return Counter(string_printable_only.split())
  77. def __str__(self) -> str:
  78. # Lazy Str Loading
  79. if self._string is None:
  80. self._string = str(self._payload, self._encoding, "strict")
  81. return self._string
  82. def __repr__(self) -> str:
  83. return "<CharsetMatch '{}' bytes({})>".format(self.encoding, self.fingerprint)
  84. def add_submatch(self, other: "CharsetMatch") -> None:
  85. if not isinstance(other, CharsetMatch) or other == self:
  86. raise ValueError("Unable to add instance <{}> as a submatch of a CharsetMatch".format(other.__class__))
  87. other._string = None # Unload RAM usage; dirty trick.
  88. self._leaves.append(other)
  89. @property
  90. def encoding(self) -> str:
  91. return self._encoding
  92. @property
  93. def encoding_aliases(self) -> List[str]:
  94. """
  95. Encoding name are known by many name, using this could help when searching for IBM855 when it's listed as CP855.
  96. """
  97. also_known_as = [] # type: List[str]
  98. for u, p in aliases.items():
  99. if self.encoding == u:
  100. also_known_as.append(p)
  101. elif self.encoding == p:
  102. also_known_as.append(u)
  103. return also_known_as
  104. @property
  105. def bom(self) -> bool:
  106. return self._has_sig_or_bom
  107. @property
  108. def byte_order_mark(self) -> bool:
  109. return self._has_sig_or_bom
  110. @property
  111. def languages(self) -> List[str]:
  112. """
  113. Return the complete list of possible languages found in decoded sequence.
  114. Usually not really useful. Returned list may be empty even if 'language' property return something != 'Unknown'.
  115. """
  116. return [e[0] for e in self._languages]
  117. @property
  118. def language(self) -> str:
  119. """
  120. Most probable language found in decoded sequence. If none were detected or inferred, the property will return
  121. "Unknown".
  122. """
  123. if not self._languages:
  124. # Trying to infer the language based on the given encoding
  125. # Its either English or we should not pronounce ourselves in certain cases.
  126. if "ascii" in self.could_be_from_charset:
  127. return "English"
  128. # doing it there to avoid circular import
  129. from charset_normalizer.cd import mb_encoding_languages, encoding_languages
  130. languages = mb_encoding_languages(self.encoding) if is_multi_byte_encoding(self.encoding) else encoding_languages(self.encoding)
  131. if len(languages) == 0 or "Latin Based" in languages:
  132. return "Unknown"
  133. return languages[0]
  134. return self._languages[0][0]
  135. @property
  136. def chaos(self) -> float:
  137. return self._mean_mess_ratio
  138. @property
  139. def coherence(self) -> float:
  140. if not self._languages:
  141. return 0.
  142. return self._languages[0][1]
  143. @property
  144. def percent_chaos(self) -> float:
  145. return round(self.chaos * 100, ndigits=3)
  146. @property
  147. def percent_coherence(self) -> float:
  148. return round(self.coherence * 100, ndigits=3)
  149. @property
  150. def raw(self) -> bytes:
  151. """
  152. Original untouched bytes.
  153. """
  154. return self._payload
  155. @property
  156. def submatch(self) -> List["CharsetMatch"]:
  157. return self._leaves
  158. @property
  159. def has_submatch(self) -> bool:
  160. return len(self._leaves) > 0
  161. @property
  162. def alphabets(self) -> List[str]:
  163. if self._unicode_ranges is not None:
  164. return self._unicode_ranges
  165. detected_ranges = set() # type: Set[str]
  166. for character in str(self):
  167. detected_range = unicode_range(character) # type: Optional[str]
  168. if detected_range:
  169. detected_ranges.add(
  170. unicode_range(character)
  171. )
  172. self._unicode_ranges = sorted(list(detected_ranges))
  173. return self._unicode_ranges
  174. @property
  175. def could_be_from_charset(self) -> List[str]:
  176. """
  177. The complete list of encoding that output the exact SAME str result and therefore could be the originating
  178. encoding.
  179. This list does include the encoding available in property 'encoding'.
  180. """
  181. return [self._encoding] + [m.encoding for m in self._leaves]
  182. def first(self) -> "CharsetMatch":
  183. """
  184. Kept for BC reasons. Will be removed in 3.0.
  185. """
  186. return self
  187. def best(self) -> "CharsetMatch":
  188. """
  189. Kept for BC reasons. Will be removed in 3.0.
  190. """
  191. return self
  192. def output(self, encoding: str = "utf_8") -> bytes:
  193. """
  194. Method to get re-encoded bytes payload using given target encoding. Default to UTF-8.
  195. Any errors will be simply ignored by the encoder NOT replaced.
  196. """
  197. if self._output_encoding is None or self._output_encoding != encoding:
  198. self._output_encoding = encoding
  199. self._output_payload = str(self).encode(encoding, "replace")
  200. return self._output_payload # type: ignore
  201. @property
  202. def fingerprint(self) -> str:
  203. """
  204. Retrieve the unique SHA256 computed using the transformed (re-encoded) payload. Not the original one.
  205. """
  206. return sha256(self.output()).hexdigest()
  207. class CharsetMatches:
  208. """
  209. Container with every CharsetMatch items ordered by default from most probable to the less one.
  210. Act like a list(iterable) but does not implements all related methods.
  211. """
  212. def __init__(self, results: List[CharsetMatch] = None):
  213. self._results = sorted(results) if results else [] # type: List[CharsetMatch]
  214. def __iter__(self):
  215. for result in self._results:
  216. yield result
  217. def __getitem__(self, item) -> CharsetMatch:
  218. """
  219. Retrieve a single item either by its position or encoding name (alias may be used here).
  220. Raise KeyError upon invalid index or encoding not present in results.
  221. """
  222. if isinstance(item, int):
  223. return self._results[item]
  224. if isinstance(item, str):
  225. item = iana_name(item, False)
  226. for result in self._results:
  227. if item in result.could_be_from_charset:
  228. return result
  229. raise KeyError
  230. def __len__(self) -> int:
  231. return len(self._results)
  232. def append(self, item: CharsetMatch) -> None:
  233. """
  234. Insert a single match. Will be inserted accordingly to preserve sort.
  235. Can be inserted as a submatch.
  236. """
  237. if not isinstance(item, CharsetMatch):
  238. raise ValueError("Cannot append instance '{}' to CharsetMatches".format(str(item.__class__)))
  239. # We should disable the submatch factoring when the input file is too heavy (conserve RAM usage)
  240. if len(item.raw) <= TOO_BIG_SEQUENCE:
  241. for match in self._results:
  242. if match.fingerprint == item.fingerprint and match.chaos == item.chaos:
  243. match.add_submatch(item)
  244. return
  245. self._results.append(item)
  246. self._results = sorted(self._results)
  247. def best(self) -> Optional["CharsetMatch"]:
  248. """
  249. Simply return the first match. Strict equivalent to matches[0].
  250. """
  251. if not self._results:
  252. return None
  253. return self._results[0]
  254. def first(self) -> Optional["CharsetMatch"]:
  255. """
  256. Redundant method, call the method best(). Kept for BC reasons.
  257. """
  258. return self.best()
  259. CoherenceMatch = Tuple[str, float]
  260. CoherenceMatches = List[CoherenceMatch]
  261. class CliDetectionResult:
  262. def __init__(self, path: str, encoding: str, encoding_aliases: List[str], alternative_encodings: List[str], language: str, alphabets: List[str], has_sig_or_bom: bool, chaos: float, coherence: float, unicode_path: Optional[str], is_preferred: bool):
  263. self.path = path # type: str
  264. self.unicode_path = unicode_path # type: Optional[str]
  265. self.encoding = encoding # type: str
  266. self.encoding_aliases = encoding_aliases # type: List[str]
  267. self.alternative_encodings = alternative_encodings # type: List[str]
  268. self.language = language # type: str
  269. self.alphabets = alphabets # type: List[str]
  270. self.has_sig_or_bom = has_sig_or_bom # type: bool
  271. self.chaos = chaos # type: float
  272. self.coherence = coherence # type: float
  273. self.is_preferred = is_preferred # type: bool
  274. @property
  275. def __dict__(self):
  276. return {
  277. 'path': self.path,
  278. 'encoding': self.encoding,
  279. 'encoding_aliases': self.encoding_aliases,
  280. 'alternative_encodings': self.alternative_encodings,
  281. 'language': self.language,
  282. 'alphabets': self.alphabets,
  283. 'has_sig_or_bom': self.has_sig_or_bom,
  284. 'chaos': self.chaos,
  285. 'coherence': self.coherence,
  286. 'unicode_path': self.unicode_path,
  287. 'is_preferred': self.is_preferred
  288. }
  289. def to_json(self) -> str:
  290. return dumps(
  291. self.__dict__,
  292. ensure_ascii=True,
  293. indent=4
  294. )
  295. CharsetNormalizerMatch = CharsetMatch