######################### From charset-normalizer ######################### .. package-meta:: charset-normalizer jawah/charset_normalizer `charset-normalizer `_ is the encoding detector ``requests`` ships with in place of chardet. It is a pure-Python library that decodes the input under every candidate codec CPython exposes, then scores each decode by a "chaos" (mess) metric and a language-coherence metric, keeping the cleanest reading. ``from_bytes(data).best()`` returns the winning ``CharsetMatch``; ``from_path`` and ``from_fp`` do the same for files and streams, a ``detect()`` shim mimics chardet's dict, and a ``normalizer`` console command exposes the whole thing from the shell. Because it is pure Python with no compiled dependency, it installs anywhere and is the default fallback detector across the ``requests`` ecosystem. turbohtml covers the same detection job with :func:`turbohtml.detect.detect`, a C implementation of the WHATWG sniffing pipeline (byte-order-mark and ```` prescan) followed by Firefox's negative-matching ``chardetng`` scorer, so the common case of declared or structurally certain input resolves without running a full scoring sweep. ********************************* turbohtml vs charset-normalizer ********************************* .. list-table:: :header-rows: 1 :widths: 20 40 40 - - Dimension - turbohtml - charset-normalizer - - Scope - Encoding detection as part of a WHATWG HTML engine; BOM + ```` prescan then ``chardetng`` scoring - Standalone encoding detection for arbitrary bytes; decode-and-score over every CPython codec - - Feature breadth - ``detect``, ``detect_all``, streaming ``EncodingDetector``, allow/exclude and language constraints - Rich match objects (chaos, coherence, alphabets, multiple languages), file/stream helpers, CLI, explain logging - - Performance - C, short-circuits certain input in microseconds; 24x-1280x ahead on declared/UTF-8/ASCII bytes (see table) - Pure Python, always runs its per-codec decode and scoring passes - - Typing - Fully typed; frozen ``EncodingMatch`` / ``Detection`` records with shipped stubs - Typed public API with inline annotations - - Dependencies - The turbohtml C extension (no third-party runtime deps) - Pure Python, zero runtime dependencies - - Maintenance - Active, part of the turbohtml project - Active, widely deployed as the ``requests`` chardet replacement Feature overlap =============== Portable one-to-one between the two libraries: - Best-guess detection: ``from_bytes(data).best()`` maps to :func:`~turbohtml.detect.detect`. - All ranked candidates: iterating ``from_bytes(data)`` maps to :func:`~turbohtml.detect.detect_all`. - The chardet-compatible dict: ``charset_normalizer.detect(data)`` maps to :func:`~turbohtml.detect.detect` (read ``match.encoding``, ``match.confidence``, ``match.language``). - Restricting the candidate set: ``cp_isolation=[...]`` maps to ``Detection(allowed=frozenset({...}))`` and ``cp_exclusion=[...]`` to ``Detection(excluded=frozenset({...}))``. - Language of the winning model: ``best().language`` maps to :attr:`EncodingMatch.language `. - The byte-order-mark flag: ``best().bom`` maps to :attr:`EncodingMatch.bom `. A mark reports the mark's own label -- ``UTF-8-SIG`` for a UTF-8 mark and ``UTF-16LE`` / ``UTF-16BE`` / ``UTF-32LE`` / ``UTF-32BE`` for the UTF-16 and UTF-32 marks -- so ``data.decode(match.encoding)`` (or ``utf-8-sig`` / ``utf-16`` / ``utf-32``) strips it, matching charset-normalizer's mark-aware decode. - A ```` charset in the first bytes is honored by both (charset-normalizer's ``preemptive_behaviour``, on by default, and turbohtml's prescan). What turbohtml adds =================== - A WHATWG-conformant front end: the byte-order-mark check and ```` prescan run before any statistical scoring, matching what an HTML parser actually does with the bytes. - ``chardetng``'s negative matching: a single decode error disqualifies a candidate outright, then character-pair frequencies rank the survivors. On a 15-sample multilingual differential every turbohtml answer round-trips back to the source text; charset-normalizer garbles three (Czech ISO-8859-2 read as cp1250, Turkish windows-1254 as cp1252, KOI8-R Russian as shift_jis_2004). - Microsecond short-circuits: declared or structurally certain input (BOM, ````, valid UTF-8, ISO-2022-JP escape sequences, pure ASCII) resolves without the scoring sweep charset-normalizer always runs. - A streaming :class:`~turbohtml.detect.EncodingDetector` (``feed`` / ``close`` / ``reset``, with a ``done`` early-stop flag) that mirrors chardet's ``UniversalDetector`` and always agrees with :func:`~turbohtml.detect.detect` of the concatenated bytes. - A ``Detection.chardet()`` preset that reproduces chardet's 0.2 minimum-confidence behavior. - A shell entry point: ``python -m turbohtml detect`` (installed as the ``turbohtml`` console script) prints the encoding of a file or stdin, covering the ``normalizer`` command's core job. What charset-normalizer has that turbohtml does not =================================================== - Arbitrary CPython codecs: charset-normalizer can propose any codec the interpreter ships (``big5hkscs``, ``shift_jis_2004``, and other exotic encodings). turbohtml detects only ``chardetng``'s web-focused set (UTF-8, ISO-2022-JP, five CJK encodings, 19 single-byte encodings). No equivalent for encodings outside that set. - ``normalizer``'s richer report: its command tabulates chaos, coherence, and alphabets, where ``python -m turbohtml detect`` prints only the winning encoding name. Workaround: :func:`~turbohtml.detect.detect_all` from a script for the ranked candidates. - ``explain=True`` verbose logging of the scoring decision. Workaround: :func:`~turbohtml.detect.detect_all` exposes the ranked candidates and their confidences as the introspection surface. - File and stream helpers ``from_path`` / ``from_fp``. Workaround: read the bytes yourself and pass them to :func:`~turbohtml.detect.detect`, or feed chunks to :class:`~turbohtml.detect.EncodingDetector`. - Rich per-match analysis on ``CharsetMatch``: chaos/coherence scores, alphabet listings, and multiple candidate languages. turbohtml reports a single ``confidence`` float and one ``language`` string per match. Workaround: none for the alphabet and multi-language breakdown. - Chunked-scan tuning knobs (``steps``, ``chunk_size``). No equivalent; turbohtml scores over the whole input. Performance =========== .. bench-table:: :file: bench/charset-normalizer.json Declared or structurally certain input short-circuits, so ASCII, UTF-8, and real web pages resolve in microseconds -- 24x to 1280x ahead of charset-normalizer, which always runs its scoring passes. On declaration-less legacy bytes the two trade wins (see the table). **************** How to migrate **************** Swap the import and call :func:`~turbohtml.detect.detect` in place of ``from_bytes(...).best()``: .. code-block:: python # charset-normalizer from charset_normalizer import from_bytes best = from_bytes(data).best() # CharsetMatch | None # turbohtml from turbohtml.detect import detect match = detect(data) # EncodingMatch API mapping: .. list-table:: :header-rows: 1 :widths: 50 50 - - `charset-normalizer `__ - turbohtml - - ``from_bytes(data).best()`` - :func:`~turbohtml.detect.detect` - - ``from_bytes(data)`` (every candidate) - :func:`~turbohtml.detect.detect_all` - - ``charset_normalizer.detect(data)`` (the chardet shim) - :func:`~turbohtml.detect.detect` - - ``from_bytes(data, cp_isolation=[...])`` - ``Detection(allowed=frozenset({...}))`` - - ``from_bytes(data, cp_exclusion=[...])`` - ``Detection(excluded=frozenset({...}))`` - - ``best().language`` - :attr:`EncodingMatch.language ` - - ``str(best())`` (the decoded text) - ``data.decode(match.encoding)`` - - ``best().could_be_from_charset`` (equal-fit codecs) - the runner-up entries of :func:`~turbohtml.detect.detect_all` A worked round-trip: .. testcode:: from turbohtml.detect import detect raw = "Précédemment, la créativité française".encode("cp1252") match = detect(raw) print(match.encoding) print(raw.decode(match.encoding)) .. testoutput:: windows-1252 Précédemment, la créativité française ********************** Gotchas and pitfalls ********************** - charset-normalizer's ``threshold`` bounds the *chaos* it tolerates (lower is stricter); :attr:`Detection.threshold ` floors the *confidence* it requires (higher is stricter). The two numbers measure different things, so pick a new value rather than copying one across. - Names differ in spelling: charset-normalizer reports Python codec names (``cp1251``), turbohtml the WHATWG canonical (``windows-1251``). Both decode through :mod:`python:codecs`, so downstream ``bytes.decode`` calls keep working. - ``str(best())`` returns the decoded text directly; :func:`~turbohtml.detect.detect` returns only the encoding, so decode explicitly with ``data.decode(match.encoding)``. - ``best()`` can be ``None`` when nothing scores; :func:`~turbohtml.detect.detect` always returns an :class:`~turbohtml.detect.EncodingMatch`, but its ``encoding`` is ``None`` for empty input or when every candidate is ruled out by ``threshold``, ``allowed``, or ``excluded``. - charset-normalizer can propose any codec CPython ships; turbohtml's web-focused candidate set will not surface an exotic codec such as ``shift_jis_2004`` for plain legacy text, so results can differ on inputs outside common web encodings.