ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

爬虫进阶:从请求伪装到数据抓取的完整反爬应对思路

爬虫进阶:从请求伪装到数据抓取的完整反爬应对思路 干爬虫这行最常被问的一句话就是别人网站反爬做得那么严你怎么还能爬出来说实话爬虫和反爬就是一场永无止境的攻防战没有哪个网站是真的“爬不动”关键在于你懂不懂对方在防什么、怎么防的。今天这篇内容我把我这几年处理反爬网站的思路和代码完整梳理一遍从请求层伪装、Cookie处理、频率控制到动态网页的接口分析全都用真实例子讲清楚。不管你是刚开始学Python的新手还是已经写过不少脚本的老手只要会requests和XPath的基本用法这套方法论就能直接拿去用。1. 爬虫和反爬先搞清楚对方到底在防什么1.1 常见的反爬套路从轻到重反爬不是一门玄学它背后就一个核心目标区分“真人浏览器”和“程序脚本”。我遇到过的大多数网站反爬手段基本是从轻到重层层叠加的。最轻的一层是User-Agent检测。服务器看一眼请求头里的User-Agent发现是个Python requests默认值直接拒绝。这招在早期几乎能拦掉大半新人因为新手最容易偷懒不带请求头。再往上走是Headers完整性校验。有些网站不光看User-Agent还会检查Accept、Accept-Language、Referer、Connection这些字段是否齐全、是否合理。就像过安检证件不全的直接拦下。这里有个很容易踩的坑你以为带了UA就够了结果网站还校验Referer跨域请求就被返回403或者跳转到验证页。然后是Cookie和Session校验。网站通过Cookie记录你的身份、访问轨迹、停留时长如果你一个全新浏览器标识突然在几秒钟内疯狂请求同一个页面风控系统立刻就能判断这不是真人操作。还有更狠的直接在Cookie里塞一段加密签名比如股吧类网站经常干这事签名和UA绑定你换个UA访问签名就失效。高频请求检测也是重灾区。我见过不少网站设置阈值比如同一个IP在30秒内超过20次请求就弹验证码或者直接封IP。这种限制对搜索引擎爬虫宽松对未知爬虫很严格因为你没法证明自己是“好人”。再往后就是动态渲染。页面框架是空壳真正的数据由JavaScript异步请求后端接口填充。你直接请求HTML拿到的只有一堆无关紧要的标签核心内容全在XHR接口里。这时候如果不懂抓包分析硬用Selenium去等渲染效率就低了一大截。最后的大招是验证码、字体反爬、WebSocket动态推送。这些属于更重的反爬手段一般出现在登录环节、评论区、核心数据接口上。应对成本高通常需要专门的方案比如打码平台或者自己实现OCR。1.2 应对思路把自己伪装成一个“真人浏览器”搞清楚了对方在防什么应对思路就很简单了把自己伪装成一个真人浏览器让服务器分不清你是人是脚本。这套思路在实际执行中分三步走。第一步是**“看”打开浏览器开发者工具手动访问目标网站完整观察一次请求过程包括请求了哪些URL、带了哪些请求头、返回了什么数据。第二步是“模拟”用requests构造同样的请求把关键的Headers字段补齐用Session管理Cookie让后续请求带上身份信息。第三步是“克制”**控制请求频率加随机延时模拟真人浏览的节奏。你会发现绝大多数网站的初级和中级反爬核心破解点都不在某个“高级技术”上而是认真程度的问题。老老实实把Headers带全、把Cookie维护好、把访问节奏放慢90%的请求都能正常拿到数据。我见过太多人一上来就想着Selenium、代理池、打码平台结果连最基础的UA都没带纯属杀鸡用牛刀。2. 请求层处理从一次普通请求到“看起来像人”2.1 完整的Headers伪装别只带一个UA很多教程会教你加一个User-Agent然后就没下文了。实际上一套完整的浏览器请求头远比这复杂。我一般在项目里维护一个默认Headers模板在开发调试时从浏览器开发者工具里复制一份原始请求头缺什么补什么。import requests HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,image/apng,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, br, Connection: keep-alive, Upgrade-Insecure-Requests: 1, Referer: https://news.example.com/ }这里面最容易被忽略的两个字段是Accept和Accept-Language。前者告诉服务器你希望接收什么类型的内容后者告诉服务器你的浏览器语言偏好。真人浏览器的这两个字段都有特定格式而程序脚本常常缺失于是很多反爬系统就拿着两个字段做依据判断。另外Referer在爬取列表页到详情页的跳转链路里也经常被校验比如你直接访问详情页不带Referer服务器就认为你越过了正常入口直接拒绝。有一个小技巧值得说一下不要用唯一一个UA跑所有请求。有些网站的UA指纹库会记录每个UA的活跃情况如果某个UA一天之内发出几千个请求明显不正常。更稳妥的做法是准备一个小型UA池每次请求随机切换。import random UA_POOL [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/120.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ... Chrome/119.0 Safari/537.36, Mozilla/5.0 (X11; Linux x86_64) ... Firefox/121.0 ] def get_headers(): headers HEADERS.copy() headers[User-Agent] random.choice(UA_POOL) return headers2.2 Session与Cookie让服务器记住你用requests直接get页面是“无名氏”访问每次请求都像第一次来。而一个真人用户访问网站是有会话状态的你打开了首页服务器给你发了Cookie你访问列表页、详情页都带着这个Cookie。爬虫也一样用requests.Session()可以自动维护Cookie让服务器认为这是同一个“用户”在连续浏览。session requests.Session() session.headers.update(HEADERS) # 第一次访问触发服务器下发Cookie resp session.get(https://news.example.com/) print(session.cookies.get_dict())如果你要爬的网站需要登录流程也很清晰先分析登录接口用requests提交账号密码服务端返回的Set-Cookie会被Session自动保存后续所有请求都会自动带上。login_data { username: your_account, password: your_password } session.post(https://news.example.com/user/login, datalogin_data) # 之后再访问会员内容Session会自动携带Cookie profile_resp session.get(https://news.example.com/user/profile)这里有个反爬细节有些网站会把Cookie和User-Agent绑定同一份Cookie换了UA就会失效。所以如果你在代码里动态切换UA必须先用新的UA重新登录或者重新让服务器下发Cookie否则会出现“Cookie有效但请求依旧被拒绝”的怪现象。我早期就吃过这个亏排查了整整一个下午最后发现是UA切换导致Cookie签名不匹配。2.3 请求频率控制别让你的脚本比打鸡血还勤新手最容易忽略也最容易翻车的就是请求频率。一个真人手动刷新页面再怎么快也是秒级间隔而你的脚本可能毫秒级并发轰炸不出十次就会触发风控。控制频率最直接的办法是主动在两次请求之间加随机休眠而且休眠时间最好不要固定值。固定间隔本身就是一种特征真人不会每2秒精确无误地刷新一次页面。我用随机延时比较多比如2到5秒之间浮动模拟人的操作节奏。import time import random for page in range(1, 11): url fhttps://news.example.com/list/{page} resp session.get(url, headersget_headers()) # 解析页面内容... time.sleep(random.uniform(2, 5))除了单线程限速还有一点要克制不要开无限大的并发。有些教程教你用ThreadPoolExecutor开100个线程去爬觉得越快越好。实际情况是绝大多数中小型网站根本扛不住这种节奏你的IP很快就进黑名单。我在处理大规模爬取任务时常用方案是控制并发在2到4个线程之间每个线程之间再加随机延时综合下来比猛冲猛打稳定得多。2.4 IP代理池被限制之后的正规解法如果你已经被目标网站限制或者目标网站对IP频率要求极其严格单靠降低请求速度已经不够了这时候需要换IP访问。爬虫里说的“代理”本质是让请求通过中间服务器转发从而隐藏或更换你的出口IP。proxies { http: http://your_proxy_ip:port, https: http://your_proxy_ip:port } resp session.get(url, proxiesproxies, headersget_headers())代理IP的来源一般有两条路一是购买正规代理服务商的IP池二是自建代理池爬取公开的免费代理列表并实时检测可用性。我一般倾向于前者的稳定性但无论哪一种都必须注意合规问题代理服务的用途也必须符合法律法规和目标网站的规则不能用于绕过法律限制或恶意攻击。使用代理的时机也很讲究。我见过有人一上来就给每个请求都换IP结果触发更严格的验证。正确做法是优先靠Headers和频率伪装只有当某个IP被明显限制时才切换代理。另外代理IP池最好做“可用性检查”一个IP连不上就自动换下一个避免单个坏代理卡死整个任务。proxy_list [ http://ip1:port, http://ip2:port, http://ip3:port ] def fetch_with_proxy(url, max_retries3): for _ in range(max_retries): proxy random.choice(proxy_list) try: resp session.get(url, proxies{http: proxy, https: proxy}, timeout10) if resp.status_code 200: return resp except requests.RequestException: continue return None3. 实战处理一个带了UA校验和频率限制的资讯网站3.1 目标分析与反爬识别理论讲了这么多没有例子等于白说。我拿一个典型的资讯类网页做完整演示这类网站很常见反爬设置也很有代表性有UA校验、有Referer校验、有页面访问频次限制另外一个特点是列表页是服务端渲染的不需要动态等待。第一步是打开开发者工具手动访问目标列表页在Network面板里找到文档请求完整记录请求头。同时留意状态码如果是200说明当前访问方式是被接受的如果是403或者返回验证页面说明触发了某种反爬判断。然后我习惯做一步验证用Python直接请求一次裸URL不带任何Headers观察返回内容。这一步能快速定位网站的反爬层级。# 验证裸请求 resp requests.get(https://news.example.com/list/1) print(resp.status_code) print(resp.text[:500])如果返回403或者跳转到验证页说明UA检测启动了。这时候带上完整的Headers再试一次如果恢复了200说明UA伪造和Headers补全已经解决基础限制。3.2 构造带伪装标识的请求与限速重试在真实项目里我的请求函数一般长这样支持随机UA、自动重试、限速休眠和异常捕获。这个函数是整个爬虫的基础后续所有页面请求都复用它。import requests import time import random from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session requests.Session() # 配置重试策略遇到连接错误、5xx错误自动重试 retry Retry( total3, backoff_factor1, status_forcelist[500, 502, 503, 504] ) adapter HTTPAdapter(max_retriesretry) session.mount(http://, adapter) session.mount(https://, adapter) def fetch_page(url): headers get_headers() try: resp session.get(url, headersheaders, timeout10) if resp.status_code 200: return resp.text elif resp.status_code 403: print(f[403] 触发反爬: {url}) time.sleep(random.uniform(5, 10)) return None else: print(f[{resp.status_code}] 请求异常: {url}) return None except requests.RequestException as e: print(f[请求失败] {url} - {e}) time.sleep(random.uniform(3, 5)) return None这里有一个值得展开的细节重试策略中的backoff_factor参数。它控制着每次重试之间的等待时间重试间隔按指数增长第一次等1秒第二次等2秒第三次等4秒。这种退避策略在爬取时非常重要一方面给服务器留出喘息空间另一方面避免因为某个瞬时错误死循环式地狂轰。回调中加随机延时也很有必要因为固定时间的重试很容易被识别为程序特征。3.3 用XPath解析列表页核心字段拿到HTML之后解析就是重头戏。我用lxml配合XPath的时间比较多因为XPath定位元素比BeautifulSoup的正则式写法更明确尤其在处理层级复杂、页面结构不规则的列表页时优势明显。from lxml import etree def parse_list_page(html): tree etree.HTML(html) items tree.xpath(//div[contains(class, news-item)]) results [] for item in items: title_node item.xpath(.//h2/a) if not title_node: continue title title_node[0].text.strip() link title_node[0].get(href) date_node item.xpath(.//span[contains(class, date)]/text()) date date_node[0].strip() if date_node else desc_node item.xpath(.//p[contains(class, desc)]/text()) desc desc_node[0].strip() if desc_node else results.append({ title: title, link: link, date: date, desc: desc }) return resultsXPath里最容易出错的不是语法本身而是网页结构调整导致节点失效。所以我一般会在定位时加一层判断比如if not title_node: continue跳过异常节点避免一个数据格式不规整就导致整个脚本中断。如果你更习惯BeautifulSoup也没问题每个人的偏好不同关键是稳定。我个人的习惯是结构非常规整的页面用CSS选择器结构复杂或者需要按层级取父节点下的多个子节点时用XPath。两条路都走得通别死磕某一种工具。3.4 分页爬取与数据落盘列表页通常有分页。爬分页的核心逻辑很简单遍历页码构造URL请求页面解析数据存储结果。真正的难点在于“爬多快”和“怎么保存”这两个工程化问题。import csv def save_to_csv(results, filenamenews.csv): with open(filename, a, newline, encodingutf-8-sig) as f: writer csv.writer(f) for item in results: writer.writerow([ item[title], item[date], item[desc], item[link] ]) for page in range(1, 11): url fhttps://news.example.com/list/{page} html fetch_page(url) if html is None: print(f第{page}页获取失败跳过) continue data parse_list_page(html) if data: save_to_csv(data) print(f第{page}页解析到 {len(data)} 条数据) time.sleep(random.uniform(2, 5))写入CSV时我用了encodingutf-8-sig单纯地写utf-8在Excel里打开中文会乱码utf-8-sig会在文件开头加上BOM标记Excel就能正确识别。这种细节看起来不起眼但在实际交付数据时往往决定最终的可用性。爬完10页后你的CSV里就有了完整的数据。但这只是最基础的“单机版爬虫”如果目标网站一小时更新一次你还得考虑定时任务、增量抓取、断点续爬。这些工程化问题在真实项目中占比很大我建议新手先把单次抓取跑通再去考虑调度和增量。4. 动态渲染网站JS生成的数据怎么拿4.1 别急着上Selenium先找数据接口有些网站的列表页源码里根本找不到新闻数据取而代之的是一堆空的div标签核心数据全靠页面加载后的JavaScript异步请求来填充。很多初学者一遇到这种情况就立刻上Selenium等浏览器自动化工具页面是能拿到了但速度慢、资源占用高、代码容易崩。正确的第一步永远是在开发者工具的Network面板里过滤XHR请求看看有没有现成的数据接口。页面渲染需要数据数据就必然有一个URL这个URL往往隐藏在某个XHR请求里返回JSON格式的内容。直接请求JSON接口既高效又稳定解析也比解析HTML方便得多。4.2 接口分析实战与JSON解析我举个例子某资讯网站点开列表页后Network面板里出现一个名为/api/v1/news/list?page1size20的请求返回JSON数据。于是爬虫的核心任务就变成了请求这个API。import json API_URL https://news.example.com/api/v1/news/list def fetch_json(page1, size20): params { page: page, size: size } resp session.get(API_URL, paramsparams, headersget_headers(), timeout10) if resp.status_code 200: return resp.json() return None data fetch_json(1) if data: news_list data.get(data, {}).get(list, []) for item in news_list: print(item.get(title), item.get(publish_time))接口请求有两个好处第一不需要解析HTML结构数据本身就是结构化JSON第二接口往往支持分页参数翻页只需要改参数比拼接页面URL靠谱得多。但接口也有它的考验很多网站的接口会校验X-Requested-With头。headers_with_xhr get_headers() headers_with_xhr[X-Requested-With] XMLHttpRequest resp session.get(API_URL, headersheaders_with_xhr, paramsparams)这个请求头是jQuery和axios之类前端框架发AJAX请求时自动带的后端会根据它判断请求是否来自浏览器环境。不加这个头有些接口会返回403加了就恢复正常。4.3 兜底方案Selenium与Playwright如果找不到数据接口或者接口加密太复杂才轮到浏览器自动化方案。我用Playwright比Selenium多一点因为Playwright在等待元素、处理验证码和并发方面体验更好但Selenium的生态更成熟、网上资料更多两边都可以。# Playwright 示例 from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(https://news.example.com/list/1, timeout30000) # 等待页面渲染完成 page.wait_for_selector(.news-item) html page.content() browser.close()使用浏览器自动化的关键点在于“等待”。如果页面还没渲染完就取源码拿到依然是一堆空标签。wait_for_selector就是等某个元素出现后再抓取这比sleep(5)这种固定等待靠谱得多因为它按条件等待页面加载快了脚本就快加载慢了也不会立刻崩。5. 高频问题排查反爬请求中的那些坑5.1 出现403 Forbidden怎么办403基本是反爬拦截的通用暗号但它背后的原因各不相同。我的排查顺序是先看是不是裸请求没带Headers再检查Referer和Origin是否合理然后看请求频率是不是太快最后考虑是不是IP被临时限制。大多数情况下403都能通过补Headers和降频解决。如果确认是IP限制但又确实需要继续爬一个正规做法是等待一段时间让封禁自动解除。有些网站的封禁是短时的比如15分钟等一会儿就能继续访问。为了确认解封时间可以写一个探测脚本每隔1分钟请求一次目标页面记录状态码恢复正常的时间点用于评估任务恢复节奏。5.2 返回200但内容和浏览器不一样这种情况比403更烦人。你请求返回的状态码是正常的但页面内容要么是一串验证码页面要么是跳转脚本要么是一堆无意义的占位数据。这通常说明网站已经在验证你的“浏览器身份”比如检查了Cookie的生成来源或者要求完成某种JS挑战。应对思路是先从浏览器开发者工具里复制完整的请求头尤其是Cookie字段手动放进请求里试一次。如果手动Cookie能拿到正常数据说明需要先访问某个特定页面来获取合法Cookie再带着这个Cookie去请求目标页。5.3 返回数据中文乱码中文乱码八成是编码问题。requests在解码时默认猜测编码一旦猜错中文就变乱码。解决办法是明确指定页面的charset。resp.encoding utf-8如果是旧网站可能是gbk或者gb2312需要根据页面meta标签里的charset判断。我一般直接看一眼返回内容里的charset标识或者在请求后打印resp.apparent_encoding再决定。5.4 请求频率导致IP被封封IP是最常见的大规模爬取问题。判断依据很简单原本正常的请求突然大面积返回403或429换一个完全干净的网络环境访问目标网站却正常基本就是当前IP进了黑名单。这时候第一反应不应该是“换代理继续猛冲”而是反思爬取频率是否太高。我在实操中会把请求间隔拉大观察是否恢复如果恢复再逐步缩短间隔找到“安全阈值”。同时把任务分成多个批次每批之间休息更长时间避免连续几小时的高频请求。5.5 反爬问题速查表现象可能原因优先排查方案403 ForbiddenUA被识别 / Headers不完整补齐全部Headers换UA重试输入验证码触发频率风控降低请求频率增加随机延时返回空壳HTML数据由JS动态渲染找XHR接口再考虑Selenium请求提示过快单IP短时间请求过多加随机休眠控制并发首页正常详情页403Referer校验请求详情页时带上Referer中文乱码编码识别错误显式设置resp.encoding网页跳转到验证页Cookie缺失或过期用Session维护Cookie先访问入口页接口正常偶尔失败触发了限流加重试策略配合指数退避6. 最后想说的几句话爬虫和反爬的攻防是一场长期的动态游戏网站的反爬策略会不断升级爬虫的方案也要跟着调整。我在实际项目里最大的体会是真正的核心竞争力不是会用某个工具而是有一套完整的排查方法论。遇到反爬先判断是哪一层拦截再决定用什么方案打而不是一上来就堆技术。另外一个很重要的习惯是合规和克制。抓取公开数据用于学习研究没问题但要注意遵守目标网站的robots协议和服务条款控制请求频率不要对目标服务器造成压力。数据使用也要注意个人信息和版权问题不能把抓下来的数据随便乱用或二次传播。这个底线守住了才能长期玩下去。我自己的一个小习惯是真正跑批量任务之前永远先手动访问一次目标页面确认返回内容和预期一致再写循环。很多失败的爬虫脚本都是因为没做这一步就盲目跑全量结果要么请求全被拦要么解析了一堆垃圾数据。慢就是快稳才能走得远。希望这篇内容能帮你少踩几个坑把爬虫写得又稳又好用。
返回列表