<?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: Clear Fetch</title>
    <description>The latest articles on DEV Community by Clear Fetch (@clearfetch).</description>
    <link>https://dev.to/clearfetch</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%2F4111537%2Fbe43658b-7619-4a9f-854f-629180d5d6ae.png</url>
      <title>DEV Community: Clear Fetch</title>
      <link>https://dev.to/clearfetch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/clearfetch"/>
    <language>en</language>
    <item>
      <title>Tennis, football and basketball results as clean JSON, without a sports data contract</title>
      <dc:creator>Clear Fetch</dc:creator>
      <pubDate>Sat, 05 Sep 2026 19:10:43 +0000</pubDate>
      <link>https://dev.to/clearfetch/tennis-football-and-basketball-results-as-clean-json-without-a-sports-data-contract-3b19</link>
      <guid>https://dev.to/clearfetch/tennis-football-and-basketball-results-as-clean-json-without-a-sports-data-contract-3b19</guid>
      <description>&lt;p&gt;Sports data is one of those areas where the gap between "public information" and "affordable to obtain" is&lt;br&gt;
absurd. Scores and results are facts, printed in every newspaper. Getting them as structured data usually means&lt;br&gt;
either a five-figure contract with a rights holder, or writing your own scraper and maintaining it forever.&lt;/p&gt;

&lt;p&gt;If you are building a model, a scoreboard or a fantasy tool rather than a betting exchange, there is a middle&lt;br&gt;
path. This post is about the details that separate usable sports data from a spreadsheet you cannot trust.&lt;/p&gt;
&lt;h2&gt;
  
  
  The status field is where most sports scrapers get it wrong
&lt;/h2&gt;

&lt;p&gt;Every match has a state, and naive scrapers reduce it to three: upcoming, live, finished. Real fixtures are&lt;br&gt;
messier. A tennis match can be retired mid-set. A football match can be abandoned at half time, replayed, awarded&lt;br&gt;
to one side, or decided on penalties. Each of those needs a different treatment in any model you build.&lt;/p&gt;

&lt;p&gt;I learned this the hard way. My tennis scraper inferred status codes from observed data, and the mapping looked&lt;br&gt;
convincing: code 5 seemed to be a walkover, codes 17 and 18 seemed to be postponed and cancelled, because that&lt;br&gt;
was consistent with the handful of matches I had checked.&lt;/p&gt;

&lt;p&gt;Then I found the authoritative list. Codes 17 and 18 are &lt;code&gt;FIRST_SET&lt;/code&gt; and &lt;code&gt;SECOND_SET&lt;/code&gt;. They mean the match is&lt;br&gt;
being &lt;strong&gt;played right now&lt;/strong&gt;. My scraper was reporting live matches as cancelled. On one day's data, seven matches&lt;br&gt;
were mislabelled, including one sitting at 6-4, 0-0 with a set already on the board.&lt;/p&gt;

&lt;p&gt;The lesson generalises beyond this one feed: when you infer an enum from samples, you are fitting to whatever&lt;br&gt;
happened to be in your sample. A match in its first set and a cancelled match both look like "no result yet" if&lt;br&gt;
you squint. Find the real mapping, or at minimum, add an assertion that catches the contradiction. Mine now&lt;br&gt;
refuses to label anything as postponed or cancelled if there are games on the board.&lt;/p&gt;
&lt;h2&gt;
  
  
  Scores are not a single number
&lt;/h2&gt;

&lt;p&gt;The second thing that bites you is assuming a score is two integers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tennis&lt;/strong&gt; needs set scores and tiebreak points. A set that ends 7-6 is meaningless without the 7-4 in the
tiebreak, and a match at 6-6 in the first set has a live tiebreak count that looks exactly like a games score
if you map it carelessly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Basketball&lt;/strong&gt; needs quarters and any overtime periods. The useful assertion here is that they must add up:
20 + 27 + 21 + 21 = 89 for the home team, or your mapping is wrong. That single check is what proved my field
mapping rather than my confidence in it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Football&lt;/strong&gt; needs halves, extra time and shootouts, and there is a trap. A match that finished 0-0 and was won
4-3 on penalties is commonly &lt;em&gt;displayed&lt;/em&gt; as 1-0, because the display score absorbs the shootout point. If you
derive the first-half score by subtracting the second half from the total, you will be wrong by exactly one
goal on every shootout in your dataset. Better to leave that field null than to publish a confident wrong
number.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Getting it
&lt;/h2&gt;

&lt;p&gt;I published one Actor per sport, because a tennis-specific tool is more useful than a generic one and easier to&lt;br&gt;
find:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://apify.com/clearfetch/flashscore-tennis-scraper" rel="noopener noreferrer"&gt;Tennis&lt;/a&gt;: ATP, WTA, ITF and Challenger, with set and
tiebreak scores, match statistics split by set, and full point-by-point sequences.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apify.com/clearfetch/flashscore-football-scraper" rel="noopener noreferrer"&gt;Football&lt;/a&gt;: every league, with goals and assists,
cards and the offence behind them, substitutions, and statistics including expected goals.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apify.com/clearfetch/flashscore-basketball-scraper" rel="noopener noreferrer"&gt;Basketball&lt;/a&gt;: quarter and overtime scores plus the
full statistics sheet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three cost $0.001 per match, so a full day of world football, roughly 1,700 matches, is under two dollars.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clearfetch/flashscore-football-scraper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;countries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;England&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finished&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ht&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;firstHalf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;home&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;home&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;away&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;away&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; (HT &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ht&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;home&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ht&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;away&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ht&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A real row, from a Premier League match:&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;"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;"stageDetail"&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;"home"&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;"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;"Newcastle"&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;"away"&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;"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;"Bournemouth"&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;"score"&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;"home"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"away"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"firstHalf"&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;"home"&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;"away"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"winner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"draw"&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;Ask for the details of that match and you get the 19 incidents behind the scoreline: who scored, who assisted,&lt;br&gt;
which card was for what offence, and the substitutions, plus 120 statistic rows including expected goals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things worth knowing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Draws exist.&lt;/strong&gt; Obvious for football, but a surprising number of scrapers model &lt;code&gt;winner&lt;/code&gt; as home or away and&lt;br&gt;
quietly drop the third case. Mine returns &lt;code&gt;draw&lt;/code&gt;, and 47 of the 1,759 matches on the day I tested were draws.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything unmapped is still there.&lt;/strong&gt; Each row carries a &lt;code&gt;raw&lt;/code&gt; object with every original field from the&lt;br&gt;
source. If you need something I did not map, you do not have to wait for me to add it.&lt;/p&gt;

&lt;p&gt;Scores and results are facts, and reading a public page is not the same as redistributing a rights-holder's&lt;br&gt;
feed. What you do with the data afterwards, particularly if you republish it, is your responsibility to check.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webscraping</category>
      <category>sports</category>
      <category>python</category>
    </item>
    <item>
      <title>How to find every Shopify store in a list of 10,000 domains</title>
      <dc:creator>Clear Fetch</dc:creator>
      <pubDate>Sat, 05 Sep 2026 19:10:42 +0000</pubDate>
      <link>https://dev.to/clearfetch/how-to-find-every-shopify-store-in-a-list-of-10000-domains-m89</link>
      <guid>https://dev.to/clearfetch/how-to-find-every-shopify-store-in-a-list-of-10000-domains-m89</guid>
      <description>&lt;p&gt;You have a list of domains. A conference attendee export, a directory scrape, a spreadsheet from a colleague.&lt;br&gt;
You want to know which of them run Shopify, which run WordPress, which have HubSpot installed, and which are on&lt;br&gt;
Cloudflare. That single question sits behind a lot of sales prospecting, competitor tracking and agency audits.&lt;/p&gt;

&lt;p&gt;The mechanics are not complicated, and it is worth understanding them before you decide whether to build or buy.&lt;/p&gt;
&lt;h2&gt;
  
  
  What a website tells you without being asked
&lt;/h2&gt;

&lt;p&gt;Every page you request hands over more than the HTML you asked for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Response headers.&lt;/strong&gt; &lt;code&gt;Server: cloudflare&lt;/code&gt; and a &lt;code&gt;cf-ray&lt;/code&gt; header are Cloudflare. &lt;code&gt;X-Powered-By: PHP/8.2&lt;/code&gt; is
self-explanatory. Shopify sends &lt;code&gt;x-shopid&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cookies.&lt;/strong&gt; &lt;code&gt;_shopify_y&lt;/code&gt;, &lt;code&gt;__utma&lt;/code&gt;, &lt;code&gt;wp-settings-*&lt;/code&gt; each pin a platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTML and meta tags.&lt;/strong&gt; &lt;code&gt;&amp;lt;meta name="generator" content="WordPress 6.9.7"&amp;gt;&lt;/code&gt; is about as direct as it gets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Script sources.&lt;/strong&gt; &lt;code&gt;cdn.shopify.com&lt;/code&gt;, &lt;code&gt;googletagmanager.com/gtm.js&lt;/code&gt;, &lt;code&gt;js.hs-scripts.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS records.&lt;/strong&gt; MX records name the mail provider. TXT records carry verification tokens for every SaaS tool
the company has ever set up, which is a surprisingly complete picture of their stack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wappalyzer popularised a fingerprint format for exactly this, and the open&lt;br&gt;
&lt;a href="https://github.com/enthec/webappanalyzer" rel="noopener noreferrer"&gt;webappanalyzer&lt;/a&gt; project maintains those fingerprints publicly: at the&lt;br&gt;
time of writing, 7,613 technologies with the patterns that identify each one.&lt;/p&gt;
&lt;h2&gt;
  
  
  The two things that make a naive implementation wrong
&lt;/h2&gt;

&lt;p&gt;If you write this yourself over a weekend, you will hit both of these.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technologies are injected at runtime.&lt;/strong&gt; Take Cloudflare's own marketing site. It uses Google Tag Manager, but&lt;br&gt;
there is no &lt;code&gt;&amp;lt;script src="...gtm.js"&amp;gt;&lt;/code&gt; anywhere in the HTML. Instead an inline script does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gtmContainerSrc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.googletagmanager.com/gtm.js?id=GTM-NDGPDFZ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gtmContainerSrc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fingerprint matcher that only looks at &lt;code&gt;&amp;lt;script src&amp;gt;&lt;/code&gt; attributes sees nothing. The same applies to headless&lt;br&gt;
storefronts: Gymshark is a Shopify store, but its front end is Next.js and the only Shopify URLs on the page are&lt;br&gt;
in an &lt;code&gt;og:image&lt;/code&gt; tag and a &lt;code&gt;srcset&lt;/code&gt; attribute.&lt;/p&gt;

&lt;p&gt;The fix is to treat vendor URLs found anywhere in the document as evidence, at reduced confidence, rather than&lt;br&gt;
only trusting script tags.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fingerprint patterns carry metadata you have to parse correctly.&lt;/strong&gt; Patterns look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;(?:&lt;span class="n"&gt;cdn&lt;/span&gt;\.|\.&lt;span class="n"&gt;my&lt;/span&gt;)&lt;span class="n"&gt;shopify&lt;/span&gt;\.&lt;span class="n"&gt;com&lt;/span&gt;\;&lt;span class="n"&gt;confidence&lt;/span&gt;:&lt;span class="m"&gt;50&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That trailing &lt;code&gt;\;confidence:50&lt;/code&gt; is a tag, not part of the regex. Split on the wrong thing and the regex keeps a&lt;br&gt;
trailing backslash, fails to compile, and gets silently dropped. In the database as it stands, 223 patterns carry&lt;br&gt;
a confidence tag and 1,392 carry a version tag, so getting this wrong costs you every version number and a good&lt;br&gt;
chunk of your detections. It is exactly the kind of bug that produces plausible-looking output that is quietly&lt;br&gt;
missing a third of the answer.&lt;/p&gt;
&lt;h2&gt;
  
  
  Doing it without writing it
&lt;/h2&gt;

&lt;p&gt;I published this as an Apify Actor so you can run it on a list without maintaining any of the above:&lt;br&gt;
&lt;a href="https://apify.com/clearfetch/tech-stack-detector" rel="noopener noreferrer"&gt;Tech Stack Detector&lt;/a&gt;. It costs $0.02 per website and&lt;br&gt;
unreachable sites are free.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/clearfetch~tech-stack-detector/run-sync-get-dataset-items?token=YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"urls": ["gymshark.com", "techcrunch.com", "notion.so"]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every detection comes back with the evidence behind it, so you can audit rather than trust:&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;"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;"Cloudflare"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&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="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="mi"&gt;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;"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;"CDN"&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;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"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;"headers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^cloudflare$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"match"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudflare"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"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;"dns"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ns"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&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="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.cloudflare&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"match"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hera.ns.cloudflare.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Filtering a thousand domains down to "everything running Shopify" is then a one-liner:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clearfetch/tech-stack-detector&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Shopify&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;site&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;techNames&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;company&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;company&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;site&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where this approach stops working
&lt;/h2&gt;

&lt;p&gt;Be clear about the limits before you rely on it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No JavaScript execution.&lt;/strong&gt; Technologies that leave no trace in the HTML, headers, cookies or DNS, and are
only visible as a &lt;code&gt;window&lt;/code&gt; global after the page boots, will be missed unless another fingerprint implies them.
Running a real browser would catch those, at roughly fifty times the cost per page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One page per site.&lt;/strong&gt; Something that only loads on a checkout or pricing page will not show up if you only
scan the homepage. Pass deeper URLs when it matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence is not certainty.&lt;/strong&gt; A vendor URL inside an inline script is good evidence, not proof. Raise the
minimum confidence if you would rather have fewer, harder detections.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Knowing those boundaries is the difference between a list you can act on and a list you have to re-check.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>api</category>
      <category>saas</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
