<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ai-Crawler on Tarragon</title><link>https://tarrragon.github.io/blog/tags/ai-crawler/</link><description>Recent content in Ai-Crawler on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 03 Aug 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/ai-crawler/index.xml" rel="self" type="application/rss+xml"/><item><title>辨識自動化流量</title><link>https://tarrragon.github.io/blog/automation/06-reading-the-data/automated-traffic/</link><pubDate>Mon, 03 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/automation/06-reading-the-data/automated-traffic/</guid><description>&lt;p>辨識自動化流量的責任是在統計裡把機器抓取與真人閱讀分開。未分離的統計會高估閱讀量，而高估的方向是單向的——沒有人會因為數字太好看而去查證。&lt;/p>
&lt;p>這一章的作法受兩個限制支配：接收端拿不到判別素材，以及會出現在這份統計裡的機器比想像中少。先講清楚這兩件事，它們決定了訊號從哪裡來、以及能期待抓到什麼。&lt;/p>
&lt;h2 id="接收端讀不到請求標頭">接收端讀不到請求標頭&lt;/h2>
&lt;p>Apps Script 的 &lt;a href="https://tarrragon.github.io/blog/automation/knowledge-cards/doget-dopost/" data-link-title="doGet / doPost" data-link-desc="Apps Script web app 的兩個進入點函式，分別接住 GET 與 POST 請求，決定端點收到請求時執行什麼">&lt;code>doPost(e)&lt;/code>&lt;/a> 事件物件提供 &lt;code>parameter&lt;/code>、&lt;code>parameters&lt;/code>、&lt;code>queryString&lt;/code>、&lt;code>contentLength&lt;/code>、&lt;code>postData&lt;/code>、&lt;code>pathInfo&lt;/code> 這幾個欄位，&lt;strong>不提供任何 HTTP 標頭&lt;/strong>。這代表接收端拿不到 &lt;code>User-Agent&lt;/code>，也拿不到來源 IP。&lt;/p>
&lt;p>傳統的伺服器端過濾（比對 UA 字串、查 IP 反解、比對已知爬蟲的 IP 區段）在這個架構下全部不可用。所有判斷素材必須由前端蒐集之後放進 payload 送出——這既是限制，也界定了設計空間：&lt;strong>能用的訊號只有瀏覽器環境暴露給 JavaScript 的那些&lt;/strong>。&lt;/p>
&lt;p>這個限制連帶產生一條必須守住的界線。前端能取得完整的 &lt;code>userAgent&lt;/code>、螢幕尺寸、時區、字型清單，把它們全部送出確實能提高辨識率，但那正好構成&lt;a href="https://tarrragon.github.io/blog/automation/knowledge-cards/browser-fingerprint/" data-link-title="Browser Fingerprint（瀏覽器指紋）" data-link-desc="由多個單獨無害的瀏覽器環境屬性組合而成的裝置特徵集合，決定自建統計蒐集判別訊號時的能力上限">瀏覽器指紋&lt;/a>——為了分辨機器而蒐集足以識別個人的資料，取捨方向是錯的。後面的每個訊號都在「有判別力」與「不構成指紋」之間取值。&lt;/p>
&lt;h2 id="會被-beacon-記錄到的機器只有一小部分">會被 beacon 記錄到的機器只有一小部分&lt;/h2>
&lt;p>beacon 靠 JavaScript 觸發，所以&lt;strong>只有會執行 JS 的抓取工具才會進入這份統計&lt;/strong>。這一條大幅縮小要辨識的對象範圍，而它與直覺相反：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>抓取工具&lt;/th>
 &lt;th>執行 JS&lt;/th>
 &lt;th>出現在統計裡&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Googlebot&lt;/td>
 &lt;td>會&lt;/td>
 &lt;td>會&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>無頭瀏覽器驅動的抓取工具（Playwright / Puppeteer）&lt;/td>
 &lt;td>會&lt;/td>
 &lt;td>會&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>監控與可用性檢查服務&lt;/td>
 &lt;td>多數會&lt;/td>
 &lt;td>會&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>GPTBot / ClaudeBot / PerplexityBot 等 AI 訓練爬蟲&lt;/td>
 &lt;td>不會&lt;/td>
 &lt;td>不會&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>facebookexternalhit 等連結預覽&lt;/td>
 &lt;td>不會&lt;/td>
 &lt;td>不會&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CCBot / AhrefsBot / SemrushBot&lt;/td>
 &lt;td>不會&lt;/td>
 &lt;td>不會&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>AI 訓練爬蟲不執行 JavaScript&lt;/strong> 是其中最反直覺的一項。Vercel 在 2025 年對自家平台流量的量測顯示，GPTBot 與 ClaudeBot 都會抓取 JS 檔案卻從不執行；連結預覽服務只讀 HTML 的 meta 標籤。&lt;strong>在主流爬蟲裡，目前已知具備完整 JS 渲染的只有 Googlebot&lt;/strong>——這是對演化中的世界所做的觀察，不是永久成立的分類，各家的渲染能力隨時可能改變。&lt;/p>
&lt;p>這件事有三個後續影響。其一，這套統計看不見的機器流量遠多於看得見的——伺服器日誌裡的爬蟲比例與這裡的數字不可比較。其二，下一節那份具名清單接住的是「會執行 JS」與「誠實申報身分」的交集，比清單本身小得多；清單裡多數條目預期永遠不會命中，保留它們是為了接住少數會渲染的變體。&lt;/p>
&lt;p>其三會影響上一章的判讀：&lt;a href="https://tarrragon.github.io/blog/automation/06-reading-the-data/visitor-identity/" data-link-title="訪客識別與 opt-out" data-link-desc="流量記錄裡的來源網址大量空白、任兩列之間看不出是不是同一個人時，補上識別欄位並保留退出機制">來源網址空白的成因&lt;/a>把「自動化抓取」列為其中一類，而這一節說明了那一類的實際佔比比直覺低——真正大量抓取的工具不執行 JS、根本不在這份資料裡。空白列偏向真人的機率因此比乍看之下高。&lt;/p>
&lt;h2 id="訊號分層與信心度">訊號分層與信心度&lt;/h2>
&lt;p>四個訊號的可靠度不同，分開記錄而不是合併成單一結論。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>來源&lt;/th>
 &lt;th>信心&lt;/th>
 &lt;th>主要誤差方向&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>webdriver&lt;/code>&lt;/td>
 &lt;td>&lt;code>navigator.webdriver === true&lt;/code>&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>幾乎不誤判真人；規避工具會蓋掉&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>ua&lt;/code>&lt;/td>
 &lt;td>UA 字串比對自我申報名稱&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>不誤判真人；抓不到偽裝者&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>lang&lt;/code>&lt;/td>
 &lt;td>&lt;code>navigator.languages&lt;/code> 為空&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>隱私擴充的真人誤中；新版無頭已不命中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>size&lt;/code>&lt;/td>
 &lt;td>&lt;code>window.outerWidth&lt;/code> 為 0&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>嵌入式 webview 的真人誤中；同上&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>這個信心排序由誤差方向推導而來，不是本站資料測得的命中率。&lt;/strong> 沒有標註任何誤判比例是刻意的——那需要一份已知組成的對照流量，而本文的每個判準都停在「這個訊號可能怎麼錯」的層次。既然正確率無從得知，處置方式就不是「先用著、之後校正」，而是&lt;a href="https://tarrragon.github.io/blog/automation/06-reading-the-data/automated-traffic/#%e6%a8%99%e8%a8%98%e8%80%8c%e9%9d%9e%e4%b8%9f%e6%a3%84" data-link-title="辨識自動化流量" data-link-desc="接收端讀不到 HTTP 標頭時在前端蒐集訊號分辨機器與真人；多數 AI 訓練爬蟲不執行 JavaScript，因此不會出現在這份統計裡">讓判定與使用分離&lt;/a>：訊號照實記下、判定留在彙總端，改了規則之後可以拿同一批舊資料重跑比對。&lt;/p>
&lt;p>&lt;strong>&lt;code>navigator.webdriver&lt;/code> 是自動化框架的預設標記。&lt;/strong> Playwright、Puppeteer、Selenium 啟動的瀏覽器這個值都是 &lt;code>true&lt;/code>，一般瀏覽器是 &lt;code>false&lt;/code>。它的判別力強而誤判極少，代價是刻意規避偵測的工具第一件事就是把它蓋掉——因此它抓得到的是「沒有隱藏身分意圖」的自動化訪客。&lt;/p>
&lt;p>&lt;strong>UA 比對抓的是自我申報。&lt;/strong> 搜尋引擎與正派的抓取服務會在 &lt;code>User-Agent&lt;/code> 裡寫明自己是誰，這是它們與網站經營者之間的慣例。比對這個字樣不會誤判真人，因為真人的瀏覽器不會自稱是機器。&lt;/p>
&lt;p>&lt;strong>空語言清單與零視窗尺寸原本是無頭環境的特徵，而這兩條正在失效。&lt;/strong> Chrome 112 起的新無頭模式會建立一個不顯示的平台視窗，並帶有正常的語言偏好，兩項特徵大多不再成立。它們的誤差方向因此從「誤判真人」偏向「漏判機器」——保留是因為舊版工具仍在使用中，但不能當作主力。&lt;/p>
&lt;p>因為誤差方向不同，&lt;strong>記錄下來的是命中了哪些訊號、而不是一個布林結論&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">botSignal&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">hits&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">try&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">navigator&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">webdriver&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">hits&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;webdriver&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// UA 比對見下一節
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">navigator&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">languages&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">navigator&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">languages&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">hits&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;lang&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">outerWidth&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">outerHeight&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">hits&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;size&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">catch&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 偵測本身出錯不該影響統計，當作沒有訊號
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">hits&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;,&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個設計讓&lt;strong>判定閾值留在彙總端&lt;/strong>——也就是試算表那一側，資料寫進去之後用公式分類的地方。日後發現某條訊號誤判太多，改公式就好，不必改前端重新部署；而前端一旦部署出去，快取的頁面還會用舊版跑一段時間。&lt;/p>
&lt;p>送出的字串寫進試算表的一個欄位，後面稱它為訊號欄。&lt;/p>
&lt;h2 id="具名比對與通用退回">具名比對與通用退回&lt;/h2>
&lt;p>UA 比對要回答的問題有兩層：「這是不是機器」以及「是哪一隻機器」。後者的價值在於處置方向不同——搜尋引擎索引是內容被收錄的證據，監控服務是自己設定的，而不具名的抓取工具才是需要留意的那一類。&lt;/p>
&lt;p>直覺的作法是列一份已知名單做精確比對。這個作法單獨使用時有一個不會顯現的失效模式：&lt;strong>名單會過時，而過時的表現是新出現的抓取工具完全不被標記&lt;/strong>——統計上呈現為自動化流量比例逐漸下降，而這個下降與「內容吸引到更多真人」在數字上不可區分。&lt;/p>
&lt;p>因此比對分成兩層，讓過時的代價落在精度而非覆蓋：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 以下兩個常數與比對邏輯接在上一段 botSignal() 的 try 區塊內，共用同一個 hits 陣列
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">// 已知抓取工具的自我申報名稱，命中時送出具體名稱
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">KNOWN_BOTS&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="sr">/claudebot|claude-user|claude-searchbot|gptbot|oai-searchbot|chatgpt-user|perplexitybot|googlebot|bingbot|applebot|duckduckbot|yandexbot|bytespider|amazonbot|ccbot|meta-externalagent|facebookexternalhit|twitterbot|linkedinbot|slackbot|discordbot|telegrambot|whatsapp|semrushbot|ahrefsbot|dotbot|petalbot/i&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 通用字樣，接住誠實申報但不在具名清單裡的自動化訪客
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">GENERIC_BOT&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="sr">/bot\b|crawler|spider|headless|slurp|bingpreview|python-requests|curl\//i&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">ua&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">navigator&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">userAgent&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">named&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">ua&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">match&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">KNOWN_BOTS&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">generic&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">named&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="kc">null&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="nx">ua&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">match&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">GENERIC_BOT&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">named&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">hits&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;ua:&amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">named&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">toLowerCase&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">generic&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">hits&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;ua:&amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">generic&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">toLowerCase&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sr">/[^a-z]+$/&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">));&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩層的比對來源不同是關鍵：具名層比對的是身分（清單裡的名字），通用層匹配的是類別詞（&lt;code>bot&lt;/code>、&lt;code>crawler&lt;/code>、&lt;code>spider&lt;/code>、&lt;code>headless&lt;/code>），後者不需要知道任何一隻抓取工具的名字就能運作。清單過時時，新工具仍然被通用層接住——失去的是「知道是誰」，不是「知道有」。&lt;/p></description><content:encoded><![CDATA[<p>辨識自動化流量的責任是在統計裡把機器抓取與真人閱讀分開。未分離的統計會高估閱讀量，而高估的方向是單向的——沒有人會因為數字太好看而去查證。</p>
<p>這一章的作法受兩個限制支配：接收端拿不到判別素材，以及會出現在這份統計裡的機器比想像中少。先講清楚這兩件事，它們決定了訊號從哪裡來、以及能期待抓到什麼。</p>
<h2 id="接收端讀不到請求標頭">接收端讀不到請求標頭</h2>
<p>Apps Script 的 <a href="/blog/automation/knowledge-cards/doget-dopost/" data-link-title="doGet / doPost" data-link-desc="Apps Script web app 的兩個進入點函式，分別接住 GET 與 POST 請求，決定端點收到請求時執行什麼"><code>doPost(e)</code></a> 事件物件提供 <code>parameter</code>、<code>parameters</code>、<code>queryString</code>、<code>contentLength</code>、<code>postData</code>、<code>pathInfo</code> 這幾個欄位，<strong>不提供任何 HTTP 標頭</strong>。這代表接收端拿不到 <code>User-Agent</code>，也拿不到來源 IP。</p>
<p>傳統的伺服器端過濾（比對 UA 字串、查 IP 反解、比對已知爬蟲的 IP 區段）在這個架構下全部不可用。所有判斷素材必須由前端蒐集之後放進 payload 送出——這既是限制，也界定了設計空間：<strong>能用的訊號只有瀏覽器環境暴露給 JavaScript 的那些</strong>。</p>
<p>這個限制連帶產生一條必須守住的界線。前端能取得完整的 <code>userAgent</code>、螢幕尺寸、時區、字型清單，把它們全部送出確實能提高辨識率，但那正好構成<a href="/blog/automation/knowledge-cards/browser-fingerprint/" data-link-title="Browser Fingerprint（瀏覽器指紋）" data-link-desc="由多個單獨無害的瀏覽器環境屬性組合而成的裝置特徵集合，決定自建統計蒐集判別訊號時的能力上限">瀏覽器指紋</a>——為了分辨機器而蒐集足以識別個人的資料，取捨方向是錯的。後面的每個訊號都在「有判別力」與「不構成指紋」之間取值。</p>
<h2 id="會被-beacon-記錄到的機器只有一小部分">會被 beacon 記錄到的機器只有一小部分</h2>
<p>beacon 靠 JavaScript 觸發，所以<strong>只有會執行 JS 的抓取工具才會進入這份統計</strong>。這一條大幅縮小要辨識的對象範圍，而它與直覺相反：</p>
<table>
  <thead>
      <tr>
          <th>抓取工具</th>
          <th>執行 JS</th>
          <th>出現在統計裡</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Googlebot</td>
          <td>會</td>
          <td>會</td>
      </tr>
      <tr>
          <td>無頭瀏覽器驅動的抓取工具（Playwright / Puppeteer）</td>
          <td>會</td>
          <td>會</td>
      </tr>
      <tr>
          <td>監控與可用性檢查服務</td>
          <td>多數會</td>
          <td>會</td>
      </tr>
      <tr>
          <td>GPTBot / ClaudeBot / PerplexityBot 等 AI 訓練爬蟲</td>
          <td>不會</td>
          <td>不會</td>
      </tr>
      <tr>
          <td>facebookexternalhit 等連結預覽</td>
          <td>不會</td>
          <td>不會</td>
      </tr>
      <tr>
          <td>CCBot / AhrefsBot / SemrushBot</td>
          <td>不會</td>
          <td>不會</td>
      </tr>
  </tbody>
</table>
<p><strong>AI 訓練爬蟲不執行 JavaScript</strong> 是其中最反直覺的一項。Vercel 在 2025 年對自家平台流量的量測顯示，GPTBot 與 ClaudeBot 都會抓取 JS 檔案卻從不執行；連結預覽服務只讀 HTML 的 meta 標籤。<strong>在主流爬蟲裡，目前已知具備完整 JS 渲染的只有 Googlebot</strong>——這是對演化中的世界所做的觀察，不是永久成立的分類，各家的渲染能力隨時可能改變。</p>
<p>這件事有三個後續影響。其一，這套統計看不見的機器流量遠多於看得見的——伺服器日誌裡的爬蟲比例與這裡的數字不可比較。其二，下一節那份具名清單接住的是「會執行 JS」與「誠實申報身分」的交集，比清單本身小得多；清單裡多數條目預期永遠不會命中，保留它們是為了接住少數會渲染的變體。</p>
<p>其三會影響上一章的判讀：<a href="/blog/automation/06-reading-the-data/visitor-identity/" data-link-title="訪客識別與 opt-out" data-link-desc="流量記錄裡的來源網址大量空白、任兩列之間看不出是不是同一個人時，補上識別欄位並保留退出機制">來源網址空白的成因</a>把「自動化抓取」列為其中一類，而這一節說明了那一類的實際佔比比直覺低——真正大量抓取的工具不執行 JS、根本不在這份資料裡。空白列偏向真人的機率因此比乍看之下高。</p>
<h2 id="訊號分層與信心度">訊號分層與信心度</h2>
<p>四個訊號的可靠度不同，分開記錄而不是合併成單一結論。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>來源</th>
          <th>信心</th>
          <th>主要誤差方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>webdriver</code></td>
          <td><code>navigator.webdriver === true</code></td>
          <td>高</td>
          <td>幾乎不誤判真人；規避工具會蓋掉</td>
      </tr>
      <tr>
          <td><code>ua</code></td>
          <td>UA 字串比對自我申報名稱</td>
          <td>高</td>
          <td>不誤判真人；抓不到偽裝者</td>
      </tr>
      <tr>
          <td><code>lang</code></td>
          <td><code>navigator.languages</code> 為空</td>
          <td>中</td>
          <td>隱私擴充的真人誤中；新版無頭已不命中</td>
      </tr>
      <tr>
          <td><code>size</code></td>
          <td><code>window.outerWidth</code> 為 0</td>
          <td>中</td>
          <td>嵌入式 webview 的真人誤中；同上</td>
      </tr>
  </tbody>
</table>
<p><strong>這個信心排序由誤差方向推導而來，不是本站資料測得的命中率。</strong> 沒有標註任何誤判比例是刻意的——那需要一份已知組成的對照流量，而本文的每個判準都停在「這個訊號可能怎麼錯」的層次。既然正確率無從得知，處置方式就不是「先用著、之後校正」，而是<a href="/blog/automation/06-reading-the-data/automated-traffic/#%e6%a8%99%e8%a8%98%e8%80%8c%e9%9d%9e%e4%b8%9f%e6%a3%84" data-link-title="辨識自動化流量" data-link-desc="接收端讀不到 HTTP 標頭時在前端蒐集訊號分辨機器與真人；多數 AI 訓練爬蟲不執行 JavaScript，因此不會出現在這份統計裡">讓判定與使用分離</a>：訊號照實記下、判定留在彙總端，改了規則之後可以拿同一批舊資料重跑比對。</p>
<p><strong><code>navigator.webdriver</code> 是自動化框架的預設標記。</strong> Playwright、Puppeteer、Selenium 啟動的瀏覽器這個值都是 <code>true</code>，一般瀏覽器是 <code>false</code>。它的判別力強而誤判極少，代價是刻意規避偵測的工具第一件事就是把它蓋掉——因此它抓得到的是「沒有隱藏身分意圖」的自動化訪客。</p>
<p><strong>UA 比對抓的是自我申報。</strong> 搜尋引擎與正派的抓取服務會在 <code>User-Agent</code> 裡寫明自己是誰，這是它們與網站經營者之間的慣例。比對這個字樣不會誤判真人，因為真人的瀏覽器不會自稱是機器。</p>
<p><strong>空語言清單與零視窗尺寸原本是無頭環境的特徵，而這兩條正在失效。</strong> Chrome 112 起的新無頭模式會建立一個不顯示的平台視窗，並帶有正常的語言偏好，兩項特徵大多不再成立。它們的誤差方向因此從「誤判真人」偏向「漏判機器」——保留是因為舊版工具仍在使用中，但不能當作主力。</p>
<p>因為誤差方向不同，<strong>記錄下來的是命中了哪些訊號、而不是一個布林結論</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">botSignal</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">var</span> <span class="nx">hits</span> <span class="o">=</span> <span class="p">[];</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">navigator</span><span class="p">.</span><span class="nx">webdriver</span> <span class="o">===</span> <span class="kc">true</span><span class="p">)</span> <span class="nx">hits</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">&#34;webdriver&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="c1">// UA 比對見下一節
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">navigator</span><span class="p">.</span><span class="nx">languages</span> <span class="o">||</span> <span class="nx">navigator</span><span class="p">.</span><span class="nx">languages</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="nx">hits</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">&#34;lang&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">window</span><span class="p">.</span><span class="nx">outerWidth</span> <span class="o">||</span> <span class="o">!</span><span class="nb">window</span><span class="p">.</span><span class="nx">outerHeight</span><span class="p">)</span> <span class="nx">hits</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">&#34;size&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="c1">// 偵測本身出錯不該影響統計，當作沒有訊號
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="k">return</span> <span class="nx">hits</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="s2">&#34;,&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個設計讓<strong>判定閾值留在彙總端</strong>——也就是試算表那一側，資料寫進去之後用公式分類的地方。日後發現某條訊號誤判太多，改公式就好，不必改前端重新部署；而前端一旦部署出去，快取的頁面還會用舊版跑一段時間。</p>
<p>送出的字串寫進試算表的一個欄位，後面稱它為訊號欄。</p>
<h2 id="具名比對與通用退回">具名比對與通用退回</h2>
<p>UA 比對要回答的問題有兩層：「這是不是機器」以及「是哪一隻機器」。後者的價值在於處置方向不同——搜尋引擎索引是內容被收錄的證據，監控服務是自己設定的，而不具名的抓取工具才是需要留意的那一類。</p>
<p>直覺的作法是列一份已知名單做精確比對。這個作法單獨使用時有一個不會顯現的失效模式：<strong>名單會過時，而過時的表現是新出現的抓取工具完全不被標記</strong>——統計上呈現為自動化流量比例逐漸下降，而這個下降與「內容吸引到更多真人」在數字上不可區分。</p>
<p>因此比對分成兩層，讓過時的代價落在精度而非覆蓋：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 以下兩個常數與比對邏輯接在上一段 botSignal() 的 try 區塊內，共用同一個 hits 陣列
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">// 已知抓取工具的自我申報名稱，命中時送出具體名稱
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">KNOWN_BOTS</span> <span class="o">=</span> <span class="sr">/claudebot|claude-user|claude-searchbot|gptbot|oai-searchbot|chatgpt-user|perplexitybot|googlebot|bingbot|applebot|duckduckbot|yandexbot|bytespider|amazonbot|ccbot|meta-externalagent|facebookexternalhit|twitterbot|linkedinbot|slackbot|discordbot|telegrambot|whatsapp|semrushbot|ahrefsbot|dotbot|petalbot/i</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">// 通用字樣，接住誠實申報但不在具名清單裡的自動化訪客
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">GENERIC_BOT</span> <span class="o">=</span> <span class="sr">/bot\b|crawler|spider|headless|slurp|bingpreview|python-requests|curl\//i</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kd">var</span> <span class="nx">ua</span> <span class="o">=</span> <span class="nx">navigator</span><span class="p">.</span><span class="nx">userAgent</span> <span class="o">||</span> <span class="s2">&#34;&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="kd">var</span> <span class="nx">named</span> <span class="o">=</span> <span class="nx">ua</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="nx">KNOWN_BOTS</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kd">var</span> <span class="nx">generic</span> <span class="o">=</span> <span class="nx">named</span> <span class="o">?</span> <span class="kc">null</span> <span class="o">:</span> <span class="nx">ua</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="nx">GENERIC_BOT</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">named</span><span class="p">)</span> <span class="nx">hits</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">&#34;ua:&#34;</span> <span class="o">+</span> <span class="nx">named</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">toLowerCase</span><span class="p">());</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">generic</span><span class="p">)</span> <span class="nx">hits</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">&#34;ua:&#34;</span> <span class="o">+</span> <span class="nx">generic</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">toLowerCase</span><span class="p">().</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/[^a-z]+$/</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">));</span></span></span></code></pre></div><p>兩層的比對來源不同是關鍵：具名層比對的是身分（清單裡的名字），通用層匹配的是類別詞（<code>bot</code>、<code>crawler</code>、<code>spider</code>、<code>headless</code>），後者不需要知道任何一隻抓取工具的名字就能運作。清單過時時，新工具仍然被通用層接住——失去的是「知道是誰」，不是「知道有」。</p>
<p>上面的 <code>GENERIC_BOT</code> 裡混了 <code>slurp</code>、<code>bingpreview</code>、<code>python-requests</code>、<code>curl/</code> 四個具體名稱，嚴格說它們屬於具名層——把產品名放進退回層，退回層就變成清單的延伸而不是獨立的一層。實際運作上它們是無害的殘留，但這個混用會讓「通用層純靠類別詞」這個保證打折：<code>wget</code>、<code>Go-http-client</code>、<code>node-fetch</code>、<code>axios</code> 這些同樣不含類別詞的 UA，兩層都接不住。</p>
<p>真正獨立於 UA 的那一層是 <code>webdriver</code> / <code>lang</code> / <code>size</code> ——它們讀的是瀏覽器環境而非字串，UA 比對整個失效時仍然運作。三層的關係值得明說：具名層給身分、通用層給類別、環境訊號層給「這個執行環境不像人用的」，而只有第三層完全不依賴任何列舉。</p>
<p>這個結構要能被觀測，標記本身得分得出層級。上面的寫法送出的是 <code>ua:googlebot</code> 與 <code>ua:bot</code>，而 <code>ua:slurp</code> 這種看起來像具名層、其實來自通用層——不比對兩條 regex 就分不出來。加一個前綴讓它自證：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">named</span><span class="p">)</span> <span class="nx">hits</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">&#34;ua:named:&#34;</span> <span class="o">+</span> <span class="nx">named</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">toLowerCase</span><span class="p">());</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">generic</span><span class="p">)</span> <span class="nx">hits</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">&#34;ua:generic:&#34;</span> <span class="o">+</span> <span class="nx">generic</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">toLowerCase</span><span class="p">().</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/[^a-z]+$/</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">));</span></span></span></code></pre></div><p>分得出層級之後，<code>COUNTIF(訊號欄, &quot;*ua:generic*&quot;) / COUNTIF(訊號欄, &quot;*ua:*&quot;)</code> 就是清單的過時指標：<strong>這個比例上升代表有新工具在被通用層接住、該更新具名清單了</strong>。這把「清單過時」從一件不可見的事變成一個可以看的數字，而且不需要有人事先知道漏了哪些新工具。抽象層的原則見 <a href="/blog/report/stale-list-costs-precision-not-coverage/" data-link-title="清單過時的代價要落在精度、不落在覆蓋" data-link-desc="判定依賴一份會過時的清單（已知名稱、白名單、簽章）時，決定清單失效之後還剩下什麼——只比對清單的設計會讓偵測能力隨時間下降，而數字看起來變好">#251 清單過時的代價要落在精度、不落在覆蓋</a>。</p>
<p>清單維護有一個實際邊界值得先講明。Anthropic 目前把不同用途拆成 <code>ClaudeBot</code>、<code>Claude-User</code>、<code>Claude-SearchBot</code> 多個 token，這類拆分會持續發生，而拆分後的新名稱在被加進清單前由通用層接住——那正是這個設計在運作。</p>
<p>另一個邊界是<strong>命名系統的混淆</strong>，而這份清單踩過一次。它上線時含有 <code>google-extended</code> 這個條目——那其實是 robots.txt 的控制 token，不會出現在任何請求的 <code>User-Agent</code> 裡，因此它從第一天起就是死的。系統不會回報「某個條目從未命中」，這個缺陷是靠一次外部查核才浮現的。robots.txt 的控制 token 與 UA 字串是兩套獨立的命名，這正是「訊號只能來自瀏覽器暴露給 JavaScript 的東西」這條限制的具體案例。</p>
<p>只送出命中的關鍵字、不送完整 <code>userAgent</code>，是前面那條界線的具體落實——那個關鍵字是抓取工具主動公開申報的身分，不構成指紋特徵。</p>
<h2 id="行為訊號對沒有規避意圖的工具有效代價是誤判方向指向真人">行為訊號對沒有規避意圖的工具有效，代價是誤判方向指向真人</h2>
<p>行為留下的痕跡比環境屬性稍微難改，但<strong>它們擋不住有規避意圖的工具</strong>。前面對 <code>webdriver</code> 說過的那句讓步在這裡同樣適用，而且成本更低：讓抓取工具在關閉前觸發一次 <code>visibilitychange</code> 是三行程式碼，隨機化頁面存活時間是一行，重用瀏覽器的 storage state 讓識別碼保持穩定是兩行——最後這項甚至提升效率，因為它本來就是自動化測試處理登入狀態的標準做法。</p>
<p>這一節的三個訊號在<strong>對象沒有隱藏意圖</strong>時判別力最強，同時<strong>誤判代價也最高</strong>——誤差方向一致指向「把真人判成機器」，而這個方向最難察覺，因為結果看起來是流量變乾淨了。</p>
<p><strong>離開事件缺席</strong>是其中最直接的一個。抓取工具取得頁面內容後直接終止瀏覽器環境，不會經歷「頁面轉為不可見」這個狀態轉換，因此不產生離開事件。</p>
<p>真人這一側同樣會缺席，而且不罕見。<a href="/blog/automation/06-reading-the-data/event-model/" data-link-title="事件模型與停留時間" data-link-desc="每次瀏覽只記一列時看不出讀者停留多久、有沒有真的在讀；補上離開事件之後，那個秒數的語意與既有報表公式的連帶影響">事件模型</a>列過那份丟失清單：記憶體回收、強制關閉、斷電、網路中斷、擴充功能攔截、接收端併發撞頂。<strong>行動流量佔比高的站，真人的離開事件缺席率本來就不低</strong>，把缺席直接當成機器訊號會系統性誤判這一群人。</p>
<p><strong>只有離開事件、沒有進入事件</strong>這個相反的形態也一樣。可觀測的只有一件事：接收端在進入事件的時間點沒有收到任何請求。抓取工具的網路攔截設定是一種相容的成因——許多框架只放行文件與必要資源，等內容抓完、進入關閉流程時攔截器已經解除——但它只是幾個候選之一。</p>
<p>真人也會產生孤兒離開事件，至少三條路徑：<strong>接收端併發撞頂</strong>（<a href="/blog/automation/knowledge-cards/execution-quota/" data-link-title="Execution Quota（執行配額）" data-link-desc="Apps Script 個人帳號的執行時間、同時併發與觸發器每日總時間上限，決定免費膠水層能承受多大的量">執行配額</a>的併發上限，見<a href="/blog/automation/05-deploy-quota-security/quota-abuse-privacy/" data-link-title="配額、濫用防護、隱私與遷移訊號" data-link-desc="免費配額實際碰撞時會怎樣、擋髒資料與過濾自己瀏覽的做法、不記 PII 的隱私立場、以及量大到該離開 Sheets 的訊號">模組五的配額碰撞</a>——爆量時同時湧入的進入事件被拒，離開事件數秒後送達時併發已消退，正好剩下孤兒）、廣告或隱私擴充功能攔截第一次請求、以及網路瞬斷。第一條特別值得注意，因為它與爆量時段重疊，會把流量高峰的真人整批劃給機器。</p>
<p><strong>停留時間的一致性</strong>洩漏的是排程。抓取工具的頁面存活時間由設定決定，因此相隔數小時、來自不同識別碼的多列可能有精確相同的秒數。這一條需要區間限定：<code>dur</code> 經過取整為秒，而<strong>真人的跳出流量集中在最短的那幾秒，在那個區間裡精確重複是常態而非訊號</strong>。分界落在哪一秒由下一節的短停留分佈量出來，不是一個可以跨站沿用的固定值。只有落在跳出區間之外的精確重複才有判別力。</p>
<p>三個訊號各自的觀察都只有個位數的記錄撐著，所以它們指出的是「該往哪裡看」，不是「這一列是什麼」。</p>
<h2 id="先量本站的基線">先量本站的基線</h2>
<p>前面所有訊號都描述「機器可能長什麼樣」，而判定需要的是「這一列與本站的真人有多不一樣」。<strong>判準寫成絕對條件（缺席、為零、幾乎沒有）在換一個站之後就不成立</strong>：行動流量佔八成的站與桌機為主的站，真人的離開事件缺席率差距很大；剛上線五篇文章的站，真人的站內導覽比例天然接近零。</p>
<p>基線要用一批<strong>組成已知</strong>的流量來量，而唯一穩定可得的那批是自己的瀏覽：在設定 opt-out 之前刻意走幾條完整的閱讀路線，那批記錄裡每一列是誰產生的都確定。</p>
<table>
  <thead>
      <tr>
          <th>基線指標</th>
          <th>量法</th>
          <th>已知的偏差</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>離開事件缺席率</td>
          <td>已知樣本裡沒有配對離開事件的進入事件佔比</td>
          <td>站主多用桌機，行動裝置的缺席率會被低估</td>
      </tr>
      <tr>
          <td>站內來源比例</td>
          <td>已知樣本裡來源網址為自家網域的佔比</td>
          <td>站主知道站的結構、走站內導覽，這個值會系統性偏高</td>
      </tr>
      <tr>
          <td>短停留分佈</td>
          <td>已知樣本的 <code>dur</code> 分佈，找出跳出區間落在哪幾秒</td>
          <td>站主讀自己的文章，短停留佔比偏低</td>
      </tr>
      <tr>
          <td>孤兒離開事件比例</td>
          <td>已知樣本裡找不到配對進入事件的比例</td>
          <td>同一台裝置、同一個網路環境，攔截與斷線情境不足</td>
      </tr>
  </tbody>
</table>
<p>第三欄不是免責聲明，它決定判讀的方向：<strong>站內來源比例的基線偏高，代表真實讀者低於這個值是正常的</strong>，拿它當「低於就可疑」的門檻會系統性誤判。四個指標的偏差方向都指向同一側——真人看起來比基線「差」。</p>
<p>有一種量法要避開：用「訊號欄乾淨」的記錄當已知樣本。那等於用被校正的偵測器定義校正用的樣本，結論會自我印證。同理，量離開事件缺席率時不能只取互動旗標為 1 的記錄——那個條件蘊含離開事件已經送達，算出來必然是零。</p>
<p>樣本規模也要誠實看待：刻意走幾條路線得到的是數十列，那個量足以看出量級差異（缺席率是一成還是六成），不足以做統計顯著性判斷。判準因此寫成「明顯偏離基線的量級」而非某個百分點門檻。</p>
<p>基線本身要重量：改版換了頁面結構、流量來源組成變化、行動裝置佔比上升，都會讓它漂移。</p>
<h2 id="這一節的限制會累積到哪裡">這一節的限制會累積到哪裡</h2>
<p>前面每一節都交代了自己的限制：只有執行 JavaScript 的訪客會被記錄、離開事件會在幾種情境下丟失、訊號的誤差方向一致指向誤判真人。這些限制加上其他篇的（退出機制、配額漏記、儲存清除）共同界定了這份資料的母體，而母體決定了哪些結論下得出來——完整的總帳與外部對照方法見<a href="/blog/automation/06-reading-the-data/data-coverage/" data-link-title="這份統計的分母是什麼" data-link-desc="報表上的數字要拿去回答問題之前，先確定它涵蓋了誰、漏掉了誰，以及哪些結論在這個涵蓋範圍下仍然成立">這份統計的分母是什麼</a>。</p>
<h2 id="標記而非丟棄">標記而非丟棄</h2>
<p>偵測到疑似自動化流量時有兩種處置：前端直接不送，或照常記錄並加上標記欄位。</p>
<p>這一章從頭到尾談的是<strong>標記</strong>，不是阻擋。靜態站託管層沒有可以攔請求的地方，這些訊號只在資料進來之後才產生作用——真的要擋抓取工具需要 CDN 或反向代理層的規則，那是另一個主題。</p>
<p><strong>照常記錄買到的是三件不需要 ground truth 的事。</strong> 判定的正確率本身無從得知——這套統計沒有一份標好答案的對照資料，前面每個訊號都只能談誤差方向、談不了誤判率。真正的理由在別處：</p>
<p><strong>改了規則之後可以重跑舊資料</strong>，比對同一批記錄在新舊規則下的分佈差多少。這回答不了「哪一個對」，但看得出「改動影響了多少列」，而那是決定要不要改的實際依據。</p>
<p><strong>日後認出新的抓取工具時可以回溯標記</strong>。今天標成 <code>ua:bot</code> 的那些列，等它被加進具名清單之後仍在表裡，過去半年的歷史因此跟著變得可讀。丟掉的列沒有這個機會。</p>
<p><strong>總量守恆檢查的前提就是列沒有被丟掉</strong>。前端過濾會讓分母本身失真，而分母失真時所有比例都不可信。</p>
<p>代價是試算表多存一些列，以及彙總時必須記得過濾。</p>
<p>對免費配額敏感、或流量大到列數成為問題時，取捨會反過來——那時應該只丟棄高信心訊號（<code>webdriver</code> 與具名 UA）命中的部分，中信心與行為訊號仍然照常記錄。</p>
<h2 id="彙總端的分類與條件順序">彙總端的分類與條件順序</h2>
<p>分類邏輯把訊號與行為組合起來。以下是虛擬碼——欄位名要換成試算表的實際範圍參照，而每個閾值都該換成前面量出的基線值，寫成固定條件只是為了讓結構清楚：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">=IFS(
</span></span><span class="line"><span class="ln">2</span><span class="cl">  AND(事件=&#34;leave&#34;, 同session同路徑的view筆數=0), &#34;待查-孤兒離開事件&#34;,
</span></span><span class="line"><span class="ln">3</span><span class="cl">  事件&lt;&gt;&#34;view&#34;, &#34;&#34;,
</span></span><span class="line"><span class="ln">4</span><span class="cl">  REGEXMATCH(訊號欄, &#34;webdriver|ua&#34;), &#34;機器-高信心&#34;,
</span></span><span class="line"><span class="ln">5</span><span class="cl">  AND(同session的leave筆數=0, 裝置&lt;&gt;&#34;mobile&#34;), &#34;待查-無離開事件&#34;,
</span></span><span class="line"><span class="ln">6</span><span class="cl">  訊號欄&lt;&gt;&#34;&#34;, &#34;訊號弱-待觀察&#34;,
</span></span><span class="line"><span class="ln">7</span><span class="cl">  同session的view筆數=1, &#34;單頁即走&#34;,
</span></span><span class="line"><span class="ln">8</span><span class="cl">  AND(同session的leave筆數&gt;0, 訊號欄=&#34;&#34;), &#34;真人&#34;,
</span></span><span class="line"><span class="ln">9</span><span class="cl">  TRUE, &#34;未分類&#34;)</span></span></code></pre></div><p>條件的順序承載語意。<strong>孤兒離開事件排在最前面</strong>，否則第二條會把所有離開事件留白，那一整類記錄就在統計裡消失了。<strong>非進入事件留白</strong>避免同一次瀏覽被計數兩次。</p>
<p>分類名稱用「待查」而非「爬蟲」是刻意的：行為訊號給的是需要進一步確認的候選。孤兒離開事件要對照<a href="/blog/automation/knowledge-cards/executions-page/" data-link-title="Executions（執行項目）" data-link-desc="Apps Script 平台記錄每一次函式執行的時間、耗時與狀態的頁面，用來判斷請求究竟有沒有抵達接收端">執行項目</a>同時段有沒有失敗紀錄——有的話那是配額問題而非機器。無離開事件那一條排除了行動裝置，因為真人在該環境的缺席率本來就高。</p>
<p>最後兩條分支的分工是刻意的。<strong>「真人」寫成顯式條件、catch-all 標成「未分類」</strong>，而不是反過來讓沒被接住的列直接落進「真人」——後者是這份報表最被消費、最不會被追問的類別，新的資料形狀靜默落進去只會讓數字變好看。<code>IFS</code> 在所有條件都不成立時回傳 <code>#N/A</code>，而滿欄的錯誤值與滿欄的留白一樣不會被追問，所以 catch-all 要給一個會被看見的標籤。這條防線的完整推導見<a href="/blog/automation/06-reading-the-data/diagnosing-silent-failures/" data-link-title="假故障與靜默失效的診斷" data-link-desc="自建的 Apps Script 流量統計看起來壞了、或看起來正常但數字不對時，分辨症狀出現的位置與問題所在的位置">假故障與靜默失效的診斷</a>。</p>
<p>用總量守恆檢查這份分類時，<strong>作用域是進入事件那些列</strong>——離開事件被第二條留白，把它們算進分母會讓差額永遠等於離開事件的列數，而一個恆常的巨大差額只會訓練出忽略它的習慣。</p>
<h2 id="下一步">下一步</h2>
<p>判定規則到位了，接下來要問的是這些規則吃進去的資料本身可不可信。判定寫對了但公式算錯、程式改了但端點沒生效、錯誤訊息指向的位置不是問題的位置——<a href="/blog/automation/06-reading-the-data/diagnosing-silent-failures/" data-link-title="假故障與靜默失效的診斷" data-link-desc="自建的 Apps Script 流量統計看起來壞了、或看起來正常但數字不對時，分辨症狀出現的位置與問題所在的位置">假故障與靜默失效的診斷</a>處理這三類。</p>
<p>爆量時段的漏記是另一回事，它來自配額而非判定，見<a href="/blog/automation/05-deploy-quota-security/quota-abuse-privacy/" data-link-title="配額、濫用防護、隱私與遷移訊號" data-link-desc="免費配額實際碰撞時會怎樣、擋髒資料與過濾自己瀏覽的做法、不記 PII 的隱私立場、以及量大到該離開 Sheets 的訊號">模組五的配額、防濫用與隱私邊界</a>。</p>
]]></content:encoded></item></channel></rss>