<?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: Devil Scrapes</title>
    <description>The latest articles on DEV Community by Devil Scrapes (@devil_scrapes).</description>
    <link>https://dev.to/devil_scrapes</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%2F3960872%2Ffa930ad0-5ebc-4ca7-b894-6bc6fb3e2b40.png</url>
      <title>DEV Community: Devil Scrapes</title>
      <link>https://dev.to/devil_scrapes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devil_scrapes"/>
    <language>en</language>
    <item>
      <title>Naver blog search: page 2 only exists if you keep page 1's token</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Wed, 14 Oct 2026 03:24:49 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/naver-blog-search-page-2-only-exists-if-you-keep-page-1s-token-4bk2</link>
      <guid>https://dev.to/devil_scrapes/naver-blog-search-page-2-only-exists-if-you-keep-page-1s-token-4bk2</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://apify.com/DevilScrapes/naver-blog-search-scraper" rel="noopener noreferrer"&gt;Naver Blog Search Scraper&lt;/a&gt; turns a Naver blog-tab search, in Korean or any UTF-8 keyword, into a deduplicated table of blog posts: post URL, &lt;code&gt;blogId&lt;/code&gt; and &lt;code&gt;logNo&lt;/code&gt;, title, snippet, blog name and URL, publish date, thumbnail and the Naver influencer profile when there is one. Naver has no public search API for its blog tab. Up to 50 keywords per run and up to 600 posts per keyword. Pricing is pay-per-result, &lt;strong&gt;$0.20 per run plus $0.002 per post, $2.20 per 1,000 posts&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Naver have a public blog search API?
&lt;/h2&gt;

&lt;p&gt;Not for the search page you see in a browser. Results for something like &lt;code&gt;강릉 맛집&lt;/code&gt; come from the blog tab of Naver's web search. The Actor reads that tab, skips ad cards, and returns only organic blog posts, with &lt;code&gt;position&lt;/code&gt; counting the de-duplicated organic posts per keyword.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does page 2 need a token from page 1?
&lt;/h2&gt;

&lt;p&gt;Only the first page of the blog tab is plain HTML. Every later page is a JSON response from a separate pager endpoint, and that endpoint is addressed by a loader object embedded in the page-1 source: a next-page URL plus an &lt;code&gt;X-Prs-Query-Info&lt;/code&gt; header token. Request the pager URL without that header and you don't get the next page. And each response carries the URL of the page after it, so pages must be fetched in order, as a chain.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Naver's blog tab only serves page 1 as HTML. Pages 2 onward come from a pager URL that must be requested with a token header embedded in page 1.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the Actor reads the loader URL and token out of the page-1 source, then walks the chain: each JSON envelope holds the next page's card HTML and the next URL, reused with the same token. Pages are paced 3.5 seconds apart. If the page-1 markup lacks the blog-tab marker, or a pager response isn't the expected JSON, that counts as a block and the session restarts rather than silently returning nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else we handle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dedupe by &lt;code&gt;blogId&lt;/code&gt; + &lt;code&gt;logNo&lt;/code&gt;&lt;/strong&gt;, parsed from the post URL, within each keyword.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restarts don't repeat rows.&lt;/strong&gt; After a block the Actor opens a fresh session with the next browser fingerprint, replays the early pages to catch up, and emits only unseen posts, up to two restarts per keyword.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dates are honest.&lt;/strong&gt; An absolute date like &lt;code&gt;2026.07.24.&lt;/code&gt; becomes an ISO &lt;code&gt;publishedAt&lt;/code&gt;. A relative one like &lt;code&gt;3주 전&lt;/code&gt; is kept as &lt;code&gt;publishedAtRelative&lt;/code&gt; and not converted to a guess. &lt;code&gt;publishedAtRaw&lt;/code&gt; always has the original text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ads are skipped&lt;/strong&gt;, and cards without a usable post link, title or blog name are dropped.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Actor gives you
&lt;/h2&gt;

&lt;p&gt;One row per post, 15 fields: &lt;code&gt;query&lt;/code&gt;, &lt;code&gt;position&lt;/code&gt;, &lt;code&gt;postUrl&lt;/code&gt;, &lt;code&gt;blogId&lt;/code&gt;, &lt;code&gt;logNo&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;snippet&lt;/code&gt;, &lt;code&gt;blogName&lt;/code&gt;, &lt;code&gt;blogUrl&lt;/code&gt;, &lt;code&gt;influencerUrl&lt;/code&gt;, &lt;code&gt;publishedAtRaw&lt;/code&gt;, &lt;code&gt;publishedAt&lt;/code&gt;, &lt;code&gt;publishedAtRelative&lt;/code&gt;, &lt;code&gt;thumbnailUrl&lt;/code&gt; and &lt;code&gt;scrapedAt&lt;/code&gt;.&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;"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;"강릉 맛집"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"postUrl"&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://blog.naver.com/104204303/224424493170"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"blogId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"104204303"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"logNo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"224424493170"&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;"강릉 물회 맛집 장안횟집 풍자 또간집 미역국이 킥"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"blogName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"blogUrl"&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://blog.naver.com/104204303"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"influencerUrl"&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://in.naver.com/104204303"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publishedAtRaw"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"publishedAt"&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;"publishedAtRelative"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"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-10-11T10:00:00Z"&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;Input: &lt;code&gt;queries&lt;/code&gt; (1 to 50 keywords) and &lt;code&gt;maxResultsPerQuery&lt;/code&gt; (default 60, up to 600).&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations 🚧
&lt;/h2&gt;

&lt;p&gt;Blog tab only: no cafe, news or Knowledge-iN results, and no post bodies, comments or likes. Results follow Naver's default order, with no sort or date filter, and depend on the proxy exit region, so the same keyword can return a slightly different set from different regions. Depth is capped at 600 posts per keyword. A keyword with no matches is a successful run that costs only the start fee.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Can I search in Korean?&lt;/strong&gt;&lt;br&gt;
Yes. Keywords are sent UTF-8 encoded, with a Korean &lt;code&gt;Accept-Language&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do two keywords return the same post?&lt;/strong&gt;&lt;br&gt;
Rows are de-duplicated within a keyword, not across keywords. Each row carries its &lt;code&gt;query&lt;/code&gt; so you can group as you like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about blocks and rate limits?&lt;/strong&gt;&lt;br&gt;
That's our job. We rotate Firefox browser fingerprints through curl-cffi, use a fresh Apify Proxy session on every restart, and retry with exponential backoff on 408, 429 and 5xx, honouring &lt;code&gt;Retry-After&lt;/code&gt;, up to 5 attempts per request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does pricing work?&lt;/strong&gt;&lt;br&gt;
$0.20 per run plus $0.002 per post: &lt;strong&gt;$2.20 per 1,000 posts&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://apify.com/DevilScrapes/naver-blog-search-scraper" rel="noopener noreferrer"&gt;Naver Blog Search Scraper on Apify&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Devil Scrapes&lt;/a&gt;. We handle the pager tokens, the fingerprints and the retries, and we don't turn "3 weeks ago" into a date we can't prove.&lt;/em&gt; 😈&lt;/p&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>korean</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Scraping HelloWork: why 45 000 EUR is not 45 and 000</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Wed, 14 Oct 2026 03:24:11 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/scraping-hellowork-why-45-000-eur-is-not-45-and-000-21c</link>
      <guid>https://dev.to/devil_scrapes/scraping-hellowork-why-45-000-eur-is-not-45-and-000-21c</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://apify.com/DevilScrapes/hellowork-jobs-scraper" rel="noopener noreferrer"&gt;HelloWork Jobs Scraper&lt;/a&gt; turns hellowork.com searches, by keyword and French city, into a deduplicated table of job offers: title, company, location split into city and department code, contract type (CDI, CDD, Intérim and so on), working time, remote policy, salary text plus parsed minimum, maximum, currency and period, posting date and apply method. HelloWork is a major French jobs board with no public API. Pricing is pay-per-result, &lt;strong&gt;$0.20 per run plus $0.002 per job, $2.20 per 1,000 jobs&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does HelloWork have a public jobs API?
&lt;/h2&gt;

&lt;p&gt;No. The search page is built for browsers, and the structured facts are scattered between visible nodes, an accessible label on each offer link and an analytics attribute. The Actor reads all three, lets the visible nodes win and uses the label to fill gaps, then returns one validated row per offer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is parsing a French salary harder than it looks?
&lt;/h2&gt;

&lt;p&gt;French number formatting breaks naive regexes. Thousands are separated by spaces, and the site uses non-breaking and narrow non-breaking spaces (U+00A0 and U+202F), so &lt;code&gt;45 000 - 90 000 € / an&lt;/code&gt; may not contain a single ordinary space. The decimal separator is a comma, and the period is written in French (&lt;code&gt;an&lt;/code&gt;, &lt;code&gt;jour&lt;/code&gt;, &lt;code&gt;mois&lt;/code&gt;, &lt;code&gt;heure&lt;/code&gt;).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A salary like "45 000 - 90 000 € / an" can be separated by narrow no-break spaces, so splitting on a normal space reads 45 and 000 as two numbers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So we normalise every space variant first, then parse the range into &lt;code&gt;salaryMin&lt;/code&gt; and &lt;code&gt;salaryMax&lt;/code&gt; (equal for a single value), &lt;code&gt;salaryCurrency&lt;/code&gt; (&lt;code&gt;EUR&lt;/code&gt;) and &lt;code&gt;salaryPeriod&lt;/code&gt; (&lt;code&gt;year&lt;/code&gt;, &lt;code&gt;day&lt;/code&gt;, &lt;code&gt;month&lt;/code&gt;, &lt;code&gt;hour&lt;/code&gt;). If the employer discloses nothing, or the text is not numeric, the numbers are &lt;code&gt;null&lt;/code&gt;, never 0, and &lt;code&gt;salaryText&lt;/code&gt; keeps what the site showed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else we handle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Titles can contain commas.&lt;/strong&gt; The offer label reads like "title à city, chez company, pour un CDI, ...". We split on the last ", pour un" because the tail segments never contain commas but titles can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote versus contract duration.&lt;/strong&gt; The same tag slot carries "2 ans" (a contract length) and "Télétravail partiel". Only text containing "travail", accent-folded, becomes &lt;code&gt;remotePolicy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anonymous employers.&lt;/strong&gt; One paragraph in the heading means no company name, so &lt;code&gt;companyName&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt; instead of repeating the title.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relative dates.&lt;/strong&gt; "il y a 5 heures" is kept in &lt;code&gt;postedText&lt;/code&gt; and converted to an ISO &lt;code&gt;postedAt&lt;/code&gt; from the run time. A month counts as 30 days, and "plus de 1 mois" is read as one month back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approximate matches are flagged, not hidden.&lt;/strong&gt; HelloWork mixes near matches after exact ones; &lt;code&gt;isApproximateMatch&lt;/code&gt; lets you filter either way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedupe by job id&lt;/strong&gt;, and paging stops using the "Afficher N offres" total, 30 jobs per page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Actor gives you
&lt;/h2&gt;

&lt;p&gt;One row per job, 25 fields, including &lt;code&gt;jobId&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;companyName&lt;/code&gt;, &lt;code&gt;location&lt;/code&gt;, &lt;code&gt;city&lt;/code&gt;, &lt;code&gt;departmentCode&lt;/code&gt;, &lt;code&gt;contractType&lt;/code&gt;, &lt;code&gt;workingTime&lt;/code&gt;, &lt;code&gt;remotePolicy&lt;/code&gt;, &lt;code&gt;salaryText&lt;/code&gt;, &lt;code&gt;salaryMin&lt;/code&gt;, &lt;code&gt;salaryMax&lt;/code&gt;, &lt;code&gt;salaryCurrency&lt;/code&gt;, &lt;code&gt;salaryPeriod&lt;/code&gt;, &lt;code&gt;postedText&lt;/code&gt;, &lt;code&gt;postedAt&lt;/code&gt;, &lt;code&gt;applyMethod&lt;/code&gt;, &lt;code&gt;isSuperRecruiter&lt;/code&gt;, &lt;code&gt;isApproximateMatch&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;query&lt;/code&gt;, &lt;code&gt;searchLocation&lt;/code&gt; and &lt;code&gt;scrapedAt&lt;/code&gt;.&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;"jobId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"80459780"&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;"Python Developer - Paris H/F"&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;"Approach People Recruitment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Paris - 75"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Paris"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"departmentCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"75"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contractType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CDI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"workingTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"temps plein"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"remotePolicy"&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;"salaryText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"45 000 - 90 000 € / an"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"salaryMin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"salaryMax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;90000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"salaryCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"salaryPeriod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"year"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"postedText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"il y a 5 heures"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"postedAt"&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-10-08"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"applyMethod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UrlWithForm"&lt;/span&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.hellowork.com/fr-fr/emplois/80459780.html"&lt;/span&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;"python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"searchLocation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Paris"&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-10-08T09:30:00Z"&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;Input: &lt;code&gt;queries&lt;/code&gt; (keywords), &lt;code&gt;locations&lt;/code&gt; (optional French cities; each query runs once per city, empty means all of France) and &lt;code&gt;maxItems&lt;/code&gt; per query and city combination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations 🚧
&lt;/h2&gt;

&lt;p&gt;Listing data only: no full job descriptions, no contract or salary filters, no company pages. &lt;code&gt;isSponsored&lt;/code&gt; is best effort, and we have not seen a sponsored card yet. Titles, contract names and remote labels stay in French as the site writes them. A search that finds nothing is a successful run that costs only the start fee.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Do I need a HelloWork account or API key?&lt;/strong&gt;&lt;br&gt;
No.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is salary empty on many rows?&lt;/strong&gt;&lt;br&gt;
Many employers don't disclose pay. We leave it empty rather than returning a misleading 0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about blocks and retries?&lt;/strong&gt;&lt;br&gt;
That's our job. We send browser-grade requests through curl-cffi and retry failed pages with exponential backoff, up to 5 attempts, honouring &lt;code&gt;Retry-After&lt;/code&gt;. HelloWork answers without a proxy, so Apify Proxy is off by default and optional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does pricing work?&lt;/strong&gt;&lt;br&gt;
$0.20 per run plus $0.002 per job: &lt;strong&gt;$2.20 per 1,000 jobs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://apify.com/DevilScrapes/hellowork-jobs-scraper" rel="noopener noreferrer"&gt;HelloWork Jobs Scraper on Apify&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Devil Scrapes&lt;/a&gt;. We handle the French number formats, the retries and the messy labels, so an empty salary means undisclosed, not zero.&lt;/em&gt; 😈&lt;/p&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>api</category>
      <category>showdev</category>
    </item>
    <item>
      <title>BizQuest listings: the anchors lie, the JSON-LD doesn't</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Wed, 14 Oct 2026 03:23:33 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/bizquest-listings-the-anchors-lie-the-json-ld-doesnt-25eg</link>
      <guid>https://dev.to/devil_scrapes/bizquest-listings-the-anchors-lie-the-json-ld-doesnt-25eg</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://apify.com/DevilScrapes/bizquest-business-for-sale-scraper" rel="noopener noreferrer"&gt;BizQuest Business-for-Sale Scraper&lt;/a&gt; turns BizQuest, the US small-business marketplace, into a deduplicated table of businesses for sale: headline, teaser, asking price in USD, city or county, state code and name, lead photo, listing URL, and three card tags (Financing Available, Includes Real Estate, SBA Loan Eligible). Scrape the nationwide feed, a state (&lt;code&gt;Texas&lt;/code&gt;, &lt;code&gt;TX&lt;/code&gt;) or a city (&lt;code&gt;Houston, TX&lt;/code&gt;), or paste a BizQuest URL. BizQuest has no export. Pricing is pay-per-result, &lt;strong&gt;$0.20 per run plus $0.006 per listing, $6.20 per 1,000 listings&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does BizQuest have an API or export?
&lt;/h2&gt;

&lt;p&gt;Not a public one. The site shows about fifty businesses per page and expects you to click through. The Actor walks the listing pages for every route you choose and returns one row per business, with the route, page and position each row was found at so you can trace any figure back to the page it came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do we read the JSON-LD and ignore the page links?
&lt;/h2&gt;

&lt;p&gt;Because the links lie. BizQuest repeats listing ids in sidebar widgets, and its result list mixes in franchise advertisements and broker promo cards that look like listings. If you collect every anchor on the page you get duplicates and things that are not businesses for sale.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On a BizQuest results page the reliable list of businesses is the inline JSON-LD, not the anchors: the anchors repeat ids from sidebar widgets.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the parser reads the page's &lt;code&gt;SearchResultsPage&lt;/code&gt; JSON-LD list and keeps an entry only if its URL has the shape of a real listing (&lt;code&gt;/business-for-sale/&amp;lt;slug&amp;gt;/BW&amp;lt;id&amp;gt;/&lt;/code&gt;). Franchise ads and broker cards have a different URL shape and are dropped. The tags are not in the JSON-LD, so they are read from the server-rendered card for the same &lt;code&gt;BW&lt;/code&gt; id.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else we handle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Out-of-range pages clamp to page 1.&lt;/strong&gt; Ask BizQuest for a page past the end and it serves page 1 again. A page whose listings were all already seen on that route ends the route, so you are never charged for page 1 twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One dedupe set for the whole run.&lt;/strong&gt; The same listing id never appears twice, across pages or across routes (nationwide plus Texas overlap heavily).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cash flow and EBITDA are gated.&lt;/strong&gt; BizQuest shows them only to signed-in visitors. Rather than guess, rows carry &lt;code&gt;financialsGated&lt;/code&gt;, which is true when the card says "Sign In to View".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Undisclosed prices stay null.&lt;/strong&gt; &lt;code&gt;askingPrice&lt;/code&gt; is a whole-dollar integer, or &lt;code&gt;null&lt;/code&gt;, never 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A blocked route doesn't end the others.&lt;/strong&gt; The run fails only if a route was blocked and nothing at all was collected.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Actor gives you
&lt;/h2&gt;

&lt;p&gt;One row per business, 18 fields: &lt;code&gt;listingId&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;askingPrice&lt;/code&gt;, &lt;code&gt;currency&lt;/code&gt;, &lt;code&gt;locality&lt;/code&gt;, &lt;code&gt;state&lt;/code&gt;, &lt;code&gt;stateName&lt;/code&gt;, &lt;code&gt;financialsGated&lt;/code&gt;, &lt;code&gt;financingAvailable&lt;/code&gt;, &lt;code&gt;includesRealEstate&lt;/code&gt;, &lt;code&gt;sbaEligible&lt;/code&gt;, &lt;code&gt;imageUrl&lt;/code&gt;, &lt;code&gt;searchRoute&lt;/code&gt;, &lt;code&gt;pageNumber&lt;/code&gt;, &lt;code&gt;position&lt;/code&gt; and &lt;code&gt;scrapedAt&lt;/code&gt;.&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;"listingId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2540957"&lt;/span&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.bizquest.com/business-for-sale/private-gym/BW2540957/"&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;"Private gym"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Members-only fitness club offering 24-hour gym access."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"askingPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;400000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"locality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Monument"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stateName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Colorado"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"financialsGated"&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;"financingAvailable"&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;"includesRealEstate"&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;"sbaEligible"&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;"searchRoute"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/businesses-for-sale/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pageNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&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-10-12T08:00:00Z"&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;Input: &lt;code&gt;includeNationwide&lt;/code&gt; (default on), &lt;code&gt;locations&lt;/code&gt;, &lt;code&gt;startUrls&lt;/code&gt;, &lt;code&gt;maxPages&lt;/code&gt; (default 3, up to 200, about 50 listings per page) and &lt;code&gt;maxItems&lt;/code&gt; (default 150, up to 10,000).&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations 🚧
&lt;/h2&gt;

&lt;p&gt;Search-card fields only: no detail pages, broker contact details or confidential financials, and no cash flow, EBITDA or revenue. Query-string filters on a pasted URL are dropped, so paste the route you want. BizQuest serves up to about 200 pages for the nationwide feed. A route with no listings is a successful run that costs only the start fee.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Do I need a BizQuest account or API key?&lt;/strong&gt;&lt;br&gt;
No. The Actor reads public listing pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about blocks?&lt;/strong&gt;&lt;br&gt;
That's our job. We rotate curl-cffi browser fingerprints (Chrome, Firefox, Safari), retry with exponential backoff and &lt;code&gt;Retry-After&lt;/code&gt;, up to 5 attempts per page, and rotate US residential Apify Proxy sessions on every block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did I get fewer rows than &lt;code&gt;maxItems&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
The route has fewer listings than your cap, or &lt;code&gt;maxPages&lt;/code&gt; stopped the run first. The status message says which.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does pricing work?&lt;/strong&gt;&lt;br&gt;
$0.20 per run plus $0.006 per unique listing: &lt;strong&gt;$6.20 per 1,000 listings&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://apify.com/DevilScrapes/bizquest-business-for-sale-scraper" rel="noopener noreferrer"&gt;BizQuest Business-for-Sale Scraper on Apify&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Devil Scrapes&lt;/a&gt;. We handle the fingerprints, the proxies and the retries, and we keep franchise ads out of your deal flow.&lt;/em&gt; 😈&lt;/p&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>api</category>
      <category>showdev</category>
    </item>
    <item>
      <title>AutoTrader.ca Listings Scraper: when an HTTP 200 is a block, not an empty search</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Wed, 14 Oct 2026 03:10:18 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/autotraderca-listings-scraper-when-an-http-200-is-a-block-not-an-empty-search-5eoc</link>
      <guid>https://dev.to/devil_scrapes/autotraderca-listings-scraper-when-an-http-200-is-a-block-not-an-empty-search-5eoc</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://apify.com/DevilScrapes/autotrader-ca-listings-scraper" rel="noopener noreferrer"&gt;AutoTrader.ca Listings Scraper&lt;/a&gt; pulls Canadian car listings by make, model and condition into JSON, CSV or Excel: price in CAD, mileage in kilometres, year, trim, body type, transmission, fuel, seller name and type, city, province, photos and the full seller description. One run covers the whole country, up to 4,000 listings per make, model and condition. AutoTrader.ca has no public listings API and actively blocks automated traffic, so we run it through residential exits and treat every kind of soft block as a block. Pricing is pay-per-result, &lt;strong&gt;$0.20 per run plus $0.0025 per listing, $2.70 per 1,000 listings&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does AutoTrader.ca have an API?
&lt;/h2&gt;

&lt;p&gt;Not for the public. Dealer feeds go in through partner integrations, and nothing comes out. If you want "every used Toyota Corolla for sale in Canada with price and mileage", the only source is the search pages themselves. Each page is a Next.js render with 20 listings inside a &lt;code&gt;__NEXT_DATA__&lt;/code&gt; script tag, along with the total result count and the number of pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  When is an HTTP 200 not a page of listings?
&lt;/h2&gt;

&lt;p&gt;When the site has decided you are a bot. AutoTrader.ca's protection doesn't always answer with a clean 403. Sometimes it returns &lt;strong&gt;HTTP 202&lt;/strong&gt; with a challenge body. Sometimes it returns &lt;strong&gt;HTTP 200&lt;/strong&gt; with a page that has no listing data in it at all. A scraper that only checks the status code reads both as "this search has no cars" and finishes green with an empty dataset.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On AutoTrader.ca, a 202 or a 200 without &lt;code&gt;__NEXT_DATA__&lt;/code&gt; is a block, not an empty search.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So we treat all three as blocks: a non-200, a 202, and a 200 that lacks the listing payload. Each one triggers a retry on a fresh residential session. The only "zero results" we believe is a page that parses cleanly and reports a total of zero. If a page still fails after five attempts we log it, keep every other page, and end the run with a status like "Scraped 380/400 results; 1 page blocked", so you can see exactly what you got.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else we clean up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Numbers become numbers.&lt;/strong&gt; "$ 13,990" becomes &lt;code&gt;price_cad: 13990&lt;/code&gt;, and the odometer becomes &lt;code&gt;mileage_km&lt;/code&gt;, while &lt;code&gt;price_formatted&lt;/code&gt; keeps the original string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descriptions become plain text.&lt;/strong&gt; Seller HTML with line breaks and entities comes back as readable text with paragraph breaks kept.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make and model are forgiving.&lt;/strong&gt; "Land Rover", "land rover" and "land_rover" all work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicates are dropped.&lt;/strong&gt; A listing that shows up on two pages is emitted once: we dedupe on &lt;code&gt;listing_id&lt;/code&gt;, and &lt;code&gt;result_type&lt;/code&gt; tells you whether it was &lt;code&gt;Organic&lt;/code&gt; or &lt;code&gt;Sponsored&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pages are heavy.&lt;/strong&gt; Each search page weighs roughly 0.78 MB for 20 listings, and that egress is part of the price you see.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Actor gives you
&lt;/h2&gt;

&lt;p&gt;One row per listing, 29 fields, including &lt;code&gt;listing_id&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;year&lt;/code&gt;, &lt;code&gt;make&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;trim&lt;/code&gt;, &lt;code&gt;body_type&lt;/code&gt;, &lt;code&gt;price_cad&lt;/code&gt;, &lt;code&gt;price_formatted&lt;/code&gt;, &lt;code&gt;price_evaluation&lt;/code&gt;, &lt;code&gt;mileage_km&lt;/code&gt;, &lt;code&gt;transmission&lt;/code&gt;, &lt;code&gt;fuel&lt;/code&gt;, &lt;code&gt;engine_cc&lt;/code&gt;, &lt;code&gt;condition&lt;/code&gt;, &lt;code&gt;city&lt;/code&gt;, &lt;code&gt;province&lt;/code&gt;, &lt;code&gt;postal_code&lt;/code&gt;, &lt;code&gt;street&lt;/code&gt;, &lt;code&gt;seller_name&lt;/code&gt;, &lt;code&gt;seller_type&lt;/code&gt;, &lt;code&gt;seller_phone&lt;/code&gt;, &lt;code&gt;ad_tier&lt;/code&gt;, &lt;code&gt;result_type&lt;/code&gt;, &lt;code&gt;image_urls&lt;/code&gt;, &lt;code&gt;image_count&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt; and &lt;code&gt;scraped_at&lt;/code&gt;.&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;"listing_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"79df9408-002a-44d1-90a3-3355eb9b1770"&lt;/span&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.autotrader.ca/offers/toyota-corolla-le-79df9408-002a-44d1-90a3-3355eb9b1770"&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;"2018 Toyota Corolla LE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"year"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2018&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"make"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Toyota"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Corolla"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"trim"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sedan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price_cad"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;13990&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price_formatted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$ 13,990"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mileage_km"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;144425&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transmission"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Automatic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fuel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Gasoline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"used"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"North York"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"province"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ON"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"seller_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;"First Choice Motors"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"seller_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dealer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Organic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scraped_at"&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-10-07T12: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;p&gt;Input: &lt;code&gt;make&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;condition&lt;/code&gt; (&lt;code&gt;used&lt;/code&gt; or &lt;code&gt;new&lt;/code&gt;) and &lt;code&gt;maxResults&lt;/code&gt; up to 4,000.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations 🚧
&lt;/h2&gt;

&lt;p&gt;Search is by make, model and condition only: no postal-code radius, province, price, year or mileage filter, so filter the dataset afterwards. One search returns at most 4,000 listings (200 pages of 20). Listing pages only: no VIN, full spec sheet, price history or sold listings. Phone numbers appear only when the listing itself shows one.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Do I need an AutoTrader.ca account?&lt;/strong&gt;&lt;br&gt;
No. The Actor reads public search pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about blocks and captchas?&lt;/strong&gt;&lt;br&gt;
That's our job. We rotate real-browser TLS fingerprints through &lt;code&gt;curl-cffi&lt;/code&gt;, route through Apify residential proxies with a fresh session on every block, and retry with exponential backoff up to 5 attempts per page, honouring &lt;code&gt;Retry-After&lt;/code&gt;. One blocked page never sinks the run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I track prices for one model over time?&lt;/strong&gt;&lt;br&gt;
Yes. Schedule the same make and model daily and diff &lt;code&gt;price_cad&lt;/code&gt; per &lt;code&gt;listing_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does pricing work?&lt;/strong&gt;&lt;br&gt;
$0.20 per run plus $0.0025 per listing: &lt;strong&gt;$2.70 per 1,000 listings&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://apify.com/DevilScrapes/autotrader-ca-listings-scraper" rel="noopener noreferrer"&gt;AutoTrader.ca Listings Scraper on Apify&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Devil Scrapes&lt;/a&gt;. We handle the residential proxies, the fingerprints, the soft blocks and the retries, so an empty dataset always means an empty search.&lt;/em&gt; 😈&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>cars</category>
      <category>apify</category>
    </item>
    <item>
      <title>Yandex Maps Places Scraper: why two pages of 25 give you 49 places</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Wed, 14 Oct 2026 03:09:41 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/yandex-maps-places-scraper-why-two-pages-of-25-give-you-49-places-3edp</link>
      <guid>https://dev.to/devil_scrapes/yandex-maps-places-scraper-why-two-pages-of-25-give-you-49-places-3edp</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://apify.com/DevilScrapes/yandex-maps-places-scraper" rel="noopener noreferrer"&gt;Yandex Maps Places Scraper&lt;/a&gt; turns a Yandex Maps search such as &lt;code&gt;cafe&lt;/code&gt; in Moscow into a deduplicated table of places: name, address, latitude and longitude, rating, rating and review counts, phone numbers, websites, categories and opening hours, plus the page and rank each place appeared at. Yandex Maps is the main local-business directory across Russia and the CIS, and it has no public places API. Pricing is pay-per-result, &lt;strong&gt;$0.20 per run plus $0.004 per place, $4.20 per 1,000 places&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Yandex Maps have a places API?
&lt;/h2&gt;

&lt;p&gt;Not one that returns business listings for search. Yandex's geocoder and static-map APIs turn addresses into coordinates and render map tiles, but the "find me every pharmacy in this city with its phone number and rating" data lives only in the Maps web app. That app talks to its own internal endpoints, and every request it makes carries a signature computed from the exact query string. Change a parameter, a parameter's order or how a space is encoded, and the signature no longer matches. We rebuild that signing step for every page, so you just give us a query and a city.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do two pages of 25 give you 49 places, not 50?
&lt;/h2&gt;

&lt;p&gt;This is the detail that decides whether your counts are honest. Yandex serves 25 places per page, but the pages overlap at the seam: the last place on page 1 can reappear as the first place on page 2. Some result slots are not businesses at all but curated "collection" cards. Count raw items and you over-report. Fail to dedupe and the same cafe lands in your CRM twice.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Yandex Maps search pages overlap at the seam, so two pages of 25 typically yield 49 unique places, not 50.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So we dedupe by Yandex's organization &lt;code&gt;id&lt;/code&gt; within each search, skip the non-business items, record the &lt;code&gt;page&lt;/code&gt; each place was first seen on and its &lt;code&gt;position&lt;/code&gt; after dedupe, and fetch a couple of extra pages when needed so &lt;code&gt;maxResults: 50&lt;/code&gt; really returns 50 places when Yandex has them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else we normalise
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Coordinates two ways.&lt;/strong&gt; Yandex serves &lt;code&gt;[lon, lat]&lt;/code&gt;, which trips up everyone who expects latitude first. We keep &lt;code&gt;coordinates&lt;/code&gt; exactly as served and also split out named &lt;code&gt;lat&lt;/code&gt; and &lt;code&gt;lon&lt;/code&gt; fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opening hours two ways.&lt;/strong&gt; &lt;code&gt;workingTime&lt;/code&gt; is the human string ("Mon-Thu 12:00–02:00"). &lt;code&gt;workingHours&lt;/code&gt; is the structured per-weekday version for code, with &lt;code&gt;from&lt;/code&gt; and &lt;code&gt;to&lt;/code&gt; hours and minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ratings rounded&lt;/strong&gt; to two decimals, with &lt;code&gt;ratingCount&lt;/code&gt; and &lt;code&gt;reviewCount&lt;/code&gt; kept separate, because they are different numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Actor gives you
&lt;/h2&gt;

&lt;p&gt;One row per place, 20 fields: &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;address&lt;/code&gt;, &lt;code&gt;lat&lt;/code&gt;, &lt;code&gt;lon&lt;/code&gt;, &lt;code&gt;coordinates&lt;/code&gt;, &lt;code&gt;rating&lt;/code&gt;, &lt;code&gt;ratingCount&lt;/code&gt;, &lt;code&gt;reviewCount&lt;/code&gt;, &lt;code&gt;phones&lt;/code&gt;, &lt;code&gt;urls&lt;/code&gt;, &lt;code&gt;categories&lt;/code&gt;, &lt;code&gt;workingTime&lt;/code&gt;, &lt;code&gt;workingHours&lt;/code&gt;, &lt;code&gt;sourceUrl&lt;/code&gt;, &lt;code&gt;query&lt;/code&gt;, &lt;code&gt;geoId&lt;/code&gt;, &lt;code&gt;page&lt;/code&gt;, &lt;code&gt;position&lt;/code&gt; and &lt;code&gt;scrapedAt&lt;/code&gt;.&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"243088808182"&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;"Alkobufet"&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;"Moscow, Sretenka Street, 36"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;55.771647&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;37.632628&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"coordinates"&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="mf"&gt;37.632628&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;55.771647&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ratingCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2757&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1321&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="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"+79152776884"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"urls"&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;"https://taplink.cc/alkobufet.shashlik"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"categories"&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;"Cafe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"the glass room"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"workingTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mon-Thu 12:00–02:00; Fri,Sat 12:00–05:00; Sun 12:00–02:00"&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://yandex.com/maps/org/alkobufet_shashlychnaya/243088808182"&lt;/span&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;"cafe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"geoId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;213&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&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-10-09T00:00:00Z"&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;Input: &lt;code&gt;searches&lt;/code&gt;, each one an object (&lt;code&gt;{"query": "cafe", "geoId": 213, "city": "moscow"}&lt;/code&gt;), a pasted Yandex Maps search URL, or the short text form &lt;code&gt;cafe | 213 moscow&lt;/code&gt;. Up to 50 searches per run and &lt;code&gt;maxResults&lt;/code&gt; up to 500 places per search.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations 🚧
&lt;/h2&gt;

&lt;p&gt;Review text, photos and per-place detail pages are not included: you get the rating and the counts. Moscow is built in; any other city needs its &lt;code&gt;geoId&lt;/code&gt; and city slug, both visible in the Yandex Maps URL (or just paste the URL). English (US) is the supported locale for now, and place names come back as Yandex serves them. A search with no matches is a successful run that costs only the start fee.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Do I need a Yandex account or API key?&lt;/strong&gt;&lt;br&gt;
No. The Actor reads the same public search the Maps web app uses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about blocks and captchas?&lt;/strong&gt;&lt;br&gt;
That's our job. We rotate real-browser TLS fingerprints (Firefox and Safari profiles) through &lt;code&gt;curl-cffi&lt;/code&gt;, use a fresh Apify Proxy session for every search and on every block, pace requests at least three seconds apart, and retry with exponential backoff on 408, 429 and 503, up to 5 attempts per page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I scrape a city other than Moscow?&lt;/strong&gt;&lt;br&gt;
Open the search on yandex.com/maps and paste the URL. It contains the &lt;code&gt;geoId&lt;/code&gt; and city slug, and the Actor reads both from it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does pricing work?&lt;/strong&gt;&lt;br&gt;
$0.20 per run plus $0.004 per unique place: &lt;strong&gt;$4.20 per 1,000 places&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://apify.com/DevilScrapes/yandex-maps-places-scraper" rel="noopener noreferrer"&gt;Yandex Maps Places Scraper on Apify&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Devil Scrapes&lt;/a&gt;. We handle the request signing, the fingerprints, the proxies and the retries, and we count unique places, not raw slots.&lt;/em&gt; 😈&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>maps</category>
      <category>apify</category>
    </item>
    <item>
      <title>ESPN Scores Scraper: a game that has not started still shows 0-0</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Mon, 12 Oct 2026 20:40:25 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/espn-scores-scraper-a-game-that-has-not-started-still-shows-0-0-1eli</link>
      <guid>https://dev.to/devil_scrapes/espn-scores-scraper-a-game-that-has-not-started-still-shows-0-0-1eli</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://apify.com/DevilScrapes/espn-scores-scraper" rel="noopener noreferrer"&gt;ESPN Scores Scraper&lt;/a&gt; pulls ESPN scoreboards for seven leagues (English Premier League, La Liga, Serie A, NBA, NFL, MLB and NHL) into JSON, CSV or Excel: one row per game with teams, scores, game state, start time in UTC, the winner, venue, city, broadcasters and a link to the ESPN summary page. Pick leagues and a date range of up to 31 days and get every game in one run. ESPN has no official public scores API, so we read its public scoreboard data and return it as a tidy dataset. Pricing is pay-per-result, &lt;strong&gt;$0.20 per run plus $0.0015 per game, $1.70 per 1,000 games&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does ESPN have a public scores API?
&lt;/h2&gt;

&lt;p&gt;Not an official, documented one. ESPN's own pages load scoreboards from internal endpoints that are undocumented, shaped differently per sport, and free to change without notice. Every "ESPN API" guide you find is someone's reverse-engineered notes. We keep that layer working, normalise it into one row shape across all seven leagues, and absorb the changes so your pipeline doesn't have to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does a game that hasn't started show 0-0?
&lt;/h2&gt;

&lt;p&gt;Because ESPN's scoreboard reports a score of 0 for both sides before kickoff. Read &lt;code&gt;home_score&lt;/code&gt; and &lt;code&gt;away_score&lt;/code&gt; alone and every upcoming fixture looks like a goalless draw. Feed that into a model and you've invented results.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On ESPN's scoreboard, a 0-0 before kickoff and a 0-0 at full time look the same. Only the game state tells them apart.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So every row carries &lt;code&gt;status_state&lt;/code&gt;: &lt;code&gt;pre&lt;/code&gt; (not started), &lt;code&gt;in&lt;/code&gt; (in progress) or &lt;code&gt;post&lt;/code&gt; (finished), plus a &lt;code&gt;completed&lt;/code&gt; flag and ESPN's own &lt;code&gt;status_detail&lt;/code&gt; text. Filter on &lt;code&gt;completed: true&lt;/code&gt; before you treat a score as a result. We also fill &lt;code&gt;winner&lt;/code&gt; from the final score when ESPN leaves it out, and keep it &lt;code&gt;null&lt;/code&gt; for draws and unfinished games.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else we handle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-day ranges overlap.&lt;/strong&gt; A game near midnight UTC can appear on two days' scoreboards. We dedupe on &lt;code&gt;event_id&lt;/code&gt; per league, so you get each game once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One bad day doesn't sink the run.&lt;/strong&gt; A league-day that keeps failing is logged and skipped, and the rest still land.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Odds are sparse.&lt;/strong&gt; &lt;code&gt;odds_details&lt;/code&gt; and &lt;code&gt;over_under&lt;/code&gt; are filled only when ESPN's scoreboard carries a line, which is often not the case. Treat them as a bonus, not a betting feed: for many games, and for whole leagues, they come back &lt;code&gt;null&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Actor gives you
&lt;/h2&gt;

&lt;p&gt;One row per game, 21 fields: &lt;code&gt;event_id&lt;/code&gt;, &lt;code&gt;league&lt;/code&gt;, &lt;code&gt;league_name&lt;/code&gt;, &lt;code&gt;event_name&lt;/code&gt;, &lt;code&gt;short_name&lt;/code&gt;, &lt;code&gt;start_time&lt;/code&gt;, &lt;code&gt;status_state&lt;/code&gt;, &lt;code&gt;status_detail&lt;/code&gt;, &lt;code&gt;completed&lt;/code&gt;, &lt;code&gt;home_team&lt;/code&gt;, &lt;code&gt;away_team&lt;/code&gt;, &lt;code&gt;home_score&lt;/code&gt;, &lt;code&gt;away_score&lt;/code&gt;, &lt;code&gt;winner&lt;/code&gt;, &lt;code&gt;venue&lt;/code&gt;, &lt;code&gt;city&lt;/code&gt;, &lt;code&gt;odds_details&lt;/code&gt;, &lt;code&gt;over_under&lt;/code&gt;, &lt;code&gt;broadcasts&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt; and &lt;code&gt;scraped_at&lt;/code&gt;.&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;"event_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"401879268"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"league"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"soccer/eng.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"league_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;"English Premier League"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event_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;"Leeds United at Arsenal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"short_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;"LEE @ ARS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start_time"&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-10-10T11:30:00Z"&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_state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pre"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"completed"&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;"home_team"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Arsenal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"away_team"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Leeds United"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"home_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&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;"away_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&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;"winner"&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;"venue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Emirates Stadium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"London"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"broadcasts"&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;"USA 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;"Universo"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scraped_at"&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-10-07T12:00:00Z"&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;Note the 0-0 on a &lt;code&gt;pre&lt;/code&gt; game. Input: &lt;code&gt;leagues&lt;/code&gt; as sport/league keys (&lt;code&gt;soccer/eng.1&lt;/code&gt;, &lt;code&gt;soccer/esp.1&lt;/code&gt;, &lt;code&gt;soccer/ita.1&lt;/code&gt;, &lt;code&gt;basketball/nba&lt;/code&gt;, &lt;code&gt;football/nfl&lt;/code&gt;, &lt;code&gt;baseball/mlb&lt;/code&gt;, &lt;code&gt;hockey/nhl&lt;/code&gt;), &lt;code&gt;startDate&lt;/code&gt; and &lt;code&gt;endDate&lt;/code&gt; (empty means today, UTC) and &lt;code&gt;maxEvents&lt;/code&gt; up to 10,000.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations 🚧
&lt;/h2&gt;

&lt;p&gt;Scoreboards only: no standings, rosters, box scores or play-by-play. Seven leagues in this version. Odds columns are frequently empty. Up to 31 days per run. Each run is a snapshot, so schedule it for live updates. A day with no games is a successful run that costs only the start fee.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Is this an official ESPN API?&lt;/strong&gt;&lt;br&gt;
No. It is an independent scraper that reads ESPN's public scoreboard data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about blocks and rate limits?&lt;/strong&gt;&lt;br&gt;
That's our job. We retry with exponential backoff on 408, 429 and 5xx, honour &lt;code&gt;Retry-After&lt;/code&gt;, and isolate failures per league-day so one hiccup never costs you the whole run. Apify Proxy is available if you want it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it return live scores?&lt;/strong&gt;&lt;br&gt;
Yes, for games in progress at the moment of the run (&lt;code&gt;status_state: "in"&lt;/code&gt;). Schedule it every few minutes for a live ticker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does pricing work?&lt;/strong&gt;&lt;br&gt;
$0.20 per run plus $0.0015 per game: &lt;strong&gt;$1.70 per 1,000 games&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://apify.com/DevilScrapes/espn-scores-scraper" rel="noopener noreferrer"&gt;ESPN Scores Scraper on Apify&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Devil Scrapes&lt;/a&gt;. We handle the undocumented endpoints, the retries and the game-state traps, so a 0-0 in your dataset means a 0-0 on the pitch.&lt;/em&gt; 😈&lt;/p&gt;

</description>
      <category>sports</category>
      <category>webscraping</category>
      <category>python</category>
      <category>apify</category>
    </item>
    <item>
      <title>2GIS Places Scraper: page 6 of every search silently returns page 1</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Mon, 12 Oct 2026 20:39:45 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/2gis-places-scraper-page-6-of-every-search-silently-returns-page-1-4p21</link>
      <guid>https://dev.to/devil_scrapes/2gis-places-scraper-page-6-of-every-search-silently-returns-page-1-4p21</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://apify.com/DevilScrapes/2gis-places-scraper" rel="noopener noreferrer"&gt;2GIS Places Scraper&lt;/a&gt; turns 2GIS searches into a clean table of local businesses for any 2GIS city, including Moscow, Saint Petersburg, Novosibirsk, Almaty and Dubai: 2GIS id, name, categories, street address, building, district, latitude and longitude, rating, review count, opening hours per weekday, last-updated date and a direct 2GIS link. Switch on &lt;code&gt;includeContacts&lt;/code&gt; and it also opens each place page to add business phones, website and e-mail. Give it lists of cities and queries and it runs every combination, deduplicated by place. Pricing is pay-per-result, &lt;strong&gt;$0.20 per run plus $0.003 per place, $3.20 per 1,000 places&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does 2GIS have an API for this?
&lt;/h2&gt;

&lt;p&gt;2GIS sells API access for maps, routing and its places catalogue under commercial keys and quotas. If you want a one-off lead list of every pharmacy in Almaty with its rating and opening hours, that means a sales conversation and an integration project before the first row. The same data is already printed on 2GIS's public search pages. We read those pages and hand you the rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does page 6 of a 2GIS search give you page 1 again?
&lt;/h2&gt;

&lt;p&gt;This is the trap that inflates naive scrapers. 2GIS serves at most five result pages of 12 places for any single search: 60 places. Ask for page 6 and it doesn't return an error or an empty page. It silently serves page 1 again. A scraper that pages "until the results run out" never sees them run out. It loops, re-downloading the same 12 places, and if it counts rows instead of ids it reports a city far bigger than it is.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;2GIS caps every search at 5 pages of 12 places, and page 6 silently returns page 1, so "page until empty" never ends.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We measured that limit and stop cleanly at page 5. We also dedupe on &lt;code&gt;placeId&lt;/code&gt; across every page, query and city in the run, so a cafe that matches both &lt;code&gt;кафе&lt;/code&gt; and &lt;code&gt;кофейня&lt;/code&gt; is emitted and billed once. To cover more ground than 60 places, use several specific queries and more cities.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else we handle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Contacts are opt-in.&lt;/strong&gt; Phones, website and e-mail live on each place's own page, not in search results. With &lt;code&gt;includeContacts: false&lt;/code&gt; (the default) those fields stay empty and the run is fast. Turn it on and we make one extra request per place to fill &lt;code&gt;phone&lt;/code&gt;, &lt;code&gt;phones&lt;/code&gt;, &lt;code&gt;website&lt;/code&gt; and &lt;code&gt;email&lt;/code&gt;, when the business publishes them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branches vs companies.&lt;/strong&gt; &lt;code&gt;placeId&lt;/code&gt; is the branch. &lt;code&gt;organizationId&lt;/code&gt; is shared by every branch of a chain, so you can group a chain's locations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Country sites.&lt;/strong&gt; Kazakhstan and UAE cities live on their own 2GIS country domains. Give the city slug (&lt;code&gt;almaty&lt;/code&gt;, &lt;code&gt;dubai&lt;/code&gt;) and we route to the right one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid placement is flagged.&lt;/strong&gt; &lt;code&gt;isPromoted&lt;/code&gt; tells you which rows are ads, so promotion doesn't skew your rankings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Actor gives you
&lt;/h2&gt;

&lt;p&gt;One row per place, 26 fields: &lt;code&gt;placeId&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;organizationId&lt;/code&gt;, &lt;code&gt;category&lt;/code&gt;, &lt;code&gt;categories&lt;/code&gt;, &lt;code&gt;address&lt;/code&gt;, &lt;code&gt;addressComment&lt;/code&gt;, &lt;code&gt;buildingName&lt;/code&gt;, &lt;code&gt;postcode&lt;/code&gt;, &lt;code&gt;city&lt;/code&gt;, &lt;code&gt;district&lt;/code&gt;, &lt;code&gt;latitude&lt;/code&gt;, &lt;code&gt;longitude&lt;/code&gt;, &lt;code&gt;rating&lt;/code&gt;, &lt;code&gt;reviewCount&lt;/code&gt;, &lt;code&gt;phone&lt;/code&gt;, &lt;code&gt;phones&lt;/code&gt;, &lt;code&gt;website&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;workingHours&lt;/code&gt;, &lt;code&gt;updatedAt&lt;/code&gt;, &lt;code&gt;isPromoted&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;query&lt;/code&gt;, &lt;code&gt;searchCity&lt;/code&gt; and &lt;code&gt;scrapedAt&lt;/code&gt;.&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;"placeId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"70000001007179642"&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;"Му-Му, кафе"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"organizationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4504136498368318"&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;"Кафе"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"categories"&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;"Кафе"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"Быстрое питание"&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;"Манежная площадь, 1 ст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;"buildingName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"district"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"latitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;55.755301&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"longitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;37.614586&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1107&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"phone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+74957818781"&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;"http://cafemumu.ru"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"workingHours"&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;"Mon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"08:00-23:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Sun"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"08:00-23: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;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://2gis.ru/moscow/firm/70000001007179642"&lt;/span&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;"кафе"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"searchCity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"moscow"&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-10-01T10: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;p&gt;That sample was run with &lt;code&gt;includeContacts: true&lt;/code&gt;. Input: &lt;code&gt;cities&lt;/code&gt; (2GIS slugs), &lt;code&gt;queries&lt;/code&gt; (in the city's language works best), &lt;code&gt;maxItemsPerQuery&lt;/code&gt; up to 60, &lt;code&gt;maxPages&lt;/code&gt; up to 5, a run-wide &lt;code&gt;maxResults&lt;/code&gt; up to 50,000, and &lt;code&gt;includeContacts&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations 🚧
&lt;/h2&gt;

&lt;p&gt;60 places per city and query, because that is all 2GIS serves. &lt;code&gt;includeContacts&lt;/code&gt; makes runs slower, with one extra page per place. Rating, review count and opening hours appear only when 2GIS lists them. Search-result data only: no review text, menus or photos. A query with no matches is a successful run that costs only the start fee.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Do I need a 2GIS account or API key?&lt;/strong&gt;&lt;br&gt;
No. Add cities and queries and start the run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about blocks and rate limits?&lt;/strong&gt;&lt;br&gt;
That's our job. We rotate real-browser TLS fingerprints (Chrome, Firefox, Safari) through &lt;code&gt;curl-cffi&lt;/code&gt;, route through Apify Proxy with a fresh exit on every failed attempt, and retry with exponential backoff on 408, 429 and 5xx, up to 5 attempts per page. One failing page or place never kills the run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are the phone numbers personal data?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;includeContacts&lt;/code&gt; reads only the business contacts a company publishes on its own 2GIS page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does pricing work?&lt;/strong&gt;&lt;br&gt;
$0.20 per run plus $0.003 per unique place: &lt;strong&gt;$3.20 per 1,000 places&lt;/strong&gt;. Contacts enrichment costs no extra per row.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://apify.com/DevilScrapes/2gis-places-scraper" rel="noopener noreferrer"&gt;2GIS Places Scraper on Apify&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Devil Scrapes&lt;/a&gt;. We handle the pager trap, the fingerprints, the proxies and the retries, and we count unique places, not repeated pages.&lt;/em&gt; 😈&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>maps</category>
      <category>apify</category>
    </item>
    <item>
      <title>Companies House UK Scraper: lead lists by town, SIC code and incorporation date</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Mon, 12 Oct 2026 19:11:59 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/companies-house-uk-scraper-lead-lists-by-town-sic-code-and-incorporation-date-1fl5</link>
      <guid>https://dev.to/devil_scrapes/companies-house-uk-scraper-lead-lists-by-town-sic-code-and-incorporation-date-1fl5</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://apify.com/DevilScrapes/companies-house-uk-scraper" rel="noopener noreferrer"&gt;Companies House UK Scraper&lt;/a&gt; turns the UK company register into lead lists: give it a town, one or more SIC codes, a status and an incorporation date window, and it returns one row per company with number, name, status, incorporation date, company type, SIC codes and registered office address. Optionally it adds SIC descriptions and the officers list. It also does plain name search for KYC checks. No Companies House API key, no registration, no rate-limit maths. Pricing is pay-per-result, &lt;strong&gt;$0.20 per run plus $0.003 per company, $3.20 per 1,000 companies&lt;/strong&gt;, and the details and officers enrichment costs nothing extra.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isn't Companies House data already free?
&lt;/h2&gt;

&lt;p&gt;The data is free. Getting it into a spreadsheet is not. The official REST API needs a registered developer account and an API key, and it is rate limited, so a lead list of a few thousand companies with officers turns into a small engineering project: auth, paging, throttling, retries and stitching three endpoints together per company. The bulk data product is a monthly snapshot of every company in the UK, which is the wrong shape when you want "active software companies in Leeds incorporated this year".&lt;/p&gt;

&lt;p&gt;The register's own website already answers that question. Its advanced search filters by registered-office town, SIC code, status and incorporation dates in one go. We drive that search and return the rows.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Companies House's advanced search filters by town, SIC code, status and incorporation date at once, which is exactly the shape of a UK lead list.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What makes the register awkward to scrape well?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Status is free text.&lt;/strong&gt; The site shows "Active", "Dissolved", "Liquidation", "Active - Proposal to Strike off" and more. We normalise to &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;dissolved&lt;/code&gt;, &lt;code&gt;liquidation&lt;/code&gt; or &lt;code&gt;other&lt;/code&gt; in &lt;code&gt;status&lt;/code&gt; and keep the original in &lt;code&gt;statusDetail&lt;/code&gt;, so you can filter cleanly and still see the nuance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search matches previous names.&lt;/strong&gt; Search "Acme" and you get companies that were once called Acme. We return those names in &lt;code&gt;previousNames&lt;/code&gt;, so you can tell a match on today's name from a historical one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Results come 20 per page and stop at 10,000.&lt;/strong&gt; We page automatically and stop when results end or your &lt;code&gt;maxResults&lt;/code&gt; is reached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One company can surface in several queries.&lt;/strong&gt; We dedupe on &lt;code&gt;companyNumber&lt;/code&gt; across every query and page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Officers are personal data.&lt;/strong&gt; We return what the register publishes: name, role, appointment and resignation dates, nationality, country of residence and date of birth as month and year only (&lt;code&gt;dobMonthYear&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Actor gives you
&lt;/h2&gt;

&lt;p&gt;One row per company: &lt;code&gt;companyNumber&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;statusDetail&lt;/code&gt;, &lt;code&gt;incorporatedOn&lt;/code&gt;, &lt;code&gt;dissolvedOn&lt;/code&gt;, &lt;code&gt;companyType&lt;/code&gt;, &lt;code&gt;sicCodes&lt;/code&gt;, &lt;code&gt;sicDescriptions&lt;/code&gt;, &lt;code&gt;address&lt;/code&gt;, &lt;code&gt;previousNames&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;officers&lt;/code&gt; and &lt;code&gt;scrapedAt&lt;/code&gt;. Each officer carries &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;officerId&lt;/code&gt;, &lt;code&gt;role&lt;/code&gt;, &lt;code&gt;appointedOn&lt;/code&gt;, &lt;code&gt;resignedOn&lt;/code&gt;, &lt;code&gt;nationality&lt;/code&gt;, &lt;code&gt;countryOfResidence&lt;/code&gt;, &lt;code&gt;dobMonthYear&lt;/code&gt; and &lt;code&gt;address&lt;/code&gt;.&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;"companyNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"13106211"&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;"ISOROBOT LIMITED"&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;"statusDetail"&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;"incorporatedOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2020-12-31"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dissolvedOn"&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;"companyType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Private limited company"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sicCodes"&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;"62012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"62020"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"70229"&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;"20 Syke Lane, Scarcroft, Leeds, England LS14 3BQ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"previousNames"&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;"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://find-and-update.company-information.service.gov.uk/company/13106211"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"officers"&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;"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-10-11T09:00:00Z"&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;Input: &lt;code&gt;mode&lt;/code&gt; (&lt;code&gt;search&lt;/code&gt; for company-name &lt;code&gt;queries&lt;/code&gt;, &lt;code&gt;advanced&lt;/code&gt; for filters), &lt;code&gt;town&lt;/code&gt;, &lt;code&gt;sicCodes&lt;/code&gt; (4 or 5 digits, for example &lt;code&gt;62020&lt;/code&gt;), &lt;code&gt;status&lt;/code&gt; (&lt;code&gt;active&lt;/code&gt;, &lt;code&gt;dissolved&lt;/code&gt;, &lt;code&gt;liquidation&lt;/code&gt; or &lt;code&gt;any&lt;/code&gt;), &lt;code&gt;incorporatedFrom&lt;/code&gt; and &lt;code&gt;incorporatedTo&lt;/code&gt;, the &lt;code&gt;includeDetails&lt;/code&gt; and &lt;code&gt;includeOfficers&lt;/code&gt; switches, and &lt;code&gt;maxResults&lt;/code&gt; up to 10,000.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations 🚧
&lt;/h2&gt;

&lt;p&gt;Public search and company-page data only: no accounts, filing history, charges or persons with significant control. Officers cover the first page of appointments (up to 35). The incorporation end-date filter is unit-tested but has not yet been confirmed against the live site. A filter that matches nothing is a successful run that costs only the start fee.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Do I need a Companies House API key?&lt;/strong&gt;&lt;br&gt;
No. Give us names or filters and we do the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about blocks and rate limits?&lt;/strong&gt;&lt;br&gt;
That's our job. Every request carries a browser-grade TLS fingerprint through &lt;code&gt;curl-cffi&lt;/code&gt;, retries with exponential backoff on 408, 429 and 5xx up to 5 attempts per page, honours &lt;code&gt;Retry-After&lt;/code&gt;, and rotates the Apify Proxy session on every block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do details and officers cost more?&lt;/strong&gt;&lt;br&gt;
No. Each adds one request per company on our side and no extra charge on yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does pricing work?&lt;/strong&gt;&lt;br&gt;
$0.20 per run plus $0.003 per company: &lt;strong&gt;$3.20 per 1,000 companies&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://apify.com/DevilScrapes/companies-house-uk-scraper" rel="noopener noreferrer"&gt;Companies House UK Scraper on Apify&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Devil Scrapes&lt;/a&gt;. We handle the paging, the fingerprints, the proxies and the retries, so your lead list stays clean.&lt;/em&gt; 😈&lt;/p&gt;

</description>
      <category>opendata</category>
      <category>webscraping</category>
      <category>python</category>
      <category>apify</category>
    </item>
    <item>
      <title>ALDI Australia Products Scraper: every row says notForSale, and that is correct</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Mon, 12 Oct 2026 19:11:21 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/aldi-australia-products-scraper-every-row-says-notforsale-and-that-is-correct-4mel</link>
      <guid>https://dev.to/devil_scrapes/aldi-australia-products-scraper-every-row-says-notforsale-and-that-is-correct-4mel</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://apify.com/DevilScrapes/aldi-au-products-scraper" rel="noopener noreferrer"&gt;ALDI Australia Products Scraper&lt;/a&gt; pulls ALDI Australia's product catalogue by search term into JSON, CSV or Excel: SKU, name, brand, pack size, unit price in integer cents, the per-100 g or per-unit comparison price, categories and a working image URL. Give it &lt;code&gt;milk&lt;/code&gt;, &lt;code&gt;bread&lt;/code&gt; or a brand name and it pages through every match, deduplicated by SKU. One thing surprises everyone the first time: &lt;strong&gt;every row says &lt;code&gt;notForSale: true&lt;/code&gt;&lt;/strong&gt;, and that is correct. ALDI Australia sells almost nothing online. This is a price and assortment feed, not a shopping cart. Pricing is pay-per-result, &lt;strong&gt;$0.20 per run plus $0.003 per product, $3.20 per 1,000 products&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is every ALDI product "not for sale"?
&lt;/h2&gt;

&lt;p&gt;Because ALDI Australia is a walk-in retailer. The catalogue on aldi.com.au shows prices and pack sizes, but for the vast majority of items there is no checkout behind it, and ALDI's own product data says so with a &lt;code&gt;notForSale&lt;/code&gt; flag. We pass that flag through exactly as ALDI reports it. We never filter it, drop it or rewrite it.&lt;/p&gt;

&lt;p&gt;That matters more than it looks. A scraper that silently removed "not for sale" items would return an almost empty dataset. One that dropped the field would let you build a storefront that implies people can buy things they can't. Keeping it verbatim keeps your price-tracking honest.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ALDI Australia's catalogue is a price reference, not a store: nearly every product is flagged not-for-sale online, and the flag is the truth, not a bug.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What else is awkward about ALDI's catalogue data?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image links arrive as templates.&lt;/strong&gt; The asset URL ALDI ships contains literal &lt;code&gt;{width}&lt;/code&gt; and &lt;code&gt;{slug}&lt;/code&gt; placeholders. Use it as-is and every image 404s. We fill both in, so &lt;code&gt;imageUrl&lt;/code&gt; opens a real picture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prices are in cents.&lt;/strong&gt; &lt;code&gt;priceAmountCents: 399&lt;/code&gt; is $3.99. We keep the integer for maths and ALDI's own &lt;code&gt;priceDisplay&lt;/code&gt; string for humans, with no float rounding drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The comparison price is optional.&lt;/strong&gt; "$2.38 per 100 g" exists only where ALDI publishes one. We return &lt;code&gt;null&lt;/code&gt; rather than computing a figure ALDI never stated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search terms overlap.&lt;/strong&gt; "milk" and "chocolate" both find milk chocolate. We dedupe on &lt;code&gt;sku&lt;/code&gt; across every term and page, so one product is emitted and billed once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Results page 30 at a time.&lt;/strong&gt; We walk the offsets until ALDI's own total count is reached or your cap is hit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Actor gives you
&lt;/h2&gt;

&lt;p&gt;One row per product, 21 fields: &lt;code&gt;sku&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;brandName&lt;/code&gt;, &lt;code&gt;urlSlugText&lt;/code&gt;, &lt;code&gt;notForSale&lt;/code&gt;, &lt;code&gt;discontinued&lt;/code&gt;, &lt;code&gt;quantityMin&lt;/code&gt;, &lt;code&gt;quantityMax&lt;/code&gt;, &lt;code&gt;quantityInterval&lt;/code&gt;, &lt;code&gt;quantityDefault&lt;/code&gt;, &lt;code&gt;quantityUnit&lt;/code&gt;, &lt;code&gt;sellingSize&lt;/code&gt;, &lt;code&gt;priceAmountCents&lt;/code&gt;, &lt;code&gt;priceDisplay&lt;/code&gt;, &lt;code&gt;comparisonPriceCents&lt;/code&gt;, &lt;code&gt;comparisonPriceDisplay&lt;/code&gt;, &lt;code&gt;currencyCode&lt;/code&gt;, &lt;code&gt;categories&lt;/code&gt;, &lt;code&gt;imageUrl&lt;/code&gt;, &lt;code&gt;searchTerm&lt;/code&gt; and &lt;code&gt;scrapedAt&lt;/code&gt;.&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;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"000000000538441002"&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;"Milk Mouse - Milk Chocolate 168g"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"brandName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CHOCEUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notForSale"&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;"discontinued"&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;"sellingSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"168 g"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priceAmountCents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;399&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priceDisplay"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$3.99"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"comparisonPriceCents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;238&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"comparisonPriceDisplay"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$2.38 per 100 g"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currencyCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AUD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"categories"&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;"Pantry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Confectionery"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"searchTerm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"milk"&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-10-02T00: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;p&gt;Input: &lt;code&gt;searchTerms&lt;/code&gt; (one or more), &lt;code&gt;servicePoint&lt;/code&gt; (the ALDI store or region code, default &lt;code&gt;G452&lt;/code&gt; on the east coast), &lt;code&gt;maxItemsPerQuery&lt;/code&gt; up to 3,000 and a run-wide &lt;code&gt;maxResults&lt;/code&gt; up to 50,000.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations 🚧
&lt;/h2&gt;

&lt;p&gt;Catalogue prices and attributes only: no live stock, no specials-week history, no purchase path. One &lt;code&gt;servicePoint&lt;/code&gt; per run, so regional price comparisons need one run per region. The catalogue is English-only, so plain product or brand words work best. A term with no matches is a successful run that costs only the start fee.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Do I need an ALDI account or API key?&lt;/strong&gt;&lt;br&gt;
No. The Actor reads ALDI Australia's public product search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about blocks and rate limits?&lt;/strong&gt;&lt;br&gt;
That's our job. We rotate real-browser TLS fingerprints (Chrome, Firefox, Safari) through &lt;code&gt;curl-cffi&lt;/code&gt;, route through Apify Proxy, and retry with exponential backoff on 408, 429 and 5xx, up to 5 attempts per page. One bad page or malformed product never kills the run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I track ALDI prices over time?&lt;/strong&gt;&lt;br&gt;
Yes. Schedule the same search terms daily and diff &lt;code&gt;priceAmountCents&lt;/code&gt; per &lt;code&gt;sku&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does pricing work?&lt;/strong&gt;&lt;br&gt;
$0.20 per run plus $0.003 per product row: &lt;strong&gt;$3.20 per 1,000 products&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://apify.com/DevilScrapes/aldi-au-products-scraper" rel="noopener noreferrer"&gt;ALDI Australia Products Scraper on Apify&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Devil Scrapes&lt;/a&gt;. We handle the fingerprints, the proxies, the retries and the template URLs. We do the dirty work so your dataset stays clean.&lt;/em&gt; 😈&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>ecommerce</category>
      <category>apify</category>
    </item>
    <item>
      <title>Flashscore Live Scores Scraper: the scores feed is not JSON, it is and</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Mon, 12 Oct 2026 19:10:43 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/flashscore-live-scores-scraper-the-scores-feed-is-not-json-it-is-and--i40</link>
      <guid>https://dev.to/devil_scrapes/flashscore-live-scores-scraper-the-scores-feed-is-not-json-it-is-and--i40</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://apify.com/DevilScrapes/flashscore-live-scores-scraper" rel="noopener noreferrer"&gt;Flashscore Live Scores Scraper&lt;/a&gt; turns Flashscore's fixtures, live scores and final results for football, tennis, basketball and hockey into clean JSON, CSV or Excel rows: teams, scores, status, kickoff time in UTC, league, country and a link to the event page. Pick a sport and the days you want (from a week back to a week ahead), optionally narrow by league, and run it. Flashscore has no bulk export and no public API, and the data behind the page is not JSON at all. It is a proprietary delimited feed, and we parse it for you. Pricing is pay-per-result, &lt;strong&gt;$0.20 per run plus $0.002 per event, $2.20 per 1,000 events&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Flashscore have an API?
&lt;/h2&gt;

&lt;p&gt;Not a public one. The scores you see on flashscore.com are painted by the site's own JavaScript from a feed host it calls in the background. That feed only answers requests that carry a header value the site's front-end bundle supplies, and the value can change when the bundle does. So "just call the API" means reverse-engineering a moving target and keeping up with it. That part is our job: when the feed host starts rejecting requests, the Actor reports it as a rotated header rather than as a vague network error, and we ship the update.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the feed actually look like?
&lt;/h2&gt;

&lt;p&gt;Not JSON. One day of football is a single text body where records are split by &lt;code&gt;¬~&lt;/code&gt;, fields by &lt;code&gt;¬&lt;/code&gt;, and keys from values by &lt;code&gt;÷&lt;/code&gt;. A trimmed slice of a real feed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;¬~ZA÷AFRICA: Olympic Games Women - Qualification¬ZY÷Africa¬ZL÷/football/world/olympic-games-women/
¬~AA÷GvGtelQO¬AD÷1791385200¬AB÷3¬AE÷Guinea W¬AF÷Cameroon W¬WM÷GUI¬WN÷CAM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The keys are two- and three-letter codes. &lt;code&gt;AA&lt;/code&gt; is the event id, &lt;code&gt;AD&lt;/code&gt; the kickoff as a Unix epoch, &lt;code&gt;AB&lt;/code&gt; the status (&lt;code&gt;1&lt;/code&gt; scheduled, &lt;code&gt;2&lt;/code&gt; live, &lt;code&gt;3&lt;/code&gt; finished), &lt;code&gt;AE&lt;/code&gt; and &lt;code&gt;AF&lt;/code&gt; the home and away sides, and &lt;code&gt;AG&lt;/code&gt; and &lt;code&gt;AH&lt;/code&gt; the scores once a match has started.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Flashscore's live scores travel as a delimited text feed with two-letter keys, and the league is a header record, not a field on the match.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What goes wrong if you parse it naively?
&lt;/h2&gt;

&lt;p&gt;Three things, each covered by a test against real captured feeds.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The league is a header, not a column.&lt;/strong&gt; A &lt;code&gt;ZA&lt;/code&gt; record names the league, then every match below it belongs to that league until the next &lt;code&gt;ZA&lt;/code&gt;. Split the body into records and treat each one alone, and every match loses its league. We carry the header forward and stamp &lt;code&gt;league&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;league_url&lt;/code&gt; and &lt;code&gt;tournament_id&lt;/code&gt; onto each event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keys repeat inside a record.&lt;/strong&gt; The first value is the right one. Last-write-wins quietly swaps fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A finished match can carry no score keys.&lt;/strong&gt; The Guinea vs Cameroon event above is status &lt;code&gt;3&lt;/code&gt; with no &lt;code&gt;AG&lt;/code&gt; or &lt;code&gt;AH&lt;/code&gt;. We return &lt;code&gt;null&lt;/code&gt; rather than inventing &lt;code&gt;0-0&lt;/code&gt;, because a fake score is worse than a missing one in any model you feed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Across several days the same event can appear in more than one feed, so we dedupe on &lt;code&gt;event_id&lt;/code&gt; and you never pay twice for one match.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Actor gives you
&lt;/h2&gt;

&lt;p&gt;One row per event, 19 fields: &lt;code&gt;event_id&lt;/code&gt;, &lt;code&gt;sport&lt;/code&gt;, &lt;code&gt;day_offset&lt;/code&gt;, &lt;code&gt;league&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;league_url&lt;/code&gt;, &lt;code&gt;tournament_id&lt;/code&gt;, &lt;code&gt;home_team&lt;/code&gt;, &lt;code&gt;away_team&lt;/code&gt;, &lt;code&gt;home_team_short&lt;/code&gt;, &lt;code&gt;away_team_short&lt;/code&gt;, &lt;code&gt;home_score&lt;/code&gt;, &lt;code&gt;away_score&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;stage_code&lt;/code&gt;, &lt;code&gt;kickoff_utc&lt;/code&gt;, &lt;code&gt;kickoff_epoch&lt;/code&gt;, &lt;code&gt;event_url&lt;/code&gt; and &lt;code&gt;scraped_at&lt;/code&gt;.&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;"event_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GvGtelQO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sport"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"football"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"day_offset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&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;"league"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AFRICA: Olympic Games Women - Qualification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"home_team"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Guinea W"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"away_team"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cameroon W"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"home_score"&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;"away_score"&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;"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;"finished"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kickoff_utc"&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-10-05T13:00:00+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event_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.flashscore.com/match/GvGtelQO/"&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;Input: &lt;code&gt;sport&lt;/code&gt; (1 football, 2 tennis, 3 basketball, 4 hockey), &lt;code&gt;dayOffsets&lt;/code&gt; (for example &lt;code&gt;[0, 1]&lt;/code&gt; for today and tomorrow, -7 to 7, up to 15 days), an optional &lt;code&gt;leagueFilter&lt;/code&gt; substring such as &lt;code&gt;premier league&lt;/code&gt;, and &lt;code&gt;maxEvents&lt;/code&gt; up to 5,000.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations 🚧
&lt;/h2&gt;

&lt;p&gt;One sport per run. Fixtures, scores and status only: no odds, lineups or match statistics. Each run is a snapshot, not a stream, so schedule it to follow live matches. Tennis days are large, so fewer days means a cheaper run. A league filter that matches nothing is a successful run that costs only the start fee.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Do I need a Flashscore account?&lt;/strong&gt;&lt;br&gt;
No. The Actor reads the same public feed the website uses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about blocks and rate limits?&lt;/strong&gt;&lt;br&gt;
That's our job. Every request carries a real-browser TLS fingerprint through &lt;code&gt;curl-cffi&lt;/code&gt;, goes through Apify Proxy with a fresh session on every retry, and backs off exponentially on rate limits and server errors, honouring &lt;code&gt;Retry-After&lt;/code&gt;. One day that still fails is logged and skipped. The run fails only if every requested day fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I track live scores?&lt;/strong&gt;&lt;br&gt;
Yes, with Apify Schedules: run it every few minutes on &lt;code&gt;dayOffsets: [0]&lt;/code&gt; and react to &lt;code&gt;status&lt;/code&gt; and score changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does pricing work?&lt;/strong&gt;&lt;br&gt;
$0.20 per run plus $0.002 per event row: &lt;strong&gt;$2.20 per 1,000 events&lt;/strong&gt;. No data, no charge beyond the start fee.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://apify.com/DevilScrapes/flashscore-live-scores-scraper" rel="noopener noreferrer"&gt;Flashscore Live Scores Scraper on Apify&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Devil Scrapes&lt;/a&gt;. We handle the feed format, the fingerprints, the proxies and the retries, so your scores table stays clean.&lt;/em&gt; 😈&lt;/p&gt;

</description>
      <category>sports</category>
      <category>webscraping</category>
      <category>python</category>
      <category>apify</category>
    </item>
    <item>
      <title>Meetup Events Scraper: the search page ships its whole Apollo cache, and that is the API</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Thu, 08 Oct 2026 16:01:17 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/meetup-events-scraper-the-search-page-ships-its-whole-apollo-cache-and-that-is-the-api-3575</link>
      <guid>https://dev.to/devil_scrapes/meetup-events-scraper-the-search-page-ships-its-whole-apollo-cache-and-that-is-the-api-3575</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://apify.com/DevilScrapes/meetup-events-scraper" rel="noopener noreferrer"&gt;Meetup Events Scraper&lt;/a&gt; pulls public Meetup.com events by keyword and city into JSON, CSV or Excel: title, start time with timezone, in-person or online, venue with coordinates, the aggregate "going" count, capacity, fee, and the hosting group with its Meetup URL. No login and no API key. Meetup's own API is positioned for Meetup Pro customers, so we read the data the public search page already embeds instead: a normalized Apollo GraphQL cache inside the page's &lt;code&gt;__NEXT_DATA__&lt;/code&gt; script tag. Pricing is pay-per-result, &lt;strong&gt;$0.20 per run plus $0.004 per event, $4.20 per 1,000 events&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Meetup have a public API?
&lt;/h2&gt;

&lt;p&gt;Not for this. Meetup's &lt;a href="https://help.meetup.com/hc/en-us/articles/41455194927373" rel="noopener noreferrer"&gt;help centre&lt;/a&gt; describes its API as a way for Meetup Pro customers to build integrations and automate the networks they manage. For the question most people actually have, "which AI meetups are happening in New York next month and how many people are going", that means a paid organiser subscription and an OAuth flow to reach data that is already printed on a public web page. The search page at &lt;code&gt;meetup.com/find/?keywords=ai&amp;amp;location=New%20York%2C%20NY&lt;/code&gt; renders without a session, and the server sends more than the HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where does the data actually come from?
&lt;/h2&gt;

&lt;p&gt;Meetup's site is a Next.js app, and like every Next.js page it carries a &lt;code&gt;&amp;lt;script id="__NEXT_DATA__"&amp;gt;&lt;/code&gt; tag with the props the page was rendered from. Under &lt;code&gt;props.pageProps.__APOLLO_STATE__&lt;/code&gt; sits the Apollo Client cache for that render: the GraphQL result of the search, flattened into entities keyed by type and id.&lt;/p&gt;

&lt;p&gt;In our golden fixture that cache has 11 keys: one &lt;code&gt;ROOT_QUERY&lt;/code&gt;, five &lt;code&gt;Event:*&lt;/code&gt; entities and five &lt;code&gt;Group:*&lt;/code&gt; entities. The search result itself lives under a &lt;code&gt;ROOT_QUERY&lt;/code&gt; key that embeds the filter in its name, something like &lt;code&gt;eventSearch:{"filter":{"city":"New York","country":"us",...}}&lt;/code&gt;, and each edge's &lt;code&gt;node&lt;/code&gt; is not an event but a pointer: &lt;code&gt;{"__ref": "Event:315229748"}&lt;/code&gt;. The event entity points at its group the same way. Venue and RSVP data are nested inline.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Meetup's public search page embeds the GraphQL response it was rendered from, as a normalized Apollo cache, in a script tag anyone can read.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the parser is a small dereferencer, not a DOM walker:&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;_make_deref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apollo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__ref&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;value&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;apollo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__ref&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;or&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;value&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;deref&lt;/span&gt;

&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;          &lt;span class="c1"&gt;# Event:315229748
&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&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;group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;    &lt;span class="c1"&gt;# Group:29431902
&lt;/span&gt;&lt;span class="n"&gt;venue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&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;venue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;    &lt;span class="c1"&gt;# inline dict, passes through
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What goes wrong if you treat the cache as a flat list?
&lt;/h2&gt;

&lt;p&gt;Four things, each of which we hit once and now test for.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The search key is not stable.&lt;/strong&gt; It contains the serialized filter, so a hardcoded key breaks the moment the location changes. We match on the &lt;code&gt;eventSearch&lt;/code&gt; prefix and take the first hit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nodes are pointers.&lt;/strong&gt; Read &lt;code&gt;edge["node"]["title"]&lt;/code&gt; and you get a &lt;code&gt;KeyError&lt;/code&gt;, because the node is &lt;code&gt;{"__ref": ...}&lt;/code&gt;. Every &lt;code&gt;__ref&lt;/code&gt; has to be resolved against the cache, including the group inside the event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Free" is the absence of a field.&lt;/strong&gt; There is no &lt;code&gt;isFree&lt;/code&gt; boolean. A free event has &lt;code&gt;feeSettings: null&lt;/code&gt;; a paid one carries &lt;code&gt;{amount, currency}&lt;/code&gt;. We derive &lt;code&gt;is_free&lt;/code&gt; from that and emit &lt;code&gt;fee_amount&lt;/code&gt; and &lt;code&gt;fee_currency&lt;/code&gt; only when a fee exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Online events still have a venue.&lt;/strong&gt; It is a placeholder with blank strings. We collapse blanks to &lt;code&gt;null&lt;/code&gt;, so an online event reads &lt;code&gt;venue_name: null&lt;/code&gt; rather than &lt;code&gt;""&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The RSVP count comes from &lt;code&gt;rsvps.totalCount&lt;/code&gt;, an aggregate. The Actor emits that number and nothing about who is going. Member identities are never requested, which keeps the dataset GDPR-safe by construction rather than by a filter you have to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Actor gives you
&lt;/h2&gt;

&lt;p&gt;One row per public event, 23 fields: &lt;code&gt;search_keyword&lt;/code&gt;, &lt;code&gt;event_id&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;event_url&lt;/code&gt;, &lt;code&gt;date_time&lt;/code&gt; (ISO-8601 with offset, e.g. &lt;code&gt;2026-07-21T08:30:00-04:00&lt;/code&gt;), &lt;code&gt;event_type&lt;/code&gt; and &lt;code&gt;is_online&lt;/code&gt;, &lt;code&gt;going_count&lt;/code&gt;, &lt;code&gt;max_tickets&lt;/code&gt;, &lt;code&gt;is_free&lt;/code&gt;, &lt;code&gt;fee_amount&lt;/code&gt;, &lt;code&gt;fee_currency&lt;/code&gt;, &lt;code&gt;venue_name&lt;/code&gt;, &lt;code&gt;venue_address&lt;/code&gt;, &lt;code&gt;venue_city&lt;/code&gt;, &lt;code&gt;venue_state&lt;/code&gt;, &lt;code&gt;venue_country&lt;/code&gt;, &lt;code&gt;latitude&lt;/code&gt;, &lt;code&gt;longitude&lt;/code&gt;, &lt;code&gt;group_name&lt;/code&gt;, &lt;code&gt;group_url&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt; and &lt;code&gt;scraped_at&lt;/code&gt;. Pass several &lt;code&gt;keywords&lt;/code&gt; in one run, optionally a &lt;code&gt;location&lt;/code&gt;, and set &lt;code&gt;eventType&lt;/code&gt; to &lt;code&gt;any&lt;/code&gt;, &lt;code&gt;physical&lt;/code&gt; or &lt;code&gt;online&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations 🚧
&lt;/h2&gt;

&lt;p&gt;One search page is one request and yields roughly 27 events per keyword, in Meetup's own relevance order. We do not paginate past it, so breadth comes from more keywords, not deeper pages. &lt;code&gt;maxEventsPerKeyword&lt;/code&gt; caps at 500. A keyword with no matches is a successful run and costs only the start fee. Descriptions are Meetup's markdown, kept verbatim; switch &lt;code&gt;includeDescription&lt;/code&gt; off for a leaner row.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Do I need a Meetup account or API key?&lt;/strong&gt;&lt;br&gt;
No. The Actor reads public search pages only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about blocks and rate limits?&lt;/strong&gt;&lt;br&gt;
That is our job. Every request replays a real browser TLS handshake through &lt;code&gt;curl-cffi&lt;/code&gt;, rotating between Chrome, Firefox and Safari profiles, runs through Apify Proxy, and retries with exponential backoff on 403, 408, 429 and 5xx responses, up to 5 attempts per page with the delay capped at 30 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it collect attendee data?&lt;/strong&gt;&lt;br&gt;
No. Only the aggregate going count. No names, no profiles, no member IDs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I get a specific group's events rather than a keyword search?&lt;/strong&gt;&lt;br&gt;
Not in this version. Use the group's name as a keyword and filter on &lt;code&gt;group_url&lt;/code&gt; in the output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does pricing work?&lt;/strong&gt;&lt;br&gt;
$0.20 per run plus $0.004 per event row: &lt;strong&gt;$4.20 per 1,000 events&lt;/strong&gt;. No data, no charge beyond the start fee.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://apify.com/DevilScrapes/meetup-events-scraper" rel="noopener noreferrer"&gt;Meetup Events Scraper on Apify&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Devil Scrapes&lt;/a&gt;. We handle the fingerprints, the proxies and the retries, and we test the parser against a real search payload before a customer ever runs it. We do the dirty work so your dataset stays clean.&lt;/em&gt; 😈&lt;/p&gt;

</description>
      <category>meetup</category>
      <category>webscraping</category>
      <category>python</category>
      <category>apify</category>
    </item>
    <item>
      <title>Bayt Jobs Scraper: the proxy country pin that was the block</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Sat, 26 Sep 2026 10:06:59 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/bayt-jobs-scraper-the-proxy-country-pin-that-was-the-block-4281</link>
      <guid>https://dev.to/devil_scrapes/bayt-jobs-scraper-the-proxy-country-pin-that-was-the-block-4281</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;We pinned our Bayt.com scraper's proxy to the country it scrapes, and that pin was the block. On Apify residential exits in the UAE, cloud QA landed &lt;strong&gt;0 rows in 15 attempts&lt;/strong&gt;. We changed one field to a UK residential exit and the same build returned &lt;strong&gt;20 rows&lt;/strong&gt;, then a full 60-row run with no blocked pages. The data stayed genuinely Emirati because Bayt decides the country from the URL path (&lt;code&gt;/en/uae/jobs/...&lt;/code&gt;), not from where your IP sits. The &lt;a href="https://apify.com/DevilScrapes/bayt-jobs-scraper" rel="noopener noreferrer"&gt;Bayt Jobs Scraper&lt;/a&gt; now defaults to &lt;code&gt;RESIDENTIAL&lt;/code&gt; / &lt;code&gt;GB&lt;/code&gt;, and you pick the market with the &lt;code&gt;country&lt;/code&gt; input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shouldn't a proxy match the country you're scraping?
&lt;/h2&gt;

&lt;p&gt;Usually, yes. A geo-random exit can quietly give you plausible but wrong data: prices in another currency, or another region's catalogue. Pinning the exit is normally the safe default, and it's our fleet rule for exactly that reason. The rule assumes the site uses your IP to pick the country. Bayt doesn't. It scopes every search by a path segment, so &lt;code&gt;/en/uae/jobs/accountant-jobs/&lt;/code&gt; returns UAE jobs whatever exit the request comes from. That leaves the pin with a cost and no benefit. The UAE residential pool was the one Bayt's Cloudflare layer challenged hardest, so pinning to it made us look &lt;em&gt;more&lt;/em&gt; suspicious.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you tell "the pin is the block" apart from "the site blocks us"?
&lt;/h2&gt;

&lt;p&gt;Change one variable. Same build, same input, same residential tier, and only the exit country different: AE went 0 for 15, GB cleared on the first attempt and kept clearing over three spaced runs. Swapping tiers or TLS profiles at the same time would have hidden which change did the work. Before you shelve a target as unreachable, try a neutral exit.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the site picks its country from the URL, the exit country is just one more fingerprint, and the local one can be the most-watched.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What the Actor gives you
&lt;/h2&gt;

&lt;p&gt;One row per job: &lt;code&gt;job_id&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;company&lt;/code&gt;, &lt;code&gt;company_url&lt;/code&gt;, &lt;code&gt;location&lt;/code&gt;, &lt;code&gt;career_level&lt;/code&gt;, &lt;code&gt;salary&lt;/code&gt; (only on postings that publish it), a &lt;code&gt;remote&lt;/code&gt; flag, &lt;code&gt;summary&lt;/code&gt;, &lt;code&gt;posted_at_raw&lt;/code&gt; (e.g. "3 days ago"), the absolute &lt;code&gt;detail_url&lt;/code&gt;, and the &lt;code&gt;keyword&lt;/code&gt; that found it. Pass several &lt;code&gt;keywords&lt;/code&gt; in one run, and set &lt;code&gt;country&lt;/code&gt; to &lt;code&gt;uae&lt;/code&gt;, &lt;code&gt;saudi-arabia&lt;/code&gt;, &lt;code&gt;egypt&lt;/code&gt; or any other Bayt market path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations 🚧
&lt;/h2&gt;

&lt;p&gt;List-page fields only, with no follow-through to detail pages. The posted date is Bayt's own relative text, kept verbatim rather than guessed into a timestamp. Salary appears on a minority of postings, and we return null rather than invent it. A search with no matches is a successful run.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Do I need a Bayt account?&lt;/strong&gt;&lt;br&gt;
No. It reads Bayt's public search result pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why a UK proxy for UAE jobs?&lt;/strong&gt;&lt;br&gt;
Because Bayt picks the market from the URL, and UK residential exits cleared where UAE ones were challenged. The rows are still the UAE (or whichever &lt;code&gt;country&lt;/code&gt; you set).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about blocks and captchas?&lt;/strong&gt;&lt;br&gt;
That's our job. We rotate browser fingerprints, retry with backoff on rate limits and challenges, and rotate residential sessions on every block.&lt;/p&gt;

&lt;p&gt;$0.20 per run plus $0.002 per job row: &lt;strong&gt;$2.20 per 1,000 results&lt;/strong&gt;. A zero-match search succeeds and costs only the start fee.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://apify.com/DevilScrapes/bayt-jobs-scraper" rel="noopener noreferrer"&gt;Bayt Jobs Scraper on Apify&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Devil Scrapes&lt;/a&gt;. We handle the fingerprints, the proxies and the retries, and we test the proxy default we ship against the target before a customer ever runs it.&lt;/em&gt;&lt;/p&gt;

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