Skip to content

feat(spiders): XML and CSV feed spiders + transparent gzip bodies (upstream v0.4.13) - #108

Merged
claude[bot] merged 2 commits into
masterfrom
claude/feed-spiders-1sub94
Aug 17, 2026
Merged

feat(spiders): XML and CSV feed spiders + transparent gzip bodies (upstream v0.4.13)#108
claude[bot] merged 2 commits into
masterfrom
claude/feed-spiders-1sub94

Conversation

@claude

@claude claude Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Ports the two portable features of upstream Scrapling v0.4.13 (XMLFeedSpider, CSVFeedSpider) plus the transparent gzip handling they rely on. Upstream v0.4.14 was evaluated and is a Python-packaging-only fix (uv), not applicable here.

What's new

XmlFeedSpider (src/spiders/templates/xml_feed.rs)

  • Iterates elements matching iter_tag (default "item" for RSS; use "entry" for Atom — any CSS selector works).
  • Default conversion maps each node's child element names to their trimmed recursive text (<title>X</title>"title": "X", last duplicate wins); an optional parse_node callback receives the response + node Selector for custom extraction.
  • HTML-void-tag rewrite: html5ever treats <link> and <meta> as void elements and swallows their text — and <link> carries every RSS item's URL. Before parsing, whole-tag case-insensitive occurrences are rewritten to xmlfeed-link/xmlfeed-meta (multibyte-safe byte scanner), item keys are translated back, and a user iter_tag naming a rewritten tag is translated too. Custom callbacks address the rewritten names in CSS queries (documented on the builder).
  • Feeds are terminal: parse never emits follow-up requests.

CsvFeedSpider (src/spiders/templates/csv_feed.rs)

  • Hand-rolled RFC 4180 parser: configurable delimiter, "-quoted fields with "" escapes and embedded delimiters/newlines, LF/CRLF endings, no phantom row from a trailing newline.
  • First row is the header unless headers(...) overrides it (then the first row is data). Short rows are padded with empty strings, extra cells dropped. Optional parse_row callback over the header→cell serde_json::Map; without it each row object is the item.

Transparent gzip bodies (src/fetchers/encoding.rs, wired in client.rs)

  • decode_body_capped detects the 1f 8b gzip magic (raw .xml.gz/.csv.gz files served without Content-Encoding: gzip) and decompresses before charset decoding — matching upstream's feed handling and benefiting SitemapSpider (sitemap.xml.gz) for free.
  • Bomb-guarded: decompression is capped at the fetcher's max_body via Read::take(limit + 1); oversized or corrupt gzip falls back to decoding the raw bytes (the exact pre-gzip behavior). New dependency: flate2 (already in the tree transitively).

Tests (12 new)

  • Feed e2e: RSS default conversion incl. the <link> URL, Atom via iter_tag("entry") + custom parse_node, CSV with header row, CSV with header override + ; delimiter + short/extra rows.
  • Gzip: roundtrip, 1 MiB bomb vs 64 KiB cap falls back, corrupt-stream fallback, non-gzip untouched, plus a live-TCP-server e2e serving a .gz body with no Content-Encoding.
  • CSV parser units: quotes/escapes/CRLF/multiline, trailing-newline, custom delimiter/empty fields.

All 20 test suites green locally; clippy -D warnings, rustfmt, and cargo doc --no-deps clean.


Generated by Claude Code

claude added 2 commits August 17, 2026 13:04
…stream v0.4.13)

Ports the one applicable feature pair from upstream v0.4.13 (v0.4.14 is
a Python-packaging fix, n/a):

- XmlFeedSpider: iterate feed nodes (RSS <item> by default, any CSS
  selector via iter_tag, e.g. "entry" for Atom). Without a callback,
  each node becomes an object of its child elements' text — RSS-to-items
  with zero parsing code. HTML-void feed tags (notably <link>, which
  carries every RSS item's URL and whose text html5ever would swallow)
  are rewritten to xmlfeed-* for parsing and translated back in item
  keys; custom callbacks address them by the rewritten name
  (documented).
- CsvFeedSpider: rows as header-keyed objects via a hand-rolled RFC
  4180 parser (quoted fields with "" escapes, embedded delimiters and
  newlines, LF/CRLF, no phantom trailing row), configurable delimiter,
  header override (first row becomes data), short rows padded, extra
  cells dropped.
- Transparent gzip decompression in the body decoder (magic-byte
  detection, flate2): raw .xml.gz / .csv.gz FILES served without
  Content-Encoding now arrive as text — this also closes the documented
  SitemapSpider .xml.gz gap. Decompression is capped at the fetcher's
  max_body_bytes so a decompression bomb cannot bypass the body-size
  cap; oversized or corrupt gzip falls back to the raw bytes (previous
  behavior).

Tests: 4 gzip decoder unit tests (roundtrip, bomb fallback, corrupt
fallback, non-gzip untouched), 3 CSV parser unit tests, 4 feed spider
parse tests (RSS default incl. the <link> case, Atom iter_tag + custom
callback, CSV header/override/delimiter), and a live-server end-to-end
test proving a .gz file body arrives decompressed. README gains a feed
spider section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDFsMaKk764vogjUW3nqpk
- rewrite_void_tags: expand self-closing <link/> to an explicit end tag
  (html5ever ignores the self-closing flag on unknown elements, which left
  the rewritten tag open and swallowed all following Atom entry siblings);
  accept CR/LF as tag-name boundaries per XML S; track quoted attribute
  values when locating the tag-closing '>'
- gunzip_capped: use MultiGzDecoder so concatenated gzip members
  (pigz/bgzip output) are fully decompressed instead of truncated
- parse_csv: correct the blank-line comment to match behavior (skipped,
  like Python's csv module) and pin it with a test
- docs: XmlFeedSpider limitations (namespaces, CDATA); decode_body's
  uncapped transparent gzip

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDFsMaKk764vogjUW3nqpk
@claude
claude Bot merged commit 1fce47b into master Aug 17, 2026
8 checks passed
@claude claude Bot mentioned this pull request Aug 17, 2026
@github-actions
github-actions Bot deleted the claude/feed-spiders-1sub94 branch August 17, 2026 13:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant