先说一件事。
2025年底,UCSD 和 Mozilla 发了篇论文。他们爬了两万个网站,发现——12.7%的热门站点都在用 Canvas 指纹追踪你。
更离谱的是,广告拦截器只挡住了 5%。剩下 95%,你装了 Adblock 也没用。
因为指纹脚本早就不从第三方域名加载了。它们把自己打包进网站自己的 JS bundle 里,从 URL 上看跟正常代码一模一样。广告拦截器哪敢拦?拦了网站就崩了。
还有个更讽刺的事:你装反指纹扩展,反而更容易被识别。
用的人太少(可能不到 0.1%),你一旦用了,就成了那个"小众群体"里的显眼包——"哦,这哥们用了 Canvas 随机化扩展,标记一下"。这就是反指纹悖论。
Canvas 指纹到底是啥?
核心代码就三行:
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.fillText('Cwm fjordbank gly', 2, 15);
const fingerprint = canvas.toDataURL();
同一台机器,这段代码永远输出同一个 base64 字符串。
换一台机器——GPU 不同、系统不同、字体库不同——结果就不一样了。
就这,成了跨站追踪的核武器。你换浏览器、清缓存、开无痕都没用。只要还是那台物理设备,Canvas 画出来的东西就不变。
原理讲完了。直接写检测器。
写个检测器,看看谁在画你
思路很简单:篡改 Canvas 的 toDataURL 方法,劫持每一次调用。看谁调了、调了几次、画了多大。
装环境
pip install playwright
playwright install chromium --with-deps
完整代码
建个 canvas_fp_detector.py:
"""
Canvas指纹检测器
功能:
- 网络拦截(屏蔽图片/字体,加速页面加载)
- 智能等待页面稳定
- 详细的调用分析(来源、次数、画布尺寸、耗时)
"""
import asyncio
from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeout
async def detect_canvas_fingerprinting(url, timeout_ms=25000):
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True,
args=["--no-sandbox", "--disable-setuid-sandbox"]
)
context = await browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0.0.0 Safari/537.36"
)
)
page = await context.new_page()
# 加速:屏蔽图片/字体等非关键资源
await page.route("**/*.{png,jpg,jpeg,gif,svg,ico,woff,woff2,ttf,eot}",
lambda route: route.abort())
# 注入Canvas拦截器
await page.add_init_script("""
(() => {
const original = HTMLCanvasElement.prototype.toDataURL;
const calls = [];
HTMLCanvasElement.prototype.toDataURL = function(...args) {
const stack = new Error().stack || '';
calls.push({
w: this.width, h: this.height,
t: args[0] || 'image/png',
s: stack.slice(0, 300),
ts: Date.now()
});
return original.apply(this, args);
};
window.__canvas_calls__ = calls;
})();
""")
print(f" 访问: {url}")
try:
await page.goto(url, wait_until="networkidle", timeout=timeout_ms)
try:
await page.wait_for_function(
"() => document.readyState === 'complete'", timeout=5000
)
await page.wait_for_timeout(2000)
except PlaywrightTimeout:
await page.wait_for_timeout(1000)
except PlaywrightTimeout:
print(f" ⚠️ 页面超时,用已有数据分析")
except Exception as e:
print(f" ❌ 访问失败: {e}")
await browser.close()
return None
data = await page.evaluate("""
() => {
const calls = window.__canvas_calls__ || [];
const bySrc = {};
for (const c of calls) {
const m = c.s.match(/https?:\/\/[^\/]+/);
const src = m ? m[0] : 'inline';
if (!bySrc[src]) bySrc[src] = [];
bySrc[src].push(c);
}
return {
total: calls.length,
sources: Object.entries(bySrc).map(([src, items]) => ({
source: src,
count: items.length,
sizes: [...new Set(items.map(i => `${i.w}x${i.h}`))],
interval_ms: items.length > 1 ?
items[items.length-1].ts - items[0].ts : 0
}))
};
}
""")
await browser.close()
return url, data
def analyze(url, data):
print(f"\n{'='*50}")
print(f"🔍 目标: {url}")
print(f"{'='*50}")
if not data:
print("❌ 检测失败")
return
total = data["total"]
print(f"📊 Canvas调用次数: {total}")
if total == 0:
print("✅ 未检测到指纹行为")
return
for src in data["sources"]:
flag = "🚨" if src["count"] >= 3 else ("⚠️" if src["count"] >= 2 else "ℹ️")
print(f" {flag} {src['source']}")
print(f" 调用{src['count']}次, 画布: {', '.join(src['sizes'])}")
if src["count"] >= 2:
print(f" 耗时: {src['interval_ms']}ms")
fp_sources = [s for s in data["sources"] if s["count"] >= 2]
if total >= 3 and len(fp_sources) >= 1:
print(f"\n🚨 判定: 疑似Canvas指纹采集 (可疑来源: {len(fp_sources)}个)")
elif total >= 2:
print(f"\n⚠️ 判定: 低度可疑,仅少量调用")
else:
print(f"\n✅ 判定: 正常")
async def main():
sites = [
"https://www.baidu.com",
"https://www.zhihu.com",
"https://www.douban.com",
"https://www.jd.com",
]
for site in sites:
result = await detect_canvas_fingerprinting(site)
if result:
analyze(*result)
if __name__ == "__main__":
asyncio.run(main())
实际跑一下

几个有意思的点:
百度调了2次,但不像是搞指纹的。调用间隔1.8秒,更像是页面渲染过程中顺带的行为。
知乎实锤。调了5次,两个来源。注意那个 cstaticdun.126.net——网易易盾,国内最大的风控SDK之一。你刷知乎的时候,它在后台偷偷画你的Canvas。一次不够,画三次。
豆瓣最干净。0次。豆瓣可能是大厂里唯一不搞Canvas指纹的。
京东最离谱。19次。持续3.6秒。三种尺寸的画布来回画。这是典型的批量指纹采集——不是画一次就够,是要从不同角度提取特征,构建精确的设备指纹。
怎么防?
这个问题得分两边聊。
如果你是普通用户:保护隐私
① 换Firefox,开抗指纹
Firefox是目前唯一默认提供持久化Canvas随机化的主流浏览器。每次会话内对相同绘图返回相同结果,一致性检测能过,但指纹已被干扰。
地址栏输入 about:config
搜索 privacy.resistFingerprinting → 设为 true
# 或者(新版Firefox)
搜索 privacy.fingerprintingProtection → 设为 true
② 别装那些"反指纹扩展"
反直觉,但这是真的。Canvas随机化扩展的用户群体太小(<0.1%),你一旦装了反而成了一个稀有特征——"用了Canvas随机化"本身就成了一个高熵指纹信号。
Firefox的做法是对的:从浏览器层面默认开启,让所有人都被随机化。当90%的用户都用抗指纹时,它就不是指纹信号了。
③ Tor Browser:最硬核的方案
Tor不仅随机化Canvas输出,还统一了所有浏览器的窗口尺寸(1000x900)、时区(UTC)、语言(en-US)。每个会话都是一个全新的指纹。代价是很多网站会把你当成爬虫直接拦了。
如果你是爬虫开发者:反反爬
这部分才是重点。既然指纹能识别出你不是真人,那怎么绕过?
① 戴好面具:改Playwright的默认特征
新手最容易暴露的问题:
# ❌ 新手写法——特征太明显
browser = await p.chromium.launch(headless=True)
# ✅ 正确做法——伪装成真实浏览器
context = await browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0.0.0 Safari/537.36",
locale="zh-CN",
timezone_id="Asia/Shanghai",
geolocation={"latitude": 39.9042, "longitude": 116.4074},
permissions=["geolocation"],
)
少了locale、timezone这种细节,Akamai一查就知道你不是真人。
② 抹掉自动化痕迹
Playwright默认会在浏览器里留下明显的自动化标记:
await page.add_init_script("""
// 删除webdriver标志
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
// 补上Chrome特有的属性
window.chrome = { runtime: {}, loadTimes: function() {}, csi: function() {}, app: {} };
// 补上plugins(无头浏览器默认是空的)
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5]
});
// 补上languages
Object.defineProperty(navigator, 'languages', {
get: () => ['zh-CN', 'zh', 'en']
});
""")
你可以在自己的检测器上加一段:打开一个网站,检查 navigator.webdriver。如果是true,你的爬虫已经被认出来了。
③ 换指纹,别只换IP
很多开发者以为轮换IP就安全了。实际上指纹一查,100个IP来的都是同一台机器的Canvas输出——这比被封IP更惨,你的整个代理池都被标记了。
正确的做法是每次会话用不同的配置组合:
FINGERPRINTS = [
{"os": "windows", "viewport": "1920x1080", "ua": "Windows NT 10.0"},
{"os": "macos", "viewport": "1440x900", "ua": "Macintosh; Intel Mac OS X 10_15"},
{"os": "linux", "viewport": "1366x768", "ua": "X11; Linux x86_64"},
]
import random
fp = random.choice(FINGERPRINTS)
context = await browser.new_context(
viewport={"width": int(fp["viewport"].split("x")[0]), ...},
user_agent=f"Mozilla/5.0 ({fp['ua']}) ..."
)
光换IP不换指纹,等于换了件外套没换脸。
④ 终极方案:用真实浏览器配置文件
Playwright支持加载真实的Chrome用户数据目录。先在普通Chrome上登录、装插件、产生浏览历史,然后把用户数据目录给Playwright用:
context = await browser.new_context(
storage_state="real_chrome_profile.json"
)
这样你的爬虫从指纹到Cookie到LocalStorage,跟真人一模一样。当然代价是每个"指纹"需要一台物理设备。
京东画17次Canvas不是为了好玩。它是想确保:就算你换了IP、换了UA、清空了Cookie,它还是能认出你。
如果想系统地学习自动化工具和反检测技术,可以到云栈社区多逛逛,那里有更多实战经验分享。
所以
十年前,5.5%的网站用Canvas指纹追踪你。今天,12.7%。
翻了一倍多。而且拦截不住。
好消息是Firefox已经默认在做了。Safari也在收紧API。反指纹正在从"小众扩展"变成"浏览器默认行为"。
坏消息是Chrome还没动。而Chrome占了七成市场份额。
京东在你电脑上画了17次。你没同意过。你甚至不知道。
下次打开京东,你可以跑一下这个脚本看看。结果可能跟上面一样。
代码实测可跑。有问题直接留言。