Node.js爬蟲實(shí)戰(zhàn):反爬策略和應(yīng)對(duì)方案(繞過限制)
道高一尺,魔高一丈。爬蟲與反爬的戰(zhàn)爭(zhēng),永無止境。
?? 開場(chǎng):一場(chǎng)貓鼠游戲
我曾經(jīng)爬過一個(gè)電商網(wǎng)站。
第一天,一切順利,數(shù)據(jù)嘩嘩地進(jìn)來。
第二天,開始出現(xiàn) 403 錯(cuò)誤,大概 10% 的請(qǐng)求被攔截。
第三天,50% 的請(qǐng)求返回驗(yàn)證碼頁面。
第四天,我的 IP 被永久封禁了。
這就是爬蟲工程師的日常:和網(wǎng)站的反爬系統(tǒng)斗智斗勇。
今天,我們來聊聊常見的反爬措施,以及如何(合法地)應(yīng)對(duì)它們。
??? 常見反爬措施
1. User-Agent 檢測(cè)
最基礎(chǔ)的反爬措施,檢查請(qǐng)求頭中的 User-Agent。
// ? 默認(rèn)的 axios User-Agent(會(huì)暴露你在用爬蟲) // axios/1.7.9 // ? 默認(rèn)的 Puppeteer User-Agent(包含 HeadlessChrome 字樣) // Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/131.0.0.0 Safari/537.36 // ? 正常瀏覽器的 User-Agent // Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
應(yīng)對(duì)方案:
// 隨機(jī) User-Agent
const userAgents = [
// Chrome on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
// Chrome on Mac
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
// Firefox on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
// Safari on Mac
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
// Edge on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
]
function getRandomUserAgent() {
return userAgents[Math.floor(Math.random() * userAgents.length)]
}
// 使用
await page.setUserAgent(getRandomUserAgent())
2. IP 頻率限制
同一 IP 請(qǐng)求過于頻繁會(huì)被封禁。
應(yīng)對(duì)方案:代理 IP 池
// src/utils/proxyPool.js
const axios = require("axios")
class ProxyPool {
constructor() {
this.proxies = []
this.currentIndex = 0
this.failedProxies = new Set()
}
/**
* 從代理服務(wù)商獲取代理列表
*/
async fetchProxies() {
// 示例:從免費(fèi)代理 API 獲取
// 生產(chǎn)環(huán)境建議使用付費(fèi)代理服務(wù)
const response = await axios.get("https://api.proxy-provider.com/list", {
params: {
apiKey: process.env.PROXY_API_KEY,
count: 100,
country: "CN",
},
})
this.proxies = response.data.proxies.map((p) => ({
host: p.ip,
port: p.port,
protocol: p.protocol || "http",
username: p.username,
password: p.password,
}))
console.log(`? 獲取到 ${this.proxies.length} 個(gè)代理`)
}
/**
* 獲取下一個(gè)可用代理
*/
getNext() {
// 過濾掉失敗的代理
const availableProxies = this.proxies.filter(
(p) => !this.failedProxies.has(`${p.host}:${p.port}`)
)
if (availableProxies.length === 0) {
throw new Error("沒有可用的代理")
}
// 輪詢
this.currentIndex = (this.currentIndex + 1) % availableProxies.length
return availableProxies[this.currentIndex]
}
/**
* 標(biāo)記代理為失敗
*/
markFailed(proxy) {
this.failedProxies.add(`${proxy.host}:${proxy.port}`)
}
/**
* 獲取代理 URL
*/
getProxyUrl(proxy) {
const { protocol, username, password, host, port } = proxy
if (username && password) {
return `${protocol}://${username}:${password}@${host}:${port}`
}
return `${protocol}://${host}:${port}`
}
}
module.exports = { ProxyPool }
在 Puppeteer 中使用代理:
const { ProxyPool } = require("./utils/proxyPool")
const proxyPool = new ProxyPool()
await proxyPool.fetchProxies()
async function crawlWithProxy(url) {
const proxy = proxyPool.getNext()
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${proxy.host}:${proxy.port}`],
})
const page = await browser.newPage()
// 如果代理需要認(rèn)證
if (proxy.username && proxy.password) {
await page.authenticate({
username: proxy.username,
password: proxy.password,
})
}
try {
await page.goto(url, { timeout: 30000 })
return await page.content()
} catch (error) {
// 代理失敗,標(biāo)記并重試
proxyPool.markFailed(proxy)
throw error
} finally {
await browser.close()
}
}
3. Cookie/Session 檢測(cè)
網(wǎng)站通過 Cookie 追蹤用戶行為,異常行為會(huì)被標(biāo)記。
應(yīng)對(duì)方案:Cookie 管理
// src/utils/cookieManager.js
const fs = require("fs-extra")
const path = require("path")
class CookieManager {
constructor(cookieDir = "./data/cookies") {
this.cookieDir = cookieDir
}
/**
* 保存 Cookie
*/
async save(domain, cookies) {
const filepath = path.join(this.cookieDir, `${domain}.json`)
await fs.ensureDir(this.cookieDir)
await fs.writeJson(filepath, cookies)
}
/**
* 加載 Cookie
*/
async load(domain) {
const filepath = path.join(this.cookieDir, `${domain}.json`)
if (await fs.pathExists(filepath)) {
return fs.readJson(filepath)
}
return null
}
/**
* 應(yīng)用 Cookie 到頁面
*/
async applyToPage(page, domain) {
const cookies = await this.load(domain)
if (cookies) {
await page.setCookie(...cookies)
return true
}
return false
}
/**
* 從頁面保存 Cookie
*/
async saveFromPage(page, domain) {
const cookies = await page.cookies()
await this.save(domain, cookies)
}
}
module.exports = { CookieManager }
4. 瀏覽器指紋檢測(cè)
網(wǎng)站通過 JavaScript 收集瀏覽器特征,識(shí)別爬蟲。
常見的指紋特征:
- Canvas 指紋
- WebGL 指紋
- 字體列表
- 屏幕分辨率
- 時(shí)區(qū)
- 語言
- 插件列表
應(yīng)對(duì)方案:使用 Stealth 插件
const puppeteer = require("puppeteer-extra")
const StealthPlugin = require("puppeteer-extra-plugin-stealth")
// 使用 Stealth 插件
puppeteer.use(StealthPlugin())
// Stealth 插件會(huì)自動(dòng)處理:
// - navigator.webdriver 屬性
// - Chrome 運(yùn)行時(shí)特征
// - 語言和插件
// - WebGL 渲染器
// - 等等...
const browser = await puppeteer.launch({ headless: "new" })
手動(dòng)偽裝指紋:
async function setupFingerprint(page) {
// 覆蓋 navigator.webdriver
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, "webdriver", {
get: () => undefined,
})
})
// 覆蓋 navigator.plugins
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, "plugins", {
get: () => [
{ name: "Chrome PDF Plugin" },
{ name: "Chrome PDF Viewer" },
{ name: "Native Client" },
],
})
})
// 覆蓋 navigator.languages
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, "languages", {
get: () => ["zh-CN", "zh", "en"],
})
})
// 覆蓋 WebGL 渲染器
await page.evaluateOnNewDocument(() => {
const getParameter = WebGLRenderingContext.prototype.getParameter
WebGLRenderingContext.prototype.getParameter = function (parameter) {
if (parameter === 37445) {
return "Intel Inc."
}
if (parameter === 37446) {
return "Intel Iris OpenGL Engine"
}
return getParameter.call(this, parameter)
}
})
}
5. 驗(yàn)證碼
最常見的反爬措施之一。
應(yīng)對(duì)方案:
方案 1:使用打碼平臺(tái)
const axios = require("axios")
/**
* 使用打碼平臺(tái)識(shí)別驗(yàn)證碼
*/
async function solveCaptcha(imageBase64, type = "common") {
// 示例:使用某打碼平臺(tái) API
const response = await axios.post("https://api.captcha-solver.com/solve", {
apiKey: process.env.CAPTCHA_API_KEY,
image: imageBase64,
type: type, // common, slide, click, etc.
})
return response.data.result
}
// 在爬蟲中使用
async function handleCaptcha(page) {
// 檢測(cè)是否有驗(yàn)證碼
const captchaElement = await page.$(".captcha-image")
if (!captchaElement) return true
// 截圖驗(yàn)證碼
const imageBuffer = await captchaElement.screenshot()
const imageBase64 = imageBuffer.toString("base64")
// 調(diào)用打碼平臺(tái)
const captchaCode = await solveCaptcha(imageBase64)
// 輸入驗(yàn)證碼
await page.type("#captcha-input", captchaCode)
await page.click("#submit-captcha")
// 等待驗(yàn)證結(jié)果
await page.waitForNavigation({ timeout: 5000 })
return true
}
方案 2:滑塊驗(yàn)證碼
/**
* 處理滑塊驗(yàn)證碼
*/
async function handleSliderCaptcha(page) {
// 獲取滑塊和背景圖
const slider = await page.$(".slider-button")
const background = await page.$(".slider-background")
if (!slider || !background) return
// 獲取滑塊位置
const sliderBox = await slider.boundingBox()
const bgBox = await background.boundingBox()
// 計(jì)算需要滑動(dòng)的距離(實(shí)際項(xiàng)目中需要圖像識(shí)別)
// 這里簡(jiǎn)化處理,假設(shè)已知距離
const distance = 200
// 模擬人類滑動(dòng)軌跡
const startX = sliderBox.x + sliderBox.width / 2
const startY = sliderBox.y + sliderBox.height / 2
await page.mouse.move(startX, startY)
await page.mouse.down()
// 生成人類化的滑動(dòng)軌跡
const tracks = generateHumanTrack(distance)
for (const track of tracks) {
await page.mouse.move(startX + track.x, startY + track.y)
await page.waitForTimeout(track.delay)
}
await page.mouse.up()
}
/**
* 生成人類化的滑動(dòng)軌跡
*/
function generateHumanTrack(distance) {
const tracks = []
let current = 0
// 先加速后減速
const mid = distance * 0.7
while (current < distance) {
let step
if (current < mid) {
// 加速階段
step = Math.random() * 10 + 5
} else {
// 減速階段
step = Math.random() * 3 + 1
}
current = Math.min(current + step, distance)
tracks.push({
x: current,
y: Math.random() * 2 - 1, // 輕微的 Y 軸抖動(dòng)
delay: Math.random() * 20 + 10,
})
}
return tracks
}
6. JavaScript 混淆與加密
網(wǎng)站使用混淆的 JS 代碼來隱藏?cái)?shù)據(jù)或生成簽名。
應(yīng)對(duì)方案:
/**
* 攔截并分析網(wǎng)絡(luò)請(qǐng)求
*/
async function interceptRequests(page) {
await page.setRequestInterception(true)
const apiRequests = []
page.on("request", (request) => {
// 記錄 API 請(qǐng)求
if (request.url().includes("/api/")) {
apiRequests.push({
url: request.url(),
method: request.method(),
headers: request.headers(),
postData: request.postData(),
})
}
request.continue()
})
page.on("response", async (response) => {
if (response.url().includes("/api/")) {
try {
const data = await response.json()
console.log("API 響應(yīng):", response.url(), data)
} catch (e) {
// 非 JSON 響應(yīng)
}
}
})
return apiRequests
}
/**
* 在瀏覽器中執(zhí)行 JS 獲取加密參數(shù)
*/
async function getEncryptedParams(page, data) {
// 假設(shè)網(wǎng)站有一個(gè)加密函數(shù) window.encrypt
const encrypted = await page.evaluate((data) => {
// 調(diào)用網(wǎng)站自己的加密函數(shù)
if (typeof window.encrypt === "function") {
return window.encrypt(data)
}
return null
}, data)
return encrypted
}
?? 高級(jí)偽裝技巧
1. 模擬真實(shí)用戶行為
/**
* 模擬人類瀏覽行為
*/
async function simulateHumanBehavior(page) {
// 隨機(jī)滾動(dòng)
await page.evaluate(() => {
const scrollHeight = document.body.scrollHeight
const randomScroll = Math.floor(Math.random() * scrollHeight * 0.5)
window.scrollTo(0, randomScroll)
})
// 隨機(jī)停留
await page.waitForTimeout(Math.random() * 2000 + 1000)
// 隨機(jī)移動(dòng)鼠標(biāo)
const viewport = page.viewport()
const randomX = Math.floor(Math.random() * viewport.width)
const randomY = Math.floor(Math.random() * viewport.height)
await page.mouse.move(randomX, randomY)
// 隨機(jī)點(diǎn)擊(非鏈接區(qū)域)
// await page.click('body')
}
/**
* 模擬打字
*/
async function humanType(page, selector, text) {
await page.click(selector)
for (const char of text) {
await page.keyboard.type(char)
// 隨機(jī)打字間隔
await page.waitForTimeout(Math.random() * 150 + 50)
}
}
2. 請(qǐng)求頭完整性
/**
* 設(shè)置完整的請(qǐng)求頭
*/
async function setFullHeaders(page) {
await page.setExtraHTTPHeaders({
Accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Cache-Control": "max-age=0",
Connection: "keep-alive",
"Sec-Ch-Ua":
'"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Ch-Ua-Platform": '"macOS"',
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
})
}
3. Referer 鏈偽造
/**
* 模擬正常的訪問路徑
*/
async function simulateNormalPath(page, targetUrl) {
// 先訪問首頁
await page.goto("https://example.com/", {
waitUntil: "networkidle2",
})
await page.waitForTimeout(2000)
// 模擬搜索
await page.type("#search-input", "關(guān)鍵詞")
await page.click("#search-button")
await page.waitForNavigation()
await page.waitForTimeout(1500)
// 點(diǎn)擊搜索結(jié)果進(jìn)入目標(biāo)頁面
// 這樣 Referer 就是搜索結(jié)果頁
await page.goto(targetUrl, {
referer: page.url(),
})
}
?? 反爬檢測(cè)自測(cè)
在開始爬取之前,可以用這些網(wǎng)站測(cè)試你的爬蟲是否會(huì)被檢測(cè):
async function testAntiDetection() {
const browser = await puppeteer.launch({ headless: "new" })
const page = await browser.newPage()
// 測(cè)試 1: Bot 檢測(cè)
await page.goto("https://bot.sannysoft.com/")
await page.screenshot({ path: "test-bot.png" })
// 測(cè)試 2: 瀏覽器指紋
await page.goto("https://browserleaks.com/canvas")
await page.screenshot({ path: "test-canvas.png" })
// 測(cè)試 3: WebRTC 泄露
await page.goto("https://browserleaks.com/webrtc")
await page.screenshot({ path: "test-webrtc.png" })
await browser.close()
console.log("檢測(cè)結(jié)果已保存為截圖,請(qǐng)查看")
}
?? 法律與道德提醒
在使用這些技術(shù)之前,請(qǐng)務(wù)必注意:
- 遵守 robots.txt - 這是基本禮儀
- 控制請(qǐng)求頻率 - 不要給目標(biāo)網(wǎng)站造成壓力
- 不爬取敏感數(shù)據(jù) - 個(gè)人隱私、付費(fèi)內(nèi)容等
- 不用于非法目的 - 不倒賣、不詐騙
- 尊重網(wǎng)站的服務(wù)條款 - 有些網(wǎng)站明確禁止爬取
記?。杭夹g(shù)是中立的,但使用技術(shù)的人要有底線。
?? 系列總結(jié)
到這里,《Node.js 爬蟲實(shí)戰(zhàn)指南》系列就完結(jié)了。
希望這個(gè)系列能幫你成為一個(gè)合格的爬蟲工程師。
記?。号老x是一門技術(shù),更是一門藝術(shù)。
到此這篇關(guān)于Node.js爬蟲實(shí)戰(zhàn):反爬策略和應(yīng)對(duì)方案(繞過限制)的文章就介紹到這了,更多相關(guān)Node.js反爬蟲和應(yīng)對(duì)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Node做中轉(zhuǎn)服務(wù)器轉(zhuǎn)發(fā)接口
這篇文章主要介紹了Node做中轉(zhuǎn)服務(wù)器轉(zhuǎn)發(fā)接口的相關(guān)資料,需要的朋友可以參考下2017-10-10
Node.js API詳解之 dgram模塊用法實(shí)例分析
這篇文章主要介紹了Node.js API詳解之 dgram模塊用法,結(jié)合實(shí)例形式分析了Node.js API中dgram模塊基本功能、函數(shù)、使用方法及操作注意事項(xiàng),需要的朋友可以參考下2020-06-06
npm使用淘寶鏡像安裝時(shí)報(bào)錯(cuò)的解決方案(npm淘寶鏡像到期盡快切換)
npm 淘寶鏡像到期了,盡快切換,本文給大家介紹了npm使用淘寶鏡像安裝時(shí)報(bào)錯(cuò)的解決方案,文中通過代碼示例和圖文講解的非常詳細(xì),具有一定的參考價(jià)值,需要的朋友可以參考下2024-02-02
nodejs實(shí)例解析(輸出hello world)
本文主要介紹nodejs實(shí)例解析:輸出hello world的完整過程。具有一定的參考價(jià)值,下面跟著小編一起來看下吧2017-01-01
xtemplate node.js 的使用方法實(shí)例解析
這篇文章主要介紹了xtemplate node.js 的使用方法實(shí)例說明,非常不錯(cuò),介紹的非常詳細(xì),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-08-08

