制到檢測(cè)實(shí)現(xiàn))
反廣告攔截器這個(gè)詞我最早是2018年在自己站點(diǎn)廣告后臺(tái)里真正接觸到的。當(dāng)時(shí)運(yùn)營(yíng)同事反饋廣告位展示量掉得厲害點(diǎn)擊率也異常排查了一圈才發(fā)現(xiàn)相當(dāng)一部分流量來(lái)自裝了廣告攔截器的瀏覽器。服務(wù)器成本一分不少?gòu)V告收入?yún)s打了折于是我們開(kāi)始認(rèn)真研究這個(gè)“曲線救國(guó)”的方向反廣告攔截器Anti-Adblock。簡(jiǎn)單說(shuō)它是一段運(yùn)行在網(wǎng)站前端的代碼用來(lái)判斷當(dāng)前瀏覽器里是否有廣告攔截器在運(yùn)行如果有就決定是彈警告、遮內(nèi)容、引導(dǎo)放行還是把被攔截的廣告位替換成自家推廣。這篇文章我打算把它從里到外拆一遍從廣告攔截器的底層原理到反廣告攔截器的檢測(cè)手段再到產(chǎn)品落地與常見(jiàn)坑位完整梳理一套你自己也能看明白、能參考落地的認(rèn)識(shí)。1. 先搞懂對(duì)手廣告攔截器到底在攔截什么反廣告攔截器本質(zhì)上是在和廣告攔截器博弈所以第一步不是研究怎么“反”而是搞清楚廣告攔截器究竟動(dòng)了哪幾個(gè)環(huán)節(jié)。只有知道對(duì)手在哪個(gè)環(huán)節(jié)攔截才能在對(duì)應(yīng)的位置設(shè)計(jì)檢測(cè)邏輯。1.1 請(qǐng)求層讓廣告腳本根本沒(méi)機(jī)會(huì)加載大部分廣告AdSense、聯(lián)盟素材、第三方跟蹤都來(lái)自外部域名比如 googleads.g.doubleclick.net、googlesyndication.com、amazon-adsystem.com 這些。廣告攔截器最基礎(chǔ)的能力就是在網(wǎng)絡(luò)請(qǐng)求發(fā)出之前把它攔下來(lái)。以 Chrome 擴(kuò)展為例Manifest V2 時(shí)代常用chrome.webRequest.onBeforeRequest監(jiān)聽(tīng)匹配到廣告域名就直接cancel掉這次請(qǐng)求。Manifest V3 之后改成了chrome.declarativeNetRequest用一組聲明式規(guī)則來(lái)阻止請(qǐng)求規(guī)則本身長(zhǎng)這樣{ id: 1, priority: 1, action: { type: block }, condition: { urlFilter: ||doubleclick.net^, resourceTypes: [script, image] } }urlFilter里的||表示域名邊界結(jié)尾的^表示分隔符這個(gè)寫(xiě)法在過(guò)濾規(guī)則里非常常見(jiàn)。只要請(qǐng)求 URL 命中規(guī)則瀏覽器直接攔截頁(yè)面腳本根本拿不到廣告內(nèi)容自然沒(méi)有任何展示。所以很多站長(zhǎng)會(huì)發(fā)現(xiàn)廣告位區(qū)域是空的而不是顯示一個(gè)“加載失敗”的圖標(biāo)。請(qǐng)求被攔截后站點(diǎn)拿不到廣告響應(yīng)前端 JS 只能讀到超時(shí)或錯(cuò)誤狀態(tài)。1.2 樣式層把廣告容器“畫(huà)”成隱形請(qǐng)求攔截之外還有一類更溫柔的攔截方式元素隱藏。過(guò)濾規(guī)則集里大量存在類似這樣的規(guī)則##.ad-banner ##.ad-container ###ads-toolbox這些規(guī)則通常來(lái)自 EasyList、uBlock Origin 的默認(rèn)過(guò)濾清單。瀏覽器擴(kuò)展會(huì)把這些選擇器轉(zhuǎn)換成頁(yè)面上的一段 CSS強(qiáng)制給匹配到的元素加上display: none !important。廣告腳本可能仍然執(zhí)行了廣告資源可能也加載了但用戶看不到任何東西。為什么會(huì)有這種方案因?yàn)橛行V告是頁(yè)面內(nèi)嵌代碼直接輸出的不經(jīng)過(guò)外部域名請(qǐng)求阻斷攔不到還有一些廣告容器同時(shí)在服務(wù)端渲染了內(nèi)容單純隱藏能減少頁(yè)面重排體驗(yàn)更平滑。但無(wú)論哪種方式廣告位在瀏覽器里被“畫(huà)”成隱形了反廣告攔截器就可以順著這個(gè)特征做文章。1.3 腳本層與過(guò)濾器生態(tài)攔截依據(jù)從哪來(lái)廣告攔截器之所以“聰明”核心是有一份不斷更新的過(guò)濾器清單。這份清單靠社區(qū)爬取、人工維護(hù)、自動(dòng)測(cè)試來(lái)更新收錄了廣告聯(lián)盟的域名、廣告容器常見(jiàn) id/class、追蹤腳本特征等。有了這份清單攔截就變成模板匹配請(qǐng)求 URL 命中就斷掉DOM 選擇器命中就隱藏。但也正因?yàn)檫@樣整個(gè)攔截體系存在一個(gè)天然弱點(diǎn)它依賴“特征”。一旦特征變化攔截效果就會(huì)下降。反廣告攔截器恰恰抓住了這一點(diǎn)你可以創(chuàng)建出“長(zhǎng)得極像廣告”的元素和請(qǐng)求讓攔截器自己暴露自己。2. 反廣告攔截器的檢測(cè)手段網(wǎng)站怎么發(fā)現(xiàn)廣告被攔截廣告攔截器是在“暗中”工作的網(wǎng)站需要把它逼出來(lái)。目前前端最實(shí)用的檢測(cè)手段可以分為三大類誘餌請(qǐng)求、DOM 樣式反向偵察、網(wǎng)絡(luò)與性能佐證。實(shí)際生產(chǎn)環(huán)境中成熟的檢測(cè)庫(kù)往往會(huì)把它們組合起來(lái)用降低誤報(bào)率。2.1 最經(jīng)典的 Bait 誘餌方案拿“假?gòu)V告”試水Bait 方案是所有檢測(cè)手段里最經(jīng)典、也最容易理解的一種。思路是這樣的我在頁(yè)面里動(dòng)態(tài)創(chuàng)建一個(gè)圖片或腳本標(biāo)簽把它的 src 指向一個(gè)“看起來(lái)很像廣告資源”的地址。如果當(dāng)前瀏覽器裝了廣告攔截器這個(gè)請(qǐng)求大概率會(huì)被過(guò)濾器直接攔截于是標(biāo)簽會(huì)立刻觸發(fā)onerror回調(diào)。如果用戶干干凈凈這個(gè)請(qǐng)求大概率會(huì)正常加載然后觸發(fā)onload。早期像 FuckAdBlock 這類庫(kù)就是這么干的代碼核心長(zhǎng)這樣function testBait(timeout) { return new Promise(function (resolve) { var img new Image(); var timer setTimeout(function () { resolve(false); }, timeout || 800); img.onload function () { clearTimeout(timer); resolve(false); }; img.onerror function () { clearTimeout(timer); resolve(true); }; // 找一個(gè)大概率被過(guò)濾清單收錄的廣告域名 img.src https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?r Math.random(); }); }這里有個(gè)關(guān)鍵細(xì)節(jié)onerror觸發(fā)的速度。廣告攔截器是在請(qǐng)求發(fā)出階段就 cancel所以onerror幾乎毫秒級(jí)到達(dá)。而如果是網(wǎng)絡(luò)斷掉、DNS 解析失敗導(dǎo)致的onerror通常需要更長(zhǎng)等待。因此代碼里設(shè)置了一個(gè)超時(shí)時(shí)間比如 800ms超過(guò)這個(gè)時(shí)間還沒(méi)有觸發(fā)onerror就認(rèn)為沒(méi)有被攔截。這個(gè)超時(shí)值太短可能誤報(bào)太長(zhǎng)則影響檢測(cè)速度需要實(shí)際調(diào)。2.2 DOM 與樣式反向偵察檢查“隱形廣告框”請(qǐng)求誘餌能檢測(cè)到“請(qǐng)求攔截型”廣告攔截器但很多攔截器更偏好元素隱藏模式。它們不攔截請(qǐng)求而是讓廣告容器隱形。這時(shí)候前端創(chuàng)建一些命中過(guò)濾規(guī)則特征的元素再用getComputedStyle讀取計(jì)算樣式就能發(fā)現(xiàn)異常。舉個(gè)例子function testDom() { var candidates [ { tag: div, className: banner-ad }, { tag: div, className: ad-container }, { tag: div, id: ad-banner } ]; var detected false; for (var i 0; i candidates.length; i) { var el document.createElement(candidates[i].tag); el.className candidates[i].className || ; if (candidates[i].id) el.id candidates[i].id; el.style.cssText position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;; document.body.appendChild(el); var cs getComputedStyle(el); if (cs.display none || cs.visibility hidden || parseFloat(cs.opacity) 0) { detected true; } document.body.removeChild(el); } return detected; }這個(gè)方案的邏輯在于我自己創(chuàng)建的 DOM 元素本來(lái)沒(méi)有任何遮遮掩掩的動(dòng)機(jī)如果它的最終顯示狀態(tài)是display:none那一定有第三方規(guī)則作用在它身上。而第三方規(guī)則只能是廣告過(guò)濾器。這里我踩過(guò)一個(gè)坑過(guò)濾器規(guī)則的針對(duì)性往往很強(qiáng)比如某些規(guī)則是###ad-wrapper但你的候選元素只有ad-containerclass就匹配不上。所以真實(shí)項(xiàng)目里不能只造一兩個(gè)特征要盡量覆蓋 EasyList 里最常見(jiàn)的廣告容器 id/class。還有一些過(guò)濾規(guī)則掛在父級(jí)上子元素getComputedStyle可能讀不到hidden這屬于檢測(cè)方案的固有盲區(qū)所以需要組合多種手段。2.3 網(wǎng)絡(luò)與性能層佐證加載時(shí)延、請(qǐng)求狀態(tài)與資源時(shí)間線前兩種方案已經(jīng)能覆蓋大多數(shù)場(chǎng)景但在一些極端情況里會(huì)產(chǎn)生誤判比如頁(yè)面響應(yīng)慢、廣告域名被公司防火墻攔了、用戶裝了“反指紋”類隱私插件。于是更高階的檢測(cè)會(huì)用瀏覽器性能 API 來(lái)佐證。原理是記錄廣告域名的資源加載時(shí)間線正常加載會(huì)出現(xiàn)在performance.getEntriesByType(resource)里被攔截則會(huì)表現(xiàn)為請(qǐng)求不完整或干脆沒(méi)有條目。也可以結(jié)合fetch()對(duì)廣告地址發(fā)一個(gè)遵守 CORS 的探測(cè)請(qǐng)求看它返回的是ok還是網(wǎng)絡(luò)錯(cuò)誤配合AbortSignal.timeout做超時(shí)控制。這類檢測(cè)更精確但代碼更重通常只在比較敏感的頁(yè)面用。反廣告攔截庫(kù)實(shí)際部署時(shí)常見(jiàn)的做法是“主檢測(cè)同步執(zhí)行輔檢測(cè)異步兜底”先用 Bait 和 DOM 檢測(cè)快速得出結(jié)果再在requestIdleCallback或setTimeout里做性能層驗(yàn)證修正異常狀態(tài)。這樣可以兼顧體驗(yàn)和準(zhǔn)確率。2.4 成熟庫(kù)的檢測(cè)策略對(duì)比FuckAdBlock、BlockAdBlock 與自研自己做一套組合檢測(cè)聽(tīng)上去不難但真正讓它穩(wěn)定運(yùn)行其實(shí)很費(fèi)心。社區(qū)里比較成熟的開(kāi)源庫(kù)有 FuckAdBlock 和它的后續(xù)維護(hù)版本 BlockAdBlock。兩者的核心思路都還貼著上面的原理只是維護(hù)成本和細(xì)節(jié)處理不同。方案主要檢測(cè)手段誤報(bào)控制維護(hù)成本適用場(chǎng)景FuckAdBlockBait 圖片 onerror依賴超時(shí)參數(shù)低原作者已很少更新小型站點(diǎn)、展示提示BlockAdBlockBait DOM 樣式檢測(cè)組合多條件組合較穩(wěn)中有社區(qū)維護(hù)內(nèi)容站、需要較準(zhǔn)檢測(cè)完全自研按需組合所有手段 業(yè)務(wù)側(cè)上報(bào)可深度定制高需要持續(xù)跟過(guò)濾器變化對(duì)準(zhǔn)確率要求極高的大站選型建議很簡(jiǎn)單如果只是“用戶開(kāi)了攔截器就提示一句”用 BlockAdBlock 足夠如果需要把檢測(cè)結(jié)果和訂單、會(huì)員體系、廣告位替換聯(lián)動(dòng)那就值得自研因?yàn)闃I(yè)務(wù)邏輯耦合程度一高通用庫(kù)往往滿足不了。3. 檢測(cè)之后怎么辦反廣告攔截的處置與產(chǎn)品化檢測(cè)本身不是目的檢測(cè)出來(lái)之后做什么才是真正影響收入、體驗(yàn)和用戶留存的部分。不同站點(diǎn)的策略差異很大但技術(shù)實(shí)現(xiàn)上基本可以歸成幾條路線。3.1 彈窗、遮罩與內(nèi)容門禁最常見(jiàn)、也最讓用戶印象深刻的處置方式是“全屏警告”檢測(cè)到攔截器后頁(yè)面插入一個(gè)position: fixed的遮罩層提示用戶關(guān)閉廣告攔截器。技術(shù)上要注意幾點(diǎn)遮罩層要有足夠高的z-index否則抵不過(guò)廣告過(guò)濾規(guī)則順手隱藏掉。諷刺的是一些網(wǎng)站反廣告攔截遮罩也會(huì)被過(guò)濾器清單收錄如果你把遮罩元素的 class 寫(xiě)成adblock-warninguBlock 一類的擴(kuò)展可能直接把整個(gè)遮罩隱藏等于做無(wú)用功。所以生產(chǎn)代碼里遮罩元素命名越中性越好比如modal-tip、paywall-tip盡量少和廣告特征沾邊。另外要注意“檢測(cè)、展示”的時(shí)序。通常是把檢測(cè)結(jié)果寫(xiě)在 Promise 里檢測(cè)完成后再掛載遮罩 DOM。延遲太久用戶以為頁(yè)面卡了太早又可能誤傷正常訪問(wèn)一般建議頁(yè)面主內(nèi)容可見(jiàn)后 300~500ms 再?gòu)?。還有種做法是“內(nèi)容門禁”遮罩只蓋住正文下方頂部露出標(biāo)題用戶往下滑動(dòng)時(shí)看到模糊或者截?cái)嗟膬?nèi)容再提示放行。這種設(shè)計(jì)比全屏彈窗溫和不少轉(zhuǎn)化率反而更好代價(jià)是技術(shù)復(fù)雜度高一點(diǎn)需要給正文容器加高度截?cái)嗪蜐u隱效果。3.2 放行憑證與狀態(tài)管理用戶被提示“請(qǐng)關(guān)閉廣告攔截器”之后通常的操作是去擴(kuò)展欄把當(dāng)前網(wǎng)站加入白名單然后刷新。網(wǎng)站怎么知道用戶已經(jīng)放行最可靠的辦法就是刷新后重新檢測(cè)檢測(cè)通過(guò)就正常加載廣告。但用戶不一定愿意刷新很多站點(diǎn)會(huì)提供一個(gè)“我已經(jīng)停用重新加載”的按鈕觸發(fā)location.reload()。還有一種做法是放行憑證用戶點(diǎn)了某個(gè)按鈕、或者主動(dòng)點(diǎn)擊“繼續(xù)閱讀”之后前端寫(xiě)一個(gè)localStorage標(biāo)記服務(wù)端存一個(gè)會(huì)話狀態(tài)下次訪問(wèn)不再?gòu)椪谡帧_@里我吃過(guò)虧只寫(xiě)localStorage標(biāo)記有個(gè)明顯問(wèn)題很多用戶是直接刪除瀏覽器數(shù)據(jù)、或使用無(wú)痕模式標(biāo)記丟失又會(huì)被反復(fù)打擾。反過(guò)來(lái)如果完全不寫(xiě)標(biāo)記每次刷新都彈用戶反感度會(huì)直線上升。比較好的策略是“檢測(cè)結(jié)果 會(huì)話標(biāo)記”雙寫(xiě)以服務(wù)端會(huì)話為主前端標(biāo)記只是加速判斷。3.3 廣告替換、白名單引導(dǎo)與合規(guī)邊界不硬碰硬也不意味著放棄收入。一些站點(diǎn)檢測(cè)到廣告被攔截后會(huì)把原本廣告位替換成其他內(nèi)容自家的促銷活動(dòng)、訂閱引導(dǎo)、公眾號(hào)二維碼、內(nèi)容推薦模塊。這些內(nèi)容不會(huì)被廣告過(guò)濾器攔因?yàn)閿r截器只針對(duì)第三方廣告聯(lián)盟特征。技術(shù)實(shí)現(xiàn)上廣告容器初始化時(shí)留一個(gè) fallback 區(qū)域檢測(cè)到攔截后再渲染替代模塊。從產(chǎn)品價(jià)值觀上講我不建議在檢測(cè)到攔截后偷偷運(yùn)行隱藏挖礦腳本也不建議欺騙性誘導(dǎo)用戶點(diǎn)擊“關(guān)閉攔截器”卻把頁(yè)面導(dǎo)去廣告頁(yè)。這類做法一旦被用戶識(shí)破損失的是長(zhǎng)期信任甚至?xí)话踩浖?biāo)記有點(diǎn)得不償失。合規(guī)上也要注意彈窗的克制性、隱私聲明的透明度特別是涉及讀取頁(yè)面狀態(tài)、用戶交互數(shù)據(jù)時(shí)最好提前做用戶告知。另外現(xiàn)在內(nèi)容平臺(tái)普遍流行“Acceptable Ads”標(biāo)準(zhǔn)廣告攔截器會(huì)放行一些符合規(guī)范的、克制且不干擾的廣告。如果你的廣告位設(shè)計(jì)合理可以考慮主動(dòng)申請(qǐng)這類白名單從根源減少被攔截的概率這比純粹的攻防對(duì)抗更健康。4. 攻防升級(jí)為什么反廣告攔截器不能一勞永逸很多第一次接觸反廣告攔截器的朋友會(huì)問(wèn)我部署一次檢測(cè)庫(kù)是不是以后都能攔截了答案是否定的。廣告攔截器和反廣告攔截器之間是一場(chǎng)持續(xù)的雙向軍備競(jìng)賽任何一方固定下來(lái)另一方很快就能找到破解點(diǎn)。4.1 檢測(cè)腳本的脆弱點(diǎn)被加入過(guò)濾列表就失效反廣告攔截器本身也是“某段 JS 代碼”它也要加載、執(zhí)行。過(guò)濾器維護(hù)者完全可以把你這個(gè) lib 的域名、路徑、甚至 JS 里的關(guān)鍵變量名收錄進(jìn)過(guò)濾清單直接從源頭入手讓你的檢測(cè)邏輯根本不加載。比如某知名檢測(cè)庫(kù)的腳本地址曾經(jīng)在 GitHub 上公開(kāi)且被大量站點(diǎn)直接引用過(guò)濾清單里只要加一行規(guī)則幾乎所有引用該 CDN 的站點(diǎn)都會(huì)失效。這也是為什么成熟的檢測(cè)庫(kù)會(huì)把核心算法拆成多個(gè)模塊、隨機(jī)分配文件名而不是乖乖待在固定的靜態(tài)路徑上。4.2 隨機(jī)化與混淆特征對(duì)抗的兩條路線為了逃避被過(guò)濾反廣告攔截器這邊的技術(shù)路線有兩類一是資源與命名隨機(jī)化二是邏輯混淆。資源與命名隨機(jī)化說(shuō)起來(lái)很直白Bait 元素不固定用ad-container這個(gè) class而是每次訪問(wèn)從一組特征里隨機(jī)挑Bait 請(qǐng)求的 URL 不寫(xiě)死域名而是通過(guò)一個(gè)隨機(jī)子域參數(shù)拼接最大化匹配過(guò)濾規(guī)則又增加維護(hù)成本。邏輯混淆則是把檢測(cè)代碼做混淆壓縮變量名縮短、函數(shù)調(diào)用扁平化讓過(guò)濾器無(wú)法用特征字符串匹配。廣告攔截器這邊也在跟著升級(jí)?,F(xiàn)在很多過(guò)濾器已經(jīng)不只看函數(shù)名和變量名而是會(huì)用“行為特征”比如動(dòng)態(tài)創(chuàng)建隱藏圖片、短時(shí)間內(nèi)發(fā)起對(duì)多處廣告域名的請(qǐng)求即使域名是隨機(jī)生成的這種動(dòng)作也被看作可疑行為。兩邊都在拿“模式識(shí)別”做文章誰(shuí)先預(yù)測(cè)到對(duì)方的模式誰(shuí)就占據(jù)上風(fēng)。4.3 體驗(yàn)、隱私與搜索流量的平衡技術(shù)對(duì)抗是有成本的。檢測(cè)腳本越復(fù)雜頁(yè)面加載損耗越高彈窗越強(qiáng)推用戶跳出率越高遮罩越像插頁(yè)廣告搜索引擎對(duì)頁(yè)面體驗(yàn)的評(píng)估越差可能直接影響自然搜索流量。我個(gè)人的判斷是反廣告攔截器最適合的應(yīng)用場(chǎng)景是那些“廣告收入是唯一收入來(lái)源且用戶粘性很高”的內(nèi)容站。對(duì)于一個(gè)剛起步的站點(diǎn)與其花大力氣對(duì)抗攔截器不如先把內(nèi)容質(zhì)量和用戶信任做起來(lái)再考慮廣告變現(xiàn)。技術(shù)方案永遠(yuǎn)要服務(wù)于產(chǎn)品階段不能陷入“為了對(duì)抗而對(duì)抗”的狀態(tài)。5. 實(shí)操記錄從零實(shí)現(xiàn)一個(gè)最小反廣告攔截 Demo說(shuō)了這么多原理最好還是動(dòng)手寫(xiě)點(diǎn)東西。這個(gè)章節(jié)我記錄一個(gè)自己折騰過(guò)的最小 Demo麻雀雖小五臟俱全讀完之后你可以對(duì)照著改代碼完整體驗(yàn)完整的檢測(cè)鏈路。5.1 Demo 目標(biāo)與運(yùn)行環(huán)境我當(dāng)時(shí)的 Demo 目標(biāo)很簡(jiǎn)單訪問(wèn)頁(yè)面時(shí)檢測(cè)是否加載了廣告攔截器如果是彈出遮罩提示放行如果不是頁(yè)面正常展示。運(yùn)行環(huán)境直接用一個(gè)普通 HTML 頁(yè)面加原生 JavaScript不依賴任何框架瀏覽器用 Chrome。為了把問(wèn)題說(shuō)透我故意沒(méi)有用任何現(xiàn)成庫(kù)檢測(cè)邏輯全部手寫(xiě)這樣每一步都有機(jī)會(huì)驗(yàn)證。代碼的核心分兩段Bait 網(wǎng)絡(luò)檢測(cè)、DOM 樣式檢測(cè)兩個(gè)結(jié)果只要有一個(gè)命中就判定為攔截器存在。5.2 核心代碼與運(yùn)行流程完整代碼我整理成下面這樣可以直接在本地服務(wù)器里跑起來(lái)看效果!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleAnti-Adblock Demo/title style #modal-tip { display: none; position: fixed; inset: 0; background: rgba(240, 240, 240, 0.96); z-index: 9999; text-align: center; padding-top: 18vh; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; } /style /head body div idmodal-tip h2看起來(lái)你正在使用廣告攔截器/h2 p請(qǐng)將本站加入白名單后刷新頁(yè)面以繼續(xù)閱讀完整內(nèi)容。/p button onclicklocation.reload()我已停用重新加載/button /div main h1這是一篇正常的內(nèi)容/h1 p如果你能看到全文說(shuō)明頁(yè)面沒(méi)有被廣告攔截器遮罩。/p /main script (function () { function testBait(timeout) { return new Promise(function (resolve) { var img new Image(); var timer setTimeout(function () { resolve(false); }, timeout || 800); img.onload function () { clearTimeout(timer); resolve(false); }; img.onerror function () { clearTimeout(timer); resolve(true); }; // 常見(jiàn)被過(guò)濾清單收錄的廣告腳本加隨機(jī)參數(shù)防止緩存 img.src https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?r Math.random(); }); } function testDom() { var candidates [ { tag: div, cls: banner-ad }, { tag: div, cls: ad-container }, { tag: div, id: ad-banner } ]; var detected false; for (var i 0; i candidates.length; i) { var el document.createElement(candidates[i].tag); if (candidates[i].cls) el.className candidates[i].cls; if (candidates[i].id) el.id candidates[i].id; el.style.cssText position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;; document.body.appendChild(el); var cs getComputedStyle(el); if (cs.display none || cs.visibility hidden || cs.opacity 0) { detected true; } document.body.removeChild(el); } return detected; } function showModal() { document.getElementById(modal-tip).style.display block; } testBait(800).then(function (baitResult) { var domResult testDom(); if (baitResult || domResult) { showModal(); } else { console.log(No adblocker detected); } }); })(); /script /body /html運(yùn)行流程很清楚頁(yè)面加載完腳本立刻發(fā)起 Bait 請(qǐng)求和 DOM 特征檢測(cè)兩個(gè) Promise 結(jié)果匯總后只要有一個(gè)判定命中就顯示遮罩否則保持正常內(nèi)容可見(jiàn)。testDom()里的候選元素覆蓋了常見(jiàn)的banner-ad、ad-container和ad-banner這幾個(gè)特征在過(guò)濾清單里出現(xiàn)頻率很高。5.3 實(shí)測(cè)結(jié)果與踩坑細(xì)節(jié)這個(gè) Demo 我在三種環(huán)境里實(shí)測(cè)過(guò)干凈 Chrome、裝 uBlock Origin 的 Chrome、裝 AdGuard 的 Chrome。結(jié)果基本準(zhǔn)確但有一個(gè)現(xiàn)象很有意思uBlock 的默認(rèn)模式會(huì)同時(shí)攔截請(qǐng)求和做元素隱藏Bait 檢測(cè)一秒內(nèi)就觸發(fā)而 AdGuard 如果只啟用元素隱藏規(guī)則Bait 請(qǐng)求不會(huì)被攔反而是 DOM 檢測(cè)在起作用。這正好說(shuō)明組合檢測(cè)的必要性。踩過(guò)的坑主要有三個(gè)。第一Bait 請(qǐng)求的 URL 如果指向一個(gè)有緩存策略的資源二次訪問(wèn)可能直接命中瀏覽器緩存onerror不觸發(fā)檢測(cè)失效。所以一定要加隨機(jī)參數(shù)讓每次檢測(cè)都發(fā)出新的請(qǐng)求。第二testDom()里創(chuàng)建的候選元素如果使用#ad-banner這種 id在同一頁(yè)面運(yùn)行多次而沒(méi)有清理干凈下一個(gè)getElementById會(huì)拿到舊元素。所以我每創(chuàng)建完一個(gè)元素就立刻removeChild保證檢測(cè)過(guò)程本身不影響頁(yè)面。第三本地靜態(tài)文件環(huán)境下跑 Demofile://協(xié)議下某些擴(kuò)展規(guī)則不生效導(dǎo)致明明開(kāi)著攔截器也沒(méi)檢測(cè)出來(lái)。需要起一個(gè)本地 HTTP 服務(wù)比如python3 -m http.server 8080用 localhost 訪問(wèn)才符合真實(shí)場(chǎng)景。6. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄把這個(gè) Demo 放到真實(shí)站點(diǎn)之前最好先看看別人在這些問(wèn)題上踩過(guò)什么坑。我見(jiàn)過(guò)不少團(tuán)隊(duì)照著開(kāi)源庫(kù)一貼就上線結(jié)果被用戶反饋淹沒(méi)。這里整理幾個(gè)高頻問(wèn)題算是經(jīng)驗(yàn)速查。6.1 誤報(bào)排查用戶明明沒(méi)開(kāi)攔截器最讓反廣告攔截器頭疼的就是誤報(bào)。用戶沒(méi)開(kāi)任何廣告攔截器頁(yè)面卻提示“請(qǐng)關(guān)閉廣告攔截器”這種體驗(yàn)幾乎是一次性的用戶可能從此不再回來(lái)。常見(jiàn)原因主要有公司網(wǎng)絡(luò)或路由器級(jí)廣告過(guò)濾比如 AdGuard Home、Pi-hole它們?cè)诰W(wǎng)絡(luò)層直接黑掉廣告域名瀏覽器里完全看不出插件痕跡但 Bait 請(qǐng)求同樣會(huì)失敗。瀏覽器安全擴(kuò)展太激進(jìn)比如某些反指紋、隱私加固插件把所有第三方腳本都攔截你的 Bait 請(qǐng)求也被“無(wú)差別攻擊”。廣告域名本身掛了或地區(qū)網(wǎng)絡(luò)抽風(fēng)onerror被正常網(wǎng)絡(luò)錯(cuò)誤觸發(fā)。排查方法很簡(jiǎn)單先在干凈無(wú)痕窗口打開(kāi)頁(yè)面看提示是否消失再關(guān)閉“隱私保護(hù)插件”對(duì)比測(cè)試。生產(chǎn)環(huán)境建議把 Bait 域名改成多個(gè)候選、配合超時(shí)和 DOM 檢測(cè)綜合判斷而且要設(shè)計(jì)“連續(xù) N 次檢測(cè)異常才提示”的防抖邏輯減少偶發(fā)網(wǎng)絡(luò)波動(dòng)導(dǎo)致的誤報(bào)。6.2 部署后日活與廣告收入變化怎么看有朋友部署反廣告攔截器后廣告收入沒(méi)漲日活反而掉了問(wèn)我是不是檢測(cè)錯(cuò)了。這個(gè)問(wèn)題很多時(shí)候不是技術(shù)問(wèn)題而是策略問(wèn)題。遮罩彈窗本身就會(huì)讓一部分用戶直接離開(kāi)尤其是那些“開(kāi)了攔截器但愿意看廣告”的中間地帶用戶。上線前一定要先設(shè)好指標(biāo)頁(yè)面可見(jiàn)率、廣告展示率、跳出率、回訪率分開(kāi)看。如果廣告展示率上去了但回訪率明顯下滑說(shuō)明你的提示策略太激進(jìn)??梢韵茸龌叶劝l(fā)布只對(duì)廣告位暴露時(shí)間長(zhǎng)、頁(yè)面瀏覽深度高的人群開(kāi)啟觀察數(shù)據(jù)再說(shuō)。技術(shù)上把檢測(cè)結(jié)果埋點(diǎn)上報(bào)比單純彈窗更有長(zhǎng)期價(jià)值。6.3 移動(dòng)端、嵌入式與 DNS 過(guò)濾場(chǎng)景的盲區(qū)移動(dòng)端瀏覽器上擴(kuò)展類廣告攔截器的安裝率沒(méi)有桌面端高但系統(tǒng)級(jí)廣告過(guò)濾、內(nèi)置瀏覽器攔截反而更普遍。這些場(chǎng)景里頁(yè)面內(nèi) JS 不一定能感知到攔截發(fā)生因?yàn)?Bait 請(qǐng)求可能在系統(tǒng)網(wǎng)絡(luò)層就被掐斷了onerror卻又不像擴(kuò)展攔截那么穩(wěn)定。即使前端檢測(cè)判定為“沒(méi)有攔截器”實(shí)際廣告素材可能依然無(wú)法展示。所以移動(dòng)端更適合看“廣告容器是否真的有內(nèi)容渲染出來(lái)”而不是只看攔截檢測(cè)。對(duì)于重度使用 DNS 過(guò)濾的用戶反廣告攔截器基本無(wú)解只能靠后端統(tǒng)計(jì)展示率間接判斷前端不要強(qiáng)求。從我實(shí)測(cè)的經(jīng)驗(yàn)來(lái)看反廣告攔截器永遠(yuǎn)只能作為站點(diǎn)收入體系里的一環(huán)而不是救命稻草。把檢測(cè)結(jié)果和用戶分層結(jié)合起來(lái)優(yōu)先做“提示、引導(dǎo)、替換”這類溫和策略技術(shù)才會(huì)真正為產(chǎn)品加分。真要長(zhǎng)期做就得保持每幾個(gè)月更新一次檢測(cè)特征的意識(shí)和過(guò)濾器清單“賽跑”的節(jié)奏體力消耗不小得做好心理準(zhǔn)備。