<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: neuralbyte</title>
    <description>The latest articles on DEV Community by neuralbyte (@neuralbyte).</description>
    <link>https://dev.to/neuralbyte</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4072820%2Fac6deaca-74f5-4c90-80c2-c396201f1382.png</url>
      <title>DEV Community: neuralbyte</title>
      <link>https://dev.to/neuralbyte</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/neuralbyte"/>
    <language>en</language>
    <item>
      <title>Migrating a Crawl Pipeline from Firecrawl: Keep the Document Contract Stable</title>
      <dc:creator>neuralbyte</dc:creator>
      <pubDate>Fri, 16 Oct 2026 08:53:37 +0000</pubDate>
      <link>https://dev.to/neuralbyte/migrating-a-crawl-pipeline-from-firecrawl-keep-the-document-contract-stable-391i</link>
      <guid>https://dev.to/neuralbyte/migrating-a-crawl-pipeline-from-firecrawl-keep-the-document-contract-stable-391i</guid>
      <description>&lt;p&gt;Changing crawl providers is a workflow migration, not a base-URL edit.&lt;/p&gt;

&lt;p&gt;Firecrawl's current Crawl API starts a job, returns a job ID, and exposes status and paginated results. Its Python SDK can wait for a crawl and return all results. Nstdata's docs describe async crawl submission, status polling, paginated page results, and storage references for large Markdown artifacts. The route examples differ across sections of the Nstdata docs, so verify the exact route and response schema in your account documentation before deploying. &lt;/p&gt;

&lt;p&gt;This post focuses on a safer seam: normalize both providers into one application-owned document type before chunking or indexing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The code is illustrative and has not been run against either paid API. Verify current API contracts before production use.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Define an internal document contract
&lt;/h2&gt;

&lt;p&gt;Keep provider response fields out of the rest of the RAG pipeline:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;python&lt;br&gt;
from dataclasses import dataclass&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@dataclass(frozen=True)&lt;br&gt;
class Document:&lt;br&gt;
    url: str&lt;br&gt;
    canonical_url: str | None&lt;br&gt;
    title: str | None&lt;br&gt;
    markdown: str&lt;br&gt;
    status_code: int | None&lt;br&gt;
    fetched_at: str&lt;br&gt;
    provider: str&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Include enough provenance to inspect bad retrieval later. In production, the job ID, original input URL, artifact reference, response metadata, and retry count may belong in a separate ingestion record.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Normalize at the adapter boundary
&lt;/h2&gt;

&lt;p&gt;The following function consumes a page metadata dictionary and the already-resolved Markdown text:&lt;/p&gt;

&lt;p&gt;`def normalize_page(page: dict, markdown: str, provider: str) -&amp;gt; Document:&lt;br&gt;
    url = page.get("url") or page.get("sourceURL") or page.get("sourceUrl")&lt;br&gt;
    if not isinstance(url, str) or not url.startswith(("http://", "https://")):&lt;br&gt;
        raise ValueError("Page has no absolute source URL")&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;body = markdown.strip()
if not body:
    raise ValueError(f"Empty page content: {url}")

metadata = page.get("metadata") or {}
status = metadata.get("statusCode", page.get("statusCode"))

return Document(
    url=url,
    canonical_url=metadata.get("canonicalUrl") or page.get("canonicalUrl"),
    title=metadata.get("title") or page.get("title"),
    markdown=body,
    status_code=int(status) if status is not None else None,
    fetched_at=page.get("fetchedAt") or page.get("fetched_at") or "",
    provider=provider,
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;`&lt;/p&gt;

&lt;p&gt;The mapper is intentionally small. Extend it only for fields your downstream logic uses. Preserve the raw provider response in an audit store so a schema change or parser issue is diagnosable.&lt;/p&gt;

&lt;p&gt;For Nstdata, resolve markdownRef before calling the normalizer. The reference token identifies stored output; it is not the Markdown itself. For Firecrawl, normalize the SDK document or each direct API result page. The Firecrawl SDK manages pagination when using its crawl-and-wait method; callers of the direct API should follow next until it is absent. &lt;a href="https://docs.firecrawl.dev/features/crawl" rel="noopener noreferrer"&gt;Firecrawl response handling&lt;/a&gt; · &lt;a href="https://docs.nstdata.io/docs/crawl" rel="noopener noreferrer"&gt;Nstdata artifact reads&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Persist job state so the migration can resume
&lt;/h2&gt;

&lt;p&gt;Use a durable record per crawl run:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;text&lt;br&gt;
run_id&lt;br&gt;
provider&lt;br&gt;
input_manifest_hash&lt;br&gt;
job_id&lt;br&gt;
submitted_at&lt;br&gt;
last_status&lt;br&gt;
last_cursor&lt;br&gt;
accepted_count&lt;br&gt;
rejected_count&lt;br&gt;
terminal_error&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A migration runner can then follow this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load a fixed, versioned URL manifest.&lt;/li&gt;
&lt;li&gt;Submit a bounded crawl with an explicit page cap and path filters.&lt;/li&gt;
&lt;li&gt;Persist the job ID before polling.&lt;/li&gt;
&lt;li&gt;Poll with bounded backoff until a terminal state.&lt;/li&gt;
&lt;li&gt;Fetch result pages until pagination ends.&lt;/li&gt;
&lt;li&gt;Resolve large artifact references and normalize each page.&lt;/li&gt;
&lt;li&gt;Record rejected URLs with a reason instead of silently dropping them.&lt;/li&gt;
&lt;li&gt;Write accepted documents into a staging index.&lt;/li&gt;
&lt;li&gt;Compare the URL set and retrieval quality with the existing index.&lt;/li&gt;
&lt;li&gt;Switch a small cohort, monitor it, then expand.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A timeout between submission and polling should resume from the saved job ID. Restarting the entire crawl can duplicate billable work. Nstdata currently documents no external webhooks, so async completion needs a poller or a separate event mechanism. &lt;/p&gt;

&lt;h2&gt;
  
  
  4. Treat acceptance as a separate status
&lt;/h2&gt;

&lt;p&gt;HTTP status and crawl task status do not decide whether a page belongs in your RAG index. A 404 can be retrieved successfully but be invalid documentation; a 200 response can contain an access challenge, empty shell, or partial JavaScript content.&lt;/p&gt;

&lt;p&gt;Define an application-level validation result:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;python&lt;br&gt;
def accept_document(doc: Document) -&amp;gt; tuple[bool, str]:&lt;br&gt;
    if doc.status_code in (403, 404):&lt;br&gt;
        return False, f"target_http_{doc.status_code}"&lt;br&gt;
    if len(doc.markdown.strip()) &amp;lt; 80:&lt;br&gt;
        return False, "content_too_short"&lt;br&gt;
    if "access denied" in doc.markdown[:500].lower():&lt;br&gt;
        return False, "challenge_or_block_page"&lt;br&gt;
    return True, "accepted"&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The threshold and block-page checks are examples to customize; a generic length rule will not fit every site. Add source-specific checks for required headings, code sections, or product fields. Keep terminal failures separate from retryable failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Run the same corpus through both providers
&lt;/h2&gt;

&lt;p&gt;Use 100 representative URLs: static docs, JavaScript pages, long code samples, tables, redirects, and pages that previously failed. Compare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accepted page count and missing URL set;&lt;/li&gt;
&lt;li&gt;canonical URLs and duplicate handling;&lt;/li&gt;
&lt;li&gt;heading, code, table, and link retention;&lt;/li&gt;
&lt;li&gt;p50/p95 time to validated content;&lt;/li&gt;
&lt;li&gt;retry count, artifact reads, and total billed cost;&lt;/li&gt;
&lt;li&gt;staging-index answer support and citation URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not compare byte-for-byte Markdown. The acceptance criterion is whether the same useful facts survive and downstream questions still cite the right source.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Roll out with a rollback path
&lt;/h2&gt;

&lt;p&gt;Keep the existing adapter behind a feature flag. Start with a small scheduled cohort, compare one complete crawl cycle, then test page deletion and update behavior before moving all traffic. Include stale-vector deletion in the cutover plan: switching the fetcher without removing obsolete chunks can make the new system look wrong even if the new page extraction is correct.&lt;/p&gt;

&lt;p&gt;A short proof of concept demonstrates a happy path. Production parity also includes filters, polling, artifact storage, retries, deletion, alerting and rollback. Calculate engineering cost from the pilot instead of treating the API-call replacement as the whole migration.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>python</category>
    </item>
    <item>
      <title>Crawl4AI vs Nstdata Crawl: A Production Checklist for Self-Hosted Crawling</title>
      <dc:creator>neuralbyte</dc:creator>
      <pubDate>Fri, 16 Oct 2026 08:48:30 +0000</pubDate>
      <link>https://dev.to/neuralbyte/crawl4ai-vs-nstdata-crawl-a-production-checklist-for-self-hosted-crawling-2gld</link>
      <guid>https://dev.to/neuralbyte/crawl4ai-vs-nstdata-crawl-a-production-checklist-for-self-hosted-crawling-2gld</guid>
      <description>&lt;p&gt;You can get Markdown from a URL with a short Crawl4AI script. The production decision starts when that script becomes a recurring job: who runs Chromium, limits concurrency, manages proxies, persists task state, and fixes failed pages?&lt;/p&gt;

&lt;p&gt;This guide compares &lt;strong&gt;self-hosted Crawl4AI&lt;/strong&gt; with &lt;strong&gt;Nstdata Crawl&lt;/strong&gt;, then separates both from Crawl4AI Cloud. No credentials or controlled test run were available for this article, so the code follows the official docs and performance claims are left to a reproducible test on your URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, separate the deployment options
&lt;/h2&gt;

&lt;p&gt;Crawl4AI is an open-source Python crawler with an in-process library and a self-hosted Docker server. Its current documentation also describes &lt;strong&gt;Crawl4AI Cloud&lt;/strong&gt;, a hosted API. This makes three options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;You run&lt;/th&gt;
&lt;th&gt;You configure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Crawl4AI library&lt;/td&gt;
&lt;td&gt;Python process and browser&lt;/td&gt;
&lt;td&gt;Browser, crawl logic, proxies, queue and monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crawl4AI Docker&lt;/td&gt;
&lt;td&gt;Container and browser runtime&lt;/td&gt;
&lt;td&gt;Server authentication, resources, upgrades and job recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crawl4AI Cloud&lt;/td&gt;
&lt;td&gt;Your client application&lt;/td&gt;
&lt;td&gt;API requests and Cloud plan/limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nstdata Crawl&lt;/td&gt;
&lt;td&gt;Your client application&lt;/td&gt;
&lt;td&gt;API credentials, scope, task polling and downstream validation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rest of this post focuses on the library/Docker versus Nstdata API comparison. Check Crawl4AI Cloud's current plans and endpoints separately if you want a hosted version of the Crawl4AI workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal Python example
&lt;/h2&gt;

&lt;p&gt;Both snippets request Markdown for &lt;a href="https://example.com/" rel="noopener noreferrer"&gt;https://example.com/&lt;/a&gt;. They are documentation-based and &lt;strong&gt;not executed&lt;/strong&gt;. Install the packages and set NSTDATA_API_TOKEN for Nstdata before trying these in your environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Crawl4AI in your Python process
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pip install crawl4ai&lt;br&gt;
crawl4ai-setup&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;`import asyncio&lt;br&gt;
from crawl4ai import AsyncWebCrawler&lt;/p&gt;

&lt;p&gt;async def main():&lt;br&gt;
    async with AsyncWebCrawler() as crawler:&lt;br&gt;
        result = await crawler.arun(url="&lt;a href="https://example.com/%22" rel="noopener noreferrer"&gt;https://example.com/"&lt;/a&gt;)&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    if not result.success:
        raise RuntimeError(result.error_message)

    print(result.markdown)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;asyncio.run(main())`&lt;/p&gt;

&lt;p&gt;This is useful when you need Python-level control. BrowserConfig controls browser-wide behavior; CrawlerRunConfig carries settings for a run. Crawl4AI documents CSS/XPath extraction and LLM-based extraction as options on top of Markdown conversion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nstdata Crawl through its Python SDK
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pip install nstdata-ai-crawl&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;`import os&lt;br&gt;
from nstdata_ai_crawl import NstDataClient, ScrapeRequestDto, Format&lt;/p&gt;

&lt;p&gt;with NstDataClient(os.environ["NSTDATA_API_TOKEN"]) as client:&lt;br&gt;
    result = client.submit_scrape_task_sync(ScrapeRequestDto(&lt;br&gt;
        url="&lt;a href="https://example.com/" rel="noopener noreferrer"&gt;https://example.com/&lt;/a&gt;",&lt;br&gt;
        formats=[Format.MARKDOWN],&lt;br&gt;
        timeout=60000,&lt;br&gt;
        onlyMainContent=True,&lt;br&gt;
    ))&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;markdown = result.data.get_markdown()
if not markdown:
    raise RuntimeError("Crawl returned no Markdown")
print(markdown)`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The SDK method and response helper above come from Nstdata's current docs. Confirm the SDK version and endpoint contract before deploying; its public documentation shows multiple route examples. &lt;a href="https://docs.nstdata.io/docs/crawl" rel="noopener noreferrer"&gt;Nstdata Crawl SDK docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The core code is short for both. The real migration delta lives in retries, timeouts, persistent job state, result shape and operational ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks between prototype and production?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Browser resources and concurrency
&lt;/h3&gt;

&lt;p&gt;Every browser page consumes CPU, memory and network. Raising concurrency can increase throughput until the target rate limit or your worker memory becomes the bottleneck. Crawl4AI's multi-URL dispatcher docs describe memory-adaptive dispatch, concurrency limits, rate limiting and monitoring helpers. With self-hosting, you still size the machine pool and decide what happens when a worker dies. &lt;/p&gt;

&lt;p&gt;Nstdata runs the browser infrastructure for API requests. Your client still needs bounded submission, account-limit awareness and a durable record of task IDs. A managed browser does not automatically give your pipeline idempotency or exactly-once ingestion.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Proxies and blocked pages
&lt;/h3&gt;

&lt;p&gt;Crawl4AI's self-hosted configuration lets you provide proxy settings and fallback behavior. That means you select, pay for and diagnose proxy endpoints. Nstdata documents proxy coordination and region selection, with proxy traffic billed separately.&lt;/p&gt;

&lt;p&gt;Treat a 403, 429, timeout and parser error as different states:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Response to test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;Is this a policy block, challenge page or access restriction?&lt;/td&gt;
&lt;td&gt;Stop if collection is not permitted; otherwise inspect allowed session/region settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;td&gt;Did request rate exceed target limits?&lt;/td&gt;
&lt;td&gt;Reduce concurrency and use bounded backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout&lt;/td&gt;
&lt;td&gt;Did navigation, a selector wait or the API time out?&lt;/td&gt;
&lt;td&gt;Isolate the stage; adjust timeout or use async processing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 with empty/challenge body&lt;/td&gt;
&lt;td&gt;Did the browser load useful content?&lt;/td&gt;
&lt;td&gt;Validate page text and expected selectors, not status alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parse error&lt;/td&gt;
&lt;td&gt;Did the DOM change or selector stop matching?&lt;/td&gt;
&lt;td&gt;Save the response for diagnosis and update extraction logic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Crawl4AI documents stealth-related browser options and proxy fallbacks; Nstdata describes fingerprint-browser handling. These are mechanisms, not guarantees of access. Test only pages you are allowed to collect. &lt;/p&gt;

&lt;h3&gt;
  
  
  3. Task state and recovery
&lt;/h3&gt;

&lt;p&gt;For self-hosted crawling, persist your own job ID, URL, attempt count, status, result location and terminal reason. A process-local queue is easy to lose during a restart. Crawl4AI has dispatcher helpers, but you still choose the durable queue and recovery model.&lt;/p&gt;

&lt;p&gt;Nstdata offers synchronous and asynchronous scraping. Async responses return a task ID; clients poll for status and retrieve output. Site crawls have status/progress and paginated pages. Its current docs say external webhooks are not available, so you need a polling worker for async completion. Check nested success and status; an HTTP 200 can indicate job acceptance rather than usable content.&lt;/p&gt;

&lt;p&gt;An ingestion state machine could be as small as:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;text&lt;br&gt;
queued -&amp;gt; submitted -&amp;gt; processing -&amp;gt; validated -&amp;gt; indexed&lt;br&gt;
                    \-&amp;gt; retryable_error -&amp;gt; submitted&lt;br&gt;
                    \-&amp;gt; terminal_error&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Make retries bounded, store the final error, and avoid retrying a permanent access denial indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare Markdown quality with an acceptance test
&lt;/h2&gt;

&lt;p&gt;Don't use a vague “RAG-ready” label as your test. Pick 100 representative public pages and define measurable checks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Expected main text appears in the output.&lt;/li&gt;
&lt;li&gt;Heading hierarchy is preserved.&lt;/li&gt;
&lt;li&gt;Code blocks, tables and links survive extraction.&lt;/li&gt;
&lt;li&gt;Navigation, cookie banners and unrelated content stay below an agreed noise threshold.&lt;/li&gt;
&lt;li&gt;Metadata includes the source URL and page title.&lt;/li&gt;
&lt;li&gt;Duplicate canonical pages are identified.&lt;/li&gt;
&lt;li&gt;Empty, blocked and partial pages are marked as failures before indexing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Include static docs, delayed JavaScript content, a click-to-expand section, a long code sample, a table, a redirect and a permitted region-specific URL. Record p50/p95 time to a usable result, error category, retries, proxy usage, token count and operator time. For a bounded crawl, compare discovered URLs against a known inventory. A maxPages: 50 cap limits work; it does not prove the 50 desired pages were found. Nstdata documents maxDepth, maxPages, includeUrls and excludeUrls; Crawl4AI provides deep-crawl strategies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Estimate total cost instead of comparing license fees
&lt;/h2&gt;

&lt;p&gt;The Crawl4AI library itself has no software license charge. Self-hosted production costs include:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;text&lt;br&gt;
compute + proxies + storage/network + monitoring + engineering/operations&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If average browser occupancy is 20 seconds per page, then 100,000 pages consume 2,000,000 browser-seconds, or about 556 browser-hours before parallelism, retries or idle capacity. This is an assumption for spreadsheet planning, not a benchmark. Replace it with measured runtime and your actual infrastructure rates.&lt;/p&gt;

&lt;p&gt;Nstdata's current docs list a pay-as-you-go base rate of &lt;strong&gt;$1.20 per 1,000 processed requests&lt;/strong&gt;. Proxy traffic is separate; the docs say JavaScript rendering, Markdown, PDF and screenshots are included in the base service. Some returned HTTP statuses, including 403 and 404, may still count as successfully retrieved requests for billing. System failures that retrieve no page content are not billed.&lt;/p&gt;

&lt;p&gt;Assuming one billable request per target, no retries, no subscription discount and no proxy traffic:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requests/month&lt;/th&gt;
&lt;th&gt;Nstdata base request estimate&lt;/th&gt;
&lt;th&gt;Self-hosted Crawl4AI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;$12&lt;/td&gt;
&lt;td&gt;Measure compute + proxy + storage + labor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;$120&lt;/td&gt;
&lt;td&gt;Measure worker capacity + proxy + monitoring + labor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;$1,200&lt;/td&gt;
&lt;td&gt;Measure scaled capacity + proxy + recovery + labor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are base-rate calculations, not final bills. Compare full monthly TCO divided by accepted pages for both. Add retry and proxy charges, not just successful requests. Evaluate Crawl4AI Cloud as its own hosted price model rather than mixing it with self-hosted costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration checklist
&lt;/h2&gt;

&lt;p&gt;Before replacing AsyncWebCrawler calls with API calls, inventory what your current crawler actually does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BrowserConfig, CrawlerRunConfig, hooks, cookies and session reuse;&lt;/li&gt;
&lt;li&gt;CSS/XPath or LLM extraction schemas;&lt;/li&gt;
&lt;li&gt;URL discovery, filtering, deduplication and refresh behavior;&lt;/li&gt;
&lt;li&gt;proxy rules, concurrency and per-domain rate limits;&lt;/li&gt;
&lt;li&gt;result fields, artifact files and downstream storage;&lt;/li&gt;
&lt;li&gt;async job IDs, retries, alerts and rollback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run both systems against the same 100 URLs. Normalize outputs to an internal record such as:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;json&lt;br&gt;
{&lt;br&gt;
  "source_url": "https://example.com/docs/page",&lt;br&gt;
  "title": "Page title",&lt;br&gt;
  "markdown": "# Page title\n...",&lt;br&gt;
  "fetched_at": "2026-10-16T00:00:00Z",&lt;br&gt;
  "status": "validated"&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is your pipeline schema, not a vendor response. It keeps provider-specific response differences out of the vector indexing code.&lt;/p&gt;

&lt;p&gt;Estimate migration ROI with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;text&lt;br&gt;
one-time migration = implementation + dual-run validation + rollback work&lt;br&gt;
monthly savings = old vendor + old maintenance - new vendor - new maintenance&lt;br&gt;
payback months = one-time migration / verified monthly savings&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Keep Crawl4AI for targets that depend on custom hooks or extraction behavior if the managed API doesn't cover them. A hybrid system is often simpler than rewriting one-off browser logic to force a uniform provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which should you use?
&lt;/h2&gt;

&lt;p&gt;Use &lt;strong&gt;self-hosted Crawl4AI&lt;/strong&gt; when you need code-level control, already operate browser infrastructure, or rely on custom extraction and navigation.&lt;/p&gt;

&lt;p&gt;Use &lt;strong&gt;Nstdata Crawl&lt;/strong&gt; when the workload is mostly standard page or bounded-site collection and you want browser operations behind a managed API. Your application still owns scope, validation, polling and storage.&lt;/p&gt;

&lt;p&gt;Consider &lt;strong&gt;Crawl4AI Cloud&lt;/strong&gt; when you want Crawl4AI's hosted API. Compare current limits and prices with Nstdata on the same URL set.&lt;/p&gt;

&lt;p&gt;The answer can vary by target. Evaluate cost per accepted page, including engineering time, on a representative pilot before migrating production traffic.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>abotwrotethis</category>
    </item>
    <item>
      <title>How I Would Collect Airbnb Listing Data Without Flattening the Quote</title>
      <dc:creator>neuralbyte</dc:creator>
      <pubDate>Tue, 13 Oct 2026 08:59:21 +0000</pubDate>
      <link>https://dev.to/neuralbyte/how-i-would-collect-airbnb-listing-data-without-flattening-the-quote-d15</link>
      <guid>https://dev.to/neuralbyte/how-i-would-collect-airbnb-listing-data-without-flattening-the-quote-d15</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Listing identity and quote identity are different keys.&lt;/li&gt;
&lt;li&gt;A visible calendar or search result is not a complete occupancy dataset.&lt;/li&gt;
&lt;li&gt;I keep displayed fee labels and currency instead of silently converting every amount into a nightly rate.&lt;/li&gt;
&lt;li&gt;Public listing research does not require private host or guest information.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The decision I would make first
&lt;/h2&gt;

&lt;p&gt;I would not make nightly_price the first and only field in an Airbnb dataset. A quote depends on the listing, dates, guest count, currency, and visible fee labels. My collector has to preserve that context before I can compare tools or compare rentals.&lt;/p&gt;

&lt;p&gt;I am comparing documented capabilities here, not reporting a hands-on benchmark of every service. The order follows my decision process rather than a performance ranking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The record I would define before implementation
&lt;/h2&gt;

&lt;p&gt;I store listing facts and quote observations separately, with dates and guest context attached to the latter. These are proposed application fields, not a vendor response schema.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What I would preserve&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;listing_key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Source-level rental identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stay_dates&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The dates used for the observation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;guest_context&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The visible selected guest context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;amount_label&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nightly, total, or another displayed meaning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;currency&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The observed currency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;captured_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When the quote or page was observed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How I would narrow the options
&lt;/h2&gt;

&lt;p&gt;A rental can have a stable source identity while its displayed quote changes with the requested stay. I would keep descriptions and listing identity in one record and quote observations in another. That makes it possible to compare compatible observations without overwriting yesterday’s context.&lt;/p&gt;

&lt;p&gt;If a provider returns a price field without enough context, I would narrow the analytical claim rather than fill in assumptions. The data might still be useful as a displayed-price sample; it would not automatically support a full delivered-cost comparison.&lt;/p&gt;

&lt;p&gt;I would compare a supervised tool, a managed source-specific route, and an owned implementation only if each fits the permitted task. I would not force every option through an identical job it was not designed to perform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bright Data Airbnb Scraper: Where I would use it
&lt;/h2&gt;

&lt;p&gt;Bright Data is a managed collection platform with source-specific scraper products and dataset-oriented delivery. I would examine the dedicated source contract when I want the provider to own that parser. Bright Data’s dedicated Airbnb scraper is aimed at teams that want listing extraction maintained by the provider. A structured listing contract can reduce the time spent on parsing titles, descriptions, amenities, and other documented fields. The useful distinction is source-specific extraction rather than generic page downloading. The provider describes this scope in &lt;a href="https://brightdata.com/products/web-scraper/airbnb" rel="noopener noreferrer"&gt;Airbnb scraper product&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The capabilities I care about
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source-specific contract:&lt;/strong&gt; A dedicated scraper documents its supported inputs and fields, reducing the need to build every source parser internally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed execution:&lt;/strong&gt; Provider-operated collection moves acquisition infrastructure outside the customer’s application, simplifying the operational boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured delivery:&lt;/strong&gt; A documented record shape supports ingestion into a warehouse or analytical workflow instead of beginning with manual page cleanup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My starting workflow would be straightforward: for this workflow, choose the dedicated source product, submit the appropriate lookup or discovery input, and map delivered records into a source-aware analytical table.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data available:&lt;/strong&gt; Public listing descriptions and amenities; date-specific quote fields only where the chosen input and output expose them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inputs and outputs:&lt;/strong&gt; The selected scraper’s documented URLs or discovery inputs → source-specific records and supported dataset delivery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; I would shortlist it for data teams that prefer a provider-owned extraction contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting started:&lt;/strong&gt; Create an account; select the exact source scraper and review its input and field contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; Web Scraper API plans meter delivered records; live collection and purchased datasets have separate terms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not suitable for:&lt;/strong&gt; The boundary I would keep explicit: Teams that need direct ownership of every parser decision or need fields and source contexts outside the dedicated scraper contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Apify Airbnb Actors: Where I would use it
&lt;/h2&gt;

&lt;p&gt;Apify is a hosted automation platform whose Actors provide source-specific inputs, executable collection jobs, and stored datasets. I would look for a matching maintained Actor before deciding to build the source collector myself. Apify Airbnb Actors suit a defined rental research job that begins with documented listing or search inputs. A suitable Actor supplies the source logic, while hosted execution and datasets make results available to the analysis pipeline. Different Actors should be treated as separate products, not one interchangeable Airbnb schema. The provider describes this scope in &lt;a href="https://apify.com/store" rel="noopener noreferrer"&gt;Actor marketplace&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The capabilities I care about
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prebuilt Actors:&lt;/strong&gt; A matching Actor supplies collection logic for its documented source, reducing the amount of site-specific code needed to begin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosted runs and datasets:&lt;/strong&gt; Execution and result storage live on the platform, allowing a team to separate collection from its own analysis application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduling and integration:&lt;/strong&gt; Platform scheduling and API access can move a reviewed collection task into a recurring workflow without keeping an analyst’s browser open.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My starting workflow would be straightforward: for this workflow, choose the source-specific Actor, submit a bounded input, inspect completed dataset items, and connect the accepted result to your analysis workflow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data available:&lt;/strong&gt; Public listing descriptions and amenities; date-specific quote fields only where the chosen input and output expose them. Only fields documented by the chosen Actor should be treated as supplied output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inputs and outputs:&lt;/strong&gt; The selected Actor’s documented URLs, queries, or other inputs → dataset items matching that Actor’s output schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; I would shortlist it for teams that want a maintained source-specific workflow with hosted execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting started:&lt;/strong&gt; Create an Apify account; select a maintained Actor and review its input schema, output sample, and charging model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; Platform resource usage plus the individual Actor’s pricing, which may use rental, result, or event charges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not suitable for:&lt;/strong&gt; The boundary I would keep explicit: Projects that cannot depend on third-party Actor maintenance or whose required fields and inputs are absent from the selected Actor contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Crawlbase Airbnb Scraper: Where I would use it
&lt;/h2&gt;

&lt;p&gt;Crawlbase is a web-data infrastructure platform combining general crawling, managed source scrapers, storage, and delivery options. I would inspect the particular scraper and delivery path rather than treat every JSON response as the same product. Crawlbase’s Airbnb route focuses on converting a supported public search input into listing records. This is useful when the research starts with search results rather than a prepared URL list. My application could preserve the query context and then decide whether additional detail-page fields are needed. The provider describes this scope in &lt;a href="https://crawlbase.com/airbnb-scraper" rel="noopener noreferrer"&gt;Airbnb scraper page&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The capabilities I care about
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Managed source routes:&lt;/strong&gt; Documented source-specific scrapers provide a clearer starting contract than treating every page as generic HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rendering and format choices:&lt;/strong&gt; The platform offers rendered collection and several content representations, supporting both custom parsers and AI consumers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synchronous or callback delivery:&lt;/strong&gt; Delivery modes and platform storage help connect a collection task to downstream processing without keeping all results in a single request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My starting workflow would be straightforward: for this workflow, select a general or source-specific route, submit the approved input, and process the immediate result or completion callback into your storage layer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data available:&lt;/strong&gt; Public listing descriptions and amenities; date-specific quote fields only where the chosen input and output expose them. On a general acquisition path, these are application-extracted fields rather than a guaranteed native schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inputs and outputs:&lt;/strong&gt; A public URL or documented source-specific input → HTML, Markdown, generic JSON, or target-specific structured JSON; callback paths complete later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; I would shortlist it for developers who want acquisition and delivery components in one platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting started:&lt;/strong&gt; Create an account and token; choose the documented scraper, rendering mode, and delivery path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; Successful-request usage follows the current rate card; site difficulty, rendering, and subscription terms affect the bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not suitable for:&lt;/strong&gt; The boundary I would keep explicit: A pipeline whose mandatory fields are missing from the selected source scraper and that has no capacity to maintain a custom parser.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Oxylabs Web API: Where I would use it
&lt;/h2&gt;

&lt;p&gt;Oxylabs Web API is a managed web-data interface with page acquisition, rendering, and multiple output representations. I would choose the representation my application needs, then check whether the target has a dedicated field contract. Oxylabs Web API fits a rental-data platform that wants managed page acquisition and several representations of the same source. HTML supports custom field rules, while readable content can support text analysis. The application still assigns listing identity and interprets date-specific quote context.&lt;/p&gt;

&lt;h3&gt;
  
  
  The capabilities I care about
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multiple representations:&lt;/strong&gt; The documented scrape interface returns HTML, Markdown, structured JSON, or screenshots according to the chosen workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript rendering:&lt;/strong&gt; A rendering path can expose public content absent from the initial HTML, reducing the need for a self-managed browser runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documented target-specific data:&lt;/strong&gt; Supported targets have their own parameters and structured outputs; these should be distinguished from a general page request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My starting workflow would be straightforward: for this workflow, choose the target and representation, send a bounded request, and validate the content or documented fields before loading accepted records.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data available:&lt;/strong&gt; Public listing descriptions and amenities; date-specific quote fields only where the chosen input and output expose them. On a general acquisition path, these are application-extracted fields rather than a guaranteed native schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inputs and outputs:&lt;/strong&gt; A target URL or supported target-specific request → the selected HTML, Markdown, JSON, or screenshot representation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; I would shortlist it for teams combining operational web collection with AI or structured-data consumers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting started:&lt;/strong&gt; Create an account and API access; review the current Web API target parameters and output contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; Scrape pricing is based on successful results, with target and rendering-dependent rates under the applicable plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not suitable for:&lt;/strong&gt; The boundary I would keep explicit: A requirement for dedicated structured fields on a target whose documented contract supplies only general page content.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Zyte API: Where I would use it
&lt;/h2&gt;

&lt;p&gt;Zyte API is a managed scraping API combining HTTP acquisition, browser rendering, and automatic extraction options. I would use this when I want managed acquisition but still need to own the extraction and acceptance decisions. Zyte API suits an engineering team collecting rental pages under its own schema. Managed acquisition and extraction options provide the source content, while the team retains the distinction between listing attributes and quote observations. This works well when the same model must also handle other travel websites.&lt;/p&gt;

&lt;h3&gt;
  
  
  The capabilities I care about
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP and browser acquisition:&lt;/strong&gt; Static content and rendered content can be acquired through the service rather than requiring a separately operated browser fleet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic extraction:&lt;/strong&gt; Supported extraction types turn page content into typed records, reducing selector work for fields covered by that contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer control:&lt;/strong&gt; Request and extraction configuration allow a team to keep source-specific behavior explicit instead of accepting one fixed visual workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My starting workflow would be straightforward: for this workflow, choose the least complex acquisition mode that returns the needed content, request supported extraction where useful, and normalize source-specific values locally.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data available:&lt;/strong&gt; Public listing descriptions and amenities; date-specific quote fields only where the chosen input and output expose them. On a general acquisition path, these are application-extracted fields rather than a guaranteed native schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inputs and outputs:&lt;/strong&gt; Target URLs and request/extraction settings → HTTP content, rendered content, or supported automatic-extraction records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; I would shortlist it for engineering teams that want managed acquisition with control over record handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting started:&lt;/strong&gt; Create an account and API key; choose HTTP or browser acquisition and supported extraction settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; Request-based billing varies by website difficulty and HTTP versus browser acquisition; commitment plans affect rates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not suitable for:&lt;/strong&gt; The boundary I would keep explicit: Projects expecting automatic extraction to resolve domain-specific identity, offer conditions, and cross-source matching without application rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Browse AI: Where I would use it
&lt;/h2&gt;

&lt;p&gt;Browse AI is a no-code extraction and monitoring platform that turns selected public-page information into an automation workflow. I would use a trained monitor for a defined page set, not assume it is the starting point for an unlimited source inventory. Browse AI fits a small set of rental pages that an analyst wants to revisit and inspect. A visual workflow can capture the fields visible in a reviewed page state without introducing a full engineering project. This suits narrow amenity or listing-description monitoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  The capabilities I care about
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reviewed data selection:&lt;/strong&gt; The workflow proposes or captures page fields that a user can inspect and customize, making setup accessible to non-developers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive page workflows:&lt;/strong&gt; A trained robot can represent permitted clicks and navigation needed to reveal the intended public content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring-oriented operation:&lt;/strong&gt; A reusable robot suits repeated checks of a defined page set, so a team can observe changes without manually revisiting every page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My starting workflow would be straightforward: for this workflow, train a robot on the chosen page, review its field preview, and set a monitoring schedule for the same defined source context.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data available:&lt;/strong&gt; Public listing descriptions and amenities; date-specific quote fields only where the chosen input and output expose them. Actual columns follow the reviewed visual project, recipe, or detected page elements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inputs and outputs:&lt;/strong&gt; A source URL and robot-defined fields or actions → extracted rows and monitoring results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; I would shortlist it for analysts monitoring a relatively narrow and inspectable source set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting started:&lt;/strong&gt; Create an account and train or configure a robot; review the selected elements and navigation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; Credit-based plans; extracted rows, screenshots, and premium-site tasks affect credit consumption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not suitable for:&lt;/strong&gt; The boundary I would keep explicit: Broad, rapidly changing source inventories where frequent retraining would outweigh the value of a narrow visual monitor.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Octoparse: Where I would use it
&lt;/h2&gt;

&lt;p&gt;Octoparse is a visual web-extraction platform built around reusable page-navigation and field-selection workflows. I would consider a visual project when the analyst, rather than an application team, needs to own the extraction steps. Octoparse gives a rental researcher control over which visible fields become columns. A reusable project can connect search-card navigation to detail-page extraction. The practical benefit is a workflow the analyst can adjust when the research needs a different description, room-type, or price label.&lt;/p&gt;

&lt;h3&gt;
  
  
  The capabilities I care about
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual field selection:&lt;/strong&gt; An analyst can connect the desired output columns to visible page elements without maintaining a full application codebase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reusable extraction project:&lt;/strong&gt; Navigation and extraction steps can be organized into a project, replacing repeated manual copying with a defined collection procedure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Templates and workflow setup:&lt;/strong&gt; Available templates provide a starting point for supported tasks; a custom project can represent fields outside the chosen template.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My starting workflow would be straightforward: for this workflow, create a list-to-detail task, select the required fields, run a small preview, and schedule the reviewed project if your plan supports cloud execution.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data available:&lt;/strong&gt; Public listing descriptions and amenities; date-specific quote fields only where the chosen input and output expose them. Actual columns follow the reviewed visual project, recipe, or detected page elements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inputs and outputs:&lt;/strong&gt; Start URLs plus a visual extraction project → the columns and export formats configured in that project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; I would shortlist it for business analysts and operations teams that prefer a visual workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting started:&lt;/strong&gt; Create an account, install the task builder, and configure or adapt a template for the source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; Plan-based subscriptions; task capacity and cloud execution depend on the selected plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not suitable for:&lt;/strong&gt; The boundary I would keep explicit: Teams that require parser changes to be reviewed as source-code diffs and covered by their own unit-test suite rather than maintained as visual projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Scrapy: Where I would use it
&lt;/h2&gt;

&lt;p&gt;Scrapy is an open-source Python framework for scheduling requests, extracting items, and processing them through application-owned pipelines. I would choose this when owning the Python crawler and its item pipelines is an intentional engineering commitment. Scrapy is useful when approved listing URLs and search inputs must flow through a durable Python pipeline. I could keep discovery, quote parsing, and data validation separate. This provides control over listing keys and observation history rather than treating every fetched page as a new rental.&lt;/p&gt;

&lt;h3&gt;
  
  
  The capabilities I care about
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Request scheduling:&lt;/strong&gt; A crawl can define source scope, callbacks, and how discovered URLs enter the workload instead of relying on a browser macro.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Item pipelines:&lt;/strong&gt; Validation, normalization, and storage can be organized around structured items, which keeps domain rules in maintainable application code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensible architecture:&lt;/strong&gt; Downloader and pipeline components let engineers adapt the framework to a multi-source collection system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My starting workflow would be straightforward: for this workflow, define approved seeds, separate list discovery from detail parsing, and send accepted items through validation and persistence pipelines.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data available:&lt;/strong&gt; Public listing descriptions and amenities; date-specific quote fields only where the chosen input and output expose them. On a general acquisition path, these are application-extracted fields rather than a guaranteed native schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inputs and outputs:&lt;/strong&gt; Seed URLs, spider callbacks, and item definitions → application-defined items, feed exports, or pipeline writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; I would shortlist it for python teams that want direct ownership of crawling and data processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting started:&lt;/strong&gt; Install Python and Scrapy; write the spider, parsing rules, and validation/storage pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; The framework is open source; you pay for your runtime, hosting, optional acquisition services, and engineering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not suitable for:&lt;/strong&gt; The boundary I would keep explicit: JavaScript-dependent collection without an additional rendering path, or a team that cannot maintain Python spiders and item pipelines.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Playwright: Where I would use it
&lt;/h2&gt;

&lt;p&gt;Playwright is an open-source browser-automation framework with navigation, interaction, locators, and page-state inspection. I would keep browser control for the parts of the job that genuinely depend on a rendered or selected page state. Playwright provides control when an authorized rental observation depends on selected dates, guests, or other rendered page state. The browser can expose exactly which state the extractor is reading. That makes it useful for targeted collection and checking disagreements in a displayed quote. The provider describes this scope in &lt;a href="https://playwright.dev/docs/intro" rel="noopener noreferrer"&gt;browser automation documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The capabilities I care about
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rendered-page access:&lt;/strong&gt; The browser executes page scripts, allowing extraction from public content that does not exist in the first HTTP response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit interaction:&lt;/strong&gt; Locators and interaction steps make the selected page state reproducible when a permitted click or option selection is required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual and DOM evidence:&lt;/strong&gt; Screenshots and page inspection help diagnose whether acquisition reached the intended item, listing, or answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My starting workflow would be straightforward: for this workflow, navigate to the approved page, establish the relevant visible state, read the chosen locators, and save both normalized values and a review artifact where necessary.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data available:&lt;/strong&gt; Public listing descriptions and amenities; date-specific quote fields only where the chosen input and output expose them. On a general acquisition path, these are application-extracted fields rather than a guaranteed native schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inputs and outputs:&lt;/strong&gt; A URL and explicit browser actions/locators → the DOM values, screenshots, or records your code extracts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; I would shortlist it for engineers needing precise control over an authorized rendered-page workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting started:&lt;/strong&gt; Install a supported runtime, Playwright, and browser binaries; implement locators, waits, and storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; The framework is open source; browser compute, hosting, optional provider services, and maintenance create the cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not suitable for:&lt;/strong&gt; The boundary I would keep explicit: Mostly static bulk collection where full browser execution adds unnecessary compute and maintenance overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Nstdata Crawl: Where I would use it
&lt;/h2&gt;

&lt;p&gt;Nstdata Crawl provides page acquisition and bounded site collection. I would use its selected artifacts as inputs to an application-owned parser or AI ingestion stage. It is not a substitute for source-specific identity, field meaning, or permission to collect the source.&lt;/p&gt;

&lt;h3&gt;
  
  
  The capabilities I care about
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Listing-content acquisition:&lt;/strong&gt; HTML and readable document outputs support parsing titles, descriptions, amenity text, and other public content present in the collected representation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rendered quote evidence:&lt;/strong&gt; Rendering and visual artifacts help a team inspect a page state when price information arrives after the initial response. The application attaches dates, guest assumptions, and price labels to that observation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded discovery:&lt;/strong&gt; Page-count, depth, and include/exclude controls keep collection aligned with an approved rental research scope rather than following every filter or unrelated path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My starting workflow would be straightforward: i would inspect a small approved URL sample, choose the artifact format the consumer needs, and keep discovery within explicit limits. The application would preserve source identity and observation time before applying its own field and acceptance rules.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data available:&lt;/strong&gt; Source page content from which the application can extract public listing descriptions and amenities; date-specific quote fields only where the chosen input and output expose them. These are not promised dedicated target fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inputs and outputs:&lt;/strong&gt; Approved URLs and bounded site-collection settings → selected Markdown, HTML, raw data, links, screenshot, or PDF artifacts; the application maps artifacts to its own records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; I would shortlist it for rental-data developers combining several public sources under one model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting started:&lt;/strong&gt; Create a Nstdata account, review an authorized sample in the Playground, and connect the current &lt;a href="https://www.nstdata.io/scraping" rel="noopener noreferrer"&gt;Nstdata Crawl interface&lt;/a&gt; with credentials stored outside prompts and source code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; URL-based usage paid from account balance or covered by applicable subscription credits; proxy traffic is billed separately under the selected source’s terms, as described in the &lt;a href="https://www.nstdata.io/pricing/crawl" rel="noopener noreferrer"&gt;Crawl billing model&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not suitable for:&lt;/strong&gt; The boundary I would keep explicit: Teams expecting a ready-made marketplace or property schema without application parsing; requests for licensed feeds, authoritative historical transactions, or unauthorized private content.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Availability is where I would slow down
&lt;/h2&gt;

&lt;p&gt;An unavailable date does not establish that a guest booked it. A date may be blocked, outside the collection scope, or missing because the acquisition did not reach the intended state. I would store the observed availability label and leave its cause unknown.&lt;/p&gt;

&lt;p&gt;I would also keep the project on approved public listing information. Host and guest identities, private messages, and account-specific material are outside this workflow. A provider’s ability to return data does not settle whether my intended use is permitted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would ship first
&lt;/h2&gt;

&lt;p&gt;I would pilot a small set of approved listings with explicit stay context and inspect both the fields and their labels. The tool choice matters, but the first useful design decision is giving a quote its own identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Is a displayed nightly amount the total cost of a stay?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Preserve the displayed price basis, nights, guest assumptions, currency, and any visible fee conditions before calculating a comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do blocked dates prove occupancy?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. A blocked date can have several explanations. Availability observations alone do not establish bookings, occupancy, or revenue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does a scraper give complete city coverage?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not automatically. Search inputs, ranking, filters, result caps, and unavailable pages affect the observed population.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I derive an exact property address from an approximate map point?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Do not present approximate public location information as a verified exact address. Keep the source's precision and avoid unnecessary location inference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should a public rental study collect guest information?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Public listing research does not need guest identities, private booking records, or messages.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>10 Best Amazon Scrapers for Product Data Collection</title>
      <dc:creator>neuralbyte</dc:creator>
      <pubDate>Tue, 29 Sep 2026 09:38:29 +0000</pubDate>
      <link>https://dev.to/neuralbyte/10-best-amazon-scrapers-for-product-data-collection-1cb9</link>
      <guid>https://dev.to/neuralbyte/10-best-amazon-scrapers-for-product-data-collection-1cb9</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nstdata Crawl is one option for teams that need managed page collection, rendering, task state, and reviewable artifacts in one workflow.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Amazon official APIs should be the first choice when their eligibility rules and data scope match the project.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bright Data and Oxylabs fit structured, higher-volume Amazon programs; Apify fits teams that want configurable hosted Actors.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ScraperAPI, ScrapingBee, ZenRows, and Zyte leave different amounts of parsing and orchestration to your application.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Benchmark tools by accepted ASIN-offer records, not by successful HTTP responses or request price alone.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A note on how I approached this
&lt;/h2&gt;

&lt;p&gt;When I compare Amazon data tools, I start by separating official seller data, page retrieval, and product normalization. Treating those as one feature is the quickest way to choose the wrong tool. This is my working framework, not a claim that one vendor wins every workload. I used current official documentation for changeable product details and kept pricing at the billing-model level.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the best Amazon scrapers for product data?
&lt;/h2&gt;

&lt;p&gt;The best Amazon scraper depends on whether you need seller-authorized catalog data, public retail-page evidence, or a fully managed structured feed. Nstdata Crawl is one option here when Amazon pages must be collected with rendered artifacts and task evidence, while Amazon's official interfaces are preferable when your seller, affiliate, or business relationship grants the required data. A useful shortlist must separate page acquisition from product identity, offer normalization, and accepted-record validation.&lt;/p&gt;

&lt;p&gt;The practical baseline is to keep retrieval separate from normalization and acceptance. The responsible Amazon data collection guide explains why a page that loads is not automatically a valid business record.&lt;/p&gt;

&lt;h2&gt;
  
  
  How did we choose these tools?
&lt;/h2&gt;

&lt;p&gt;I used six criteria that would change a real selection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon-specific coverage:&lt;/strong&gt; product detail, search, seller, review, offer, and category surfaces are different workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity and provenance:&lt;/strong&gt; every record should retain ASIN, marketplace, canonical URL, seller or offer context, currency, and retrieval time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rendering and localization:&lt;/strong&gt; the tool must make locale, postal context, session behavior, and JavaScript state observable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure evidence:&lt;/strong&gt; task state, final URL, page title, status, and raw or visual artifacts must expose wrong-page results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance boundary:&lt;/strong&gt; compare who owns browsers, retries, parser updates, scheduling, storage, and schema changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; normalize usage-based, subscription, compute, and managed-service charges to cost per accepted record.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The evidence pass used current first-party documentation, including &lt;a href="https://developer-docs.amazon.com/sp-api/lang-en_EN/docs/manage-product-listings-guide" rel="noopener noreferrer"&gt;Amazon Selling Partner API listing guide&lt;/a&gt;, &lt;a href="https://brightdata.com/products/web-scraper/amazon" rel="noopener noreferrer"&gt;Bright Data Amazon Scraper&lt;/a&gt;, &lt;a href="https://docs.zyte.com/zyte-api/usage/reference.html" rel="noopener noreferrer"&gt;Zyte API reference&lt;/a&gt;, &lt;a href="https://docs.apify.com/api" rel="noopener noreferrer"&gt;Apify API documentation&lt;/a&gt;. Vendor pricing is described by billing model rather than numeric rates because plans and units change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Operating model&lt;/th&gt;
&lt;th&gt;Main trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nstdata Crawl&lt;/td&gt;
&lt;td&gt;managed Amazon page acquisition with audit artifacts&lt;/td&gt;
&lt;td&gt;pay per crawled URL, with optional subscription credits&lt;/td&gt;
&lt;td&gt;You must build and maintain the Amazon product schema, offer matching, and legal basis; access is not guaranteed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon Selling Partner API&lt;/td&gt;
&lt;td&gt;authorized sellers and vendors&lt;/td&gt;
&lt;td&gt;Amazon account and application eligibility&lt;/td&gt;
&lt;td&gt;It is not a general competitor-retail scraper and cannot be used as an unrestricted public catalog feed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bright Data Amazon Scraper&lt;/td&gt;
&lt;td&gt;prebuilt structured Amazon datasets&lt;/td&gt;
&lt;td&gt;usage-based or subscription platform billing&lt;/td&gt;
&lt;td&gt;Enterprise breadth can be unnecessary for a small, narrow product watchlist.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oxylabs E-Commerce Scraper API&lt;/td&gt;
&lt;td&gt;developer-led Amazon and retail extraction&lt;/td&gt;
&lt;td&gt;usage-based plans or contract&lt;/td&gt;
&lt;td&gt;Target and output coverage should be tested because one retail schema rarely fits every marketplace state.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apify Amazon Actors&lt;/td&gt;
&lt;td&gt;configurable hosted Amazon workflows&lt;/td&gt;
&lt;td&gt;compute, event, or Actor-specific usage&lt;/td&gt;
&lt;td&gt;Actor quality, maintenance, schema, and pricing vary by publisher, so each Actor is a separate dependency.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScraperAPI&lt;/td&gt;
&lt;td&gt;teams that already own an Amazon parser&lt;/td&gt;
&lt;td&gt;request or credit based&lt;/td&gt;
&lt;td&gt;A successful response can still be a consent, challenge, or wrong-market page; semantic validation remains yours.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zyte API&lt;/td&gt;
&lt;td&gt;Scrapy-centered and extraction-aware teams&lt;/td&gt;
&lt;td&gt;usage-based API or managed service&lt;/td&gt;
&lt;td&gt;The selected product and extraction mode determine what your application still owns, so scope must be explicit.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScrapingBee&lt;/td&gt;
&lt;td&gt;compact Python or REST integrations&lt;/td&gt;
&lt;td&gt;credit-based subscription&lt;/td&gt;
&lt;td&gt;Credit consumption changes with options, and Amazon-specific normalization is still application work.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZenRows&lt;/td&gt;
&lt;td&gt;teams wanting a single page-acquisition endpoint&lt;/td&gt;
&lt;td&gt;request-credit plans&lt;/td&gt;
&lt;td&gt;It is not a seller API and does not remove the need to detect marketplaces, variants, and offer states.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DataForSEO Merchant API&lt;/td&gt;
&lt;td&gt;search and merchant-result research&lt;/td&gt;
&lt;td&gt;pay-as-you-go task billing&lt;/td&gt;
&lt;td&gt;Coverage follows supported merchant endpoints and is not equivalent to arbitrary Amazon page collection.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Nstdata Crawl: Best for managed Amazon page acquisition with audit artifacts
&lt;/h2&gt;

&lt;p&gt;Nstdata Crawl is an API-based collection layer for public web pages and bounded site jobs. It fits Amazon research when the acquisition team needs rendering, proxy routing, task state, and multiple representations without operating browser workers. The main value is not an Amazon-specific schema; it is a reviewable source layer that can feed your own ASIN, offer, and price normalization. Billing follows a per-crawled-URL model, with proxy traffic accounted for separately when selected. It is a strong operational fit when reproducibility matters, but it does not replace Amazon-authorized seller APIs or domain-specific validation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Page evidence:&lt;/strong&gt; request Markdown, HTML, links, PDF, or other currently supported formats for validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded jobs:&lt;/strong&gt; set page and depth limits so category discovery cannot expand without control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task inspection:&lt;/strong&gt; validate body-level state and page identity before accepting a product record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; pay per crawled URL, with optional subscription credits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limitation:&lt;/strong&gt; You must build and maintain the Amazon product schema, offer matching, and legal basis; access is not guaranteed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Amazon Selling Partner API: Best for authorized sellers and vendors
&lt;/h2&gt;

&lt;p&gt;Amazon's official seller interface exposes catalog, listing, pricing, inventory, notification, and other seller workflows according to approved roles. It is the correct starting point for data tied to your own selling relationship.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Catalog and listing resources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Role-based authorization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Event and batch workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; Amazon account and application eligibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limitation:&lt;/strong&gt; It is not a general competitor-retail scraper and cannot be used as an unrestricted public catalog feed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bright Data Amazon Scraper: Best for prebuilt structured Amazon datasets
&lt;/h2&gt;

&lt;p&gt;Bright Data offers prebuilt Amazon collection products for teams that prefer structured outputs and managed infrastructure. Its broader platform also supports asynchronous jobs and data delivery patterns.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Amazon-specific collectors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Structured output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Managed job execution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; usage-based or subscription platform billing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limitation:&lt;/strong&gt; Enterprise breadth can be unnecessary for a small, narrow product watchlist.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Oxylabs E-Commerce Scraper API: Best for developer-led Amazon and retail extraction
&lt;/h2&gt;

&lt;p&gt;Oxylabs positions its ecommerce API around retail page retrieval and parsed product results. It suits teams that want an API boundary but still own downstream validation and data modeling.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Retail-focused API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Rendered acquisition options&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Structured result workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; usage-based plans or contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limitation:&lt;/strong&gt; Target and output coverage should be tested because one retail schema rarely fits every marketplace state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Apify Amazon Actors: Best for configurable hosted Amazon workflows
&lt;/h2&gt;

&lt;p&gt;Apify runs Amazon-focused Actors inside a platform with schedules, datasets, logs, integrations, and API access. It is useful when a team wants to inspect or extend an existing automation rather than adopt a fixed endpoint.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Actor marketplace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Cloud schedules and storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; REST and client APIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; compute, event, or Actor-specific usage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limitation:&lt;/strong&gt; Actor quality, maintenance, schema, and pricing vary by publisher, so each Actor is a separate dependency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ScraperAPI: Best for teams that already own an Amazon parser
&lt;/h2&gt;

&lt;p&gt;ScraperAPI focuses on acquiring pages while handling proxy and rendering concerns through an HTTP API. It fits applications that already have stable ASIN and offer parsers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; HTTP retrieval API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Rendering option&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Geographic request controls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; request or credit based.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limitation:&lt;/strong&gt; A successful response can still be a consent, challenge, or wrong-market page; semantic validation remains yours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Zyte API: Best for Scrapy-centered and extraction-aware teams
&lt;/h2&gt;

&lt;p&gt;Zyte API combines page acquisition with browser HTML and optional structured extraction behind one endpoint. It is especially relevant to teams using Scrapy or evaluating provider-managed product extraction.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; HTTP and browser outputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Structured extraction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Scrapy ecosystem fit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; usage-based API or managed service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limitation:&lt;/strong&gt; The selected product and extraction mode determine what your application still owns, so scope must be explicit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ScrapingBee: Best for compact Python or REST integrations
&lt;/h2&gt;

&lt;p&gt;ScrapingBee provides a request-oriented scraping API with JavaScript rendering and extraction features. It can reduce browser operations for small and medium Amazon jobs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Simple REST calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; JavaScript rendering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Extraction rules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; credit-based subscription.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limitation:&lt;/strong&gt; Credit consumption changes with options, and Amazon-specific normalization is still application work.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ZenRows: Best for teams wanting a single page-acquisition endpoint
&lt;/h2&gt;

&lt;p&gt;ZenRows offers a web scraping API designed to handle rendering and access complexity behind a request. It works best when the team wants raw or rendered content for its own parser.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Scraping API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; JavaScript rendering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Proxy selection handled by service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; request-credit plans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limitation:&lt;/strong&gt; It is not a seller API and does not remove the need to detect marketplaces, variants, and offer states.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  DataForSEO Merchant API: Best for search and merchant-result research
&lt;/h2&gt;

&lt;p&gt;DataForSEO's merchant-focused APIs suit teams that need search-result or shopping-market data rather than full Amazon page archives. Its structured response model can simplify comparative research.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Merchant search data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Structured API responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability:&lt;/strong&gt; Batch-oriented workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing model:&lt;/strong&gt; pay-as-you-go task billing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limitation:&lt;/strong&gt; Coverage follows supported merchant endpoints and is not equivalent to arbitrary Amazon page collection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How should you choose?
&lt;/h2&gt;

&lt;p&gt;Choose Amazon's official APIs for authorized seller or affiliate workflows; consider Nstdata Crawl when source-page evidence and managed rendering are the main gap; choose Bright Data or Oxylabs for structured managed feeds; choose Apify for customizable hosted workflows; and choose a retrieval API when your own parser is already the durable asset.&lt;/p&gt;

&lt;p&gt;Build a small golden corpus and apply one acceptance contract to every tool. The multi-store Amazon proxy workflow and ecommerce product-data guide provide useful patterns for retries, source evidence, and idempotent storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What responsible-use controls are required?
&lt;/h2&gt;

&lt;p&gt;Collect only public or otherwise authorized information, honor applicable terms and law, and avoid account, checkout, or private customer data. Store the minimum source evidence needed for validation, set retention limits, and keep locale, seller, offer, and timestamp provenance with each record.&lt;/p&gt;

&lt;p&gt;The price-monitoring pipeline adds a checklist for bounded concurrency, retention, and failure handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The best Amazon scraper is the option whose operating boundary matches the data you are allowed to collect. Start with a frozen set of product, search, unavailable, localized, and expected-failure pages; score accepted records and diagnostics; then expand only after identity and offer normalization are stable. If multiple proxy sources later need centralized routing and monitoring, evaluate a separate proxy-management layer rather than treating it as part of the scraper.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Is it legal to scrape Amazon?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Amazon scraping legality depends on the data, method, jurisdiction, contracts, and use. Use official interfaces where available, collect only public or authorized data, and obtain legal review for the specific project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What fields should an Amazon product scraper collect?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A defensible record usually includes ASIN, marketplace, canonical URL, title, seller or offer context, price, currency, availability evidence, retrieval time, and source status.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should you use an Amazon API or scrape pages?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use an Amazon API when its eligibility and field scope meet the requirement; scrape permitted public pages only when the needed observation is absent and the use is lawful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you benchmark Amazon scrapers?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run every candidate on the same small corpus and measure accepted-record rate, wrong-page rate, field accuracy, latency, diagnostics, and total cost after retries and review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why do Amazon prices differ between runs?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Marketplace, delivery location, seller, promotion, membership, variant, currency, and session context can all change the displayed offer, so those dimensions must be stored.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Zyte vs. Apify vs. Crawlbase: How I’d Choose Between Them</title>
      <dc:creator>neuralbyte</dc:creator>
      <pubDate>Mon, 28 Sep 2026 08:35:20 +0000</pubDate>
      <link>https://dev.to/neuralbyte/zyte-vs-apify-vs-crawlbase-how-id-choose-between-them-1p55</link>
      <guid>https://dev.to/neuralbyte/zyte-vs-apify-vs-crawlbase-how-id-choose-between-them-1p55</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Apify is the strongest fit when a team wants deployable Actors, scheduling, storage, integrations, and a marketplace in one platform.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zyte is a strong fit for Scrapy-centered teams and managed extraction workflows that value its crawler and data-service ecosystem.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Crawlbase is a simpler fit when the primary need is an API-oriented crawling or scraping layer rather than a hosted application platform.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The platforms are not interchangeable: compare who owns crawler code, browser behavior, storage, scheduling, extraction, and incident response.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use a representative corpus and calculate cost per accepted record; request or credit prices alone are not comparable.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I approached it this way
&lt;/h2&gt;

&lt;p&gt;I find this comparison easier when I ignore the overlapping feature lists and write down who owns each production responsibility. Code hosting, browser maintenance, storage, schedules, retries, and extraction are the fields that change the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the main difference between Zyte, Apify, and Crawlbase?
&lt;/h2&gt;

&lt;p&gt;The main difference is platform scope and ownership. Apify centers on Actors and cloud execution, Zyte combines scraping infrastructure with Scrapy-oriented and managed data services, and Crawlbase emphasizes API-based page acquisition and selected data products.&lt;/p&gt;

&lt;p&gt;A fair comparison starts by deciding which of those layers the provider should own.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do Zyte, Apify, and Crawlbase compare?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision field&lt;/th&gt;
&lt;th&gt;Zyte&lt;/th&gt;
&lt;th&gt;Apify&lt;/th&gt;
&lt;th&gt;Crawlbase&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary model&lt;/td&gt;
&lt;td&gt;Scraping APIs, Scrapy cloud tooling, managed data&lt;/td&gt;
&lt;td&gt;Actor runtime, marketplace, schedules, storage&lt;/td&gt;
&lt;td&gt;Crawling and scraping APIs plus selected data APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom code hosting&lt;/td&gt;
&lt;td&gt;Scrapy-centered cloud workflows&lt;/td&gt;
&lt;td&gt;General Actor containers&lt;/td&gt;
&lt;td&gt;Not the main platform model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prebuilt solutions&lt;/td&gt;
&lt;td&gt;Extraction and managed services&lt;/td&gt;
&lt;td&gt;Large Actor marketplace&lt;/td&gt;
&lt;td&gt;Targeted APIs and crawlers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage and scheduling&lt;/td&gt;
&lt;td&gt;Depends on selected service&lt;/td&gt;
&lt;td&gt;Integrated platform components&lt;/td&gt;
&lt;td&gt;Application often owns more orchestration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Scrapy and managed-data teams&lt;/td&gt;
&lt;td&gt;Developer-led automation platform&lt;/td&gt;
&lt;td&gt;API-first acquisition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key trade-off&lt;/td&gt;
&lt;td&gt;Product surface requires careful scoping&lt;/td&gt;
&lt;td&gt;Marketplace quality varies by Actor&lt;/td&gt;
&lt;td&gt;Narrower application-platform layer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use current first-party documentation: &lt;a href="https://docs.zyte.com/" rel="noopener noreferrer"&gt;Zyte documentation&lt;/a&gt;, &lt;a href="https://docs.apify.com/platform" rel="noopener noreferrer"&gt;Apify platform documentation&lt;/a&gt;, and &lt;a href="https://crawlbase.com/docs/" rel="noopener noreferrer"&gt;Crawlbase documentation&lt;/a&gt;. Product names, inclusions, and billing units can change.&lt;/p&gt;

&lt;h2&gt;
  
  
  When is Zyte the better fit?
&lt;/h2&gt;

&lt;p&gt;Zyte is the better fit when the organization already uses Scrapy, wants a Scrapy-focused cloud workflow, or needs a provider to take on more of a managed data project. Its API and extraction offerings should be evaluated separately because they represent different responsibility boundaries.&lt;/p&gt;

&lt;p&gt;Key strengths include alignment with the Scrapy ecosystem, managed acquisition options, and services for organizations that want delivered data rather than only raw pages. The limitations are decision complexity and portability: a team must determine which product owns rendering, parsing, maintenance, and delivery, then test that exact combination.&lt;/p&gt;

&lt;h2&gt;
  
  
  When is Apify the better fit?
&lt;/h2&gt;

&lt;p&gt;Apify is the better fit when teams want to package crawlers as Actors, schedule and run them in the cloud, store outputs, connect integrations, or adopt an existing marketplace Actor. It supports both custom development and reusable automation.&lt;/p&gt;

&lt;p&gt;The main advantage is breadth: runtime, storage, queues, schedules, API access, and marketplace distribution can live together. The limitation is variability. An Actor is a separate dependency with its own maintainer, schema, pricing, and update cadence; marketplace availability does not guarantee production quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  When is Crawlbase the better fit?
&lt;/h2&gt;

&lt;p&gt;Crawlbase is the better fit when the application mainly needs an HTTP-facing crawling or scraping service and prefers to own downstream parsing, queues, and storage. This can keep the integration small for page acquisition and selected target-oriented workflows.&lt;/p&gt;

&lt;p&gt;The trade-off is that teams seeking a general code-hosting platform, broad marketplace, or deep Scrapy workflow may need additional components. Evaluate dynamic rendering, geography, response evidence, and failure semantics on the exact pages rather than inferring them from product categories.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do the platforms handle dynamic pages and extraction?
&lt;/h2&gt;

&lt;p&gt;All three vendors describe ways to retrieve modern pages, but the unit of control differs. Zyte can place extraction and browser behavior behind its API or services. Apify lets Actor code use browser and crawler libraries inside its runtime. Crawlbase places more emphasis on the request API. These differences affect debugging: provider-managed extraction is simpler to call, while custom code provides more control and more maintenance.&lt;/p&gt;

&lt;p&gt;Create a corpus with static, rendered, long, localized, duplicate, and expected-failure pages. Measure main-content completeness, field accuracy, artifacts, diagnostics, latency, retries, and billable units.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do pricing and operations differ?
&lt;/h2&gt;

&lt;p&gt;Pricing should be compared by model rather than old plan numbers. Possible units include requests, credits, compute time, storage, data transfer, platform usage, or managed project scope. Normalize everything to accepted business records after failures and review.&lt;/p&gt;

&lt;p&gt;Operationally, ask who patches browsers, controls concurrency, owns retries, stores raw artifacts, deploys parser updates, and responds when a target template changes. The cheapest successful demo can become the most expensive production path if it leaves those responsibilities undefined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which platform should you choose?
&lt;/h2&gt;

&lt;p&gt;Choose Zyte for a Scrapy-centered or managed-data operating model, Apify for a broad hosted automation platform and marketplace, and Crawlbase for a narrower API-first acquisition layer. Do not migrate solely because another platform lists more features.&lt;/p&gt;

&lt;p&gt;Run a dual-write pilot before switching. Preserve canonical URLs, source hashes, timestamps, error classes, and parser versions. The new provider should feed the same validation and storage contract so provider differences remain observable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would keep in production
&lt;/h2&gt;

&lt;p&gt;I would run the same small corpus through the finalists and measure accepted records, failure evidence, and the engineering work left outside the platform. I would also keep the business schema and raw artifacts portable so a future migration is an engineering task rather than a data rescue.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Is Apify better than Zyte?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Apify is generally a better fit for Actor-based cloud automation and marketplace workflows, while Zyte is a better fit for Scrapy-centered and managed-data workflows. The answer depends on the operating model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is Crawlbase a full replacement for Apify?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not for every workload. Crawlbase can cover API-based acquisition, while Apify also provides a code runtime, marketplace, schedules, storage, and other platform components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Which platform is best for Scrapy?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Zyte has the strongest direct relationship with the Scrapy ecosystem, but teams should verify the current cloud and API products that match their workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How should these platforms be benchmarked?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use the same authorized URLs, expected fields, rendering conditions, timeouts, and acceptance rules, then compare accepted records, diagnostics, latency, retries, and total billable units.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should a team migrate all crawlers at once?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Start with a representative low-risk workflow, dual-run it, compare outputs, and migrate only after recovery and cost behavior are understood.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>automation</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>TypeScript vs. JavaScript for Crawlers: My Rule of Thumb</title>
      <dc:creator>neuralbyte</dc:creator>
      <pubDate>Mon, 28 Sep 2026 08:32:27 +0000</pubDate>
      <link>https://dev.to/neuralbyte/typescript-vs-javascript-for-crawlers-my-rule-of-thumb-pce</link>
      <guid>https://dev.to/neuralbyte/typescript-vs-javascript-for-crawlers-my-rule-of-thumb-pce</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TypeScript and JavaScript crawlers run on the same JavaScript runtimes; TypeScript does not inherently make page retrieval faster.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript is usually the better fit for long-lived crawlers with multiple schemas, adapters, queues, and contributors.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JavaScript is often better for short experiments, small scripts, and teams that value zero compile configuration.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime validation remains necessary in both languages because remote HTML and JSON are untrusted and types disappear after compilation.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Choose based on change cost, team workflow, and deployment constraints—not a claim that one language scrapes more sites.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I approached it this way
&lt;/h2&gt;

&lt;p&gt;TypeScript does not make network requests faster. I choose it when the crawler has enough schemas, queues, storage adapters, and contributors that refactoring risk becomes expensive. For a short, disposable script, JavaScript can still be the clearer choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the main difference between a TypeScript and JavaScript crawler?
&lt;/h2&gt;

&lt;p&gt;The main difference is when errors are detected: TypeScript adds compile-time checking and type tooling, while JavaScript relies more heavily on runtime checks and tests. At runtime, TypeScript has been emitted as JavaScript and does not gain automatic network or browser performance.&lt;/p&gt;

&lt;p&gt;Language choice matters most as the crawler accumulates response types, job states, storage records, and recovery paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do TypeScript and JavaScript crawlers compare?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision field&lt;/th&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;JavaScript&lt;/th&gt;
&lt;th&gt;What changes the choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;Compiler or runtime transform&lt;/td&gt;
&lt;td&gt;Direct Node execution&lt;/td&gt;
&lt;td&gt;Prototype speed and deployment tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refactoring&lt;/td&gt;
&lt;td&gt;Static references and interfaces&lt;/td&gt;
&lt;td&gt;Tests and runtime discovery&lt;/td&gt;
&lt;td&gt;Codebase size and change frequency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data contracts&lt;/td&gt;
&lt;td&gt;Types plus runtime validation&lt;/td&gt;
&lt;td&gt;Runtime validation&lt;/td&gt;
&lt;td&gt;Number of adapters and schemas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Library use&lt;/td&gt;
&lt;td&gt;Type declarations improve tooling&lt;/td&gt;
&lt;td&gt;Works with the same runtime packages&lt;/td&gt;
&lt;td&gt;Quality of third-party types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Onboarding&lt;/td&gt;
&lt;td&gt;More explicit contracts&lt;/td&gt;
&lt;td&gt;Less syntax to learn&lt;/td&gt;
&lt;td&gt;Team background&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Compiles to JavaScript&lt;/td&gt;
&lt;td&gt;Native source language&lt;/td&gt;
&lt;td&gt;Generated code and runtime dominate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html" rel="noopener noreferrer"&gt;official TypeScript handbook&lt;/a&gt; describes TypeScript as a static type checker for JavaScript programs. The &lt;a href="https://nodejs.org/docs/latest/api/" rel="noopener noreferrer"&gt;Node.js API documentation&lt;/a&gt; remains the runtime reference for both.&lt;/p&gt;

&lt;h2&gt;
  
  
  When is TypeScript better for a crawler?
&lt;/h2&gt;

&lt;p&gt;TypeScript is better when a crawler has several request kinds, parsers, domain schemas, task states, and storage adapters. Discriminated unions can make terminal states explicit, while interfaces keep parser output consistent across sources. Editor tooling also reduces the cost of renaming fields or changing queue messages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FetchResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accepted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wrong-page&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing-content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;retry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rate-limit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The limitation is false confidence. A remote JSON object does not become safe because code assigns it a TypeScript interface. Use a runtime schema validator and reject malformed input. Compiler configuration, build output, source maps, and type-declaration conflicts also add maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  When is JavaScript better for a crawler?
&lt;/h2&gt;

&lt;p&gt;JavaScript is better when the task is a short, well-tested script, the deployment surface runs JavaScript directly, or the team does not benefit from a type build step. Modern JavaScript supports modules, async iterators, classes, optional chaining, and the same browser libraries used from TypeScript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid product record&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trade-off appears during change. A renamed field can remain hidden until a particular branch runs. Strong tests and runtime validation are therefore more important as the JavaScript crawler grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does TypeScript make a crawler more reliable?
&lt;/h2&gt;

&lt;p&gt;TypeScript can reduce mistakes inside the codebase, but reliability still depends on target validation, bounded retries, idempotent storage, concurrency, and observability. A systematic study found that broad assumptions about TypeScript and defect outcomes require nuance rather than a universal claim; see the &lt;a href="https://arxiv.org/abs/2203.11115" rel="noopener noreferrer"&gt;software-quality comparison study&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Use types for internal contracts and runtime schemas for external boundaries. Tests should include successful pages, wrong-page states, missing fields, invalid JSON, timeouts, and duplicate jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which language works better with Crawlee, Playwright, or Puppeteer?
&lt;/h2&gt;

&lt;p&gt;Both languages use the same underlying Node.js libraries. TypeScript often provides a better editor experience when packages publish accurate declarations; JavaScript avoids compiling and can be simpler for examples. The &lt;a href="https://crawlee.dev/js/docs/introduction" rel="noopener noreferrer"&gt;official Crawlee documentation&lt;/a&gt; supports the shared JavaScript/TypeScript ecosystem.&lt;/p&gt;

&lt;p&gt;Select libraries based on the crawler architecture. Crawlee fits queued crawling and storage, Playwright fits browser automation, and Puppeteer provides Chrome-oriented control. Do not infer extraction quality from language bindings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where does a managed crawl API change the decision?
&lt;/h2&gt;

&lt;p&gt;A managed API reduces the amount of browser and routing code in either language. This makes language choice primarily an application-maintainability decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should you choose between TypeScript and JavaScript?
&lt;/h2&gt;

&lt;p&gt;Choose TypeScript for a multi-contributor, long-lived crawler with evolving schemas and several integration boundaries. Choose JavaScript for a small, bounded script when build configuration would add more friction than protection. A practical migration path is to enable &lt;code&gt;checkJs&lt;/code&gt;, add JSDoc types, introduce runtime schemas, then rename files only when the value is clear.&lt;/p&gt;

&lt;p&gt;Run the same tests and load profile in both cases. The output schema, retry policy, and acceptance criteria should not change with the language.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would keep in production
&lt;/h2&gt;

&lt;p&gt;My default is TypeScript for a crawler that will live, change, or have multiple owners, and JavaScript for a tightly scoped script with a short lifespan. Neither choice replaces runtime validation, because remote HTML and JSON do not honor local types.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Is TypeScript faster than JavaScript for crawling?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No inherent speed advantage exists because TypeScript is compiled to JavaScript. Runtime version, generated code, browser work, parsing, and network behavior dominate performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can JavaScript use TypeScript type definitions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Editors and &lt;code&gt;checkJs&lt;/code&gt; can use JSDoc and published declaration files without converting every source file to TypeScript.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do TypeScript types validate scraped JSON?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. TypeScript types are erased at runtime, so external HTML and JSON require explicit runtime validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is TypeScript better for Playwright crawlers?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;TypeScript often improves tooling for larger Playwright projects, but JavaScript supports the same runtime API and may be simpler for small scripts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can a JavaScript crawler migrate gradually to TypeScript?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Add tests and runtime schemas first, enable JavaScript checking, annotate high-risk boundaries, and migrate modules incrementally.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>node</category>
    </item>
    <item>
      <title>Scraping JavaScript-Rendered Pages with Python: What I Try First</title>
      <dc:creator>neuralbyte</dc:creator>
      <pubDate>Mon, 28 Sep 2026 08:30:45 +0000</pubDate>
      <link>https://dev.to/neuralbyte/scraping-javascript-rendered-pages-with-python-what-i-try-first-5d5</link>
      <guid>https://dev.to/neuralbyte/scraping-javascript-rendered-pages-with-python-what-i-try-first-5d5</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use an authorized JSON endpoint when possible; it is usually more stable and cheaper than rendering a browser.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Playwright is the primary Python method in this guide because its locators, contexts, and network controls fit modern client-rendered pages.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Selenium remains useful in organizations with an established WebDriver stack.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scrapy plus scrapy-playwright fits queued crawls where only selected requests need rendering.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A managed rendering API is appropriate when browser operations, retries, and artifacts should be service responsibilities.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I approached it this way
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;requests.get()&lt;/code&gt; returns an almost empty document, I compare three views: the original response, the rendered DOM, and Fetch/XHR traffic. That comparison usually tells me whether I need a browser at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do JavaScript-rendered pages return empty content in Python?
&lt;/h2&gt;

&lt;p&gt;JavaScript-rendered pages return empty or partial content because &lt;code&gt;requests&lt;/code&gt; and similar clients download the initial response but do not execute the application. The first diagnostic step is comparing View Source, the rendered DOM, and Fetch/XHR responses.&lt;/p&gt;

&lt;p&gt;A skeleton page can return status 200 while the data request fails later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do you need before scraping a rendered page?
&lt;/h2&gt;

&lt;p&gt;You need permission, a precise schema, a small target corpus, explicit time and page budgets, and one or more meaningful readiness signals. Install the selected tools in an isolated environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;playwright selenium scrapy scrapy-playwright beautifulsoup4
python &lt;span class="nt"&gt;-m&lt;/span&gt; playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the &lt;a href="https://playwright.dev/python/docs/intro" rel="noopener noreferrer"&gt;official Playwright Python guide&lt;/a&gt;, &lt;a href="https://www.selenium.dev/documentation/webdriver/" rel="noopener noreferrer"&gt;Selenium WebDriver documentation&lt;/a&gt;, and &lt;a href="https://github.com/scrapy-plugins/scrapy-playwright" rel="noopener noreferrer"&gt;scrapy-playwright repository&lt;/a&gt; for current APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Method 1: Capture the underlying JSON request
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Inspect Fetch/XHR traffic
&lt;/h4&gt;

&lt;p&gt;On a site you own or are authorized to inspect, open DevTools → Network → Fetch/XHR, reload, and trigger the content. Identify a public request that returns the required records.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Reproduce one request
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/api/items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 3: Bound pagination
&lt;/h4&gt;

&lt;p&gt;Stop on a documented cursor, empty page, or configured maximum. Do not replay private application calls or copy session credentials without approval.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 2: Render with Playwright
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Start a browser context
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headless&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1365&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait_until&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domcontentloaded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[data-testid=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[data-testid=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all_inner_texts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Wait for evidence, not time
&lt;/h4&gt;

&lt;p&gt;Prefer a stable selector or the specific response that delivers data. &lt;code&gt;networkidle&lt;/code&gt; can be unsuitable for applications with analytics or long-lived connections.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Validate the page state
&lt;/h4&gt;

&lt;p&gt;Check final URL, title, expected controls, row count, and a screenshot on failure. Reject generic errors and consent-only states.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 3: Use Selenium with explicit waits
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Navigate with WebDriver
&lt;/h4&gt;

&lt;p&gt;Create the driver, set a page-load timeout, and open one approved page. Avoid implicit waits mixed with explicit waits because combined timing is difficult to reason about.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Wait for the result container
&lt;/h4&gt;

&lt;p&gt;Use &lt;code&gt;WebDriverWait&lt;/code&gt; and an expected condition for the actual content. Avoid &lt;code&gt;time.sleep()&lt;/code&gt; as the primary readiness mechanism.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Always close the driver
&lt;/h4&gt;

&lt;p&gt;Use &lt;code&gt;try/finally&lt;/code&gt; or a context wrapper so failed jobs do not leak browser processes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 4: Add rendering to Scrapy selectively
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Enable scrapy-playwright
&lt;/h4&gt;

&lt;p&gt;Configure the download handler and reactor exactly as documented for the installed version. Pin Scrapy, Playwright, and plugin versions together.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Mark only rendered requests
&lt;/h4&gt;

&lt;p&gt;Send static pages through Scrapy's normal downloader and add &lt;code&gt;meta={"playwright": True}&lt;/code&gt; only where rendering is required. This preserves concurrency and reduces browser cost.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Close page objects
&lt;/h4&gt;

&lt;p&gt;If a callback receives a Playwright page, close it on both success and exception paths. Monitor open pages and browser memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 5: Use a managed rendering service
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Define a minimal request
&lt;/h4&gt;

&lt;p&gt;Request only necessary formats and interactions.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Poll bounded asynchronous work
&lt;/h4&gt;

&lt;p&gt;Use terminal states, a maximum duration, and exponential backoff with jitter. Inspect body-level &lt;code&gt;success&lt;/code&gt;, status, and error information rather than the transport code alone.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Validate cost per accepted page
&lt;/h4&gt;

&lt;p&gt;Include rejected and retried pages when comparing cost with self-hosted browsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you scrape infinite scroll and lazy content?
&lt;/h2&gt;

&lt;p&gt;Prefer the underlying cursor or page endpoint when permitted. If scrolling is required, record the current item count, scroll once, wait for an increase, and stop when the count is unchanged or a maximum loop count is reached. Lazy images may require viewport intersection, but scrolling the entire page can accidentally trigger unbounded feeds.&lt;/p&gt;

&lt;p&gt;Keep URL discovery and item acceptance separate so repeated cards do not become duplicate records.&lt;/p&gt;

&lt;h2&gt;
  
  
  What errors should a rendered-page scraper handle?
&lt;/h2&gt;

&lt;p&gt;Handle navigation timeout, selector timeout, browser crash, access denied, wrong locale, empty result, parse failure, and storage failure as distinct states. Retry only transient classes. Capture sanitized diagnostics, not cookies or tokens. The &lt;a href="https://www.rfc-editor.org/rfc/rfc9309" rel="noopener noreferrer"&gt;Robots Exclusion Protocol&lt;/a&gt; is one policy input; it does not replace legal and contractual review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would keep in production
&lt;/h2&gt;

&lt;p&gt;I prefer the underlying authorized request when it is stable, Playwright for modern interaction-heavy pages, Selenium for existing WebDriver environments, and scrapy-playwright when only selected crawl requests need rendering. Fixed sleeps are the one method I avoid in every version.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Can Beautiful Soup scrape JavaScript-rendered content?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Beautiful Soup can parse rendered HTML after another tool executes JavaScript, but it does not run JavaScript itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should Playwright wait for networkidle?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Only when network quiet is a meaningful condition for the target. A stable selector or known data response is often more precise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can Scrapy render JavaScript?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Scrapy does not render JavaScript by itself, but integrations such as scrapy-playwright can route selected requests through a browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why does headless mode return different content?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Viewport, locale, browser version, timing, permissions, and target behavior can differ. Compare configurations and retain a screenshot before assuming the selector is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How many browser pages should run concurrently?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Concurrency depends on memory, CPU, target policy, and page weight. Start low, measure browser resource use and accepted-page rate, and set a hard cap.&lt;/p&gt;

</description>
      <category>python</category>
      <category>javascript</category>
      <category>playwright</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Scraping Dynamic Websites with Python: My Practical Decision Tree</title>
      <dc:creator>neuralbyte</dc:creator>
      <pubDate>Mon, 28 Sep 2026 08:29:19 +0000</pubDate>
      <link>https://dev.to/neuralbyte/scraping-dynamic-websites-with-python-my-practical-decision-tree-1917</link>
      <guid>https://dev.to/neuralbyte/scraping-dynamic-websites-with-python-my-practical-decision-tree-1917</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The most efficient way to scrape a dynamic website with Python is to call its authorized JSON/XHR endpoint when one exists.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use Playwright when the data depends on browser rendering or interaction; use Selenium when its ecosystem or an existing test stack makes it the better fit.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wait for a meaningful selector or response, not a fixed sleep.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Separate retrieval, validation, parsing, and storage so a rendered error page cannot become accepted data.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bound pagination, scrolling, retries, and concurrency before running beyond a small test set.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I approached it this way
&lt;/h2&gt;

&lt;p&gt;My first question is not “Which browser library should I install?” It is “Where does the page get its data?” If an authorized JSON response already contains the records, replaying that bounded request is usually simpler than running Chromium.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does it mean to scrape dynamic websites with Python?
&lt;/h2&gt;

&lt;p&gt;Scraping dynamic websites with Python means collecting content that appears after client-side JavaScript runs or after the page makes additional network requests. A page is dynamic when the initial HTML does not contain the data the user sees after loading, scrolling, selecting a filter, or clicking a control.&lt;/p&gt;

&lt;p&gt;Before opening a browser, compare View Source with the rendered DOM and inspect the browser Network panel.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do you need before you start?
&lt;/h2&gt;

&lt;p&gt;Install Python, &lt;code&gt;httpx&lt;/code&gt;, Playwright, Beautiful Soup, and optionally Selenium. Define the fields, permitted domains, pagination boundary, request budget, timeout, and terminal failure states. Use a local fixture or a site you own for development.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;httpx beautifulsoup4 playwright selenium
python &lt;span class="nt"&gt;-m&lt;/span&gt; playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://playwright.dev/python/docs/intro" rel="noopener noreferrer"&gt;Playwright Python documentation&lt;/a&gt; and &lt;a href="https://www.selenium.dev/documentation/webdriver/" rel="noopener noreferrer"&gt;Selenium WebDriver documentation&lt;/a&gt; should be checked for current installation and wait APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do dynamic pages load data?
&lt;/h2&gt;

&lt;p&gt;Dynamic pages commonly fetch JSON through XHR or Fetch, hydrate server-rendered HTML, or build components entirely in the browser. Infinite scroll is usually paginated data hidden behind an event. The correct method depends on where the authoritative content exists, not on which library is most popular.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Method 1: Reuse an authorized JSON endpoint
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Identify the request
&lt;/h4&gt;

&lt;p&gt;Open DevTools → Network → Fetch/XHR on a site you own or are authorized to inspect. Trigger the action that loads the data, then record the request URL, method, public parameters, response shape, and pagination token. Do not copy private tokens or replay undocumented account endpoints without permission.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Reproduce one bounded request
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/api/products&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 3: Validate and paginate
&lt;/h4&gt;

&lt;p&gt;Validate types and required identifiers before storage. Stop at an explicit &lt;code&gt;next&lt;/code&gt; token or a configured page limit; never continue until an endpoint returns an error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 2: Render with Playwright
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Open a controlled browser
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headless&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/dynamic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait_until&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domcontentloaded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[data-testid=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;result-card&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[data-testid=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;result-card&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all_inner_texts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Replace sleeps with evidence
&lt;/h4&gt;

&lt;p&gt;Wait for an element, response, or application state that proves the data loaded. A fixed delay is simultaneously too slow on fast pages and too short on slow pages.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Handle scroll or clicks with a cap
&lt;/h4&gt;

&lt;p&gt;Record the item count before and after each action. Stop when the count no longer changes, the next control disappears, or a maximum iteration count is reached.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 3: Use Selenium for an existing WebDriver stack
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Create the driver
&lt;/h4&gt;

&lt;p&gt;Use Selenium when an organization already maintains WebDriver tests, browser profiles, or Grid infrastructure. Keep dependencies pinned and use headless mode only after the flow works visibly in development.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Use explicit waits
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.common.by&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;By&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.support.ui&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebDriverWait&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.support&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;expected_conditions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;EC&lt;/span&gt;

&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/dynamic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WebDriverWait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;until&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;EC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;presence_of_all_elements_located&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CSS_SELECTOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[data-testid=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;result-card&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 3: Capture failure evidence
&lt;/h4&gt;

&lt;p&gt;On failure, save the final URL, title, screenshot, and a sanitized HTML excerpt. Never log cookies, authorization headers, or personal data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 4: Use a managed rendering API
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Define the output contract
&lt;/h4&gt;

&lt;p&gt;Request only required artifacts and verify current browser actions in the official documentation.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Inspect body-level task status
&lt;/h4&gt;

&lt;p&gt;A successful submission is not proof that the target page loaded. Check task state, page status, expected title, content signals, and error fields.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Parse through the same adapter
&lt;/h4&gt;

&lt;p&gt;Normalize managed output and self-hosted browser output into the same source schema. This keeps downstream validation independent of the retrieval provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is the dynamic scraper returning empty HTML?
&lt;/h2&gt;

&lt;p&gt;The initial response may contain only an application shell, the selector may run before hydration, the request may have reached a consent or error page, or the content may be inside an iframe or shadow DOM. Inspect the Network panel, final URL, title, response size, and a screenshot before changing selectors. Avoid retrying persistent authorization failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you run dynamic scraping in production?
&lt;/h2&gt;

&lt;p&gt;Use a queue with bounded concurrency, per-domain limits, idempotent document IDs, structured errors, and separate retry policies for timeouts, rate limits, and parse failures. Monitor accepted-page rate, render duration, browser memory, selector-missing rate, and duplicate content.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.rfc-editor.org/rfc/rfc9309" rel="noopener noreferrer"&gt;Robots Exclusion Protocol standard&lt;/a&gt; is one technical signal, not a complete permission decision. Keep the collection public or otherwise authorized, and do not bypass authentication, paywalls, or access controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would keep in production
&lt;/h2&gt;

&lt;p&gt;My order of preference is an authorized data endpoint, then Playwright, then Selenium when an existing WebDriver stack makes it practical. Whichever route I take, I wait for evidence, cap every loop, and reject pages that do not match the expected identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Can Requests scrape a dynamic website?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Requests can scrape a dynamic website when the required data is present in the initial HTML or an authorized JSON endpoint. It cannot execute the page's JavaScript.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is Playwright better than Selenium for scraping?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Playwright often provides convenient modern browser waits and contexts, while Selenium fits mature WebDriver ecosystems. The better choice depends on team tooling and target behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you scrape infinite scroll safely?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Prefer the underlying paginated endpoint when permitted; otherwise scroll with item-count checks, a maximum iteration limit, and a clear stop condition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why should fixed sleeps be avoided?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fixed sleeps do not prove content readiness and make jobs slow or flaky. Wait for a specific element, response, or state instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you prevent duplicate dynamic-page records?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use canonical URLs or stable entity IDs, content hashes, pagination cursors, and idempotent writes.&lt;/p&gt;

</description>
      <category>selenium</category>
      <category>webscraping</category>
      <category>python</category>
    </item>
    <item>
      <title>Extracting Text from HTML with Python: 4 Methods I Actually Use</title>
      <dc:creator>neuralbyte</dc:creator>
      <pubDate>Mon, 28 Sep 2026 08:28:10 +0000</pubDate>
      <link>https://dev.to/neuralbyte/extracting-text-from-html-with-python-4-methods-i-actually-use-3p82</link>
      <guid>https://dev.to/neuralbyte/extracting-text-from-html-with-python-4-methods-i-actually-use-3p82</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Beautiful Soup is the clearest general-purpose method for extracting text from imperfect HTML.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;lxml is a strong choice when XPath and high-throughput parsing matter.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trafilatura is better when the goal is main article text rather than every visible navigation label.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inscriptis is useful when text layout, tables, and lists need better preservation.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remove scripts and style content, preserve block boundaries, normalize Unicode, and validate against expected text before storage.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I approached it this way
&lt;/h2&gt;

&lt;p&gt;I stopped asking for the single best HTML-to-text library because the output contract changes the answer. DOM text, article text, XPath-selected text, and layout-aware text are different products. The four methods below are the ones I reach for in those four situations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the best way to extract text from HTML with Python?
&lt;/h2&gt;

&lt;p&gt;The best method depends on whether the input is a snippet, a complete document, or a rendered page. Regex alone is not a reliable HTML parser.&lt;/p&gt;

&lt;p&gt;Text extraction should preserve enough structure for its destination: search indexing may need headings and paragraphs, while a compact classifier may need normalized plain text.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do you need before you start?
&lt;/h2&gt;

&lt;p&gt;Install the four libraries in an isolated environment. Use one shared fixture so the outputs can be compared fairly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;beautifulsoup4 lxml trafilatura inscriptis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;HTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;div&amp;gt;&amp;lt;style&amp;gt;.x{display:none}&amp;lt;/style&amp;gt;&amp;lt;nav&amp;gt;Home&amp;lt;/nav&amp;gt;&amp;lt;main&amp;gt;
&amp;lt;h1&amp;gt;Parsing HTML&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Keep &amp;lt;strong&amp;gt;meaningful&amp;lt;/strong&amp;gt; text.&amp;lt;/p&amp;gt;
&amp;lt;script&amp;gt;ignore()&amp;lt;/script&amp;gt;&amp;lt;/main&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The practical workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Method 1: Use Beautiful Soup for readable general-purpose parsing
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Build the parse tree
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;
&lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Remove non-content nodes
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;script, style, noscript, template&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decompose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 3: Preserve boundaries and normalize whitespace
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Beautiful Soup tolerates malformed markup and has an approachable selector API. Its limitation is that &lt;code&gt;get_text()&lt;/code&gt; does not know which visible blocks are primary content. Use semantic selectors such as &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;article&lt;/code&gt;, or a verified content container when available. Check the &lt;a href="https://www.crummy.com/software/BeautifulSoup/bs4/doc/" rel="noopener noreferrer"&gt;official Beautiful Soup documentation&lt;/a&gt; for parser behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 2: Use lxml for XPath and throughput
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Parse the document
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;lxml&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;
&lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromstring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Remove unwanted elements
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;xpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;//script|//style|//noscript|//template&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 3: Extract selected text nodes
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;xpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;//main//text()[normalize-space()]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;lxml is fast and gives precise XPath control, but careless XPath can flatten meaningful structure or collect hidden content. The &lt;a href="https://lxml.de/lxmlhtml.html" rel="noopener noreferrer"&gt;official lxml.html documentation&lt;/a&gt; describes its HTML helpers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 3: Use Trafilatura for main-content extraction
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Pass the complete document
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;trafilatura&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trafilatura&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;include_links&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;include_images&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Handle a missing result
&lt;/h4&gt;

&lt;p&gt;Trafilatura can return no main content for short, unusual, or navigation-heavy pages. Treat that as a validation failure and fall back to a scoped DOM method rather than storing an empty document.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Keep metadata separately
&lt;/h4&gt;

&lt;p&gt;Main-content extraction deliberately removes boilerplate. If title, canonical URL, author, or publication date matters, extract those fields into metadata instead of expecting them to survive plain-text conversion. Verify current options in the &lt;a href="https://trafilatura.readthedocs.io/" rel="noopener noreferrer"&gt;Trafilatura documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 4: Use Inscriptis when layout carries meaning
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Convert HTML to layout-aware text
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;inscriptis&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_text&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Review lists and tables
&lt;/h4&gt;

&lt;p&gt;Inscriptis aims to preserve visible layout better than a simple descendant-text join. This can help with tables, lists, and documents where line breaks communicate structure.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Normalize for the destination
&lt;/h4&gt;

&lt;p&gt;Do not apply aggressive whitespace collapsing if table columns or list indentation matter. Create separate normalization profiles for search, LLM ingestion, and archival review.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you fetch HTML before extracting text?
&lt;/h2&gt;

&lt;p&gt;Use a bounded HTTP client for static pages and validate status, content type, encoding, final URL, and expected page identity. If the page requires JavaScript, use a browser or managed rendering layer before parsing.&lt;/p&gt;

&lt;p&gt;A parser cannot recover data that never arrived in its input.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you clean extracted text without damaging it?
&lt;/h2&gt;

&lt;p&gt;Normalize Unicode, convert non-breaking spaces, remove repeated blank lines, and retain block boundaries. Avoid deleting all short lines because headings and labels can be short. Avoid lowercasing content intended for display or entity extraction. Keep the raw source or a permitted hash so a cleaning regression can be investigated.&lt;/p&gt;

&lt;p&gt;Build golden fixtures containing headings, paragraphs, entities, lists, tables, hidden nodes, malformed tags, and non-ASCII text. Compare exact output or a structured block representation in automated tests. The &lt;a href="https://html.spec.whatwg.org/" rel="noopener noreferrer"&gt;WHATWG HTML standard&lt;/a&gt; is the primary reference when parser behavior depends on document structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would keep in production
&lt;/h2&gt;

&lt;p&gt;I use Beautiful Soup for readable general parsing, lxml when XPath and throughput matter, Trafilatura for article bodies, and Inscriptis when layout carries meaning. I keep several fixtures and compare missing content, boilerplate, and structure instead of judging one attractive output by eye.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Can Python extract text from malformed HTML?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Beautiful Soup and lxml both recover many malformed documents, but their repairs may differ, so test the selected parser against representative fixtures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why does get_text include JavaScript or CSS?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The parser treats script and style contents as text nodes unless those elements are removed. Decompose them before extracting text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is regex suitable for removing HTML tags?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Regex can clean a known fragment, but it is not a reliable general HTML parser because nesting, malformed markup, entities, and embedded languages complicate the grammar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you extract only article text?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use a verified &lt;code&gt;article&lt;/code&gt; or content selector when the template is known, or a main-content library such as Trafilatura when templates vary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Which method is fastest?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;lxml and other native-backed parsers are often faster than Beautiful Soup, but throughput should be measured with the real documents and required cleanup.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>html</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>How I’d Use Web Data for a Stronger Ecommerce Holiday Season</title>
      <dc:creator>neuralbyte</dc:creator>
      <pubDate>Mon, 28 Sep 2026 08:26:37 +0000</pubDate>
      <link>https://dev.to/neuralbyte/how-id-use-web-data-for-a-stronger-ecommerce-holiday-season-2nkf</link>
      <guid>https://dev.to/neuralbyte/how-id-use-web-data-for-a-stronger-ecommerce-holiday-season-2nkf</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Holiday web data is useful only when it drives a defined decision such as price review, inventory allocation, promotion timing, assortment, or content correction.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Start with a product and competitor registry before collecting prices; otherwise variants, bundles, currencies, and promotions will be compared incorrectly.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Collect more frequently only for volatile, high-value items and keep every observation attributable to a URL, market, timestamp, and parser version.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Separate raw observations from accepted business records and route anomalies to review.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use the 2026 holiday period to build a repeatable operating loop, not a one-time data dump.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I approached it this way
&lt;/h2&gt;

&lt;p&gt;Collecting more competitor pages is not the goal. I want each observation to route to a named decision: review a price, investigate availability, correct campaign copy, or update a delivery promise. If no action consumes a field, I leave it out.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can web data improve an ecommerce holiday season?
&lt;/h2&gt;

&lt;p&gt;Web data can improve the holiday season by making pricing, inventory signals, assortment changes, promotions, and competitor availability observable early enough to act. The useful output is a decision queue, not a large table of scraped pages.&lt;/p&gt;

&lt;p&gt;A competitor's displayed price is meaningless if it refers to a different size, seller, bundle, market, or membership condition.&lt;/p&gt;

&lt;h2&gt;
  
  
  What web data should ecommerce teams collect?
&lt;/h2&gt;

&lt;p&gt;Collect only fields tied to a decision: canonical product identity, displayed price and currency, promotion text, availability signal, delivery promise, seller, variant, rating context, category placement, and retrieval timestamp. Add content and visual evidence only when needed for review. Avoid customer, account, or checkout data.&lt;/p&gt;

&lt;p&gt;Internal data should include inventory, margin constraints, forecast, campaign calendar, fulfillment capacity, and conversion. External observations do not replace these systems; they add market context. The &lt;a href="https://nrf.com/research-insights/holiday-data-and-trends" rel="noopener noreferrer"&gt;National Retail Federation holiday data&lt;/a&gt; can provide macro context, while business decisions should rely on the company's own market and catalog.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Method 1: Build a decision-first product registry
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Define the questions
&lt;/h4&gt;

&lt;p&gt;List decisions such as “review price when three matched competitors change,” “reallocate stock when an approved availability signal changes,” or “fix content when key attributes disappear.” Assign an owner and response time.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Create stable product keys
&lt;/h4&gt;

&lt;p&gt;Map internal SKU, GTIN when available, brand, model, variant, pack size, market, currency, and approved competitor URLs. Do not match on title similarity alone.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Freeze acceptance rules
&lt;/h4&gt;

&lt;p&gt;Specify how to handle coupons, member prices, marketplaces, out-of-stock listings, ranges, bundles, and taxes. Unknown conditions should go to review, not be coerced into a numeric price.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 2: Collect bounded public observations
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Schedule by volatility
&lt;/h4&gt;

&lt;p&gt;Refresh volatile hero products more often than stable long-tail pages. Use an explicit per-domain request budget and stop on persistent access denial.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Validate each page
&lt;/h4&gt;

&lt;p&gt;Require the expected product identity, market, currency, and seller before accepting price or availability. Store the final URL, retrieval time, source hash, and parser version.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Keep raw and accepted layers separate
&lt;/h4&gt;

&lt;p&gt;Raw observations support debugging; accepted records power decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 3: Use managed crawling for selected sources
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Define the crawl boundary
&lt;/h4&gt;

&lt;p&gt;Set page and depth limits, include product paths, and exclude accounts, checkout, search loops, and unrelated files.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Request the minimum artifacts
&lt;/h4&gt;

&lt;p&gt;Use machine-readable content for extraction and screenshots only where visual promotion or page-state review matters. More artifacts increase storage and review cost.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Measure accepted-record cost
&lt;/h4&gt;

&lt;p&gt;A cheap request that produces ambiguous data is not cheap operationally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 4: Turn changes into an action queue
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Detect material changes
&lt;/h4&gt;

&lt;p&gt;Compare normalized price, availability, delivery, and promotion state. Use thresholds appropriate to category and margin rather than alerting on every text change.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Add business context
&lt;/h4&gt;

&lt;p&gt;Join inventory, margin floor, forecast, campaign, and fulfillment capacity. A competitor price drop should not automatically trigger a loss-making response.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Route and record decisions
&lt;/h4&gt;

&lt;p&gt;Send pricing issues to merchandising, content defects to catalog operations, stock risks to planning, and delivery changes to fulfillment. Record the action and outcome so thresholds can improve.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 5: Monitor campaign execution and customer-facing truth
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Audit your own public pages
&lt;/h4&gt;

&lt;p&gt;Verify promotion dates, price consistency, coupon conditions, stock labels, shipping promises, mobile rendering, and structured data. Use a separate synthetic or QA account only when authorized.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Compare advertised and landing-page states
&lt;/h4&gt;

&lt;p&gt;Campaign links should resolve to the intended product and market. Capture evidence when an ad promise, landing page, and checkout condition disagree, but do not collect personal checkout data.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Review daily during peak periods
&lt;/h4&gt;

&lt;p&gt;Prioritize high-impact discrepancies and assign owners. The &lt;a href="https://support.google.com/merchants/answer/7052112" rel="noopener noreferrer"&gt;Google Merchant Center product-data specification&lt;/a&gt; is a useful reference for required feed attributes when shopping channels are in scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should teams prepare the 2026 holiday timeline?
&lt;/h2&gt;

&lt;p&gt;Begin with catalog and policy work before increasing collection frequency. Several weeks before peak campaigns, validate product matches, markets, and alert ownership. Before launch, run a failure drill for stale pages, provider outage, parser breakage, and inventory-feed delay. During peak weeks, shorten review queues for hero products and watch accepted-record freshness.&lt;/p&gt;

&lt;p&gt;After the season, measure which signals led to decisions and which alerts were ignored. Remove unused fields and archive raw artifacts according to policy. The &lt;a href="https://www.cisa.gov/news-events/news/holiday-online-shopping" rel="noopener noreferrer"&gt;CISA holiday online-shopping guidance&lt;/a&gt; is consumer-focused, but it reinforces the importance of trustworthy domains and secure customer experiences.&lt;/p&gt;

&lt;h2&gt;
  
  
  What metrics show whether web data is working?
&lt;/h2&gt;

&lt;p&gt;Track source freshness, accepted-record rate, unmatched products, wrong-market rate, field-level accuracy, alert precision, review time, decision time, and business outcomes. Do not claim causation from a simple correlation between data use and sales. Use holdouts or phased rollouts when testing a pricing or promotion rule.&lt;/p&gt;

&lt;p&gt;Operational metrics matter too: queue age, retry rate, page-state failures, parser changes, and cost per accepted record.&lt;/p&gt;

&lt;h2&gt;
  
  
  What responsible-use controls are required?
&lt;/h2&gt;

&lt;p&gt;Collect only public or otherwise authorized business information, respect applicable terms and robots directives, minimize retained content, and do not bypass authentication or access controls. Do not collect customer identities, carts, or account data from third parties. Review competition, pricing, privacy, and consumer-protection obligations with qualified counsel.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.rfc-editor.org/rfc/rfc9309" rel="noopener noreferrer"&gt;Robots Exclusion Protocol&lt;/a&gt; standardizes one technical signal but does not determine legality. Maintain a domain allowlist, contact owner, request budgets, incident stop switch, and retention schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would keep in production
&lt;/h2&gt;

&lt;p&gt;I would build the decision registry before the crawler and rehearse the full loop before peak traffic. The most useful metrics are accepted-record rate, time to decision, false-alert rate, and the share of observations that produced a documented action.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: What ecommerce web data is most useful during the holidays?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Price, promotion, availability, delivery, seller, variant, and category-placement observations are most useful when each maps to a defined business decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How often should competitor products be checked?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Frequency should follow volatility, product value, permission, and decision speed. High-value promotional items may justify more frequent checks than stable long-tail products.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should retailers automatically match competitor prices?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Automated matching can ignore margin, seller quality, bundles, membership conditions, inventory, and strategy. Route material changes through business rules and review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do teams compare products accurately?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use stable identifiers where possible and include brand, model, variant, pack size, seller, market, and currency. Send ambiguous matches to review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How should screenshots be used in holiday monitoring?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use screenshots as time-stamped review evidence for promotions, availability, and page state, not as the only machine-readable data source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What should happen after the holiday season?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Measure which signals caused useful actions, reduce noisy rules, update product mappings, and apply retention policy to raw artifacts and logs.&lt;/p&gt;

</description>
      <category>data</category>
      <category>analytics</category>
      <category>automation</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Building a Zalando Scraper with Python: The Approach I’d Use</title>
      <dc:creator>neuralbyte</dc:creator>
      <pubDate>Mon, 28 Sep 2026 08:24:48 +0000</pubDate>
      <link>https://dev.to/neuralbyte/building-a-zalando-scraper-with-python-the-approach-id-use-3mf</link>
      <guid>https://dev.to/neuralbyte/building-a-zalando-scraper-with-python-the-approach-id-use-3mf</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Build a Zalando scraper only for public pages you are permitted to collect, and check the site's current terms and robots policy before running it.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Start with one product URL and extract embedded structured data before adding browser automation.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use Playwright only when the required fields appear after JavaScript execution; plain HTTP is faster when the initial response already contains them.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Store stable product identifiers, canonical URLs, currency, availability, retrieval time, and a content hash so updates are idempotent.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat consent pages, locale redirects, missing variants, and access-denied responses as explicit terminal states.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I approached it this way
&lt;/h2&gt;

&lt;p&gt;The tempting version of this project starts with CSS selectors and a large product list. I prefer to start with one permitted product URL, look for JSON-LD, and write an acceptance test before adding pagination. That order makes markup changes much easier to diagnose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a Zalando scraper and why would you need one?
&lt;/h2&gt;

&lt;p&gt;A Zalando scraper is a controlled program that reads permitted public product pages and converts selected fields into a consistent record. Appropriate uses include internal QA, approved catalog research, and monitoring products your organization is authorized to observe.&lt;/p&gt;

&lt;p&gt;The goal is not to copy the entire storefront. A reliable job starts from an approved URL inventory and a narrow schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do you need before you start?
&lt;/h2&gt;

&lt;p&gt;You need Python 3.11 or newer, an isolated environment, &lt;code&gt;httpx&lt;/code&gt;, &lt;code&gt;beautifulsoup4&lt;/code&gt;, and Playwright for pages that truly require a browser. You also need written permission or a documented lawful basis, a small test URL set, a locale and currency policy, and a destination schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;httpx beautifulsoup4 playwright
python &lt;span class="nt"&gt;-m&lt;/span&gt; playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Define these fields before writing selectors: &lt;code&gt;product_id&lt;/code&gt;, &lt;code&gt;canonical_url&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;brand&lt;/code&gt;, &lt;code&gt;currency&lt;/code&gt;, &lt;code&gt;price_text&lt;/code&gt;, &lt;code&gt;availability&lt;/code&gt;, &lt;code&gt;color&lt;/code&gt;, &lt;code&gt;size_options&lt;/code&gt;, &lt;code&gt;retrieved_at&lt;/code&gt;, and &lt;code&gt;source_hash&lt;/code&gt;. Do not collect customer, account, or checkout data. Read the &lt;a href="https://www.zalando.com/terms/" rel="noopener noreferrer"&gt;current Zalando terms&lt;/a&gt; and the site's robots file for the exact host and locale you plan to access.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does a Zalando product page actually work?
&lt;/h2&gt;

&lt;p&gt;A product page can combine server-rendered HTML, embedded JSON, and client-side requests. The visible DOM is not always the best source. Inspect the initial response and &lt;code&gt;&amp;lt;script type="application/ld+json"&amp;gt;&lt;/code&gt; blocks first because structured product data is usually more stable than presentation classes. If required data appears only after rendering, use a browser and wait for a meaningful product element rather than sleeping for an arbitrary duration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Method 1: Extract JSON-LD from a permitted product page
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Fetch one URL with a bounded client
&lt;/h4&gt;

&lt;p&gt;Use a descriptive user agent, a timeout, redirect limits, and a low request rate. The target URL must come from your approved inventory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TARGET_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;follow_redirects&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CatalogQA/1.0 contact@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Parse product JSON-LD
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;

&lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;script[type=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/ld+json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Product&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No Product JSON-LD found; inspect the response before changing selectors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 3: Normalize without inventing values
&lt;/h4&gt;

&lt;p&gt;Map only present fields. Preserve the original price string and currency, and use &lt;code&gt;None&lt;/code&gt; for unavailable data. Never infer a size or stock state from an absent button.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 2: Render the page with Playwright
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Launch a bounded browser context
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headless&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-GB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;viewport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TARGET_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;wait_until&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domcontentloaded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;script[type=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/ld+json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rendered_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Detect the wrong page state
&lt;/h4&gt;

&lt;p&gt;Check the final URL, page title, expected product identifier, locale, and presence of product evidence. Reject consent-only, login, challenge, and generic error pages. The &lt;a href="https://playwright.dev/python/docs/intro" rel="noopener noreferrer"&gt;official Playwright Python documentation&lt;/a&gt; is the source for current installation and waiting APIs.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Save a review artifact
&lt;/h4&gt;

&lt;p&gt;Save a screenshot or HTML hash for failures, but apply retention limits. Review artifacts should help diagnose a selector or rendering change without becoming an uncontrolled copy of the page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 3: Use a managed crawl API for acquisition
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Keep acquisition separate from parsing
&lt;/h4&gt;

&lt;p&gt;Submit only approved public URLs, request the smallest set of formats, and inspect body-level success rather than assuming a successful HTTP response means the product loaded.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Validate the returned artifact
&lt;/h4&gt;

&lt;p&gt;Confirm canonical URL, product evidence, locale, currency, and content completeness before parsing.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Feed the same normalization layer
&lt;/h4&gt;

&lt;p&gt;The HTTP, Playwright, and managed methods should all produce the same internal source record. Provider-specific fields belong in the acquisition adapter, not in downstream catalog tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is the Zalando scraper returning empty or inconsistent data?
&lt;/h2&gt;

&lt;p&gt;Empty data usually means the response is a different page state, the field moved into embedded JSON, the locale redirected, or the content requires rendering. Compare &lt;code&gt;response.url&lt;/code&gt;, status, title, body length, and a saved hash. If Playwright finds the field but HTTP does not, inspect authorized network requests before adding more browser interactions.&lt;/p&gt;

&lt;p&gt;Inconsistent prices often come from locale, currency, promotion, membership, or variant context. Store those dimensions with every observation.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you make the scraper safe and maintainable?
&lt;/h2&gt;

&lt;p&gt;Use a bounded queue, low concurrency, retry only transient failures, and stop on persistent denial. Version the parser, keep fixtures for each supported page state, and alert on missing-field rate rather than silently writing nulls. The &lt;a href="https://www.rfc-editor.org/rfc/rfc9309" rel="noopener noreferrer"&gt;Robots Exclusion Protocol&lt;/a&gt; describes standardized robots rules, but compliance also requires terms, privacy, copyright, and internal review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would keep in production
&lt;/h2&gt;

&lt;p&gt;I would ship the smallest version first: one permitted URL, one stable product identifier, a validated schema, and an idempotent write. Browser rendering should be an escalation path rather than the default, and every expansion in scope should have a clear stop condition.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Is it legal to scrape Zalando?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Legality depends on the target, method, jurisdiction, terms, and data use. Collect only public or otherwise authorized pages and obtain legal guidance for the specific project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should a Zalando scraper use Beautiful Soup or Playwright?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use Beautiful Soup when the initial response or embedded JSON contains the required data; use Playwright only when JavaScript rendering is necessary. This reduces cost and failure surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why does the scraper see a different language or price?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Locale redirects, currency settings, geography, promotions, and variant context can change the page. Store those dimensions and reject records that do not match the requested market.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How often should product pages be refreshed?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Refresh frequency should follow business need, page volatility, permission, and site load. Use change history to slow stable products and prioritize volatile ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you prevent duplicate products?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use a stable product identifier plus market and variant dimensions, canonicalize URLs conservatively, and make writes idempotent with content hashes.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>data</category>
    </item>
    <item>
      <title>Building a TechCrunch Scraper with Python: RSS First, HTML Second</title>
      <dc:creator>neuralbyte</dc:creator>
      <pubDate>Mon, 28 Sep 2026 08:23:45 +0000</pubDate>
      <link>https://dev.to/neuralbyte/building-a-techcrunch-scraper-with-python-rss-first-html-second-4hb4</link>
      <guid>https://dev.to/neuralbyte/building-a-techcrunch-scraper-with-python-rss-first-html-second-4hb4</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use TechCrunch's public feed for recent article discovery when it satisfies the project; it is simpler and more stable than crawling listing pages.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use article HTML only for fields that the feed does not supply, and keep the original URL and publication metadata.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefer semantic article markup and JSON-LD over presentation classes.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Respect current terms, robots directives, copyright, and rate limits; store only the content needed for the authorized use.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make the pipeline idempotent with canonical URLs, publication identifiers, retrieval timestamps, and content hashes.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I approached it this way
&lt;/h2&gt;

&lt;p&gt;For this kind of news workflow, I start with the public feed and fetch article pages only when the feed does not contain a required field. That keeps discovery simple and reduces both traffic and parser maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a TechCrunch scraper and why would you build one?
&lt;/h2&gt;

&lt;p&gt;A TechCrunch scraper is a bounded pipeline that discovers permitted public articles and extracts selected metadata for monitoring, search, or research. For many projects, a feed reader plus selective article parsing is safer and simpler than crawling category pages.&lt;/p&gt;

&lt;p&gt;News text is copyrighted, so store the minimum required fields and links unless the intended use and license permit more.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do you need before you start?
&lt;/h2&gt;

&lt;p&gt;Install &lt;code&gt;feedparser&lt;/code&gt;, &lt;code&gt;httpx&lt;/code&gt;, &lt;code&gt;beautifulsoup4&lt;/code&gt;, and &lt;code&gt;trafilatura&lt;/code&gt;. Define &lt;code&gt;canonical_url&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;authors&lt;/code&gt;, &lt;code&gt;published_at&lt;/code&gt;, &lt;code&gt;section&lt;/code&gt;, &lt;code&gt;summary&lt;/code&gt;, &lt;code&gt;retrieved_at&lt;/code&gt;, and &lt;code&gt;content_hash&lt;/code&gt;. Decide whether full text is genuinely necessary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;feedparser httpx beautifulsoup4 trafilatura
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the &lt;a href="https://techcrunch.com/feed/" rel="noopener noreferrer"&gt;current TechCrunch feed&lt;/a&gt;, terms, and robots file before execution. Use a descriptive user agent and a low, bounded request rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does the TechCrunch collection workflow work?
&lt;/h2&gt;

&lt;p&gt;The feed supplies recent entries and stable article links. Each accepted link can be normalized, checked against the document registry, and fetched only when new or changed. Article HTML may expose JSON-LD and semantic markup for title, author, and timestamps. Main-content extraction is a separate step from discovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Method 1: Discover articles through RSS
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Parse the feed
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;feedparser&lt;/span&gt;

&lt;span class="n"&gt;feed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;feedparser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://techcrunch.com/feed/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bozo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Feed parse error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bozo_exception&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
           &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Validate each entry
&lt;/h4&gt;

&lt;p&gt;Require an HTTPS TechCrunch URL and a non-empty title. Reject duplicate canonical URLs before page retrieval.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Save a discovery checkpoint
&lt;/h4&gt;

&lt;p&gt;Store the feed retrieval time and entry identifier. Do not assume feed order alone is stable enough for recovery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 2: Extract metadata from article HTML
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Fetch one new article
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NewsResearch/1.0 contact@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;follow_redirects&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Parse JSON-LD
&lt;/h4&gt;

&lt;p&gt;Use Beautiful Soup to locate &lt;code&gt;application/ld+json&lt;/code&gt; and select &lt;code&gt;NewsArticle&lt;/code&gt; or &lt;code&gt;Article&lt;/code&gt; objects. Preserve headline, author objects, date fields, and canonical URL without inventing missing values.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Extract only required content
&lt;/h4&gt;

&lt;p&gt;Use a verified &lt;code&gt;article&lt;/code&gt; selector or Trafilatura for main text. Record extraction method and parser version so later changes can be reproduced. The &lt;a href="https://trafilatura.readthedocs.io/" rel="noopener noreferrer"&gt;Trafilatura documentation&lt;/a&gt; describes current extraction controls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 3: Use the WordPress REST surface when available and permitted
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Confirm the endpoint from the site
&lt;/h4&gt;

&lt;p&gt;Do not assume a WordPress endpoint is public or stable. Discover it through official page metadata or documentation and verify that the intended use is allowed.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Request narrow fields and bounded pages
&lt;/h4&gt;

&lt;p&gt;Use &lt;code&gt;_fields&lt;/code&gt; when supported, a small &lt;code&gt;per_page&lt;/code&gt;, and explicit page limits. The &lt;a href="https://developer.wordpress.org/rest-api/" rel="noopener noreferrer"&gt;WordPress REST API handbook&lt;/a&gt; documents general interfaces, but the site controls what it exposes.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Treat rendered HTML as untrusted input
&lt;/h4&gt;

&lt;p&gt;Sanitize content before display and retain source attribution. REST output does not grant reuse rights beyond the applicable license and terms.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 4: Use a managed collection layer
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Submit only approved article URLs
&lt;/h4&gt;

&lt;p&gt;Keep feed discovery as the source of truth and use bounded page jobs.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Validate body-level success
&lt;/h4&gt;

&lt;p&gt;Check final URL, title, publication evidence, language, and content completeness.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Normalize into the same registry
&lt;/h4&gt;

&lt;p&gt;The managed path and HTTP path should produce identical internal metadata. Provider fields must not leak into business keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is the TechCrunch scraper missing articles or text?
&lt;/h2&gt;

&lt;p&gt;Feeds can be truncated, delayed, or limited to recent items. Article templates can change, content can be embedded, and a response can be an error or consent state. Compare feed IDs, canonical URLs, JSON-LD, final URL, and accepted-field coverage. Do not solve a missing field by collecting unrelated page regions.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.rfc-editor.org/rfc/rfc9309" rel="noopener noreferrer"&gt;Robots Exclusion Protocol&lt;/a&gt; is a technical policy signal, not a content license.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you schedule and deduplicate the pipeline?
&lt;/h2&gt;

&lt;p&gt;Poll the feed at a respectful interval, upsert by canonical URL or stable entry ID, and fetch only unseen or explicitly refreshed pages. Compute a normalized content hash to distinguish metadata edits from new documents. Retry transient network failures with a cap and send parse changes to review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would keep in production
&lt;/h2&gt;

&lt;p&gt;I would keep this pipeline intentionally small: RSS for discovery, structured metadata for identity, selective HTML parsing for missing fields, and an idempotent store keyed by canonical URL or another stable identifier. Re-check the site’s current policies before scheduling it.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Does TechCrunch provide an RSS feed?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;TechCrunch exposes a public feed at the URL used in this tutorial, but its scope and behavior should be rechecked before production use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should a TechCrunch scraper store full article text?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Only when the project has a valid need and applicable rights. Metadata, summaries, and canonical links are often sufficient for monitoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you avoid duplicate TechCrunch articles?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use canonical URLs or stable feed identifiers, normalize conservative tracking parameters, and perform idempotent upserts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why does the feed contain fewer articles than the site?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Feeds commonly expose a recent subset rather than a complete archive. Use only approved discovery paths and do not assume the feed is exhaustive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: When is a browser required?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A browser is required only when the permitted field depends on client rendering or interaction and cannot be obtained from the feed, structured data, or initial HTML.&lt;/p&gt;

</description>
      <category>python</category>
      <category>rss</category>
      <category>webscraping</category>
    </item>
  </channel>
</rss>
