<?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: Oaida Adrian</title>
    <description>The latest articles on DEV Community by Oaida Adrian (@darksider4all_afa2428f63d0).</description>
    <link>https://dev.to/darksider4all_afa2428f63d0</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%2F4014906%2Fc97aa091-845d-4fe5-b6fd-5a98bf7a23fa.jpg</url>
      <title>DEV Community: Oaida Adrian</title>
      <link>https://dev.to/darksider4all_afa2428f63d0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/darksider4all_afa2428f63d0"/>
    <language>en</language>
    <item>
      <title>Scraping Romanian Public Contracts: A Native-Language Tender &amp; Awarded Deals Scraper</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Sun, 16 Aug 2026 19:30:10 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/scraping-romanian-public-contracts-a-native-language-tender-awarded-deals-scraper-4lob</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/scraping-romanian-public-contracts-a-native-language-tender-awarded-deals-scraper-4lob</guid>
      <description>&lt;p&gt;Romania publishes every public tender and awarded contract through e-licitatie.ro — the successor to the old SEAP system. The data is public, the opportunities are real (millions of RON in awards every month), and almost nobody outside Romania is building on it. Being native-language gives us an edge no English-first competitor has.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;The niche's existing actors are thin: the top reference has single-digit runs. Tenders come with deadlines and award values, but most scrapers stop at the listing page. Ours goes deeper — tenders &lt;strong&gt;and&lt;/strong&gt; awarded contracts, with detail enrichment, CPV classification, and winner + CUI (company ID) extraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The source
&lt;/h2&gt;

&lt;p&gt;The Angular SPA at e-licitatie.ro calls its own JSON API — but it 403s without the right headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST api-pub/NoticeCommon/GetCNoticeListFiltered/   # tenders
POST api-pub/NoticeCommon/GetCANoticeList/          # awarded contracts
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick: the API requires a &lt;code&gt;Referer&lt;/code&gt; of &lt;code&gt;https://e-licitatie.ro/pub&lt;/code&gt; plus &lt;code&gt;Origin&lt;/code&gt; and &lt;code&gt;X-Requested-With&lt;/code&gt;. Missing referer, instant 403 "Access Denied: Referrer cannot be null". Detail enrichment needs &lt;code&gt;GetSection21View&lt;/code&gt; (description, CPV, estimated value — keyed off the item's own &lt;code&gt;sysNoticeTypeId&lt;/code&gt;, not the noticeId) and &lt;code&gt;GetSection1View&lt;/code&gt; for the contracting authority. Winners come from &lt;code&gt;GetCANoticeContracts&lt;/code&gt; — and its filter payload is strict: a bare payload 400s.&lt;/p&gt;

&lt;h2&gt;
  
  
  The native-language advantage
&lt;/h2&gt;

&lt;p&gt;Romanian titles, Romanian descriptions, CPV codes joined to EU TED. The reference actors are English-first and miss the nuance — e.g., distinguishing &lt;em&gt;licitație deschisă&lt;/em&gt; (open tender) from &lt;em&gt;achiziție directă&lt;/em&gt; (direct award). Our output carries the original Romanian fields untouched, so downstream users can do their own analysis without a translation layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smoke test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tenders&lt;/strong&gt;: keyword &lt;code&gt;infrastructura&lt;/code&gt;, dateFrom 2026-01-01 → &lt;strong&gt;5 items, 5/5 complete&lt;/strong&gt; — real titles, authorities, CPV names, estimated RON values, deadlines, status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Awarded&lt;/strong&gt;: keyword &lt;code&gt;drumuri&lt;/code&gt;, dateFrom 2026-06-01 → &lt;strong&gt;4 items&lt;/strong&gt;, including winner &lt;strong&gt;ROUTE CENTER CONSTRUCT (CUI 29170569)&lt;/strong&gt; with a contract value of &lt;strong&gt;4,822,401.88 RON&lt;/strong&gt; and an award date&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest bits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Authority filtering is client-side containment — the API ignores authority text server-side&lt;/li&gt;
&lt;li&gt;Some CAN (award) records show zero value and no winner — that's data-side, the tender was annulled&lt;/li&gt;
&lt;li&gt;Publication dates are what the portal publishes; actual contract signatures can lag&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;👉 &lt;a href="https://apify.com/darknezz/ro-public-contracts-scraper" rel="noopener noreferrer"&gt;&lt;strong&gt;Romanian Public Contracts Scraper on Apify Store&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;p&gt;While you're here, these might be worth a read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59"&gt;I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One API Call&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS Aggregation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and JSON-LD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Real-Time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Generation Tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a 12-City US Building Permits Scraper With Python</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Sun, 16 Aug 2026 19:30:09 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/building-a-12-city-us-building-permits-scraper-with-python-138b</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/building-a-12-city-us-building-permits-scraper-with-python-138b</guid>
      <description>&lt;p&gt;Building permits are the earliest public signal of construction activity — new housing, commercial builds, renovation waves. Every major US city publishes them, but each through a different portal with a different schema. I built one scraper that covers twelve of them with a single output shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;The reference actor in this niche covers the big cities but has blind spots: in its own all-cities run it scanned &lt;strong&gt;zero&lt;/strong&gt; rows for Washington DC and never even attempted West Sacramento. Ours covers all twelve: ten Socrata portals (Chicago, NYC, LA, San Francisco, Austin, Seattle, Cincinnati, Mesa, Montgomery County, Baton Rouge) plus two ArcGIS servers (DC, West Sacramento).&lt;/p&gt;

&lt;h2&gt;
  
  
  The reverse-engineering
&lt;/h2&gt;

&lt;p&gt;Two very different API families to reconcile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Socrata&lt;/strong&gt; — the well-documented &lt;code&gt;resource&lt;/code&gt; endpoint family, clean JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ArcGIS&lt;/strong&gt; — the old FeatureServer contract, where field names vary per server and some servers reject modern ordering parameters outright&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The resilience ladder for the old ArcGIS servers: try &lt;code&gt;TIMESTAMP&lt;/code&gt; + &lt;code&gt;orderBy&lt;/code&gt; first, fall back to &lt;code&gt;where&lt;/code&gt;-only queries, then to unfiltered pulls with client-side date filtering. West Sacramento's server rejects &lt;code&gt;TIMESTAMP&lt;/code&gt; entirely — the ladder is what makes it work.&lt;/p&gt;

&lt;h2&gt;
  
  
  One schema to rule them all
&lt;/h2&gt;

&lt;p&gt;Eighteen canonical keys: city, permit_number, permit_type, status, issue_date, application_date, address, work_description, valuation, contractor_name, applicant, latitude, longitude, source, source_type, source_url, scraped_at, cityKey. NYC's "Permit is not yet issued" placeholder rows get filtered. Adding a city is one entry in the config table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smoke test
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;24/24 non-zero&lt;/strong&gt; — two runs per city across all twelve, full 18-key schema on every item, plus local unit tests covering ten Socrata normalisations, both ArcGIS servers, and live fetches.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest bits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Coverage is US-only and city-scoped — counties and rural areas aren't in this pass&lt;/li&gt;
&lt;li&gt;Valuation can be null on older records; some cities simply don't publish it&lt;/li&gt;
&lt;li&gt;ArcGIS servers rate-limit; the ladder paces requests per city&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;👉 &lt;a href="https://apify.com/darknezz/building-permits-scraper" rel="noopener noreferrer"&gt;&lt;strong&gt;Building Permits Scraper on Apify Store&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;p&gt;While you're here, these might be worth a read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59"&gt;I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One API Call&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS Aggregation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and JSON-LD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Real-Time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Generation Tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building an EU Safety Gate (RAPEX) Product Recall Scraper With Python</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Sun, 16 Aug 2026 19:30:08 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/building-an-eu-safety-gate-rapex-product-recall-scraper-with-python-2m26</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/building-an-eu-safety-gate-rapex-product-recall-scraper-with-python-2m26</guid>
      <description>&lt;p&gt;Every week the EU publishes the product recalls that keep dangerous goods off the shelves — the Safety Gate (RAPEX) portal. It's open data, but it's buried behind a JavaScript SPA, so most teams never build on it. I did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;The reference actor in this niche couldn't even be trialled: it carries a $5 minimum charge and our account had $4.69 of budget left. One dollar short, locked out. So instead of paying to watch someone else's implementation, I reverse-engineered the upstream directly — a strictly better outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  The source
&lt;/h2&gt;

&lt;p&gt;The EU Safety Gate portal exposes a &lt;strong&gt;pure JSON API&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /public/api/search&lt;/code&gt; — the search contract&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /public/api/notification/{id}?language=en&lt;/code&gt; — per-alert detail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No auth, no cookies, no browser. I captured the exact endpoints and payloads from the live SPA's network traffic, then ground-truthed the date semantics against real notifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smoke test
&lt;/h2&gt;

&lt;p&gt;Input &lt;code&gt;{category: "Electrical appliances and equipment", country: "DE", dateFrom: "2026-07-01"}&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SUCCEEDED, 17 items, 17/17 complete&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Every record has reference, productName, riskTypes, riskDescription&lt;/li&gt;
&lt;li&gt;Unique reference numbers, date window exactly &lt;code&gt;2026-07-01..2026-08-11&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run cost: &lt;strong&gt;$0.007&lt;/strong&gt; — seven-tenths of a cent for 17 recalls&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest bits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The API covers consumer products, vehicles, and some professional equipment — not medical devices or food&lt;/li&gt;
&lt;li&gt;Search is a POST contract, so you need the right payload shape; a bare GET returns nothing&lt;/li&gt;
&lt;li&gt;Risk descriptions are free text in the EU's own taxonomy — normalise carefully&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;👉 &lt;a href="https://apify.com/darknezz/safety-gate-scraper" rel="noopener noreferrer"&gt;&lt;strong&gt;EU Safety Gate (RAPEX) Product Recall Scraper on Apify Store&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;p&gt;While you're here, these might be worth a read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59"&gt;I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One API Call&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS Aggregation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and JSON-LD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Real-Time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Generation Tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Built a Water Utility Risk Intelligence Tool With Python and MCP</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Sun, 16 Aug 2026 19:30:07 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-water-utility-risk-intelligence-tool-with-python-and-mcp-1hj0</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-water-utility-risk-intelligence-tool-with-python-and-mcp-1hj0</guid>
      <description>&lt;p&gt;Water utilities fail slowly: aging pipes, drought pressure, unaffordable rates, crumbling infrastructure. The data to assess all of it is public — you just need to pull nine different government APIs and score what comes back. That's the tool I built.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;The niche's reference actor is MCP-standby-only: its batch runs emit &lt;strong&gt;zero dataset items&lt;/strong&gt;. It's an MCP server you call by hand — fine for a demo, useless for monitoring a portfolio of utilities. Our build fixes that: &lt;strong&gt;batch mode returns a full assessment item per utility&lt;/strong&gt;, and the engine is also exposed as eight fastmcp MCP tools for interactive use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoring engine
&lt;/h2&gt;

&lt;p&gt;Four transparent models, one composite score, five verdicts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Water Vulnerability&lt;/strong&gt; — contamination and source risk&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure&lt;/strong&gt; — pipe age and condition signals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drought/Climate&lt;/strong&gt; — precipitation and heat trends&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Affordability&lt;/strong&gt; — rate and income pressure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Composite weights: vulnerability 0.30, infrastructure 0.25, drought 0.25, affordability 0.20. Verdicts: CRITICAL ≥80, HIGH_RISK ≥60, ELEVATED ≥40, MANAGEABLE ≥20, LOW_RISK &amp;lt;20. Every signal that moved a score is returned in the output — no black-box scoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nine keyless upstreams
&lt;/h2&gt;

&lt;p&gt;USGS earthquakes, FEMA disaster declarations (v2 — the v1 endpoint 404s), NOAA weather alerts (custom UA required), Federal Register water-scoped notices, CFPB complaints (default UA only — a custom UA gets 403, a lovely asymmetry), BLS unemployment + CPI, World Bank precipitation/GINI indicators, and Open-Meteo geocoding. OpenAQ v3 needs a key, so it's an optional input that degrades gracefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smoke test
&lt;/h2&gt;

&lt;p&gt;Local engine: NYC → &lt;strong&gt;26 MANAGEABLE&lt;/strong&gt;, Flint → &lt;strong&gt;21 MANAGEABLE&lt;/strong&gt;, Sacramento → &lt;strong&gt;26 MANAGEABLE&lt;/strong&gt; with an aging-infrastructure signal. Cloud: NYC full assessment → composite 26 with all four dimensions populated; Phoenix drought → &lt;strong&gt;LOW_RISK 8&lt;/strong&gt; with a real NOAA heat signal. The MCP server boots via fastmcp, lists all eight tools, and executes end-to-end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest bits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It's a screening tool, not an engineering audit — a composite score won't tell you which valve is rusting&lt;/li&gt;
&lt;li&gt;OpenAQ is keyed and optional; the other eight sources are keyless by design&lt;/li&gt;
&lt;li&gt;Some upstreams rate-limit aggressively; the batch mode paces requests&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;👉 &lt;a href="https://apify.com/darknezz/water-utility-risk-intelligence" rel="noopener noreferrer"&gt;&lt;strong&gt;Water Utility Risk Intelligence on Apify Store&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;p&gt;While you're here, these might be worth a read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59"&gt;I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One API Call&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS Aggregation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and JSON-LD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Real-Time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Generation Tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>mcp</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building an Aviation Hub API: Airports, Airlines, Live Flights &amp; Weather From Six Keyless Sources</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Sun, 16 Aug 2026 19:29:56 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/building-an-aviation-hub-api-airports-airlines-live-flights-weather-from-six-keyless-sources-58j9</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/building-an-aviation-hub-api-airports-airlines-live-flights-weather-from-six-keyless-sources-58j9</guid>
      <description>&lt;p&gt;The aviation niche looks impenetrable at first: proprietary flight feeds, licensing fees, and a wall of "contact sales" forms. But most of the data people actually pay for is already public — it's just scattered across six or seven government and community sources that nobody bothered to stitch together.&lt;/p&gt;

&lt;p&gt;So I built an aviation hub actor that does exactly that: one input, six keyless upstreams, six output modes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;The reference actor in this niche did the same job, but its &lt;code&gt;live&lt;/code&gt; mode returned zero rows in our trial — its single ADS-B feed was empty at the time. One feed, no fallback, no resilience. Ours ships &lt;strong&gt;six modes&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Airports&lt;/strong&gt; — 85K+ entries from OurAirports (public domain), including runways&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airlines&lt;/strong&gt; — the OpenFlights registry with country and callsign data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routes&lt;/strong&gt; — geocoded endpoints plus great-circle distance in kilometres&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live&lt;/strong&gt; — adsb.lol community ADS-B &lt;strong&gt;with an OpenSky anonymous fallback&lt;/strong&gt;, so live never returns empty when either feed has coverage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weather&lt;/strong&gt; — NOAA METAR JSON from aviationweather.gov&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Digest&lt;/strong&gt; — a market summary: busiest airports, biggest airlines, most common aircraft types&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reverse-engineering
&lt;/h2&gt;

&lt;p&gt;The trickiest part was the route mode: OpenFlights gives you source/destination pairs, but not coordinates. I geocode both ends and compute great-circle distance — one formula, no API key, works for every pair in the registry.&lt;/p&gt;

&lt;p&gt;The live mode needed the fallback architecture: try adsb.lol first, and if the feed is thin or empty, flip to OpenSky's anonymous endpoint. The reference's single-feed design was its weakness; the fallback is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smoke test
&lt;/h2&gt;

&lt;p&gt;Six modes, six cloud runs, all non-zero:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;airports&lt;/strong&gt; — Romanian airports (Bacău, Belfast shapes correct)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;airlines&lt;/strong&gt; — Tarom returned with country + callsign&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;routes&lt;/strong&gt; — OTP→LHR, American Airlines at 5,539.8 km great-circle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;live&lt;/strong&gt; — BAW416 (G-DBCA, A319) tracked near EGLL via adsb.lol&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;weather&lt;/strong&gt; — LROP METAR, VFR conditions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;digest&lt;/strong&gt; — 85,892 airports, matching the reference digest total exactly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest bits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live coverage depends on community ADS-B volunteers; the OpenSky fallback helps but isn't a paid-quality feed&lt;/li&gt;
&lt;li&gt;METAR is aviation weather only — no TAF long-range forecasts in this mode&lt;/li&gt;
&lt;li&gt;Route distances are great-circle, not flight-path&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;👉 &lt;a href="https://apify.com/darknezz/aviation-hub" rel="noopener noreferrer"&gt;&lt;strong&gt;Aviation Hub on Apify Store&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;p&gt;While you're here, these might be worth a read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59"&gt;I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One API Call&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS Aggregation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and JSON-LD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Real-Time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Generation Tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>aviation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Stopped Scraping Business Directories and Built an MCP Server on Official Registry Data</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Thu, 13 Aug 2026 17:05:25 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-registry-data-4caf</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-registry-data-4caf</guid>
      <description>&lt;h1&gt;
  
  
  I Stopped Scraping Business Directories and Built an MCP Server on Official Registry Data
&lt;/h1&gt;

&lt;p&gt;For a long time I built B2B lead-generation tools the way everyone does: scrape a directory, fight the anti-bot, re-scrape when the layout changes, pray the data is current. Then I found out the Romanian government publishes the &lt;em&gt;entire&lt;/em&gt; company registry as open data — 4.2 million firms, refreshed monthly, with legal status, activity codes and directors. No scraping required.&lt;/p&gt;

&lt;p&gt;This post is the story of that switch: how I turned an official open-data snapshot into a self-hosted MCP server with three tools that actually do work for an AI agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;If you want to build a list of Romanian companies — say, every SRL in Cluj with a CAEN code for software services — you have three options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scrape a directory site.&lt;/strong&gt; Fragile, slow, legally grey, and almost always stale. Directories are themselves scraped from the registry, so you're scraping a copy of a copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buy a database.&lt;/strong&gt; Expensive, opaque provenance, and you still have to build the pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the official source.&lt;/strong&gt; The ONRC (Romania's Trade Register) publishes a full snapshot of every registered company on the national open-data portal, data.gov.ro, which runs CKAN.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Option three is the one nobody seems to talk about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why official registry data beats scraping
&lt;/h2&gt;

&lt;p&gt;The ONRC open-data programme publishes monthly CSV snapshots:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;firme&lt;/strong&gt; — 4.2M companies: name, CUI (tax ID), registration code, legal form, address, website&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;reprezentanti legali&lt;/strong&gt; — legal representatives (directors) per company&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;caen autorizat&lt;/strong&gt; — CAEN activity codes per company&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stare firma&lt;/strong&gt; — company status history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nomenclatoare&lt;/strong&gt; — the &lt;em&gt;decode tables&lt;/em&gt;: status codes and CAEN activity names&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole registry, structured, and licensed for reuse. No login, no API key, no rate limit, no anti-bot. The fragility is gone: the government keeps the source fresh, and I just re-download the monthly snapshot.&lt;/p&gt;

&lt;p&gt;The catch is that "open data" is not "clean data". Those CSVs are enormous (the firms file alone is ~690 MB), use &lt;code&gt;^&lt;/code&gt; as a delimiter, carry a BOM, encode Romanian diacritics, and store statuses and activities as &lt;strong&gt;codes&lt;/strong&gt; that mean nothing without the nomenclator tables. That's the real engineering work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline: CKAN → SQLite → MCP
&lt;/h2&gt;

&lt;p&gt;The pattern I landed on works for any CKAN-backed government portal:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find the dataset via the CKAN API.&lt;/strong&gt; &lt;code&gt;GET {portal}/api/3/action/package_search&lt;/code&gt; finds the ONRC organization; &lt;code&gt;package_show&lt;/code&gt; returns the resource file URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect the schema cheaply.&lt;/strong&gt; A single HTTP range request (&lt;code&gt;curl -r 0-1200&lt;/code&gt;) reads the CSV header before committing to a download.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stream-load into SQLite.&lt;/strong&gt; Never read a multi-hundred-MB CSV into memory. &lt;code&gt;csv.reader&lt;/code&gt; + &lt;code&gt;executemany&lt;/code&gt; in batches of 5,000 rows, with &lt;code&gt;errors="replace"&lt;/code&gt; for diacritics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load the nomenclators&lt;/strong&gt; into their own tables, then &lt;code&gt;LEFT JOIN&lt;/code&gt; at query time to decode status and CAEN codes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expose as MCP tools&lt;/strong&gt; that open the database read-only.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole thing lives in a small Python package — a loader script, a nomenclator loader, and a FastMCP server. The MCP server (Streamable HTTP) runs on a homelab box and exposes three tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three tools
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;lookup_business&lt;/code&gt; — the registry search
&lt;/h3&gt;

&lt;p&gt;Search by name or CUI (tax ID). Digits hit the exact CUI index; anything else is a case-insensitive &lt;code&gt;LIKE&lt;/code&gt; on the name. Each result is enriched with decoded CAEN activities, directors, and status — the nomenclator joins are what make it useful. A real call:&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="nf"&gt;lookup_business&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dedeman&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dedeman"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"companyName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEDEMAN SRL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cui"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2816464"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"registrationCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"J1992002621040"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"registrationDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"05/11/1992"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"legalForm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SRL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"euid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ROONRC.J1992002621040"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Municipiul Bacău, Bacău, Str. ALEXEI TOLSTOI, 8, 600093"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"county"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bacău"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"website"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"www.dedeman.ro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"caenActivities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0125"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"activity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cultivarea altor pomi fructiferi, a arbuștilor fructiferi, căpșunilor și a nuciferelor"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1610"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"activity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tăierea și rindeluirea lemnului"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"directors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"PAVAL I. DRAGOS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BRINZEA S. STEFAN"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1048"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"funcțiune"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"onrc"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is decoded: &lt;code&gt;1048&lt;/code&gt; is "funcțiune" (active), the CAEN codes come back as readable activities. An agent can ask "what does this company actually do?" and get a straight answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;extract_contacts&lt;/code&gt; — find the humans
&lt;/h3&gt;

&lt;p&gt;Once you know a company exists, you need the contact points. This tool crawls the company website (bounded to a few pages, prioritising contact/about pages) and extracts emails, phone numbers and social profiles. It filters aggressively — no image files, no &lt;code&gt;example.com&lt;/code&gt; placeholders, no &lt;code&gt;noreply@&lt;/code&gt; — and matches emails against the site's own domain to cut the noise:&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="nf"&gt;extract_contacts&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://www.bitdefender.ro&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.bitdefender.ro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bitdefender.ro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pagesCrawled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"emails"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"phones"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"facebook"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.facebook.com/bitdefender"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"twitter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://twitter.com/bitdefender"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instagram"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.instagram.com/bitdefender"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"linkedin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.linkedin.com/company/bitdefender"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"youtube"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.youtube.com/c/Bitdefender"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"socialLinks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://www.facebook.com/bitdefender"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://www.twitter.com/bitdefender"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://www.instagram.com/bitdefender"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://www.linkedin.com/company/bitdefender"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://www.youtube.com/c/Bitdefender"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Honest limitations: corporate homepages often carry no public email (hence &lt;code&gt;emails: null&lt;/code&gt; here), and obfuscated emails (Cloudflare's &lt;code&gt;data-cfemail&lt;/code&gt;, &lt;code&gt;name [at] domain [dot] com&lt;/code&gt;) need decoders. The tool handles both, but you learn to expect gaps on big corporate sites — the SMB sites are where the gold is.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;lookup_domain&lt;/code&gt; — verify before you call
&lt;/h3&gt;

&lt;p&gt;Email validation is a lead-gen step most people skip. This tool wraps WHOIS + DNS + SPF/DMARC so an agent can check a domain before adding it to a list — is it registered, who owns it, does it even have mail?&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="nf"&gt;lookup_domain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dedeman.ro&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dedeman.ro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"whois"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"registrar"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ICI - Registrar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"creationDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2001-04-23"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"nameServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ns1.dedeman.ro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ns2.orange.ro"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"52.16.150.45"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"MX"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"5 mx.dedeman.ro."&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"security"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hasSPF"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"spf"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v=spf1 mx ip4:91.216.225.16/32 ... -all"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hasDMARC"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single call tells you the domain is 24 years old, points at a real mail server, and has both SPF and DMARC — a company that takes email seriously. For the ones that fail, you've just saved a bounced email.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The decode tables are the real work.&lt;/strong&gt; The registry itself is just codes; the nomenclators turn &lt;code&gt;1048&lt;/code&gt; into "funcțiune". Budget real time for them — they're in a &lt;em&gt;separate&lt;/em&gt; CKAN dataset and it's easy to miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The join key isn't the public ID.&lt;/strong&gt; ONRC joins internally on the registration code (&lt;code&gt;COD_INMATRICULARE&lt;/code&gt;) while everyone searches by CUI (tax ID). Get that mapping right or every lookup "silently fails".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSVs from governments are hostile by accident.&lt;/strong&gt; Caret delimiters, BOMs, &lt;code&gt;.CSV&lt;/code&gt;-suffixed resource names, &lt;code&gt;errors="replace"&lt;/code&gt; for diacritics. A range request to read the header first saves hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly snapshots beat real-time scraping.&lt;/strong&gt; The data is a month stale at worst and &lt;em&gt;complete&lt;/em&gt; — no pagination war, no missing pages, no layout break. If a registry publishes open data, use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents need tools, not scrapers.&lt;/strong&gt; An LLM can't responsibly scrape 4M companies, but it can absolutely call &lt;code&gt;lookup_business("Dedeman")&lt;/code&gt; and reason about the result. MCP is the right seam between "registry data" and "agent capability".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;I'm packaging the same pipeline as an Apify actor so it can run on demand (Ro Business Data MCP, coming soon to &lt;a href="https://apify.com/darknezz" rel="noopener noreferrer"&gt;my Apify account&lt;/a&gt;) — but the whole pattern is portable to any CKAN portal. Poland, France, Germany and most EU states publish similar company registries. If your lead-gen data is scraped from a directory, check whether your government already publishes the real thing. It probably does.&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;p&gt;While you're here, these might be worth a read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59"&gt;I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One API Call&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS Aggregation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and JSON-LD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Real-Time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO Audits &amp;amp; AI Training)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Generation Tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content (Not Just Summaries)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>mcp</category>
      <category>scraping</category>
    </item>
    <item>
      <title>I Built a Canada Product Recalls &amp; Safety Alerts Scraper That Reads Open Government Data</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Thu, 13 Aug 2026 14:45:52 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data
&lt;/h1&gt;

&lt;p&gt;Every few weeks another consumer product gets recalled — a battery that overheats, a fan that catches fire, a vehicle part that fails. For compliance teams, importers and retailers, staying on top of those announcements is a job in itself. Canada publishes every recall on a government website, but the site has no JSON API and no RSS feed. It is plain HTML, served by Drupal.&lt;/p&gt;

&lt;p&gt;So I built an Apify actor that reads it directly — no API key, no login, no proxy budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  The niche, and why build our own
&lt;/h2&gt;

&lt;p&gt;The product-recall scraper niche on Apify is small but telling. The reference implementation that dominates it is &lt;code&gt;dromb/canada-recalls-safety-alerts&lt;/code&gt; — a reliable, well-shaped actor that turns Health Canada's recall database into clean JSON. When I looked at the niche, that actor &lt;em&gt;was&lt;/em&gt; the reliability benchmark. It worked, it had a sensible schema, and it had traction.&lt;/p&gt;

&lt;p&gt;Our house rule is simple: when a niche is worth entering, build our own implementation rather than depend on someone else's actor — same source, same schema, same promises, our code. That way the data pipeline we sell isn't hostage to another developer's maintenance schedule. This post is the story of that build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data source
&lt;/h2&gt;

&lt;p&gt;The database lives at &lt;strong&gt;recalls-rappels.canada.ca&lt;/strong&gt; (part of the open.canada.ca family) — Health Canada's official recall, alert and safety advisory database covering consumer products, vehicles, food, drugs and health products. It's Drupal, which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No public JSON API. The structured data exists, but you have to parse it out of HTML.&lt;/li&gt;
&lt;li&gt;A full-text search index at &lt;code&gt;/en/search?search_api_fulltext=&amp;lt;keyword&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Detail pages at &lt;code&gt;/en/alert-recall/&amp;lt;slug&amp;gt;&lt;/code&gt; with structured fields (Drupal machine-names).&lt;/li&gt;
&lt;li&gt;A polite 418 for plain script clients — it wants a browser User-Agent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole design flows from those four facts: &lt;strong&gt;one source, one job&lt;/strong&gt;, HTML scraping, and a browser User-Agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  First contact: the 418
&lt;/h2&gt;

&lt;p&gt;The very first request to the search page came back &lt;code&gt;418 I'm a teapot&lt;/code&gt;. Not a 403, not a captcha — a teapot. The site is configured to serve plain-scripted clients nothing. The fix is one line of honesty:&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="n"&gt;HTTP_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="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36&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;Accept&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;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8&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;Accept-Language&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;en-CA,en;q=0.9,fr-CA;q=0.8,fr;q=0.7&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;p&gt;The search page also 301-redirects to itself, so redirects must be followed. Once you look like a browser and follow redirects, the site is completely open — no login anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Run a full-text search against the Drupal search index (&lt;code&gt;/en/search?search_api_fulltext=&amp;lt;keyword&amp;gt;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Page through the results (&lt;code&gt;page=,0,N&lt;/code&gt; pagination).&lt;/li&gt;
&lt;li&gt;Visit each recall detail page (&lt;code&gt;/en/alert-recall/&amp;lt;slug&amp;gt;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Parse the structured Drupal fields into clean records.&lt;/li&gt;
&lt;li&gt;Apply the category and date post-filters, cap at &lt;code&gt;maxItems&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Archived recalls are excluded — the search surface excludes them, and the reference actor behaves the same way, so every row is an active recall.&lt;/p&gt;

&lt;p&gt;Detail fetches are paced (&lt;code&gt;DETAIL_DELAY_SECONDS = 0.25&lt;/code&gt;) with exponential backoff on 4xx/5xx — it's a government site, be polite.&lt;/p&gt;

&lt;h2&gt;
  
  
  The schema — reverse-engineered from the reference
&lt;/h2&gt;

&lt;p&gt;The hardest part wasn't scraping. It was matching the reference actor's schema &lt;strong&gt;exactly&lt;/strong&gt;, so that anyone using the community actor can switch to ours with zero changes. I pulled a 50-item sample of the reference's dataset and reverse-engineered the semantics field by field.&lt;/p&gt;

&lt;p&gt;The final record has 17 fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recallId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;82426&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Portable Clip-On Rechargeable Mini Fan recalled due to fire hazard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dateUpdated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recallClass"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Household items"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Fire hazard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"affectedProducts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Portable Clip-On Rechargeable Mini Fan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"companies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Consumer product safety"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hazard"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Fire hazard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"whatToDo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Consumers should immediately stop using the recalled product and return it to CTG Brands Inc. for a refund..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"archived"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://recalls-rappels.canada.ca/en/alert-recall/portable-clip-rechargeable-mini-fan-recalled-due-fire-hazard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dataSource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"open.canada.ca"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rawSourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"recall_record"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting semantics, all verified against the reference dataset:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;category&lt;/code&gt;&lt;/strong&gt; strips the top-level taxonomy prefix. The site's raw value is &lt;code&gt;Consumer products - Electronics&lt;/code&gt;; the reference emits just &lt;code&gt;Electronics&lt;/code&gt;. Multi-item categories join differently (&lt;code&gt;Light Truck &amp;amp; Van - SUV&lt;/code&gt; stays as-is because the top-level prefix doesn't apply).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;summary&lt;/code&gt; and &lt;code&gt;issue&lt;/code&gt;&lt;/strong&gt; always mirror each other — the last &lt;code&gt;-&lt;/code&gt; segment of the issue-type label.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;companies&lt;/code&gt;&lt;/strong&gt; is the issuing organization from publisher metadata (&lt;code&gt;Consumer product safety&lt;/code&gt;, &lt;code&gt;CFIA&lt;/code&gt;, &lt;code&gt;TC&lt;/code&gt;), not the manufacturer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;recallClass&lt;/code&gt;&lt;/strong&gt; is &lt;code&gt;Type I/II/III&lt;/code&gt; for health-product recalls, &lt;code&gt;Class 1/2/3&lt;/code&gt; for food recalls, else &lt;code&gt;null&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hazard&lt;/code&gt;&lt;/strong&gt; is always &lt;code&gt;null&lt;/code&gt; in practice — the field exists in the reference schema but the live pages don't populate it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;archived&lt;/code&gt;&lt;/strong&gt; is always &lt;code&gt;false&lt;/code&gt; and &lt;strong&gt;&lt;code&gt;status&lt;/code&gt;&lt;/strong&gt; always &lt;code&gt;active&lt;/code&gt;, because the search surface excludes archived recalls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Getting this right was a weekend of diffing: parse a page, compare with the reference row for the same recall, adjust, repeat across consumer/vehicle/drug/CFIA recall types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filters
&lt;/h2&gt;

&lt;p&gt;All input fields are optional — &lt;code&gt;{}&lt;/code&gt; returns the most recent active recalls:&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;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;keyword&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Full-text search term (&lt;code&gt;battery&lt;/code&gt;, &lt;code&gt;croissant&lt;/code&gt;, &lt;code&gt;vehicle&lt;/code&gt;). Empty = browse latest recalls.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;category&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Case-insensitive partial match against the parsed category (&lt;code&gt;Electronics&lt;/code&gt;, &lt;code&gt;Household items&lt;/code&gt;, &lt;code&gt;Vehicles&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dateFrom&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Only recalls last updated on or after this date (&lt;code&gt;YYYY-MM-DD&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxItems&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;Maximum records to extract (default 50, max 500).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;keyword&lt;/code&gt; filter runs server-side through Drupal's search index; &lt;code&gt;category&lt;/code&gt; and &lt;code&gt;dateFrom&lt;/code&gt; are post-filters on the parsed records. The &lt;code&gt;dateFrom&lt;/code&gt; filter is what makes an incremental feed possible: run it weekly with your last-run date, and you get only the new recalls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smoke test
&lt;/h2&gt;

&lt;p&gt;The cloud smoke run that proved the actor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keyword"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"battery"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Electronics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dateFrom"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-01"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;strong&gt;SUCCEEDED, 4 well-formed Electronics items&lt;/strong&gt;, all past the date filter, all schema keys exact with native types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;recallId&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;82213&lt;/td&gt;
&lt;td&gt;Super Off-Road power bank recalled due to fire hazard&lt;/td&gt;
&lt;td&gt;2026-08-12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;82139&lt;/td&gt;
&lt;td&gt;Wyze Cam v3 recalled — battery may overheat&lt;/td&gt;
&lt;td&gt;2026-08-06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;82210&lt;/td&gt;
&lt;td&gt;Arizer vaporizer battery recalled&lt;/td&gt;
&lt;td&gt;2026-08-08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;82150&lt;/td&gt;
&lt;td&gt;Steambow AR-6 recalled — arrow rest may break&lt;/td&gt;
&lt;td&gt;2026-07-10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every field parses, every type is native (&lt;code&gt;recallId&lt;/code&gt; is an int, &lt;code&gt;archived&lt;/code&gt; a bool — not strings), and the reference comparison is clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls worth naming
&lt;/h2&gt;

&lt;p&gt;Three Apify-specific traps bit during this build:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Input schema requires &lt;code&gt;editor&lt;/code&gt; on every property.&lt;/strong&gt; Apify's input schema validation rejects any field without an &lt;code&gt;editor&lt;/code&gt; — add it to all four inputs or the push fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;actor.json&lt;/code&gt; dataset fields are string-only, and enforced on push.&lt;/strong&gt; Declaring &lt;code&gt;storages.dataset.fields&lt;/code&gt; with &lt;code&gt;type: "integer"&lt;/code&gt; fails at build with &lt;code&gt;Schema validation failed&lt;/code&gt;. The fix is to drop the &lt;code&gt;storages&lt;/code&gt; block entirely — native types are then stored, exactly like the reference actor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;run-sync-get-dataset-items?timeout=N&lt;/code&gt; sets the RUN's timeout, not the API wait.&lt;/strong&gt; A 20-page crawl needs &lt;code&gt;N &amp;gt;= 480&lt;/code&gt;. And the sync socket read can time out client-side while the run keeps going — poll &lt;code&gt;/v2/actor-runs/{id}&lt;/code&gt; and fetch &lt;code&gt;defaultDatasetId&lt;/code&gt; when that happens.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The actor is &lt;code&gt;darknezz/canada-recalls-safety-alerts&lt;/code&gt;. From the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/darknezz~canada-recalls-safety-alerts/run-sync-get-dataset-items?token=YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"keyword":"battery","category":"Electronics","dateFrom":"2026-06-01"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or with the Python SDK:&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;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;run&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;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;darknezz~canada-recalls-safety-alerts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_input&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;keyword&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;battery&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;category&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;Electronics&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;dateFrom&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;2026-06-01&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&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;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&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;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recallId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;item&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compliance monitoring&lt;/strong&gt; — weekly cron on a category (electronics, toys, food) and alert on new recalls via Telegram or webhook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Importer / retailer screening&lt;/strong&gt; — check a product line against active recalls before ordering or listing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety trend analysis&lt;/strong&gt; — pull all recalls in a category over a window and analyse hazard mix, issuing agency and product types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data enrichment&lt;/strong&gt; — join recall IDs against internal product SKUs for a live exposure report.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTML scraping&lt;/strong&gt; — the parser targets stable Drupal machine-names; a site redesign would need a rebuild.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;English only&lt;/strong&gt; — records come from the &lt;code&gt;/en/&lt;/code&gt; surface; French (&lt;code&gt;/fr/&lt;/code&gt;) records are not covered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Archived recalls excluded&lt;/strong&gt; — matching the reference actor; historical records are not returned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Politeness pacing&lt;/strong&gt; — very large result sets (500+ items) take proportionally longer by design.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this pattern wins
&lt;/h2&gt;

&lt;p&gt;One source, one job. The Canadian government publishes this data for public consumption; the actor just makes it machine-readable at a predictable price. No third-party aggregator, no licensing ambiguity, no API key to rotate — and because the schema matches the established reference exactly, the actor slots into pipelines that already consume recall data.&lt;/p&gt;

&lt;p&gt;The teapot was the only wall. Everything else was parsing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;👉 &lt;a href="https://apify.com/darknezz/canada-recalls-safety-alerts" rel="noopener noreferrer"&gt;&lt;strong&gt;Canada Product Recalls &amp;amp; Safety Alerts on Apify Store&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One API Call&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS Aggregation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and JSON-LD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Real-Time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO Audits &amp;amp; AI Training)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Generation Tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content (Not Just Summaries)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Tue, 11 Aug 2026 22:15:10 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login
&lt;/h1&gt;

&lt;p&gt;When I shipped my &lt;a href="https://apify.com/darknezz/telegram-channel-scraper" rel="noopener noreferrer"&gt;Telegram Channel Scraper&lt;/a&gt;, the same question kept coming back: &lt;em&gt;"can you get the member count too?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Turns out you can — without a Telegram account, without API keys, without a phone number, and without a proxy budget. So I built &lt;strong&gt;Telegram Members Scraper — Group &amp;amp; Channel Stats&lt;/strong&gt;, and it is free to use on the Apify Store.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Telegram hosts some of the largest communities on the internet — public groups and channels show their size right on their public info page. Community managers, growth teams and researchers all want the same thing: &lt;em&gt;how big is this community, and is it growing?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The official Telegram API needs &lt;code&gt;api_id&lt;/code&gt; / &lt;code&gt;api_hash&lt;/code&gt; plus a logged-in account. Telegram deliberately hides member rosters from the unauthenticated web. The result: an oddly empty niche for a very common question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The extrapolation pattern: reuse the winner
&lt;/h2&gt;

&lt;p&gt;Rather than write a fresh scraper, I &lt;strong&gt;copied my proven telegram-channel-scraper codebase and adapted it&lt;/strong&gt; — the extrapolation play. Same &lt;code&gt;httpx&lt;/code&gt; + &lt;code&gt;BeautifulSoup&lt;/code&gt; stack, same no-login design philosophy, same one-item-per-chat output shape. The hardest part wasn't the code; it was being honest about what the public web surface can and cannot give you.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the public web actually exposes (the honest finding)
&lt;/h3&gt;

&lt;p&gt;Telegram does &lt;strong&gt;not&lt;/strong&gt; expose member lists on its public pages. A group's &lt;code&gt;/s/&lt;/code&gt; URL redirects to a bare info card — title and member count only. Channels expose a &lt;code&gt;/s/&lt;/code&gt; post preview, but only the channel itself as author.&lt;/p&gt;

&lt;p&gt;So the honest scope of a no-login Telegram scraper is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Groups&lt;/strong&gt; → title, description, &lt;strong&gt;member count&lt;/strong&gt;, &lt;strong&gt;online count&lt;/strong&gt;, photo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Channels&lt;/strong&gt; → title, description, &lt;strong&gt;subscriber count&lt;/strong&gt;, photo, message preview availability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not per-user rosters. Anyone promising usernames/IDs/join dates without auth is using a different (authenticated) surface. I documented that limitation right in the README instead of burying it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Telegram account needed&lt;/strong&gt; — reads the public web profile (&lt;code&gt;t.me/&amp;lt;chat&amp;gt;&lt;/code&gt;), nothing to authenticate or configure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groups and channels, one input&lt;/strong&gt; — detects the chat type automatically and returns the right size metric (&lt;code&gt;members&lt;/code&gt; for groups, &lt;code&gt;subscribers&lt;/code&gt; for channels)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One dataset item per chat&lt;/strong&gt; — clean JSON, ready for pipelines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional preview probe&lt;/strong&gt; — for channels, collects visible author accounts from the &lt;code&gt;/s/&lt;/code&gt; post preview&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example output
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chatUsername"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DatascienceChats"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chatType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chatTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Data Science,ML &amp;amp; AI Nugget Chats"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chatDescription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Discussion community for DS, ML, IOT, AI, DEEP LEARNING and much more…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"memberCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;13871&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"onlineCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;380&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subscriberCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"photoUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cdn4.telegram-cdn.org/file/…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"profileUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://t.me/DatascienceChats"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messagePreviewAvailable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"previewAuthors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scrapedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-10T12:00:00+00:00"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Smoke test (before shipping)
&lt;/h2&gt;

&lt;p&gt;Three chats, all public, no login:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Chat&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DatascienceChats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;group&lt;/td&gt;
&lt;td&gt;~13.9K members, ~340 online&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;durov&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;channel&lt;/td&gt;
&lt;td&gt;~11.2M subscribers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python_community&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;group&lt;/td&gt;
&lt;td&gt;51 members&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;3/3 extracted non-zero, titles and counts verified against the live pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Input
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"@DatascienceChats"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://t.me/durov"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checkPreview"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;array&lt;/td&gt;
&lt;td&gt;Public group/channel usernames (&lt;code&gt;@name&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;) or links (&lt;code&gt;t.me/name&lt;/code&gt;, &lt;code&gt;t.me/s/name&lt;/code&gt;) — &lt;strong&gt;required&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;checkPreview&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;td&gt;Probe the &lt;code&gt;/s/&lt;/code&gt; message preview for channels and collect visible authors (default &lt;code&gt;true&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Use cases
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Growth tracking&lt;/strong&gt; — chart member/subscriber counts of competitor or topic communities over time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community segmentation&lt;/strong&gt; — classify public groups vs broadcast channels and their relative sizes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Market research&lt;/strong&gt; — size the Telegram audience for a niche before launching a channel or product&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead-list hygiene&lt;/strong&gt; — confirm a group exists, is public, and roughly how many people it reaches before outreach&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI / datasets&lt;/strong&gt; — build metadata corpora of public Telegram communities for classification and analytics&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The tool is live and &lt;strong&gt;free&lt;/strong&gt; on the Apify Store: &lt;a href="https://apify.com/darknezz/telegram-members-scraper" rel="noopener noreferrer"&gt;Telegram Members Scraper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pass a few chat usernames, get back their public profile and membership size as JSON. No account, no API keys, no phone number, no proxy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations (stated plainly)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Public chats only&lt;/strong&gt; — private groups/channels are not accessible without authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No member roster&lt;/strong&gt; — Telegram hides member lists from the web; you get the membership &lt;em&gt;size&lt;/em&gt; and chat profile, not per-user rows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counts are as displayed&lt;/strong&gt; — Telegram rounds large numbers on the public page (e.g. &lt;code&gt;13.9K&lt;/code&gt; → &lt;code&gt;13900&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need an actual member roster, that requires the authenticated Telegram API — a different architecture, legal posture and price point. This tool deliberately stays on the no-login web surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;p&gt;While you're here, these might be worth a read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Gene...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Rea...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and ...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>telegram</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a WHOIS &amp; DNS Lookup Tool: Domain Intelligence in One...</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Thu, 09 Jul 2026 16:23:49 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b</guid>
      <description>&lt;h1&gt;
  
  
  Why Domain Intelligence Matters
&lt;/h1&gt;

&lt;p&gt;Whether you're doing security research, monitoring your own infrastructure, or evaluating a potential acquisition target, domain data tells a story. WHOIS records reveal ownership history. DNS records expose infrastructure topology. SPF/DMARC configs show email security posture.&lt;/p&gt;

&lt;p&gt;The problem? Gathering all this data usually means jumping between multiple tools — &lt;code&gt;whois&lt;/code&gt; CLI, &lt;code&gt;dig&lt;/code&gt;, &lt;code&gt;nslookup&lt;/code&gt;, online MX lookup tools... It's tedious and doesn't scale.&lt;/p&gt;

&lt;p&gt;So I built a &lt;strong&gt;&lt;a href="https://apify.com/darknezz/whois-dns-lookup" rel="noopener noreferrer"&gt;WHOIS &amp;amp; DNS Lookup Tool on Apify&lt;/a&gt;&lt;/strong&gt; that returns everything in a single API call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;Give it a domain (or a list of domains), and you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WHOIS Data&lt;/strong&gt;: Registrar, creation date, expiration date, registrant info, status flags&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS Records&lt;/strong&gt;: A, AAAA, MX, NS, TXT, CNAME, SOA, CAA, SRV&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email Security&lt;/strong&gt;: SPF and DMARC record detection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP Resolution&lt;/strong&gt;: Quick A-record IP lookup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All in clean, structured JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Output
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"whois"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"registrar"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RESERVED-Internet Assigned Numbers Authority"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"creationDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1995-08-14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"expirationDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-13"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"nameServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"a.iana-servers.net"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b.iana-servers.net"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"93.184.216.34"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"MX"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0 ."&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"TXT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"v=spf1 -all"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SOA"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ns.icann.org. noc.icann.org. 2025081403 7200 3600 1209600 3600"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"security"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"spf"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v=spf1 -all"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dmarc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v=DMARC1;p=reject;sp=reject;adkim=s;aspf=s"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resolvedIP"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"93.184.216.34"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The tool uses two battle-tested Python libraries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pypi.org/project/python-whois/" rel="noopener noreferrer"&gt;python-whois&lt;/a&gt;&lt;/strong&gt; for WHOIS registration data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pypi.org/project/dnspython/" rel="noopener noreferrer"&gt;dnspython&lt;/a&gt;&lt;/strong&gt; for DNS record resolution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It wraps them in the &lt;a href="https://docs.apify.com/sdk/python/" rel="noopener noreferrer"&gt;Apify SDK&lt;/a&gt; for cloud execution, automatic dataset storage, and pay-per-event pricing.&lt;/p&gt;

&lt;h3&gt;
  
  
  SPF/DMARC Detection
&lt;/h3&gt;

&lt;p&gt;Email security is often overlooked in domain analysis. The tool checks TXT records for SPF policies and queries &lt;code&gt;_dmarc.{domain}&lt;/code&gt; for DMARC configurations — two critical indicators of email deliverability posture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;What You Get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security Audit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full DNS topology + WHOIS ownership trail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Domain Monitoring&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Track expiry dates and DNS changes over time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Email Deliverability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SPF/DMARC validation for sender domains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Competitive Intel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Infrastructure discovery via DNS records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SEO Analysis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Name server patterns, CDN detection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;You can run the tool directly on Apify:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://apify.com/darknezz/whois-dns-lookup" rel="noopener noreferrer"&gt;WHOIS &amp;amp; DNS Lookup Tool&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The full source code is available on GitHub:&lt;/p&gt;

&lt;p&gt;📦 &lt;strong&gt;&lt;a href="https://github.com/darksider4all/multi-tool-content-api" rel="noopener noreferrer"&gt;multi-tool-content-api&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;$0.01 per domain looked up. No subscriptions, no minimums — pure pay-per-use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This is tool #10 in a growing portfolio of content and data extraction APIs. Each one solves a specific data gathering need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;RSS Feed Aggregator&lt;/li&gt;
&lt;li&gt;llms.txt Generator&lt;/li&gt;
&lt;li&gt;RO Business Scraper&lt;/li&gt;
&lt;li&gt;Sitemap Content Extractor&lt;/li&gt;
&lt;li&gt;Product Hunt Scraper&lt;/li&gt;
&lt;li&gt;Sentiment Analyzer&lt;/li&gt;
&lt;li&gt;Property Listing Scraper&lt;/li&gt;
&lt;li&gt;Press Release Monitor&lt;/li&gt;
&lt;li&gt;AI Web Crawler&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WHOIS &amp;amp; DNS Lookup Tool&lt;/strong&gt; ← this one&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All available individually on Apify, or unified through a single Apify endpoint.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Python, &lt;a href="https://docs.apify.com/sdk/python/" rel="noopener noreferrer"&gt;Apify SDK&lt;/a&gt;, and a passion for clean data APIs.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;p&gt;While you're here, these might be worth a read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content (...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Gene...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Rea...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and ...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>dns</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building an AI Web Crawler That Outputs LLM-Ready Content Chunks</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Wed, 08 Jul 2026 06:34:02 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg</guid>
      <description>&lt;h1&gt;
  
  
  Building an AI Web Crawler That Outputs LLM-Ready Content Chunks
&lt;/h1&gt;

&lt;p&gt;The biggest challenge in building RAG (Retrieval-Augmented Generation) pipelines isn't the vector database or the embeddings — it's getting clean, structured content from web pages in the first place. Raw HTML is noisy, full of navigation, ads, and boilerplate that pollutes your embeddings.&lt;/p&gt;

&lt;p&gt;I built an &lt;strong&gt;AI Web Crawler&lt;/strong&gt; that extracts clean, chunked content from any website — ready to feed directly into LLMs and RAG systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Web Content for AI
&lt;/h2&gt;

&lt;p&gt;When you feed raw HTML to an LLM, you waste tokens on navigation menus, cookie banners, and footer links. When you feed it poorly cleaned text, retrieval quality drops because the chunks don't align with semantic boundaries.&lt;/p&gt;

&lt;p&gt;The solution: a crawler that understands document structure and outputs content chunks designed for AI consumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Smart Content Extraction
&lt;/h3&gt;

&lt;p&gt;The crawler strips boilerplate (nav, footer, scripts, styles) and extracts only the main content — the actual article, documentation, or page text that matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Semantic Chunking
&lt;/h3&gt;

&lt;p&gt;Content is split into chunks that respect heading boundaries:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chunk_content&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;span class="n"&gt;headings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_chunk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;current&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;heading&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;headings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;section&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_section&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;span class="n"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_chunk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;chunks&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="nf"&gt;split_long&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_chunk&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;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Token Estimation
&lt;/h3&gt;

&lt;p&gt;Every chunk includes an estimated token count, so you know exactly how much context you're working with before sending to your LLM:&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="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;  &lt;span class="c1"&gt;# rough estimate
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Structured Data Extraction
&lt;/h3&gt;

&lt;p&gt;Beyond plain text, the crawler captures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Headings&lt;/strong&gt; — document structure/hierarchy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tables&lt;/strong&gt; — structured data preserved as markdown&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal/External Links&lt;/strong&gt; — relationship mapping&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON-LD&lt;/strong&gt; — structured data from schema.org&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenGraph&lt;/strong&gt; — social metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAG Pipelines&lt;/strong&gt;: Feed chunks directly to your vector database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge Base Building&lt;/strong&gt;: Crawl documentation sites systematically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Monitoring&lt;/strong&gt;: Track changes on competitor pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset Creation&lt;/strong&gt;: Build training data from web sources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Agent Tools&lt;/strong&gt;: Give your autonomous agents a web reader&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Apify Store&lt;/strong&gt;: &lt;a href="https://apify.com/darknezz/ai-web-crawler" rel="noopener noreferrer"&gt;AI Web Crawler&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/darksider4all/multi-tool-content-api" rel="noopener noreferrer"&gt;multi-tool-content-api&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Apify actor uses &lt;strong&gt;pay-per-event pricing&lt;/strong&gt; — you only pay for pages successfully crawled.&lt;/p&gt;

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

&lt;p&gt;The future of web scraping is AI-ready output. By chunking content semantically, estimating tokens, and preserving structure, we bridge the gap between raw web pages and LLM-ready knowledge.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Python, BeautifulSoup4, httpx, and the Apify platform.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;p&gt;While you're here, these might be worth a read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content (...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Gene...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Rea...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and ...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>llm</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Building a Real-Time Press Release Monitor with Python and RSS...</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Tue, 07 Jul 2026 06:09:17 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l</guid>
      <description>&lt;h1&gt;
  
  
  Building a Real-Time Press Release Monitor with Python and RSS Aggregation
&lt;/h1&gt;

&lt;p&gt;Staying on top of company announcements is hard. Press releases are scattered across PRWeb, Business Wire, GlobeNewswire, and dozens of smaller wires. Each has its own RSS feed, and none of them offer keyword filtering out of the box.&lt;/p&gt;

&lt;p&gt;I built a &lt;strong&gt;Press Release Monitor&lt;/strong&gt; that aggregates all major PR wires into a single, searchable stream with keyword filtering and full-text extraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;PR professionals, journalists, and analysts need to track company announcements as they happen. But the current workflow is manual: visit each PR wire site, search for your keywords, copy results. Slow and error-prone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Multi-Source RSS Aggregation
&lt;/h2&gt;

&lt;p&gt;The monitor pulls from multiple PR distribution feeds simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PRWeb&lt;/strong&gt; — broad press release distribution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google News PR Search&lt;/strong&gt; — catches releases picked up by news aggregators&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dow Jones MarketWatch&lt;/strong&gt; — financial market announcements
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;PR_FEEDS&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;prweb_all&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;https://www.prweb.com/rss2/daily.xml&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;google_news_pr&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;https://news.google.com/rss/search?q=press+release+announces&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;dowjones_markets&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;https://feeds.content.dowjones.io/public/rss/RSSMarketsMain&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;h2&gt;
  
  
  Keyword Filtering
&lt;/h2&gt;

&lt;p&gt;Once all releases are aggregated, the monitor applies keyword filters:&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;keywords&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;release&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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&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="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;matching&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;releases&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&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;kw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you track specific companies, products, or topics across all wires at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full-Text Extraction
&lt;/h2&gt;

&lt;p&gt;For deeper analysis, the monitor optionally visits each release page and extracts the full text using BeautifulSoup:&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="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;resp&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;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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;soup&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&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;style&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;nav&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;footer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="n"&gt;tag&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;span class="n"&gt;full_text&lt;/span&gt; &lt;span class="o"&gt;=&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="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Auto-Detected Company Names
&lt;/h2&gt;

&lt;p&gt;A smart parser extracts the issuing company from each title using common PR naming conventions:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_company&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&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;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;parts&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="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# "Company Name: Announces..."
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; announces &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; announces &lt;/span&gt;&lt;span class="sh"&gt;"&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;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;You can use the Press Release Monitor right now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Apify Store&lt;/strong&gt;: &lt;a href="https://apify.com/darknezz/press-release-monitor" rel="noopener noreferrer"&gt;Press Release Monitor&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/darksider4all/multi-tool-content-api" rel="noopener noreferrer"&gt;multi-tool-content-api&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Apify actor uses &lt;strong&gt;pay-per-event pricing&lt;/strong&gt; at $0.01 per press release extracted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Media Monitoring&lt;/strong&gt;: Track mentions of your company across all PR wires&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competitive Intelligence&lt;/strong&gt;: Monitor competitor announcements in real-time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Investment Research&lt;/strong&gt;: Watch for market-moving press releases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead Generation&lt;/strong&gt;: Find companies announcing funding, product launches, or expansion&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;By aggregating multiple RSS feeds with keyword filtering and full-text extraction, we turned scattered press releases into a single, searchable stream. The future of media monitoring is automated, real-time, and keyword-aware.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Python, feedparser, BeautifulSoup4, httpx, and the Apify platform.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;p&gt;While you're here, these might be worth a read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content (...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Gene...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Rea...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and ...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>automation</category>
      <category>news</category>
    </item>
    <item>
      <title>Building a Universal Property Listing Scraper with Python and ...</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Mon, 06 Jul 2026 06:15:02 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj</guid>
      <description>&lt;h1&gt;
  
  
  Building a Universal Property Listing Scraper with Python and JSON-LD
&lt;/h1&gt;

&lt;p&gt;Ever wanted to extract structured data from real estate listings across multiple sites without writing custom parsers for each one? I built a &lt;strong&gt;Property Listing Scraper&lt;/strong&gt; that works with any property website — from Zillow to Rightmove to Imobiliare — using a combination of JSON-LD, OpenGraph metadata, and smart HTML pattern matching.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Every real estate site structures its data differently. Zillow uses one schema, Rightmove uses another, and smaller sites like Imobiliare.ro have their own format entirely. Building separate scrapers for each is a maintenance nightmare.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Multi-Layer Extraction
&lt;/h2&gt;

&lt;p&gt;The scraper uses three extraction layers, falling back gracefully:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. JSON-LD (Structured Data)
&lt;/h3&gt;

&lt;p&gt;Many modern property sites embed structured data using Schema.org vocabulary. This is the gold standard — clean, machine-readable, and standardised.&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;script&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;find_all&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&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&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="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&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;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&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="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&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="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="c1"&gt;# Extract price, address, coordinates, images
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. OpenGraph Meta Tags
&lt;/h3&gt;

&lt;p&gt;When JSON-LD isn't available, we fall back to OpenGraph metadata that most sites provide for social sharing.&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="n"&gt;og_title&lt;/span&gt; &lt;span class="o"&gt;=&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;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;og: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;og_image&lt;/span&gt; &lt;span class="o"&gt;=&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;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;og:image&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;h3&gt;
  
  
  3. Regex Pattern Matching
&lt;/h3&gt;

&lt;p&gt;As a final fallback, we scan the page text for common price and property patterns:&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="n"&gt;price_match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;([$£€]\s*[\d,]+(?:\.\d+)?)&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="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;bed_match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(\d+)\s*(?:bed|bedroom|camera)&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Extracted Data Fields
&lt;/h2&gt;

&lt;p&gt;Each property listing yields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Price &amp;amp; Currency&lt;/strong&gt; — parsed from any format (USD, EUR, GBP, RON)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Property Type&lt;/strong&gt; — apartment, house, studio, land, office&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bedrooms &amp;amp; Bathrooms&lt;/strong&gt; — multi-language support (English, Italian, Romanian)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Area&lt;/strong&gt; — square metres detection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Address Components&lt;/strong&gt; — street, city, region, postal code, country&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coordinates&lt;/strong&gt; — latitude/longitude from geo data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images&lt;/strong&gt; — up to 20 high-quality images per listing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Search Page Support
&lt;/h2&gt;

&lt;p&gt;The scraper automatically detects whether a URL is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;An individual listing&lt;/strong&gt; — extracts data directly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A search results page&lt;/strong&gt; — discovers listing links, then scrapes each one concurrently (5 parallel requests)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;You can use the scraper right now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Apify Store&lt;/strong&gt;: &lt;a href="https://apify.com/darknezz/property-listing-scraper" rel="noopener noreferrer"&gt;Property Listing Scraper&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/darksider4all/multi-tool-content-api" rel="noopener noreferrer"&gt;multi-tool-content-api&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Apify actor uses &lt;strong&gt;pay-per-event pricing&lt;/strong&gt; at $0.01 per property extracted — you only pay for actual results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Market Analysis&lt;/strong&gt;: Track property prices across multiple markets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead Generation&lt;/strong&gt;: Build databases of available properties&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price Monitoring&lt;/strong&gt;: Watch specific neighbourhoods for price changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Investment Research&lt;/strong&gt;: Compare yields across countries and currencies&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;By combining JSON-LD extraction with meta tags and regex fallbacks, we achieve universal compatibility without sacrificing data quality. The scraper handles the messy reality of real estate websites so you can focus on analysing the data.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Python, BeautifulSoup4, httpx, and the Apify platform.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;p&gt;While you're here, these might be worth a read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content (...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Gene...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Rea...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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