Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/charset_normalizer/md.py: 90%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

610 statements  

1from __future__ import annotations 

2 

3from functools import lru_cache 

4from logging import getLogger 

5 

6 

7from .constant import ( 

8 COMMON_CJK_CHARACTERS, 

9 COMMON_SAFE_ASCII_CHARACTERS, 

10 TRACE, 

11 CompatibleFamillyRange, 

12 _ACCENTUATED, 

13 _ARABIC, 

14 _ARABIC_ISOLATED_FORM, 

15 _BASIC_LATIN_COMPATIBLE_RANGE_FAMILIES, 

16 _CJK, 

17 _COMPATIBLE_RANGE_FAMILIES, 

18 _COMPATIBLE_WITH_ANY_RANGE_FAMILIES, 

19 _HANGUL, 

20 _HALFWIDTH_KATAKANA, 

21 _HIRAGANA, 

22 _KATAKANA, 

23 _LATIN, 

24 _LIGATURE, 

25 _RANGE_FAMILIES, 

26 _SENTENCE_OPEN_PUNCTUATION, 

27 _SUPERSCRIPT, 

28 _THAI, 

29) 

30from .utils import ( 

31 _character_flags, 

32 is_emoticon, 

33 is_punctuation, 

34 is_separator, 

35 is_symbol, 

36 remove_accent, 

37 unicode_range, 

38) 

39 

40# Combined bitmask for CJK/Hangul/Katakana/Hiragana/Thai glyph detection. 

41_GLYPH_MASK: int = _CJK | _HANGUL | _KATAKANA | _HIRAGANA | _THAI 

42 

43 

44class CharInfo: 

45 """Pre-computed character properties shared across all detectors.""" 

46 

47 __slots__ = ( 

48 "character", 

49 "printable", 

50 "alpha", 

51 "upper", 

52 "lower", 

53 "space", 

54 "digit", 

55 "is_ascii", 

56 "case_variable", 

57 "flags", 

58 "accentuated", 

59 "latin", 

60 "is_cjk", 

61 "is_katakana", 

62 "is_halfwidth_katakana", 

63 "is_arabic", 

64 "is_ligature", 

65 "is_superscript", 

66 "is_sentence_open_punctuation", 

67 "is_glyph", 

68 "punct", 

69 "sym", 

70 "range", 

71 "sep", 

72 "emoticon", 

73 "safe", 

74 "common_cjk", 

75 "unaccented", 

76 ) 

77 

78 character: str 

79 printable: bool 

80 alpha: bool 

81 upper: bool 

82 lower: bool 

83 space: bool 

84 digit: bool 

85 is_ascii: bool 

86 case_variable: bool 

87 flags: int 

88 accentuated: bool 

89 latin: bool 

90 is_cjk: bool 

91 is_katakana: bool 

92 is_halfwidth_katakana: bool 

93 is_arabic: bool 

94 is_ligature: bool 

95 is_superscript: bool 

96 is_sentence_open_punctuation: bool 

97 is_glyph: bool 

98 punct: bool 

99 sym: bool 

100 range: str | None 

101 sep: bool 

102 emoticon: bool 

103 safe: bool 

104 common_cjk: bool 

105 unaccented: str 

106 

107 def __init__(self, character: str) -> None: 

108 """Compute all properties for *character* (built once per codepoint, 

109 every branch assigns every slot).""" 

110 self.character = character 

111 

112 # ASCII fast-path: for characters with ord < 128, we can skip 

113 # _character_flags() entirely and derive most properties from ord. 

114 o: int = ord(character) 

115 if o < 128: 

116 self.is_ascii = True 

117 self.accentuated = False 

118 self.unaccented = character 

119 self.emoticon = False 

120 self.common_cjk = False 

121 self.safe = character in COMMON_SAFE_ASCII_CHARACTERS 

122 self.is_cjk = False 

123 self.is_katakana = False 

124 self.is_halfwidth_katakana = False 

125 self.is_arabic = False 

126 self.is_ligature = False 

127 self.is_superscript = False 

128 self.is_sentence_open_punctuation = False 

129 self.is_glyph = False 

130 # ASCII alpha: a-z (97-122) or A-Z (65-90) 

131 if 65 <= o <= 90: 

132 # Uppercase ASCII letter 

133 self.alpha = True 

134 self.upper = True 

135 self.lower = False 

136 self.space = False 

137 self.digit = False 

138 self.printable = True 

139 self.case_variable = True 

140 self.flags = _LATIN 

141 self.latin = True 

142 self.punct = False 

143 self.sym = False 

144 elif 97 <= o <= 122: 

145 # Lowercase ASCII letter 

146 self.alpha = True 

147 self.upper = False 

148 self.lower = True 

149 self.space = False 

150 self.digit = False 

151 self.printable = True 

152 self.case_variable = True 

153 self.flags = _LATIN 

154 self.latin = True 

155 self.punct = False 

156 self.sym = False 

157 elif 48 <= o <= 57: 

158 # ASCII digit 0-9 

159 self.alpha = False 

160 self.upper = False 

161 self.lower = False 

162 self.space = False 

163 self.digit = True 

164 self.printable = True 

165 self.case_variable = False 

166 self.flags = 0 

167 self.latin = False 

168 self.punct = False 

169 self.sym = False 

170 elif o == 32 or (9 <= o <= 13): 

171 # Space, tab, newline, etc. 

172 self.alpha = False 

173 self.upper = False 

174 self.lower = False 

175 self.space = True 

176 self.digit = False 

177 self.printable = o == 32 

178 self.case_variable = False 

179 self.flags = 0 

180 self.latin = False 

181 self.punct = False 

182 self.sym = False 

183 else: 

184 # Other ASCII (punctuation, symbols, control chars) 

185 self.printable = character.isprintable() 

186 self.alpha = False 

187 self.upper = False 

188 self.lower = False 

189 self.space = False 

190 self.digit = False 

191 self.case_variable = False 

192 self.flags = 0 

193 self.latin = False 

194 self.punct = is_punctuation(character) if self.printable else False 

195 self.sym = is_symbol(character) if self.printable else False 

196 else: 

197 # Non-ASCII path 

198 self.is_ascii = False 

199 self.safe = False 

200 self.printable = character.isprintable() 

201 self.alpha = character.isalpha() 

202 self.upper = character.isupper() 

203 self.lower = character.islower() 

204 self.space = character.isspace() 

205 self.digit = character.isdigit() 

206 self.case_variable = self.lower != self.upper 

207 

208 # Flag-based classification (single unicodedata.name() call, lru-cached) 

209 flags: int = _character_flags(character) 

210 if self.alpha: 

211 self.emoticon = False 

212 else: 

213 self.emoticon = is_emoticon(character) 

214 self.flags = flags 

215 self.accentuated = bool(flags & _ACCENTUATED) 

216 self.latin = bool(flags & _LATIN) 

217 self.is_cjk = bool(flags & _CJK) 

218 self.is_katakana = bool(flags & _KATAKANA) 

219 self.is_halfwidth_katakana = bool(flags & _HALFWIDTH_KATAKANA) 

220 self.is_arabic = bool(flags & _ARABIC) 

221 self.is_ligature = bool(flags & _LIGATURE) 

222 self.is_superscript = bool(flags & _SUPERSCRIPT) 

223 self.is_sentence_open_punctuation = bool(flags & _SENTENCE_OPEN_PUNCTUATION) 

224 self.is_glyph = bool(flags & _GLYPH_MASK) 

225 

226 if self.latin and self.accentuated: 

227 self.unaccented = remove_accent(character) 

228 else: 

229 self.unaccented = character 

230 

231 self.common_cjk = self.is_cjk and character in COMMON_CJK_CHARACTERS 

232 

233 # Eagerly compute punct and sym (avoids property dispatch overhead 

234 # on 300K+ accesses in the hot loop). 

235 if self.printable: 

236 self.punct = is_punctuation(character) 

237 self.sym = is_symbol(character) 

238 else: 

239 self.punct = False 

240 self.sym = False 

241 

242 self.range = unicode_range(character) 

243 self.sep = is_separator(character) 

244 

245 

246# Per-codepoint cache of CharInfo instances 

247# At most UTF-8 size allocated. 

248@lru_cache(maxsize=None) 

249def _char_info(character: str) -> CharInfo: 

250 """Build (once per codepoint) and cache the CharInfo for *character*.""" 

251 return CharInfo(character) 

252 

253 

254# ASCII table indexed by codepoint. 

255_ASCII_CHAR_INFO: list[CharInfo] = [ 

256 CharInfo(chr(_codepoint)) for _codepoint in range(128) 

257] 

258 

259 

260class MessDetectorPlugin: 

261 """ 

262 Base abstract class used for mess detection plugins. 

263 All detectors MUST extend and implement given methods. 

264 """ 

265 

266 __slots__ = () 

267 

268 def feed_info(self, character: str, info: CharInfo) -> None: 

269 """ 

270 The main routine to be executed upon character. 

271 Insert the logic in witch the text would be considered chaotic. 

272 """ 

273 raise NotImplementedError # Defensive: 

274 

275 def reset(self) -> None: # Defensive: 

276 """ 

277 Permit to reset the plugin to the initial state. 

278 """ 

279 raise NotImplementedError 

280 

281 @property 

282 def ratio(self) -> float: 

283 """ 

284 Compute the chaos ratio based on what your feed() has seen. 

285 Must NOT be lower than 0.; No restriction gt 0. 

286 """ 

287 raise NotImplementedError # Defensive: 

288 

289 

290class TooManySymbolOrPunctuationPlugin(MessDetectorPlugin): 

291 __slots__ = ( 

292 "_punctuation_count", 

293 "_symbol_count", 

294 "_character_count", 

295 "_last_printable_char", 

296 ) 

297 

298 def __init__(self) -> None: 

299 self._punctuation_count: int = 0 

300 self._symbol_count: int = 0 

301 self._character_count: int = 0 

302 

303 self._last_printable_char: str | None = None 

304 

305 def feed_info(self, character: str, info: CharInfo) -> None: 

306 """Optimized feed using pre-computed character info.""" 

307 self._character_count += 1 

308 

309 if character != self._last_printable_char and not info.safe: 

310 if info.punct: 

311 self._punctuation_count += 1 

312 elif not info.digit and info.sym and not info.emoticon: 

313 self._symbol_count += 2 

314 

315 self._last_printable_char = character 

316 

317 def reset(self) -> None: # Abstract 

318 self._punctuation_count = 0 

319 self._character_count = 0 

320 self._symbol_count = 0 

321 

322 @property 

323 def ratio(self) -> float: 

324 if self._character_count == 0: 

325 return 0.0 

326 

327 ratio_of_punctuation: float = ( 

328 self._punctuation_count + self._symbol_count 

329 ) / self._character_count 

330 

331 return ratio_of_punctuation if ratio_of_punctuation >= 0.3 else 0.0 

332 

333 

334class TooManyAccentuatedPlugin(MessDetectorPlugin): 

335 __slots__ = ("_character_count", "_accentuated_count") 

336 

337 def __init__(self) -> None: 

338 self._character_count: int = 0 

339 self._accentuated_count: int = 0 

340 

341 def feed_info(self, character: str, info: CharInfo) -> None: 

342 """Optimized feed using pre-computed character info.""" 

343 self._character_count += 1 

344 

345 if info.accentuated: 

346 self._accentuated_count += 1 

347 

348 def reset(self) -> None: # Abstract 

349 self._character_count = 0 

350 self._accentuated_count = 0 

351 

352 @property 

353 def ratio(self) -> float: 

354 if self._character_count < 8: 

355 return 0.0 

356 

357 ratio_of_accentuation: float = self._accentuated_count / self._character_count 

358 return ratio_of_accentuation if ratio_of_accentuation >= 0.35 else 0.0 

359 

360 

361class UnprintablePlugin(MessDetectorPlugin): 

362 __slots__ = ("_unprintable_count", "_character_count", "_has_escape") 

363 

364 def __init__(self) -> None: 

365 self._unprintable_count: int = 0 

366 self._character_count: int = 0 

367 self._has_escape: bool = False 

368 

369 def feed_info(self, character: str, info: CharInfo) -> None: 

370 """Optimized feed using pre-computed character info.""" 

371 if character == "\x1b": 

372 self._has_escape = True 

373 

374 if ( 

375 not info.printable 

376 and not info.space 

377 and character != "\x1a" 

378 and character != "\ufeff" 

379 ): 

380 self._unprintable_count += 1 

381 self._character_count += 1 

382 

383 def reset(self) -> None: # Abstract 

384 self._unprintable_count = 0 

385 self._has_escape = False 

386 

387 @property 

388 def ratio(self) -> float: 

389 if self._character_count == 0: # Defensive: 

390 return 0.0 

391 

392 if self._has_escape: 

393 return 1.0 

394 

395 return (self._unprintable_count * 8) / self._character_count 

396 

397 

398class SuspiciousDuplicateAccentPlugin(MessDetectorPlugin): 

399 __slots__ = ( 

400 "_successive_count", 

401 "_character_count", 

402 "_last_latin_character", 

403 "_last_was_accentuated", 

404 ) 

405 

406 def __init__(self) -> None: 

407 self._successive_count: int = 0 

408 self._character_count: int = 0 

409 

410 self._last_latin_character: CharInfo | None = None 

411 self._last_was_accentuated: bool = False 

412 

413 def feed_info(self, character: str, info: CharInfo) -> None: 

414 """Optimized feed using pre-computed character info.""" 

415 self._character_count += 1 

416 if ( 

417 self._last_latin_character is not None 

418 and info.accentuated 

419 and self._last_was_accentuated 

420 ): 

421 if info.upper and self._last_latin_character.upper: 

422 self._successive_count += 1 

423 if info.unaccented == self._last_latin_character.unaccented: 

424 self._successive_count += 1 

425 self._last_latin_character = info 

426 self._last_was_accentuated = info.accentuated 

427 

428 def reset(self) -> None: # Abstract 

429 self._successive_count = 0 

430 self._character_count = 0 

431 self._last_latin_character = None 

432 self._last_was_accentuated = False 

433 

434 @property 

435 def ratio(self) -> float: 

436 if self._character_count == 0: 

437 return 0.0 

438 

439 return (self._successive_count * 2) / self._character_count 

440 

441 

442class SuspiciousRange(MessDetectorPlugin): 

443 __slots__ = ( 

444 "_suspicious_successive_range_count", 

445 "_character_count", 

446 "_last_printable_seen", 

447 "_last_printable_range", 

448 ) 

449 

450 def __init__(self) -> None: 

451 self._suspicious_successive_range_count: int = 0 

452 self._character_count: int = 0 

453 self._last_printable_seen: str | None = None 

454 self._last_printable_range: str | None = None 

455 

456 def feed_info(self, character: str, info: CharInfo) -> None: 

457 """Optimized feed using pre-computed character info.""" 

458 self._character_count += 1 

459 

460 if info.space or info.punct or info.safe: 

461 self._last_printable_seen = None 

462 self._last_printable_range = None 

463 return 

464 

465 if self._last_printable_seen is None: 

466 self._last_printable_seen = character 

467 self._last_printable_range = info.range 

468 return 

469 

470 unicode_range_a: str | None = self._last_printable_range 

471 unicode_range_b: str | None = info.range 

472 

473 # Identical non-None ranges can never be suspicious. 

474 if unicode_range_a != unicode_range_b or unicode_range_a is None: 

475 if is_suspiciously_successive_range(unicode_range_a, unicode_range_b): 

476 self._suspicious_successive_range_count += 1 

477 

478 self._last_printable_seen = character 

479 self._last_printable_range = unicode_range_b 

480 

481 def reset(self) -> None: # Abstract 

482 self._character_count = 0 

483 self._suspicious_successive_range_count = 0 

484 self._last_printable_seen = None 

485 self._last_printable_range = None 

486 

487 @property 

488 def ratio(self) -> float: 

489 if self._character_count <= 13: 

490 return 0.0 

491 

492 ratio_of_suspicious_range_usage: float = ( 

493 self._suspicious_successive_range_count * 2 

494 ) / self._character_count 

495 

496 return ratio_of_suspicious_range_usage 

497 

498 

499class SuperWeirdWordPlugin(MessDetectorPlugin): 

500 __slots__ = ( 

501 "_word_count", 

502 "_bad_word_count", 

503 "_foreign_long_count", 

504 "_is_current_word_bad", 

505 "_foreign_long_watch", 

506 "_character_count", 

507 "_bad_character_count", 

508 "_buffer_length", 

509 "_buffer_last_char", 

510 "_buffer_last_char_accentuated", 

511 "_buffer_accent_count", 

512 "_buffer_glyph_count", 

513 "_buffer_upper_count", 

514 "_buffer_first_lower", 

515 "_buffer_has_non_ascii", 

516 "_buffer_last_char_ligature", 

517 "_buffer_has_internal_ligature", 

518 "_is_current_word_invalid", 

519 "_invalid_word_count", 

520 ) 

521 

522 def __init__(self) -> None: 

523 self._word_count: int = 0 

524 self._bad_word_count: int = 0 

525 self._foreign_long_count: int = 0 

526 

527 self._is_current_word_bad: bool = False 

528 self._foreign_long_watch: bool = False 

529 

530 self._character_count: int = 0 

531 self._bad_character_count: int = 0 

532 

533 self._buffer_length: int = 0 

534 self._buffer_last_char: str | None = None 

535 self._buffer_last_char_accentuated: bool = False 

536 self._buffer_accent_count: int = 0 

537 self._buffer_glyph_count: int = 0 

538 self._buffer_upper_count: int = 0 

539 self._buffer_first_lower: bool = False 

540 self._buffer_has_non_ascii: bool = False 

541 self._buffer_last_char_ligature: bool = False 

542 self._buffer_has_internal_ligature: bool = False 

543 self._is_current_word_invalid: bool = False 

544 self._invalid_word_count: int = 0 

545 

546 def feed_info(self, character: str, info: CharInfo) -> None: 

547 """Optimized feed using pre-computed character info.""" 

548 if info.alpha: 

549 if self._buffer_last_char_ligature: 

550 self._buffer_has_internal_ligature = True 

551 self._buffer_last_char_ligature = info.is_ligature 

552 if self._buffer_length == 0: 

553 self._buffer_first_lower = info.lower 

554 self._buffer_length += 1 

555 self._buffer_last_char = character 

556 

557 if info.upper: 

558 self._buffer_upper_count += 1 

559 if not info.is_ascii: 

560 self._buffer_has_non_ascii = True 

561 

562 self._buffer_last_char_accentuated = info.accentuated 

563 

564 if info.accentuated: 

565 self._buffer_accent_count += 1 

566 if info.is_glyph: 

567 self._buffer_glyph_count += 1 

568 elif not self._foreign_long_watch and (not info.latin or info.accentuated): 

569 self._foreign_long_watch = True 

570 return 

571 if not self._buffer_length: 

572 return 

573 if info.is_sentence_open_punctuation or ( 

574 info.is_superscript and self._buffer_has_internal_ligature 

575 ): 

576 self._is_current_word_bad = True 

577 self._is_current_word_invalid = True 

578 if info.space or info.punct or info.sep: 

579 self._word_count += 1 

580 buffer_length: int = self._buffer_length 

581 

582 self._character_count += buffer_length 

583 

584 if buffer_length >= 4: 

585 if self._buffer_accent_count / buffer_length >= 0.5: 

586 self._is_current_word_bad = True 

587 elif ( 

588 self._buffer_last_char_accentuated 

589 and self._buffer_last_char.isupper() # type: ignore[union-attr] 

590 and self._buffer_upper_count != buffer_length 

591 ): 

592 self._foreign_long_count += 1 

593 self._is_current_word_bad = True 

594 elif self._buffer_glyph_count == 1: 

595 self._is_current_word_bad = True 

596 self._foreign_long_count += 1 

597 elif ( 

598 self._buffer_has_non_ascii 

599 and self._buffer_first_lower 

600 and self._buffer_upper_count == buffer_length - 1 

601 ): 

602 # Inverse capitalization detector. 

603 # No natural writing produces such words. 

604 # see https://github.com/jawah/charset_normalizer/issues/731 

605 self._foreign_long_count += 1 

606 self._is_current_word_bad = True 

607 if buffer_length >= 24 and self._foreign_long_watch: 

608 probable_camel_cased: bool = ( 

609 self._buffer_upper_count > 0 

610 and self._buffer_upper_count / buffer_length <= 0.3 

611 ) 

612 

613 if not probable_camel_cased: 

614 self._foreign_long_count += 1 

615 self._is_current_word_bad = True 

616 

617 if self._is_current_word_bad: 

618 self._bad_word_count += 1 

619 self._bad_character_count += buffer_length 

620 self._is_current_word_bad = False 

621 if self._is_current_word_invalid: 

622 self._invalid_word_count += 1 

623 self._is_current_word_invalid = False 

624 

625 self._foreign_long_watch = False 

626 self._buffer_length = 0 

627 self._buffer_last_char = None 

628 self._buffer_last_char_accentuated = False 

629 self._buffer_accent_count = 0 

630 self._buffer_glyph_count = 0 

631 self._buffer_upper_count = 0 

632 self._buffer_first_lower = False 

633 self._buffer_has_non_ascii = False 

634 self._buffer_last_char_ligature = False 

635 self._buffer_has_internal_ligature = False 

636 elif ( 

637 character not in {"<", ">", "-", "=", "~", "|", "_"} 

638 and not info.digit 

639 and info.sym 

640 ): 

641 self._is_current_word_bad = True 

642 self._buffer_length += 1 

643 self._buffer_last_char = character 

644 self._buffer_last_char_accentuated = False 

645 

646 def reset(self) -> None: # Abstract 

647 self._buffer_length = 0 

648 self._buffer_last_char = None 

649 self._buffer_last_char_accentuated = False 

650 self._is_current_word_bad = False 

651 self._foreign_long_watch = False 

652 self._bad_word_count = 0 

653 self._word_count = 0 

654 self._character_count = 0 

655 self._bad_character_count = 0 

656 self._foreign_long_count = 0 

657 self._buffer_accent_count = 0 

658 self._buffer_glyph_count = 0 

659 self._buffer_upper_count = 0 

660 self._buffer_first_lower = False 

661 self._buffer_has_non_ascii = False 

662 self._buffer_last_char_ligature = False 

663 self._buffer_has_internal_ligature = False 

664 self._is_current_word_invalid = False 

665 self._invalid_word_count = 0 

666 

667 @property 

668 def ratio(self) -> float: 

669 if self._invalid_word_count: 

670 return 1.0 

671 

672 if self._word_count <= 10 and self._foreign_long_count == 0: 

673 return 0.0 

674 

675 return self._bad_character_count / self._character_count 

676 

677 

678class CjkUncommonPlugin(MessDetectorPlugin): 

679 """ 

680 Detect messy CJK text that probably means nothing. 

681 """ 

682 

683 __slots__ = ("_character_count", "_uncommon_count") 

684 

685 def __init__(self) -> None: 

686 self._character_count: int = 0 

687 self._uncommon_count: int = 0 

688 

689 def feed_info(self, character: str, info: CharInfo) -> None: 

690 """Optimized feed using pre-computed character info.""" 

691 self._character_count += 1 

692 

693 if not info.common_cjk: 

694 self._uncommon_count += 1 

695 

696 def reset(self) -> None: # Abstract 

697 self._character_count = 0 

698 self._uncommon_count = 0 

699 

700 @property 

701 def ratio(self) -> float: 

702 if self._character_count < 4: 

703 return 0.0 

704 

705 uncommon_form_usage: float = ( 

706 2 * self._uncommon_count - self._character_count 

707 ) / (5 * max(self._character_count, 16)) 

708 

709 # we can be pretty sure it's garbage when uncommon characters are widely 

710 # used. otherwise it could just be traditional chinese for example. 

711 return max(0.0, uncommon_form_usage) 

712 

713 

714class SuspiciousKatakanaPlugin(MessDetectorPlugin): 

715 """Detect implausible halfwidth Katakana and uncommon CJK combinations.""" 

716 

717 __slots__ = ( 

718 "_katakana_count", 

719 "_halfwidth_katakana_count", 

720 "_cjk_count", 

721 "_uncommon_cjk_count", 

722 ) 

723 

724 def __init__(self) -> None: 

725 self._katakana_count: int = 0 

726 self._halfwidth_katakana_count: int = 0 

727 self._cjk_count: int = 0 

728 self._uncommon_cjk_count: int = 0 

729 

730 def feed_info(self, character: str, info: CharInfo) -> None: 

731 """Optimized feed using pre-computed character info.""" 

732 if info.is_katakana: 

733 self._katakana_count += 1 

734 if info.is_halfwidth_katakana: 

735 self._halfwidth_katakana_count += 1 

736 return 

737 

738 self._cjk_count += 1 

739 if not info.common_cjk: 

740 self._uncommon_cjk_count += 1 

741 

742 def reset(self) -> None: # Abstract 

743 self._katakana_count = 0 

744 self._halfwidth_katakana_count = 0 

745 self._cjk_count = 0 

746 self._uncommon_cjk_count = 0 

747 

748 @property 

749 def ratio(self) -> float: 

750 if ( 

751 self._halfwidth_katakana_count >= 4 

752 and self._halfwidth_katakana_count == self._katakana_count 

753 and 3 <= self._cjk_count == self._uncommon_cjk_count 

754 ): 

755 return 1.0 

756 

757 return 0.0 

758 

759 

760class ArchaicUpperLowerPlugin(MessDetectorPlugin): 

761 __slots__ = ( 

762 "_buf", 

763 "_character_count_since_last_sep", 

764 "_successive_upper_lower_count", 

765 "_successive_upper_lower_count_final", 

766 "_character_count", 

767 "_last_alpha_seen_upper", 

768 "_last_alpha_seen_lower", 

769 "_current_ascii_only", 

770 ) 

771 

772 def __init__(self) -> None: 

773 self._buf: bool = False 

774 

775 self._character_count_since_last_sep: int = 0 

776 

777 self._successive_upper_lower_count: int = 0 

778 self._successive_upper_lower_count_final: int = 0 

779 

780 self._character_count: int = 0 

781 

782 self._last_alpha_seen_upper: bool = False 

783 self._last_alpha_seen_lower: bool = False 

784 self._current_ascii_only: bool = True 

785 

786 def feed_info(self, character: str, info: CharInfo) -> None: 

787 """Optimized feed using pre-computed character info.""" 

788 is_concerned: bool = info.alpha and info.case_variable 

789 chunk_sep: bool = not is_concerned 

790 

791 if chunk_sep and self._character_count_since_last_sep > 0: 

792 if ( 

793 self._character_count_since_last_sep <= 64 

794 and not info.digit 

795 and not self._current_ascii_only 

796 ): 

797 self._successive_upper_lower_count_final += ( 

798 self._successive_upper_lower_count 

799 ) 

800 

801 self._successive_upper_lower_count = 0 

802 self._character_count_since_last_sep = 0 

803 self._buf = False 

804 self._character_count += 1 

805 self._current_ascii_only = True 

806 

807 return 

808 

809 if self._current_ascii_only and not info.is_ascii: 

810 self._current_ascii_only = False 

811 

812 if self._character_count_since_last_sep > 0: 

813 if (info.upper and self._last_alpha_seen_lower) or ( 

814 info.lower and self._last_alpha_seen_upper 

815 ): 

816 if self._buf: 

817 self._successive_upper_lower_count += 2 

818 self._buf = False 

819 else: 

820 self._buf = True 

821 else: 

822 self._buf = False 

823 

824 self._character_count += 1 

825 self._character_count_since_last_sep += 1 

826 self._last_alpha_seen_upper = info.upper 

827 self._last_alpha_seen_lower = info.lower 

828 

829 def reset(self) -> None: # Abstract 

830 self._character_count = 0 

831 self._character_count_since_last_sep = 0 

832 self._successive_upper_lower_count = 0 

833 self._successive_upper_lower_count_final = 0 

834 self._last_alpha_seen_upper = False 

835 self._last_alpha_seen_lower = False 

836 self._buf = False 

837 self._current_ascii_only = True 

838 

839 @property 

840 def ratio(self) -> float: 

841 if self._character_count == 0: # Defensive: 

842 return 0.0 

843 

844 return self._successive_upper_lower_count_final / self._character_count 

845 

846 

847class ArabicIsolatedFormPlugin(MessDetectorPlugin): 

848 __slots__ = ("_character_count", "_isolated_form_count") 

849 

850 def __init__(self) -> None: 

851 self._character_count: int = 0 

852 self._isolated_form_count: int = 0 

853 

854 def reset(self) -> None: # Abstract 

855 self._character_count = 0 

856 self._isolated_form_count = 0 

857 

858 def feed_info(self, character: str, info: CharInfo) -> None: 

859 """Optimized feed using pre-computed character info.""" 

860 self._character_count += 1 

861 

862 if info.flags & _ARABIC_ISOLATED_FORM: 

863 self._isolated_form_count += 1 

864 

865 @property 

866 def ratio(self) -> float: 

867 if self._character_count < 8: 

868 return 0.0 

869 

870 isolated_form_usage: float = self._isolated_form_count / self._character_count 

871 

872 return isolated_form_usage 

873 

874 

875@lru_cache(maxsize=None) 

876def is_suspiciously_successive_range( 

877 unicode_range_a: str | None, unicode_range_b: str | None 

878) -> bool: 

879 """ 

880 Determine if two Unicode ranges seen next to each other can be considered suspicious. 

881 """ 

882 if unicode_range_a is None or unicode_range_b is None: 

883 return True 

884 

885 familly_a: str = _RANGE_FAMILIES[unicode_range_a] 

886 familly_b: str = _RANGE_FAMILIES[unicode_range_b] 

887 

888 if familly_a == familly_b: 

889 return False 

890 

891 if ( 

892 familly_a in _COMPATIBLE_WITH_ANY_RANGE_FAMILIES 

893 or familly_b in _COMPATIBLE_WITH_ANY_RANGE_FAMILIES 

894 ): 

895 return False 

896 

897 if CompatibleFamillyRange(familly_a, familly_b) in _COMPATIBLE_RANGE_FAMILIES: 

898 return False 

899 

900 # Basic Latin is commonly interspersed with East Asian scripts, but the 

901 # compatibility must not extend to every range in the Latin family. 

902 if unicode_range_a == "Basic Latin": 

903 return familly_b not in _BASIC_LATIN_COMPATIBLE_RANGE_FAMILIES 

904 

905 if unicode_range_b == "Basic Latin": 

906 return familly_a not in _BASIC_LATIN_COMPATIBLE_RANGE_FAMILIES 

907 

908 return True 

909 

910 

911def mess_ratio( 

912 decoded_sequence: str, maximum_threshold: float = 0.2, debug: bool = False 

913) -> float: 

914 """ 

915 Compute a mess ratio given a decoded bytes sequence. The maximum threshold does stop the computation earlier. 

916 """ 

917 

918 seq_len: int = len(decoded_sequence) 

919 

920 if seq_len < 511: 

921 step: int = 32 

922 elif seq_len < 1024: 

923 step = 64 

924 else: 

925 step = 128 

926 

927 # str.isascii() is O(1) (the flag lives in the str header). Seven of the 

928 # ten detectors provably keep a 0.0 ratio on ASCII-only input and are 

929 # therefore not fed at all. 

930 is_pure_ascii: bool = decoded_sequence.isascii() 

931 

932 # Cached per-codepoint character properties (see CharInfo). ASCII 

933 # characters resolve through the immutable import-time table; anything 

934 # else goes through the lru_cache-backed slow path. 

935 ascii_info = _ASCII_CHAR_INFO 

936 char_info = _char_info 

937 

938 mean_mess_ratio: float 

939 info: CharInfo 

940 

941 # Create each detector as a named local variable (unrolled from the generic loop). 

942 # This eliminates per-character iteration over the detector list and 

943 # per-character eligible() virtual dispatch, while keeping every plugin class 

944 # intact and fully readable. 

945 d_sp: TooManySymbolOrPunctuationPlugin = TooManySymbolOrPunctuationPlugin() 

946 d_ta: TooManyAccentuatedPlugin = TooManyAccentuatedPlugin() 

947 d_up: UnprintablePlugin = UnprintablePlugin() 

948 d_sda: SuspiciousDuplicateAccentPlugin = SuspiciousDuplicateAccentPlugin() 

949 d_sr: SuspiciousRange = SuspiciousRange() 

950 d_sw: SuperWeirdWordPlugin = SuperWeirdWordPlugin() 

951 d_cu: CjkUncommonPlugin = CjkUncommonPlugin() 

952 d_sk: SuspiciousKatakanaPlugin = SuspiciousKatakanaPlugin() 

953 d_au: ArchaicUpperLowerPlugin = ArchaicUpperLowerPlugin() 

954 d_ai: ArabicIsolatedFormPlugin = ArabicIsolatedFormPlugin() 

955 

956 # Local references avoid repeated bound-method creation in the hot loop. 

957 d_sp_feed = d_sp.feed_info 

958 d_ta_feed = d_ta.feed_info 

959 d_up_feed = d_up.feed_info 

960 d_sda_feed = d_sda.feed_info 

961 d_sr_feed = d_sr.feed_info 

962 d_sw_feed = d_sw.feed_info 

963 d_cu_feed = d_cu.feed_info 

964 d_sk_feed = d_sk.feed_info 

965 d_au_feed = d_au.feed_info 

966 d_ai_feed = d_ai.feed_info 

967 

968 for block_start in range(0, seq_len, step): 

969 for character in decoded_sequence[block_start : block_start + step]: 

970 # Character properties computed once per distinct codepoint 

971 # (shared across all plugins and all mess_ratio calls). 

972 # ord() doubles as the ASCII table index. 

973 codepoint: int = ord(character) 

974 if codepoint < 128: 

975 info = ascii_info[codepoint] 

976 else: 

977 info = char_info(character) 

978 

979 # Detectors with eligible() == always True 

980 d_up_feed(character, info) 

981 d_sw_feed(character, info) 

982 

983 if is_pure_ascii: 

984 # The seven remaining detectors provably stay at 0.0 (see above). 

985 if info.printable: 

986 d_sp_feed(character, info) 

987 continue 

988 

989 d_au_feed(character, info) 

990 

991 # Detectors with eligible() == isprintable 

992 if info.printable: 

993 d_sp_feed(character, info) 

994 d_sr_feed(character, info) 

995 

996 # Detectors with eligible() == isalpha 

997 if info.alpha: 

998 d_ta_feed(character, info) 

999 # SuspiciousDuplicateAccent: isalpha() and is_latin() 

1000 if info.latin: 

1001 d_sda_feed(character, info) 

1002 # CjkUncommon and SuspiciousKatakana: is_cjk() 

1003 if info.is_cjk: 

1004 d_cu_feed(character, info) 

1005 d_sk_feed(character, info) 

1006 elif info.is_katakana: 

1007 d_sk_feed(character, info) 

1008 # ArabicIsolatedForm: is_arabic() 

1009 if info.is_arabic: 

1010 d_ai_feed(character, info) 

1011 

1012 mean_mess_ratio = ( 

1013 d_sp.ratio 

1014 + d_ta.ratio 

1015 + d_up.ratio 

1016 + d_sda.ratio 

1017 + d_sr.ratio 

1018 + d_sw.ratio 

1019 + d_cu.ratio 

1020 + d_sk.ratio 

1021 + d_au.ratio 

1022 + d_ai.ratio 

1023 ) 

1024 

1025 if mean_mess_ratio >= maximum_threshold: 

1026 break 

1027 else: 

1028 # Flush last word buffer in SuperWeirdWordPlugin via trailing newline. 

1029 nl_info = ascii_info[10] # "\n" 

1030 d_sw_feed("\n", nl_info) 

1031 if not is_pure_ascii: 

1032 d_au_feed("\n", nl_info) 

1033 d_up_feed("\n", nl_info) 

1034 

1035 mean_mess_ratio = ( 

1036 d_sp.ratio 

1037 + d_ta.ratio 

1038 + d_up.ratio 

1039 + d_sda.ratio 

1040 + d_sr.ratio 

1041 + d_sw.ratio 

1042 + d_cu.ratio 

1043 + d_sk.ratio 

1044 + d_au.ratio 

1045 + d_ai.ratio 

1046 ) 

1047 

1048 if debug: # Defensive: 

1049 logger = getLogger("charset_normalizer") 

1050 

1051 logger.log( 

1052 TRACE, 

1053 "Mess-detector extended-analysis start. " 

1054 f"intermediary_mean_mess_ratio_calc={step} mean_mess_ratio={mean_mess_ratio} " 

1055 f"maximum_threshold={maximum_threshold}", 

1056 ) 

1057 

1058 if seq_len > 16: 

1059 logger.log(TRACE, f"Starting with: {decoded_sequence[:16]}") 

1060 logger.log(TRACE, f"Ending with: {decoded_sequence[-16::]}") 

1061 

1062 for dt in [d_sp, d_ta, d_up, d_sda, d_sr, d_sw, d_cu, d_sk, d_au, d_ai]: 

1063 logger.log(TRACE, f"{dt.__class__}: {dt.ratio}") 

1064 

1065 return round(mean_mess_ratio, 3)