<?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: Jefferson Valandro</title>
    <description>The latest articles on DEV Community by Jefferson Valandro (@jeffev).</description>
    <link>https://dev.to/jeffev</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%2F4158694%2F98f15f5d-1493-420a-a8bd-6120bc473057.jpg</url>
      <title>DEV Community: Jefferson Valandro</title>
      <link>https://dev.to/jeffev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jeffev"/>
    <language>en</language>
    <item>
      <title>7 tricks and 3 traps from scraping Twitch, YouTube, TikTok, Kick, Bluesky and Patreon with no login and no browser</title>
      <dc:creator>Jefferson Valandro</dc:creator>
      <pubDate>Sat, 10 Oct 2026 18:35:50 +0000</pubDate>
      <link>https://dev.to/jeffev/7-tricks-and-3-traps-from-scraping-twitch-youtube-tiktok-kick-bluesky-and-patreon-with-no-login-230</link>
      <guid>https://dev.to/jeffev/7-tricks-and-3-traps-from-scraping-twitch-youtube-tiktok-kick-bluesky-and-patreon-with-no-login-230</guid>
      <description>&lt;p&gt;Over the last few weeks I built a family of scrapers for creator data: profiles, followers, social links and business contacts. My rule was no headless browser and no login, plain HTTP only. That makes them cheaper, faster and less fragile. Most of this isn't documented anywhere, so here is what worked, what didn't, and the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Twitch: the public GraphQL does almost everything, except pagination
&lt;/h2&gt;

&lt;p&gt;The Twitch website talks to &lt;code&gt;gql.twitch.tv/gql&lt;/code&gt; with a public &lt;code&gt;Client-Id&lt;/code&gt; (the web player's). With it you get profiles, followers, live status, VODs, clips and even the &lt;strong&gt;channel panels&lt;/strong&gt;, which is where streamers put their business email.&lt;/p&gt;

&lt;p&gt;The trap: paginating with &lt;code&gt;after:&lt;/code&gt; returns &lt;code&gt;failed integrity check&lt;/code&gt;, because it needs an integrity token generated in the browser. The workaround is to not paginate. Each query accepts &lt;code&gt;first: 100&lt;/code&gt;, so I run one query per language and take the 100 most-watched streams of each.&lt;/p&gt;

&lt;p&gt;In practice, for VALORANT + Portuguese, &lt;strong&gt;6 out of 10 streamers had a contact email in their panels&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Bluesky: search works without login, but only on one host
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;public.api.bsky.app&lt;/code&gt; serves profiles, feeds and followers, but &lt;code&gt;searchPosts&lt;/code&gt; returns 403 there. On &lt;code&gt;api.bsky.app&lt;/code&gt; the same search works unauthenticated. The &lt;code&gt;cursor&lt;/code&gt; for page two, though, returns 403.&lt;/p&gt;

&lt;p&gt;The workaround is to walk back in time: the next call uses &lt;code&gt;until&lt;/code&gt; = the oldest &lt;code&gt;indexedAt&lt;/code&gt; of the previous page. That gets thousands of posts per keyword, no login.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. YouTube: the About tab is already in the HTML
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/@channel/about&lt;/code&gt; ships an &lt;code&gt;aboutChannelViewModel&lt;/code&gt; inside &lt;code&gt;ytInitialData&lt;/code&gt;, with subscribers, views, country, join date, links and the description. It also has a &lt;code&gt;signInForBusinessEmail&lt;/code&gt; field. The business email itself sits behind login and captcha, but that field tells you &lt;strong&gt;whether the channel has one&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Channel search uses the &lt;code&gt;sp=EgIQAg==&lt;/code&gt; filter plus &lt;code&gt;youtubei/v1/search&lt;/code&gt; continuations. Two gotchas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the right continuation token is inside &lt;code&gt;continuationItemRenderer&lt;/code&gt;, not the first &lt;code&gt;continuationCommand&lt;/code&gt; you find;&lt;/li&gt;
&lt;li&gt;in the new layout, the subscriber count comes in a field called &lt;code&gt;videoCountText&lt;/code&gt;. Really.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. TikTok: profiles are easy, video lists are not
&lt;/h2&gt;

&lt;p&gt;The whole profile is in &lt;code&gt;&amp;lt;script id="__UNIVERSAL_DATA_FOR_REHYDRATION__"&amp;gt;&lt;/code&gt;: followers, likes, bio link and business category. So are the stats of a single video, from its URL. A profile's video list needs a signature (&lt;code&gt;X-Bogus&lt;/code&gt;), and I skipped it.&lt;/p&gt;

&lt;p&gt;Fun fact: from the cloud, residential proxies got an &lt;strong&gt;empty&lt;/strong&gt; page, while datacenter IPs worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Kick, Linktree and Patreon
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kick&lt;/strong&gt;: &lt;code&gt;kick.com/api/v2/channels/&amp;lt;slug&amp;gt;&lt;/code&gt; returns the channel and its social links. &lt;code&gt;web.kick.com/api/v1/livestreams&lt;/code&gt; lists live streams with a real cursor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linktree&lt;/strong&gt;: everything is in &lt;code&gt;__NEXT_DATA__&lt;/code&gt;, including country, plan and page creation date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patreon&lt;/strong&gt;: there is a public JSON:API with search and membership tiers. Patron counts show up in search results even when the creator hides them on their page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Trap 1: Cloudflare looks at your HTTP library
&lt;/h2&gt;

&lt;p&gt;On Patreon, the same URL from the same cloud IP returned 200 with &lt;code&gt;requests&lt;/code&gt; and 403 ("Just a moment...") with &lt;code&gt;httpx&lt;/code&gt;. That's the TLS fingerprint. So "it worked in my test" means nothing if the test used a different library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2: memory with 2.7 million rows
&lt;/h2&gt;

&lt;p&gt;Another product of mine matches Google Maps places to Brazil's company registry. Searching São Paulo loaded the whole city (2.7M companies) and blew past 4 GB.&lt;/p&gt;

&lt;p&gt;The fix was a two-pass filter in PyArrow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;count how many companies in the city contain each searched word;&lt;/li&gt;
&lt;li&gt;keep only rows with the same phone, the same postal code, or rare words (name matching gives up on words with more than 400 occurrences anyway).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before shipping, I diffed old against new on 210 places: &lt;strong&gt;zero differences&lt;/strong&gt;. Memory went from 3.9 GB (crashing) to 350 MB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3: marketplace search rewards whoever got there first
&lt;/h2&gt;

&lt;p&gt;I publish these on the Apify Store, priced per result. Plenty of people open the pages, about 15% open the input form, and half of those run it. But in the store's own search, new products barely show up, because ranking weighs users and reviews. Outside posts brought the visitors.&lt;/p&gt;

&lt;p&gt;The first scraper outside my Brazilian-company niche to get a paying customer was the Twitch one, on the day it went live.&lt;/p&gt;




&lt;p&gt;If you want to try them, they're on Apify (the free plan includes $5 of monthly credit):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Twitch: &lt;a href="https://apify.com/jeffev/twitch-scraper" rel="noopener noreferrer"&gt;https://apify.com/jeffev/twitch-scraper&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;YouTube channels with email: &lt;a href="https://apify.com/jeffev/youtube-channel-scraper" rel="noopener noreferrer"&gt;https://apify.com/jeffev/youtube-channel-scraper&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TikTok profiles: &lt;a href="https://apify.com/jeffev/tiktok-profile-scraper" rel="noopener noreferrer"&gt;https://apify.com/jeffev/tiktok-profile-scraper&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Kick: &lt;a href="https://apify.com/jeffev/kick-scraper" rel="noopener noreferrer"&gt;https://apify.com/jeffev/kick-scraper&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Linktree and link-in-bio pages: &lt;a href="https://apify.com/jeffev/link-in-bio-scraper" rel="noopener noreferrer"&gt;https://apify.com/jeffev/link-in-bio-scraper&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Patreon: &lt;a href="https://apify.com/jeffev/patreon-creator-scraper" rel="noopener noreferrer"&gt;https://apify.com/jeffev/patreon-creator-scraper&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bluesky: &lt;a href="https://apify.com/jeffev/bluesky-scraper" rel="noopener noreferrer"&gt;https://apify.com/jeffev/bluesky-scraper&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>api</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I turned Brazil's 73M-establishment company registry into second-fast lookups, with no server and no database</title>
      <dc:creator>Jefferson Valandro</dc:creator>
      <pubDate>Sat, 03 Oct 2026 00:36:17 +0000</pubDate>
      <link>https://dev.to/jeffev/i-turned-brazils-73m-establishment-company-registry-into-second-fast-lookups-with-no-server-and-2bge</link>
      <guid>https://dev.to/jeffev/i-turned-brazils-73m-establishment-company-registry-into-second-fast-lookups-with-no-server-and-2bge</guid>
      <description>&lt;p&gt;Brazil's tax authority (Receita Federal) publishes its entire company registry (CNPJ) as open data every month: every company, its address, phone, e-mail, industry code (CNAE), size, tax regime and partners. It's a goldmine for B2B sales, and it's painful to use: 37 zip files, ~8 GB compressed, ~27 GB of semicolon-separated text, and several traps along the way.&lt;/p&gt;

&lt;p&gt;I wanted answers like &lt;em&gt;"restaurants opened in the last 90 days in Campinas, with a phone number"&lt;/em&gt; or &lt;em&gt;"which company owns &lt;code&gt;acme.com.br&lt;/code&gt;?"&lt;/em&gt; in seconds, without paying for a server or a database. Here's the setup and, more usefully, everything that went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receita zips ─► DuckDB (on disk) ─► Parquet ─► private Hugging Face dataset
                                                        │
                 Apify Actors (DuckDB / pyarrow) ◄──────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DuckDB&lt;/strong&gt; joins establishments, companies, the Simples/MEI table and lookup tables.&lt;/li&gt;
&lt;li&gt;The result becomes &lt;strong&gt;Parquet laid out four ways&lt;/strong&gt;, one per query shape: per state sorted by industry code (lead search), per CNPJ prefix (bulk lookup), per city (matching Google Maps places) and per e-mail domain.&lt;/li&gt;
&lt;li&gt;The files live in a &lt;strong&gt;private Hugging Face dataset&lt;/strong&gt;: free, no credit card, and it serves HTTP range requests.&lt;/li&gt;
&lt;li&gt;Each search runs as an &lt;strong&gt;Apify Actor&lt;/strong&gt; that reads only the row groups it needs. Because files are sorted, per-row-group min/max statistics tell you exactly where to look.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The traps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The files say ISO-8859-1. They aren't.&lt;/strong&gt; Some bytes only make sense in Windows-1252 and DuckDB's latin-1 reader rejects the file. I transcode during extraction: &lt;code&gt;chunk.decode("cp1252", errors="replace")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;ignore_errors=true&lt;/code&gt; silently dropped 74% of the companies.&lt;/strong&gt; Names like &lt;code&gt;BAR ""DO ZE"" LTDA&lt;/code&gt; use doubled quotes as escapes. With default settings DuckDB treated those lines as malformed and &lt;code&gt;ignore_errors&lt;/code&gt; skipped them without a word: my first build had 8M active establishments instead of 28M. The fix is &lt;code&gt;escape='"'&lt;/code&gt;; the real lesson is &lt;strong&gt;always compare parsed rows with the file's line count&lt;/strong&gt;. The converter now refuses to publish when they differ.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. 32 GB of RAM wasn't enough.&lt;/strong&gt; Joining 28M establishments with 70M companies and sorting got the process killed. A disk-backed DuckDB database, &lt;code&gt;memory_limit='6GB'&lt;/code&gt;, a temp directory and one state at a time fixed it. For the 73M-row lookup table, a single global sort hadn't finished after 20 minutes; partitioning by CNPJ prefix in one pass and sorting each ~700k-row piece took 15 minutes total.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. DuckDB over HTTP was slow for point lookups.&lt;/strong&gt; Every query re-validated the file and followed Hugging Face's redirect: ~2 s per CNPJ. Switching to &lt;code&gt;pyarrow&lt;/code&gt; + &lt;code&gt;HfFileSystem&lt;/code&gt; changed that: read each file's footer once, group the requested keys by the row group they fall in (min/max stats), fetch each row group once, 64 in parallel. Shrinking row groups from 20k to 2k rows cut the bytes downloaded for 2,000 scattered keys from ~3.2 GB to ~0.5 GB. Net result: &lt;strong&gt;209 s → 53 s for 2,000 lookups, at 1/6 of the cost.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The registered e-mail is often the accountant's.&lt;/strong&gt; One accounting firm appears as the contact e-mail of 438 companies. To find a company by domain, the chosen name has to &lt;em&gt;resemble&lt;/em&gt; the domain (&lt;code&gt;corsi.com.br&lt;/code&gt; → CORSI CONTABILIDADE); names that only share the city ("... CAMPINAS") don't count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Matching Google Maps places to the registry.&lt;/strong&gt; Phone and ZIP + street number produce candidates; name similarity and industry compatibility decide (a dentist must not match the real-estate agency in the same building). On 180 real places in Campinas, 60% matched, almost all with high confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy (LGPD)
&lt;/h2&gt;

&lt;p&gt;It's public data, but: individual micro-entrepreneurs (MEI) are excluded from lead searches by default, partners come without their personal tax ID, and the personal ID that the registry appends to sole-proprietor names is stripped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping it fresh
&lt;/h2&gt;

&lt;p&gt;A Windows scheduled task checks for a new month every night. When one appears it downloads, converts, compares row counts with the previous month, uploads to Hugging Face, &lt;strong&gt;verifies every file actually arrived&lt;/strong&gt; (I once had an upload "succeed" without a whole folder), and runs a test against each Actor. Any failure becomes a desktop notification instead of a silently broken product.&lt;/p&gt;

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

&lt;p&gt;The four searches are public on Apify, pay per result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://apify.com/jeffev/leads-empresas-brasil-cnpj" rel="noopener noreferrer"&gt;Brazilian company leads by industry and city&lt;/a&gt;, including companies opened in the last N days&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apify.com/jeffev/consulta-cnpj-lote" rel="noopener noreferrer"&gt;Bulk CNPJ lookup&lt;/a&gt; with registration status, alphanumeric CNPJ supported&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apify.com/jeffev/encontrar-cnpj" rel="noopener noreferrer"&gt;Find the CNPJ of Google Maps places&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apify.com/jeffev/cnpj-por-dominio" rel="noopener noreferrer"&gt;CNPJ by website domain or e-mail&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've worked with Brazilian open data and hit another trap, I'd love to hear about it in the comments.&lt;/p&gt;

</description>
      <category>duckdb</category>
      <category>python</category>
      <category>opendata</category>
      <category>dataengineering</category>
    </item>
  </channel>
</rss>
