################ From cssselect ################ .. package-meta:: cssselect scrapy/cssselect `cssselect `_ translates a CSS selector into an equivalent XPath 1.0 expression. It parses the selector in Python, walks the parsed tree, and emits an XPath string; it does not itself match against a document. That translator is the engine behind ``lxml.cssselect()``, `parsel `_ (and so Scrapy), and `pyquery `_, wherever a library wants CSS syntax on top of an XPath evaluator. Its public surface is small: a ``GenericTranslator`` (XML rules) and an ``HTMLTranslator`` (HTML lowercasing rules), each exposing ``css_to_xpath(css, prefix="descendant-or-self::")``, plus its ``SelectorError`` / ``SelectorSyntaxError`` / ``ExpressionError`` error types. :func:`turbohtml.convert.css_to_xpath` does the same job in a single C pass over the parsed selector, and :class:`turbohtml.convert.GenericTranslator` / :class:`turbohtml.convert.HTMLTranslator` keep cssselect's translator-object shape and error names so a port is mechanical. Because turbohtml runs a CSS selector engine and an XPath engine in the same process, the emitted XPath is validated differentially against the native CSS engine rather than trusted on its own. ************************ turbohtml vs cssselect ************************ .. list-table:: :header-rows: 1 :widths: 20 40 40 - - Dimension - turbohtml - cssselect - - Scope - CSS-to-XPath translation as one feature of a full HTML5 parser, CSS/XPath query engine, and serializer - CSS-to-XPath translation only; matching is left to the host XPath evaluator - - Feature breadth - Selectors 4 pseudo-classes with full complex arguments in ``:is()``/``:where()``/``:not()``/``:has()``, WHATWG form and ``:empty`` semantics - Selectors 3 plus documented approximations; several cases carry ``FIXME`` notes in the source - - Performance - C translator; 4x faster on a bare type selector, 32x-41x on realistic selectors (see below) - Pure-Python tokenizer and parser dominate on anything past a trivial selector - - Typing - Fully typed, ships ``py.typed`` - No inline type annotations; third-party stubs only - - Dependencies - None (self-contained C extension) - None at runtime - - Maintenance - Actively developed as part of turbohtml - Stable and maintained by the Scrapy project, low change rate Feature overlap =============== Portable 1:1 without behavior change: - ``GenericTranslator().css_to_xpath(css)`` and ``HTMLTranslator().css_to_xpath(css)`` — same method name and signature, including the positional ``prefix`` second argument. - The default ``prefix="descendant-or-self::"`` and its role of scoping each translated arm to the context node's subtree. - A comma-separated selector list translating to an XPath ``|`` union, one arm per selector. - The two failure modes as typed errors: :class:`turbohtml.SelectorSyntaxError` for a selector the grammar rejects and ``ExpressionError`` for a valid selector with no XPath 1.0 form. What turbohtml adds =================== - A plain :func:`turbohtml.convert.css_to_xpath` function, so callers who never wanted a translator object can skip it. - Full complex selectors inside logical pseudo-classes: ``:is(nav a)``, ``:where(...)``, ``:not(...)``, and the relative ``:has(> a)`` / ``:has(+ a)`` / ``:has(~ a)`` forms. - WHATWG form-state pseudo-classes — ``:disabled``/``:enabled``/``:checked``/``:required``/``:optional``/ ``:read-only``/``:read-write`` — including the ``fieldset`` first-``legend`` exemption cssselect marks as a ``FIXME``, and ``:nth-child(An+B of S)``. - Selectors 4 ``:empty`` (whitespace-only elements match) and the WHATWG case-insensitive attribute set (``type``, ``lang``, ``rel``, ...) compared case-insensitively, as browsers do. - Context-free output: every emitted predicate avoids bare ``position()`` tests, so a translated fragment keeps its meaning when embedded in a larger expression. - The native CSS engine itself (:meth:`turbohtml.Node.select`), so translation to XPath is optional rather than the only way to run a selector. What cssselect has that turbohtml does not ========================================== - A true XML / case-sensitive translation mode. cssselect's ``GenericTranslator`` matches element and attribute names case-sensitively; turbohtml always applies the HTML lowercasing rules. ``HTMLTranslator(xhtml=True)`` accepts and records the flag for signature compatibility but does not change the output. No equivalent for genuinely case-sensitive XML selection. - The non-standard cssselect extensions ``:contains()`` and ``[attr!=value]``. These are not CSS and do not parse in turbohtml; they raise ``SelectorSyntaxError``. Workaround: use the XPath ``contains(., ...)`` predicate directly, or ``:not([attr=value])`` for negated attribute matching. - ``:scope`` anywhere other than the leftmost compound. turbohtml translates ``:scope > div`` but raises ``ExpressionError`` for ``:scope`` deeper in a selector, matching cssselect's own leftmost-only behavior; neither is more general here. Performance =========== The C translator is 4x faster than cssselect on a bare type selector and 32x to 41x faster on realistic selectors, where cssselect's Python tokenizer dominates. Each ratio is against turbohtml: .. bench-table:: :file: bench/cssselect.json **************** How to migrate **************** Swap the import; cssselect's ``HTMLTranslator`` / ``GenericTranslator`` live under :mod:`turbohtml.convert` with the same method, and a bare :func:`~turbohtml.convert.css_to_xpath` function is available for call sites that never needed a translator object. .. list-table:: :header-rows: 1 :widths: 50 50 - - cssselect - turbohtml - - ``from cssselect import HTMLTranslator`` - ``from turbohtml.convert import HTMLTranslator`` - - ``HTMLTranslator().css_to_xpath(css)`` - ``HTMLTranslator().css_to_xpath(css)`` or ``css_to_xpath(css)`` - - ``GenericTranslator().css_to_xpath(css)`` - ``GenericTranslator().css_to_xpath(css)`` (HTML rules apply either way) - - ``css_to_xpath(css, prefix="//")`` - ``css_to_xpath(css, prefix="//")`` - - ``from cssselect import SelectorSyntaxError, ExpressionError`` - ``from turbohtml.convert import SelectorSyntaxError, ExpressionError`` .. code-block:: python # cssselect from cssselect import HTMLTranslator xpath = HTMLTranslator().css_to_xpath("ul > li.item") # turbohtml, either shape from turbohtml.convert import HTMLTranslator, css_to_xpath xpath = HTMLTranslator().css_to_xpath("ul > li.item") xpath = css_to_xpath("ul > li.item") .. testcode:: from turbohtml.convert import css_to_xpath print(css_to_xpath("div#main a[href^='https']")) .. testoutput:: descendant-or-self::div[@id = 'main']/descendant::a[starts-with(@href, 'https')] The ``prefix`` argument works as in cssselect (default ``descendant-or-self::``). A selector the grammar rejects raises :class:`turbohtml.SelectorSyntaxError` -- the one error every turbohtml selector-parse path shares, a :class:`ValueError` -- and a valid selector with no XPath 1.0 form raises ``ExpressionError``, under the cssselect-shaped ``SelectorError``. .. testcode:: from turbohtml.convert import ExpressionError, SelectorSyntaxError, css_to_xpath try: css_to_xpath("li:") except SelectorSyntaxError as error: print(error) try: css_to_xpath(":dir(rtl)") except ExpressionError as error: print(error) .. testoutput:: invalid CSS selector "li:": expected an identifier at position 3 :dir() cannot be expressed in XPath 1.0 ********************** Gotchas and pitfalls ********************** - The emitted string differs from cssselect's; only the selected node-set is the contract. Compare results rather than expression text. - Both translator classes apply the HTML rules (element and attribute names lowercase). turbohtml has no XML (case-sensitive) mode, so ``HTMLTranslator(xhtml=True)`` only records the flag. - ``:scope`` translates only as the leftmost compound (``:scope > div``), matching how cssselect's own translation behaves; anywhere else raises ``ExpressionError``. - cssselect's non-standard extensions ``:contains()`` and ``[attr!=value]`` are not CSS and do not parse; they raise ``SelectorSyntaxError``. Use the XPath ``contains(., ...)`` predicate or ``:not([attr=value])`` directly. - ``[lang|="en"]``, ``[type=CHECKBOX]``, ``li:empty`` on whitespace-only elements, and ``:disabled`` on hidden inputs or around ``legend`` select per the current specs, so their node-sets can differ from cssselect's approximations (the differential suite pins each divergence). - ``SelectorSyntaxError`` is :class:`turbohtml.SelectorSyntaxError`, a :class:`ValueError` (not cssselect's ``SyntaxError``), unifying it with the CSS matching engine; ``ExpressionError`` subclasses ``RuntimeError`` under ``SelectorError``. Code catching cssselect's classes by import must switch the import to :mod:`turbohtml.convert`.