欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

为什么你的爬虫活不过一个晚上:请求指纹、IP 池与节奏控制的一些实在经验

为什么你的爬虫活不过一个晚上:请求指纹、IP 池与节奏控制的一些实在经验

—— 写给那些"一跑就被封"、又不明所以的朋友

去年帮一个朋友抓某公开榜单,脚本写得飞起,前两个小时数据哗哗地进库,正得意呢,第二天一早全变 403,IP 直接进了黑名单。那会儿我俩的第一反应是"对方反爬好厉害",后来翻日志才发现自己蠢——不是人家多强,是我们自己"太像个机器人":同一个 User-Agent、同一个出口 IP、每 1.000 秒准时发一个请求,这套特征摊在任何一个反爬系统面前都等于举着牌子写"我是爬虫"。

这事儿之后我算是把"怎么像个真人一样把页面拿下来"认真琢磨了一遍。下面这几块不是什么黑科技,但几乎是所有反爬的第一道关卡,先把它们过顺了,再谈更深的对抗。


你以为的"伪装",其实啥都没做

直接用 requests.get() 发出的请求,默认 User-Agent 是 python-requests/2.x,服务端看到这个字段基本不用猜。更隐蔽的是,浏览器平时还会带一堆"习惯动作":AcceptAccept-LanguageRefererConnection: keep-alive 这些头,裸 requests 要么不带、要么顺序不对,凑在一起就更可疑。

还有一层很多人忽略的:TLS 指纹。现代反爬会用 JA3 / JA4 这种握手特征来识别客户端——requests 底层是固定套件,握手长什么样一抓一个准,光改 UA 救不了。这一步要么上 curl_cffi 这种能模拟浏览器指纹的库,要么就别把目标选在重指纹校验的站点上。

一个带 UA 池、带齐常用头的基础请求长这样:

import random
import requests

UA_POOL = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 "
    "(KHTML, like Gecko) Version/17.4 Safari/605.1.15",
    "Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0",
]

def build_headers():
    return {
        "User-Agent": random.choice(UA_POOL),
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,"
                  "image/avif,image/webp,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
        "Referer": "https://www.baidu.com/",
        "Connection": "keep-alive",
    }

resp = requests.get("https://example.com/list", headers=build_headers(), timeout=10)
print(resp.status_code, len(resp.text))
一个容易被忽略的点:UA 池不是随便堆字符串。一个 2019 年的 Chrome UA 跑在 2026 年的请求里,配上现代 TLS 握手,反而更扎眼。池子里的版本分布,最好和你目标受众真实的浏览器占比对得上——抓中文站点就多放点 Chrome/Edge 的近期版本,别塞一堆十年前的古董。

IP 不是越多越好,是越"干净"越好

很多新手一上来就搜"免费代理池",结果踩得满脚血。免费代理三个通病:慢、存活率低、还可能是别人摆的蜜罐——你以为在用它隐藏自己,其实对方正拿你的请求做中间人,账号密码token全看光。真要规模采集,老老实实买付费住宅代理,比免费池省下的 debugging 时间值钱得多。

买代理也分两类:数据中心 IP 便宜量大,但特征明显,大站一眼识破;住宅 IP 是真实家庭宽带出口,隐蔽性好,价格也高。选型就是一笔账——目标站反爬强度高,就上住宅;只是普通资讯站,数据中心足够。

不管哪种,把死代理塞进轮询都是纯浪费请求。发之前先打个验活请求:

def is_alive(proxy, test="https://httpbin.org/ip", timeout=5):
    try:
        r = requests.get(test, proxies={"http": proxy, "https": proxy},
                         timeout=timeout)
        return r.status_code == 200
    except requests.RequestException:
        return False

def pick_proxy(pool):
    # 随机挑一个,先验活再返回;连续几次都死就放弃本轮
    for _ in range(5):
        p = random.choice(pool)
        if is_alive(p):
            return p
    return None

这里有个权衡:每次都验活会多吃一个请求、拖慢整体速度。规模小的时候无所谓,量级上去了可以把验活挪到后台定时任务里批量刷,主抓取线程只读"已验证存活"的名单。


节奏这件事,规律就是原罪

我见过最典型的自杀式写法:for url in urls: fetch(url); time.sleep(1)。固定 1 秒间隔,人类根本做不到——人点网页有快有慢,还会走神。机器人才会"滴、滴、滴"这么均匀。

两个改法:一是把间隔换成随机抖动;二是遇到失败做指数退避,别傻重试。真碰上 429,服务端通常会回 Retry-After,照着它等,比自己硬刚礼貌也有效:

import time, random, requests

def fetch_with_care(url, headers, proxy, backoff=1.0):
    try:
        r = requests.get(url, headers=headers,
                         proxies={"http": proxy, "https": proxy}, timeout=10)
        if r.status_code == 429:
            wait = int(r.headers.get("Retry-After", 30))
            time.sleep(wait + random.uniform(0, 2))
            return fetch_with_care(url, headers, proxy, backoff)
        r.raise_for_status()
        time.sleep(random.uniform(0.5, 2.5))   # 抖动,而非固定间隔
        return r
    except requests.RequestException:
        # 失败退避:本轮等 backoff 秒,下次翻倍,封顶 30 秒
        time.sleep(min(backoff, 30) + random.uniform(0, 1))
        return None
把并发数当成"礼貌程度"。一个小站长用 2 核机器,你 32 线程并发把人家打挂,这已经不是爬虫合规的灰色地带,是实打实的 DOS 了。延迟、并发、重试上限这些参数,最好写进配置文件而不是硬编码,不同站点给不同档位,既稳又不添堵。

解析层:正则不是不能用,但别滥用

刚学爬虫的人容易走两个极端:要么全程正则硬抠,要么无脑上重型框架。我的习惯是看场景——如果页面结构稳定、你只抠一两个字段,正则最快最省事,零依赖;但凡是批量抓列表、HTML 层级复杂、哪天对方改个 class 名就崩的活儿,就用选择器。

CSS 选择器 / XPath(parsel / lxml)容错明显更好,能按层级定位、能批量取,结构微调不至于全废:

from parsel import Selector

sel = Selector(text=resp.text)
for item in sel.css("div.card"):
    title = item.css("h2.title::text").get(default="").strip()
    link  = item.css("a::attr(href)").get(default="")
    if title and link:
        print(title, link)

取舍就一句话:正则适合"结构钉死、单点提取",选择器适合"结构化、批量、要抗小改动"。别为了显酷上框架,也别为了省事正则梭哈。


收个尾,但没必要煽情

爬虫这门手艺,难的从来不是"把页面拿下来"——requests 一行就完事了。难的是"拿得稳、拿得久、还不给对方添堵"。上面这三关——请求指纹、IP 干净度、访问节奏——几乎横在所有反爬的第一线,先把它们想透,再去碰验证码、JS 逆向那些更深的对抗,会顺手很多。

技术中性提醒:动手前先看目标站的 robots.txt;明确标注受限、需要授权的接口不要硬钻;涉及个人信息的字段不要落库;把抓取频率压在对方站点能轻松承受的范围内。把这些工具用在公开数据的合规采集上,才是它能长久跑下去的前提。

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功