################### Querying the tree ################### Three engines branch off any node, each answering the same question (which nodes do I want?) in a different language: a native CSS matcher, an XPath 1.0 engine, and the ``find`` filter grammar. They share the node model and the interned atoms underneath, so all three resolve names to the same integer and return plain lists of nodes. .. mermaid:: flowchart LR node([a Node]) node --> css["select / select_one
matches / closest
(CSS)"] node --> xpath["xpath / xpath_one
xpath_iter
(XPath 1.0)"] node --> find["find / find_all
(filter grammar)"] css --> result([matched nodes]) xpath --> result find --> result classDef src fill:#e3f2fd,stroke:#1565c0,color:#0d47a1 classDef engine fill:#fff3e0,stroke:#e65100,color:#bf360c classDef out fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20 class node src class css,xpath,find engine class result out The query surface builds on that node model. Navigation covers parents, siblings, and the lazy :attr:`~turbohtml.Node.descendants`, :attr:`~turbohtml.Node.ancestors`, and document-order :attr:`~turbohtml.Node.following` / :attr:`~turbohtml.Node.preceding` iterators, plus the sequence protocol over a node's children. :meth:`~turbohtml.Node.find` and :meth:`~turbohtml.Node.find_all` filter a chosen :class:`~turbohtml.Axis` by tag and attributes, where a filter is a string, regex, callable, or list; a ``text`` predicate adds the same grammar over each element's collected text, the search ``bs4`` spelled ``find(string=...)``. Because a regex or callable ``text`` predicate runs Python mid-walk -- which suspends the per-tree lock -- the C side snapshots the candidate elements and their gathered text under the lock first, then runs the predicate over that snapshot, so a concurrent mutation can never tear the walk. :meth:`~turbohtml.Node.select` and :meth:`~turbohtml.Node.select_one` run a native CSS matcher covering type, id, class, attribute, the four combinators, the structural pseudo-classes (including ``:nth-child(An+B of S)``, which indexes only the siblings matching ``S``), the ``:is()``/``:where()``/``:has()``/``:not()`` functional pseudo-classes, and the ``:scope``, form/UI (``:checked``, ``:disabled``, ``:default``, ...), ``:lang()`` and ``:dir()`` pseudo-classes a static tree can determine. :meth:`~turbohtml.Node.matches` and :meth:`~turbohtml.Node.closest` test a node in place. ``:is()`` and ``:where()`` parse their argument as a forgiving selector list (a bad arm is dropped, the rest stay usable), while ``:not()`` and ``:has()`` take a real list where any bad arm is an error, as the Selectors standard specifies. The pseudo-classes that depend on live interaction or navigation state (``:hover``, ``:focus``, ``:target``, ``:visited``, ``:link``, ...) parse but match nothing, since a parsed document has no such state. Selectors compile against the tree, so a tag or attribute name resolves to the same interned atom the parser assigned and each match is an integer compare. Compiling against the tree also captures its document mode, so ``#id`` and ``.class`` fold ASCII case in a quirks-mode document and compare exactly otherwise, as the Selectors standard requires. :meth:`~turbohtml.Node.xpath`, :meth:`~turbohtml.Node.xpath_one`, and :meth:`~turbohtml.Node.xpath_iter` evaluate XPath 1.0 over the same model: a native-C engine compiles each expression once into an immutable, per-tree-cached program, resolves name tests to interned atoms, and collapses the ``//`` abbreviation to a single ``descendant`` walk, so the structural axes, predicates, operators, unions, and the core function library run at lxml's speed. A ``$name`` variable bound through a keyword argument carries a scalar or a node-set across that boundary: an :class:`~turbohtml.Element` or an iterable of them is marshaled into the engine's node-set value, ordered and de-duplicated like any other, so a prior result can feed a later expression (``doc.xpath("$rows/td", rows=doc.xpath("//tr"))``) without re-walking the tree; elements wrapped against a different document are rejected rather than dereferenced into a foreign arena. Because that program holds no tree pointers and no mutable state, :class:`turbohtml.XPath` exposes it directly: a hot expression compiles once and a single re-entrant, thread-shareable object evaluates against many context nodes, the same design lxml's ``etree.XPath`` uses. The EXSLT ``re:``, ``set:``, ``str:``, ``math:``, and ``date:`` namespaces dispatch in the same C engine, so the regexp, node-set, string, numeric, and date helpers ``libexslt`` gives lxml work without registering a namespace. A prefix-to-URI mapping passed as ``namespaces`` is resolved during evaluation rather than baked into the compiled program, so the one cached program serves every mapping; a prefixed name test then constrains the match to the foreign-content namespace the tree builder tagged (SVG or MathML), while unprefixed tests stay namespace-agnostic over the null-namespace HTML tree. The core API stays one-name-per-concept and returns plain lists, so the jQuery-style chaining pyquery users expect lives in an optional Python-side wrapper, :class:`turbohtml.query.Query`, whose traversal and mutation methods each return a wrapper. Output runs back through :attr:`~turbohtml.Node.html`, :meth:`~turbohtml.Node.serialize`, and :meth:`~turbohtml.Node.encode`, WHATWG-conformant by default with the escaping selectable through :class:`~turbohtml.Formatter`. A registered ``extensions=`` function crosses the same value boundary in both directions: the four XPath value types marshal to and from Python, so a node-set argument arrives as a list of elements and a returned element or iterable of elements becomes a node-set the engine can feed into later steps. The extension only ever sees live wrappers bound to the queried tree, never the C node model, so a returned element from another document is rejected rather than silently mixing arenas. :meth:`~turbohtml.Element.css_path` and :meth:`~turbohtml.Element.xpath_path` invert the query surface: given a node, they return the locator that finds it again, the way browser devtools "copy selector" and lxml's ``getpath`` do. The design rule is round-trip identity -- feeding ``css_path()`` back to :meth:`~turbohtml.Node.select` or ``xpath_path()`` to :meth:`~turbohtml.Node.xpath` on the document returns exactly the original node -- which dictates the form. The CSS path anchors at the nearest ancestor (or the element itself) carrying a document-unique ``id`` so the result stays short and survives reordering, falling back to ``:nth-of-type()`` steps from the root; an ``id`` is used only when it is a bare identifier the selector parser reads back verbatim and is unique under the document's own case-folding mode, so the shortcut can never resolve to a different node. The XPath form is always positional (``/html/body/div[2]/p[3]``), the shape ``getpath`` produces. Both walk only the ancestor chain, a pure read snapshotted under the per-tree lock, so no mutation can rewire the path mid-build. ***************************** Extracting strings (parsel) ***************************** Scraping is string work: the caller wants ``"/p/42"`` or ``"42"``, not a node to read an attribute off. Scrapy's ``parsel`` made that the center of its API with ``::text`` / ``::attr()`` pseudo-elements and ``Selector.re()`` / ``.re_first()``, and the migration path needs the same primitives without bolting non-standard pseudo-elements onto the CSS engine. turbohtml keeps the selector pure and adds the extraction step as three node methods instead. :meth:`~turbohtml.Element.attr` returns the *raw* attribute value as one string: ``class="a b c"`` reads back as ``"a b c"`` rather than the token list :attr:`Element.attrs ` exposes, a valueless attribute as ``""``, and an absent one as the supplied default. It is the single-string counterpart to the live mapping, the one ``parsel``'s ``::attr(name)`` translates to. :meth:`~turbohtml.Node.re` and :meth:`~turbohtml.Node.re_first` run a pattern (a ``str`` compiled once through :func:`re.compile`, or a pattern you compiled yourself) over the node's :attr:`~turbohtml.Node.text`, or over an attribute value when ``attr=`` is given. They follow ``parsel``'s group rule (yield the lone capturing group when the pattern has exactly one, else the whole match) because that is what makes a single pattern pull just the digits out of ``Order 1138``. The regex itself stays in Python's battle-tested :mod:`re`; only the source string is produced in C, under the same per-tree critical section :attr:`~turbohtml.Node.text` takes so a concurrent mutation cannot rewire the subtree mid-read. Unlike ``parsel``, these run on one node rather than a whole ``SelectorList``, so a comprehension over :meth:`~turbohtml.Node.select` covers a page: the explicit loop the rest of the query API also asks for, rather than a hidden fan-out. ***************** Reading a table ***************** A ```` is the one structure where a node tree is the *wrong* shape for the caller. A scraper wants a grid of strings, and HTML's ``rowspan`` / ``colspan`` mean the cell at visual position ``(row, col)`` is not the *n*-th ```` -- a cell can cover several columns, and a row above can reach down into the row below. ``pandas.read_html`` is the tool everyone reaches for, and it pulls in NumPy and pandas to return a ``DataFrame``. turbohtml resolves the spans itself, in C, and hands back plain lists and dicts, so the dependency is gone but the ``DataFrame`` is one call away: ``pandas.DataFrame(table.records())``. :meth:`~turbohtml.Element.rows` builds a dense grid. It walks the ```` elements that belong to the table -- skipping any nested table's subtree, whose rows belong to *that* table -- and places each ``
`` of the *n*-th ``
``/```` at the next free column, filling every slot a ``rowspan`` or ``colspan`` covers with a copy of the cell's text. Rows are padded to a rectangle, so a ragged table reads back uniform and an empty cell is ``""``. The whole grid is snapshotted into C memory under the per-tree critical section *before* any Python object is built, the same free-threading discipline the link and text walks follow: the read never dereferences a live ``first_child``/``next_sibling`` pointer across an allocation that could let another thread rewire the subtree. :meth:`~turbohtml.Element.records` keys the first row (the header, normally the ``thead`` row, which the parser emits first) over each later row; a duplicated header keeps the rightmost column's value, the way a ``dict`` does. :meth:`~turbohtml.Node.tables` runs the same grid build for every table in a subtree, nested tables included as their own entries.