ARTICLE DETAIL

资讯详情

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

爬虫原理与反爬实战:从requests到Scrapy,破解前端防护与后端限流

爬虫原理与反爬实战:从requests到Scrapy,破解前端防护与后端限流 爬虫这个事儿圈内人聊起来总是两极分化有人靠它搞数据采集养活整个业务线有人因为不懂反爬天天被网站封IP还有人写了一堆代码最后被对方的JavaScript验证码折磨到怀疑人生。其实爬虫没那么玄乎本质就是“模拟浏览器行为、批量获取网页公开数据、再按需提取信息”的一套流程。这篇东西我不打算写成教科书就按我自己这些年踩坑、排查、重构的经验把爬虫从原理到实战、从前端防护到后端限流尽量讲透。适合刚准备入门的Python新手也适合写了两三个月爬虫但总被反爬搞得焦头烂活的人甚至后端同学也可以看看别人是怎么“破解”你接口的反向做防护。1. 爬虫到底是怎么工作的从一次网络请求说起很多人一上来就装Scrapy、配代理池其实连最基本的“一次HTTP请求到底发生了什么”都没搞清楚。我个人觉得爬虫入门最该花时间的地方就是先把requests库和浏览器开发者工具吃透。这俩搞明白后面所有框架、分布式、去重方案都是锦上添花。1.1 一次普通请求的完整流程你在浏览器地址栏输入一个网址并回车背后发生的事情大致是DNS解析把域名换成IP然后和服务器建立TCP连接HTTPS还要多做一次TLS握手接着发送HTTP请求头服务器处理完返回响应体浏览器再把HTML、CSS、JS渲染成页面。爬虫做的事情就是跳过“渲染”这一步直接把服务器返回的原始HTML拿过来或者直接请求后端接口拿JSON数据然后自己用代码提取需要的内容。1.2 解析响应的几种方式拿到HTML之后怎么把数据抠出来这是爬虫的核心分水岭。常见的解析方式有三种正则表达式最原始但最灵活适合结构简单、模式固定的字符串提取比如拿title(.*?)/title这种。缺点是写起来容易出错还特别难维护。XPath基于DOM树路径定位节点比正则易读得多。Python里配合lxml使用性能很好。热词里提到的“text函数”就是XPath里的text()用来获取节点文本内容坑是它只能拿当前节点的直接文本拿不到子节点拼出来的全文。BeautifulSoup适合快速原型API友好但大流量下性能一般。Jmespath、CSS Selector、JsonPath分别对应复杂JSON、HTML CSS选择器、嵌套JSON提取按需用就行。1.3 爬虫的最小实现requests XPath我建议新手第一节课就先跑通这个最小闭环不要着急上框架。代码大概长这样import requests from lxml import html url https://example.com/news headers {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)} resp requests.get(url, headersheaders, timeout10) resp.encoding resp.apparent_encoding doc html.fromstring(resp.text) titles doc.xpath(//h2/a/text()) for t in titles: print(t.strip())这段代码虽然短但已经包含了爬虫四个必备步骤构造请求、处理响应、解析内容、输出结果。注意resp.encoding那行很多新手中文乱码就是没设置编码直接用resp.text会按响应头的charset解码如果服务器没给对就会乱。用apparent_encoding能根据字节推断出实际编码算是第一道保险。2. 常见爬虫技术栈与框架选型工具选型没有绝对好坏只有合不合适。我自己经历过从纯requests脚本到Scrapy项目再到自己封装调度器的过程。这里把几个常用方案拿来做对比你们也好判断什么场景上什么。2.1 requests还是urllib为什么是requestsurllib是Python自带的HTTP库能用但不好用不支持自动重试、不自动管理Cookie、处理代理也麻烦。requests封装了这些细节一行requests.get搞定还支持会话对象Session()可以跨请求保持Cookie、复用连接池。爬虫里最重要的是Session因为很多网站登录态就是靠Cookie维持的用同一个Session连续请求就像你在同一台浏览器里没关过页面反爬系统不容易把你识别成机器人。2.2 XPath还是正则text函数的坑有一个经典场景页面上有个商品描述区域HTML长这样div classdesc 这个是span classhot爆款/span商品 /div如果你用XPath写//div[classdesc]//text()得到的是三个独立文本节点[“这个是”, “爆款”, “商品”]需要用.join(...)拼起来。换成//div[classdesc]/text()只拿到第一层文本会漏掉span里的内容。这是text函数最常见的坑搜索引擎里搜“爬虫 xpath text函数”十有八九都是这类问题。我的习惯是能用XPath就用XPath因为它定位节点比正则稳定但正则也有不可替代的场景比如提取JSON字段里的某个字符串、清理空行、匹配时间格式。真正项目里我经常XPath和正则混用比如先用XPath拿到列表节点再用正则提取里面的日期或编号。2.3 Scrapy框架什么时候值得上Scrapy不是一个简单的并发请求库它是一整套爬虫框架自带调度器、下载器、中间件、管道Pipeline还支持Twisted异步并发。用它和requests的最大区别是Scrapy帮你把“取数据、洗数据、存数据”的流程解耦了。比如多个页面共用一种清洗逻辑你只要在Pipeline里写一次要加代理、换UA、加延时配置Downloader Middleware就行。我个人是这么选的如果只是临时扒几十个页面requests写个脚本就够如果一个项目要持续跑、要抓几万几十万页、要管理多个爬虫任务立刻上Scrapy。不要犹豫犹豫只会让你后期把requests脚本改成兼职调度器修到怀疑人生。2.4 分布式爬虫的简单思路热词里有人搜分布式爬虫这块其实没想象中复杂。分布式要解决的问题就三个任务怎么分发、数据怎么汇总、去重怎么做。常见做法是用Redis做URL队列所有爬虫节点从同一个Redis里取任务去重用SETNX或者scrapy-redis框架的指纹集合抓到的数据各自写入数据库或者推送到消息队列。要注意的是分布式带来的效率提升不等于线性增长瓶颈通常在目标网站的反爬阈值和你的代理池质量上别盲目堆机器先把单机策略调优再扩容。3. 反爬机制与前端防护为什么不是所有数据都能轻松抓爬虫和反爬就是一场猫鼠游戏。有些网站数据是明文HTML静态爬就完事有些网站重兵防守各种字体反爬、JS加密、行为验证码让你怀疑人生。这个章节我想从进攻的角度反推防守方都做了什么方便你们既知道怎么破也知道怎么写接口保护自己的数据。3.1 前端反爬源码查看、调试禁用、动态渲染有热词提到“怎么在前端进行防止爬虫防止查看页面源码”。说实话纯前端几乎没有绝对防爬的手段因为浏览器必须把HTML和JS下载到本地才能渲染用户能用开发者工具看到的东西爬虫也能通过抓包看到。前端能做的只是增加爬虫的成本而不是彻底禁止。具体措施通常有这么几类右键禁用、快捷键禁用只能防小白开发者工具照样能开CtrlShiftI不行还能通过菜单打开。源码乱码化比如HTML压缩成一行、变量名改成无意义字符提升人工阅读成本。JS动态渲染页面初始HTML为空数据全靠异步JavaScript请求接口后填充。这样直接抓HTML只能拿到一堆空壳必须找到真实接口或用无头浏览器执行JS。代码混淆和加密把关键JS逻辑混淆让逆向分析变难也有些会做动态加密参数比如前端算出一个sign后才允许请求。字体反爬页面渲染后的数字和实际HTML里的字符映射不一致字体文件动态变化直接抓取文本会得到乱码或错误数字。这些东西作为前端开发者可以适当加上尤其是数据敏感、希望提高采集门槛的场景。但务必清醒前端防护是“减缓器”不是“防火墙”真正的防线在后端。3.2 后端反爬Java Controller层的防护思路热词里“java controller层 如何防护 防止爬虫”搜得很具体。我就用Java后端举例实际上所有后端语言的思路大同小异。Controller接口防爬的核心是让合法用户的请求看起来稳定、可追溯让爬虫程序的请求不稳定、成本飙升。常用手段包括UA校验检查User-Agent是否来自已知浏览器爬虫默认UA很容易被拦但伪装UA不是难事所以只能作为低层过滤。Referer校验判断请求来源页面是否合法。很多直接用脚本调接口的爬虫Referer往往缺失或伪造。频率限制这是最有效的低成本方案。用Redis计数器按IP、用户ID做滑动窗口限流比如单IP每分钟最多60次超过就返回429或验证码。Controller层可以写一个拦截器在进入方法体之前先走限流逻辑。接口签名要求前端对参数拼接固定盐值做MD5或HMAC重放校验。这也是目前APP接口最常见的防爬思路。动态Token每次请求前先通过一个预请求获取一次性Token后端校验过期时间与绑定范围。图形验证码或滑块当行为异常时触发让机器需要过图灵测试。登录鉴权把核心接口放到登录态后面加大批量获取难度。一段极简的Java拦截器思路大概是public class RateLimitInterceptor implements HandlerInterceptor { Autowired private StringRedisTemplate redis; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String ip getIpAddr(request); String key rate:api: ip; Long count redis.opsForValue().increment(key); if (count ! null count 1) { redis.expire(key, Duration.ofSeconds(60)); } if (count ! null count 60) { response.setStatus(429); response.getWriter().write(too many requests); return false; } return true; } }这个代码背后有三个容易被忽略的细节。一是Redis事务或increment原子性高并发下不要用“先get再set”那套会漏二是限流维度单IP限流不够遇到 NAT 或机房出口IP会误杀很多人最好能识别用户登录态并结合IP双重维度三是超限后的处理除了返回429还可以直接跳到验证码流程让真人通过验证后继续用。Java后端做防爬最佳位置其实是网关层比如Spring Cloud Gateway或Nginx层做统一限流再配合Controller拦截验证业务异常职责更清晰。3.3 字体反爬、验证码与行为检测字体反爬今年太常见了尤其招聘网站、小说网站、部分电商价格。原理是HTML里头是正常的数字字符但CSS里用自定义字体font-face将它映射成另一种字形人类看着是“3”DOM里却是另一个Unicode。破解思路也不复杂下载字体文件用fontTools解析cmap表建立Unicode到真实数字的映射替换文本。难点是有些网站每次响应都换个字体文件映射表一直在变那就得动态解析。验证码这块我不建议你们去破解也不推荐接打码平台干违规的事。正规业务里触发验证码说明对方已经认定你高危正确的做法是检查自己的访问频率、退避一段时间、降低并发、补齐合理Header、用真人行为模型随机间隔、鼠标轨迹模拟降低嫌疑。记住过验证码的本质不是绕过而是“证明你是真人”。行为检测则是更隐蔽的一层。后端会记录请求间隔、点击顺序、页面停留时间等指标用机器学习判断是否为人。爬虫如果每个请求间隔都精确1秒反而是最明显的机器人特征。常规应对是给请求时间加一个随机扰动比如time.sleep(random.uniform(0.5, 2.0))在工具层面很多框架自带AutoThrottle都能做。4. 爬虫的工程化与实战一个公开数据抓取的完整案例理论聊完我拿一个很典型的“公开列表页详情页”抓取场景完整走一遍工程化流程。为了避免敏感目标这里用模拟的新闻站点举例数据全是公开的标题、发布时间和正文你换成自己业务里允许抓取的网站逻辑是通用的。4.1 目标分析与准备工作假设需求是抓取站点news.example.test某个栏目的文章列表保存字段包括标题、链接、发布时间、摘要和正文。第一步不是写代码而是用浏览器开发者工具分析页面结构。打开Network刷新页面看HTML文档里有没有数据。有些站点的列表页是直接渲染在HTML里的那么XPath直接解析就行如果发现列表数据由接口返回就要定位接口地址和参数。顺着Network找到真实接口后我习惯把请求头完整复制出来重点看这几个字段User-Agent伪装成最新版Chrome或Edge。Accepttext/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8Referer来源页。Cookie如果接口需要登录态。自定义Header比如某些站点要X-Requested-With: XMLHttpRequest。把这些信息整理成一个REQ_HEADERS字典后续所有请求共用一份再写个简单的随机UA函数防止同一个UA用到封号。4.2 抓取与解析的代码实现下面是简化版的实现但完整表达了我要讲的思路import requests import time import random from urllib.parse import urljoin from lxml import html BASE_URL https://news.example.test/list/{page} HEADERS { User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, } def fetch_list(page): url BASE_URL.format(pagepage) resp requests.get(url, headersHEADERS, timeout15) resp.raise_for_status() return resp.text def parse_list(html_text): doc html.fromstring(html_text) items [] nodes doc.xpath(//div[classlist]/div[contains(class,item)]) for node in nodes: title node.xpath(.//h3/a/text()) link node.xpath(.//h3/a/href) pub_time node.xpath(.//span[classtime]/text()) if title and link: items.append({ title: title[0].strip(), link: urljoin(BASE_URL, link[0]), pub_time: pub_time[0].strip() if pub_time else , }) return items for page in range(1, 6): html_text fetch_list(page) parsed parse_list(html_text) print(page, len(parsed)) for item in parsed: print(item[title], item[link]) time.sleep(random.uniform(1.0, 2.5))这段代码有几个细节解析时先取大的列表节点//div[classlist]/div[contains(class,item)]再逐条提取字段既减少上下文干扰还能过滤掉无关节点链接使用urljoin拼接完整URL避免相对路径问题循环里加了随机sleep虽然拉慢速度但能大幅降低被封概率。列表页拿到以后如果还要抓正文就再对每个link发起一次请求解析正文容器。注意详情页同样需要防反爬建议在详情页循环里用同一个requests.Session()维持Cookie。另外一定要对返回内容做非空判断遇到h1标题缺失或正文为空时记录下来而不是直接报错退出这样爬虫能断点续跑。4.3 数据清洗与存储抓出来的字段往往不干净比如标题带多余空格和换行、时间格式不统一、正文中混入广告script标签。清洗我会按字段单独处理文本字段用.strip()加正则替换比如re.sub(r\s, , text)把多个空白压成一个空格时间字段统一转成YYYY-MM-DD HH:mm:ss格式方便数据库排序正文里非需要标签全部去除保留段落结构即可。存储这一层小型项目直接写SQLite或MySQL就行。我习惯先把解析结果转换成字典再用pandas.DataFrame批量入库SQL写起来少很多。当然Scrapy项目里更优雅的是写一个Pipeline一条条yield item由Pipeline统一做去重和落库。这里贴个简单的SQLite存储片段import sqlite3 conn sqlite3.connect(news.db) conn.execute( CREATE TABLE IF NOT EXISTS articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, link TEXT UNIQUE, pub_time TEXT, content TEXT ) ) conn.executemany( INSERT OR IGNORE INTO articles(title, link, pub_time) VALUES (:title, :link, :pub_time), parsed_items ) conn.commit() conn.close()INSERT OR IGNORE配合UNIQUE约束是日常去重的最笨但最稳定的办法。但要注意如果爬虫重复跑标题或链接相同的数据会被忽略不会产生重复记录数据量可控时比查一遍表再插入要省事很多。4.4 定时调度与增量抓取一次抓完不算工程化能每天定时增量才算。最简单的方案是服务器上写好Crontab还是不是需要一个调度框架我的建议是单机任务先用APScheduler包一层Python里设置间隔任务非常方便from apscheduler.schedulers.blocking import BlockingScheduler def job(): run_spider() scheduler BlockingScheduler() scheduler.add_job(job, cron, hour6, minute0) scheduler.start()增量抓取的核心是“已抓过的不重复抓”。除了数据库唯一索引去重还可以在代码里维护一个“已抓取链接集合”每次启动时从数据库读取旧链接如果新抓到的链接不在集合里说明是新增文章否则跳过。这里有个坑如果你只根据更新时间去重有些网站文章的更新时间永远不变那就会漏掉内容被修订的情况。所以要明确业务需求是要“新增”还是“变更”变更检测最傻但有效的方法是对正文取Hash不同则更新。5. 常见问题与排查技巧实录做爬虫最花时间的不是写代码而是排错。我把自己这几年遇到的典型问题整理成一张速查表附带排查方向你们遇到类似问题可以按图索骥。5.1 请求被拒403、418、429、502这几个状态码含义完全不同。403 Forbidden多半是请求头不完整、被WAF拦截或IP被拉黑418 Im a teapot是著名反爬玩笑码说明被识别成爬虫了常见于UA异常或请求顺序异常429 Too Many Requests就是触发频率限制502 Bad Gateway有时候是目标服务器自己的问题也可能是你的代理质量太差出站IP被源站网关拒绝。排查思路从易到难先检查UA和Header是否完整再降低频率、加随机延时然后换代理池IP试试最后看是不是需要登录Cookie。不要一上来就无限重试很多时候重试只会加快封号进度。5.2 页面结构与源码不一致常见于动态渲染网站浏览器看到的DOM是JS执行后的结果而requests.get拿到的是执行前的HTML。你按照浏览器F12里的XPath去写选择器结果返回空列表。遇到这种情况先按CtrlU查看源码和你抓到的是否一致如果不一致就说明数据要另找接口。按住F12切到Network的XHR请求重新刷新页面看哪条Ajax请求返回了JSON里面大概率就有你要的数据。这条解决很多“为什么我爬不到”的疑问。当然也不是所有数据都在XHR里有些页面用Blob、WebSocket或service worker推送数据那就更复杂一点。我的建议是优先找公开JSON接口实在找不到再用Selenium或Playwright模拟浏览器渲染但无头浏览器开销大并发能力弱慎用。5.3 中文乱码与编码问题文章前面提过resp.encoding设置这里再展开一下。乱码的根源是HTTP响应的字节序列和Python解码方式不一致。比如服务器用GBK编码返回但响应头没写requests默认按ISO-8859-1或UTF-8解码自然乱码。处理方式resp requests.get(url, headersheaders) # 先判断请求头里的charset再尝试apparent_encoding if resp.encoding and charset in resp.headers.get(content-type, ): content resp.content.decode(resp.encoding, errorsignore) else: content resp.content.decode(resp.apparent_encoding, errorsignore)更稳妥的办法是不依赖resp.text直接用resp.content.decode(guess_encoding(resp.content))guess_encoding可以用chardet或charset_normalizer库推断。记住无论如何都加上errorsignore避免单字符解码失败拖垮整个脚本。5.4 验证码与滑块上面说过不建议破解验证码。实际操作中遇到验证码我会先退一步看自己的请求行为有没有问题最常见的诱因是短时间请求频率过高、缺少浏览器指纹信息、IP质量差数据中心IP。应对方式按顺序来降低并发到1~2加大延时模拟真人浏览节奏。补全所有浏览器Header包括Accept、Accept-Language、Sec-Fetch-*等。用稳定住宅代理替换机房IP。让爬虫先从首页访问一次拿到基本Cookie再访问目标页模拟会话顺序。如果业务允许就错峰运行避开对方业务高峰。如果这样依旧触发验证码说明对方风控非常严格那么技术上可以上无头浏览器打码但合规上要谨慎。我更倾向于放弃这个源或用官方API没必要为了抓点公开数据铤而走险。5.5 性能与反爬平衡很多新手喜欢把并发调到100起步觉得越快越好。实际上单机爬虫的瓶颈往往不在本机CPU而在目标网站的限流和自家带宽。我常用的调优思路是这样的单IP并发能稳定跑的并发数通常是个位数比如3~5。用Scrapy的CONCURRENT_REQUESTS设置并配合DOWNLOAD_DELAY制造随机延迟。多IP并发如果拥有代理池可以适当提高并发但要注意代理质量。质量较差的代理需要增加重试机制否则错误率会吃掉并发收益。去重与队列大量URL去重放RedisSADD批量存入时用Pipeline合并写库避免每抓一条就连接一次数据库。日志与监控写日志一定要带上状态码、耗时、异常类型方便出问题时从日志回放。有条件就接一个简单的数据库统计表每天记录抓取成功数和失败原因长期看能发现很多隐性反爬变化。还有一个容易被忽略的坑机器习惯性问题。如果你每次都是整点触发抓取比如Crontabhour0对方运维很可能发现“每天凌晨0点准时大量请求”的规律然后针对性封禁。所以启动时间也可以加一点随机偏移比如设置hour0, minuterandom.randint(0, 59)在代码里动态生成一个随机启动时间。说回经验。爬虫做久了会养成一种习惯拿到一个网站先别急着写代码先是花10分钟看开发者工具找接口、理结构、看请求头判断它是静态页面还是动态渲染估算一下反爬强度。省下来的时间比写代码多得多。另外我强烈建议所有爬虫项目都在代码里预留“刹车”开关——出现大量失败或验证码时自动暂停整个任务退避一段时间再恢复而不是疯狂重试把IP全送进黑名单。这比任何花哨的并发调优都管用。最后分享一个小技巧给每个爬虫项目写一个README.md记录目标站点、抓取频率、字段定义、启动方法、最近一次封禁原因和解决过程。这看起来跟代码无关但等你一个月后再回来看这个项目会感激当初记下的这些备注。爬虫的本质不是跟网站对着干而是在公开数据里做系统的信息整理守住合规底线、懂得适可而止这行才能做得长久。
返回列表