做爬虫这些年最深的感受:复杂数据抓取任务从来不是“会写代码”就能解决的,真正难的是应对层出不穷的反爬机制、动态渲染页面、分布式调度,还有数据量上来之后的管理和清洗。我见过不少人拿着一个Requests库到处硬磕,磕到反爬就换UA、加延迟,最后被逼到写浏览器自动化脚本,代码写得像补丁拼盘。其实工具选对了,同样一个任务能省一半甚至大半的时间。这篇就把我实测过、也一直在用的6款爬虫软件逐个拆开讲清楚,每款的核心场景、上手方式、坑点、选型依据都写明白,希望能帮你少走弯路。
1. 复杂数据抓取任务到底难在哪
1.1 一个爬虫需求为什么这么容易翻车
很多人以为爬虫就是把页面拉下来、提取数据、存进文件,三步就完事。但一旦任务变成“复杂”级别,你会遇到一连串问题:
- 目标页面大量依赖JavaScript渲染,直接请求返回的HTML里根本没有数据。
- 列表页、详情页、翻页参数、登录态、Token校验相互纠缠。
- 数据量从几千条变成几十万条,单机串行爬取慢到让人怀疑人生。
- 反爬策略从简单的UA校验升级为行为分析、IP频控、验证码、混淆参数。
- 数据格式不统一,抓回来还要做去重、清洗、字段合并。
我认识不少人在这些环节里反复折腾,最后不是技术上不会,而是被“工具不合适”给拖垮了。Requests加BeautifulSoup写起来确实快,但你没有中间件机制、没有去重队列、没有并发控制、没有断点续爬,几乎所有工程化能力都要自己从零造轮子。这不是技术能力问题,是选型问题。
1.2 6款软件的定位画像
先给下面要讲的6款软件画一个粗略的功能地图,后面每款再展开细节:
| 软件 | 核心定位 | 最擅长解决的复杂度 | 上手成本 |
|---|---|---|---|
| Requests + BeautifulSoup | 轻量请求与解析 | 静态页面、简单API、快速原型 | 极低 |
| Scrapy | 高并发爬虫框架 | 大规模采集、分布式扩展、数据管道 | 中等 |
| Playwright | 浏览器自动化控制 | 动态渲染、复杂交互、反爬对抗 | 中等 |
| 八爪鱼采集器 | 可视化采集工具 | 零代码快速抓取、模板化采集 | 极低 |
| Colly | Go语言爬虫组件库 | 高并发轻量采集、自定义中间件 | 中高 |
| Crawlee | Node.js爬虫框架 | JS全栈场景、代理轮换、失败重试 | 中等 |
从这个表能看出来,没有一款工具能通吃所有任务,关键是你手里有多少种武器,遇到具体场景能不能快速选出合适的那把。
2. 轻量快跑组合:Requests + BeautifulSoup
2.1 为什么这个组合仍然值得掌握
现在浏览器自动化工具满天飞,但Requests加BeautifulSoup这套组合我依然认为是爬虫工程师的基本功。原因很简单:它足够轻,轻到你可以在一个脚本里完成抓取、解析、清洗、落盘的全流程,完全不需要引入框架级的东西。
它的适用边界很清晰:单进程、小批量、页面结构相对稳定,或者目标网站提供了接口数据而你只需要在静态HTML里捞一把。超过这个边界再考虑上重型工具。
好多人在这个阶段就翻车,主要是因为不注意两点。第一是Session的复用,第二是请求头伪装。很多网站对你的身份识别的核心不是UA,而是Cookie和完整的请求头上下文,你用requests.get每次独立请求,和用requests.Session传递登录态、复用连接池,效果天差地别。
2.2 一个顺手的基础模板
下面这段代码是我经常拿来起步的一个模板,接下需求后先往这个框架里填目标地址和解析逻辑:
import requests from bs4 import BeautifulSoup session = requests.Session() session.headers.update({ "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-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Accept-Encoding": "gzip, deflate, br", }) resp = session.get("https://example.com/list") soup = BeautifulSoup(resp.text, "html.parser") for item in soup.select(".item-title a"): title = item.get_text(strip=True) href = item.get("href") print(title, href)注意把常见反爬手法前置处理掉:目标站点要求Referer就加上Referer,要求Origin就带上Origin。先自己访问一遍浏览器开发者工具里的请求头,然后在Session里复刻出来。很多人一上来就怼代理池,其实大多数场景先补请求头就能解决七八成问题。
2.3 我踩过的几个细节坑
- 编码乱码:resp.text依赖HTTP头里的charset,但有些站点用UTF-8返回却标记了GBK。别太信任resp.text,直接resp.content然后手动解:
html = resp.content.decode("utf-8", errors="ignore")- 列表页和详情页的请求频率要分开:列表页一次能拿几十条,详情页一次拿一条,详情页并发需求更大,但频率限制也更狠。用同一套延迟策略会浪费大量时间,或者暴力到被封。
- 解析器用lxml:纯Python的html.parser在节点多时会明显变慢。同时养好写选择器的习惯,优先target靠近数据的稳定容器节点,别因为网站改个class就抓瞎。
Requests + BeautifulSoup看起来简单,但你把它吃透了,后面看Scrapy的设计动机才会理解得更深。
3. 真正的重型武器:Scrapy框架
3.1 框架设计的精妙之处
Scrapy和Requests最大的区别,在于它把“下载页面”和“解析数据”彻底解耦了。你用yield告诉框架“下一个目标是谁,用哪个回调处理”,框架自己管理调度、去重、并发、导出。这种回调链式设计让一个爬虫能轻松扩展到百万级页面,而代码结构依然清晰。
复杂任务里经常遇到一类场景:先爬列表页拿到ID,再根据ID组装详情页URL,详情页里又嵌套着需要登录才能看到的字段。这种多级页面流转,用Scrapy表达会非常舒服。
一个基本Spider长这样:
import scrapy class ExampleSpider(scrapy.Spider): name = "example" start_urls = ["https://example.com/list"] def parse(self, response): for href in response.css(".item a::attr(href)").getall(): yield scrapy.Request(url=response.urljoin(href), callback=self.parse_detail) next_page = response.css(".next::attr(href)").get() if next_page: yield scrapy.Request(url=response.urljoin(next_page), callback=self.parse) def parse_detail(self, response): yield { "title": response.css("h1::text").get(), "content": response.css(".content::text").get(), }yield Request只是发起调度,真正的流程是框架的下载器去下载,下载完成后自动回调对应解析函数。正因为有这个设计,Scrapy天然就是异步并发模型,一个进程能同时维持几十上百个网络连接。
3.2 关键配置和调优参数
多数人用Scrapy觉得“慢”,其实是没调配置。下面这段配置是我做中大型采集任务的实际起点:
# settings.py CONCURRENT_REQUESTS = 32 DOWNLOAD_DELAY = 0.5 CONCURRENT_REQUESTS_PER_DOMAIN = 16 USER_AGENT = "Mozilla/5.0 ..." ROBOTSTXT_OBEY = False RETRY_ENABLED = True RETRY_TIMES = 3 RETRY_HTTP_CODES = [500, 502, 503, 504, 408, 429] DOWNLOADER_MIDDLEWARES = { "myproj.middlewares.MyProxyMiddleware": 543, } ITEM_PIPELINES = { "myproj.pipelines.DuplicatesPipeline": 300, "myproj.pipelines.MySQLPipeline": 500, }CONCURRENT_REQUESTS代表全局最大并发,DOWNLOAD_DELAY是同一域名两次请求的间隔。数字不是越大越好,要考虑目标站点的承受能力和你自己的机器带宽。我一般从并发16、延迟1.0开始,观察错误率再往上压。RETRY_HTTP_CODES里务必加上429,很多反爬是通过返回429提醒你触发频控了,不处理的话后面就是封IP。
3.3 管道里做清洗和入库
数据抓下来不是终点,清洗和入库才算完成闭环。Scrapy的Item Pipeline是用来做这件事的。举个例子,抓到的价格字段可能是字符串“¥1,299.00”,入库前需要清洗成浮点数:
class CleanPipeline: def process_item(self, item, spider): if "price" in item: item["price"] = float(item["price"].replace("¥", "").replace(",", "")) return item class DuplicatesPipeline: def __init__(self): self.ids_seen = set() def process_item(self, item, spider): if item["id"] in self.ids_seen: raise DropItem(f"Duplicate item found: {item['id']}") self.ids_seen.add(item["id"]) return itemPipeline的优先级由settings里的数值决定,数值越小越先执行。清理、去重这些前置步骤放前面,入库放后面。
3.4 实战里的几个记忆点
- 下载中间件干脏活:代理IP、自动加签名Header、绕过JS质检,都写在DOWNLOADER_MIDDLEWARES里,不要在Spider页面代码里塞逻辑。中间件从Request进入下载器之前拦截,你能在这个位置改请求头、换代理、丢弃请求。
- 分布式不是标配:Scrapy单机就能支撑很大的采集量。真的到了单机带宽耗尽的情况,再考虑用Scrapy-Redis做Scheduler共享,思路是让多台机器消费同一个Redis里的请求队列。别一上来就上分布式,很多团队高估了数据量。
- 断点续抓:Scrapy自带的Job持久化中间件可以暂停恢复任务。启动时加参数
-s JOBDIR=crawl_state,中断后重新执行时会从上次停止的位置继续,不用从头再爬。这个我建议正式采集都配上,不然爬了一半挂了真的难受。
4. 动态页面终结者:Playwright
4.1 为什么要上浏览器自动化
现在的新站点越来越多走前端渲染,服务端返回的HTML只是个空壳,数据全是JavaScript异步加载出来的。你用Requests拿到的页面里面什么都没有,这时必须让一个真实的浏览器加载页面、执行脚本、等待接口返回,然后再从渲染完成的DOM里提取数据。
Playwright就是干这个的,它控制的是完整Chromium内核,页面加载、点击、滚动、等待元素、捕获网络请求,全部能用API操作。和Selenium相比,它的自动等待机制做得非常成熟:wait_for_selector会等元素出现再执行下一步,不需要手动写一堆time.sleep去糊弄。
4.2 抓动态行情页的典型流程
以抓取一个行情仪表盘为例,页面加载后表格数据通过XHR接口填充,用Playwright的写法是启动浏览器、打开页面、等待接口返回、然后再抽取数据:
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto("https://example.com/dashboard", wait_until="domcontentloaded") # 等待某个网络响应完成,相当于等数据渲染好 page.wait_for_response(lambda r: "api/data" in r.url) rows = page.locator(".table tbody tr").all_text_contents() for row in rows: print(row.strip()) browser.close()wait_for_response是这类采集里的一个关键技巧。页面加载完不等于数据渲染完,你得等背后那个XHR返回了才行。不等待直接抽DOM,经常拿到的只有空表格。
4.3 路由拦截:把动态页面改造成纯数据源
Playwright里我觉得最实用的能力是page.route(),也就是路由拦截。你可以拦截掉图片、字体、统计脚本,甚至拦截XHR响应直接把JSON数据导出来。
比如目标页面看似是复杂的图表页面,但所有数据都来自某个JSON接口。与其等图表渲染再去DOM里挖数据,不如直接拦截这个接口:
def intercept_response(response): if "/api/chart" in response.url: data = response.json() print(data["series"]) with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() page.on("response", intercept_response) page.goto("https://example.com/chart-page") browser.close()这招能省去大量选择器调试和时间,也大幅降低解析失败的概率。凡是看到页面上有网络请求返回JSON,优先考虑拦截方案,不要盲目处理DOM。
4.4 反检测的几个实用手段
Playwright控制的浏览器有个明显特征:navigator.webdriver为true。很多反爬系统根据这个字段识别。一个常见的缓解方法是启动时注入脚本覆盖该属性:
page.add_init_script(""" Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); """)但这只是入门。更完整的思路包括:
- 使用真实用户的UA、屏幕尺寸、语言时区等上下文参数。
- 避免机械化的行为模式,比如每次访问间隔固定、鼠标轨迹完全笔直。
- 合理控制并发和频次,与其花大力气伪装,不如用合规的姿态、合理的速度去采集。
工具对抗只是单点手段,整体采集策略的克制和安全边界更重要。
5. 零代码方案:八爪鱼采集器
5.1 推荐它给程序员是不是很意外
我第一次用八爪鱼这类可视化采集器时也觉得有点“不专业”,但后来在赶一个一次性需求时彻底改观:要抓的是一个活动页的报名数据,页面结构混乱、规则复杂,产品和技术排期冲突,最后我用八爪鱼20分钟配置完流程,跑完数据直接导出。
它的本质是把“采集流程”做成可视化编排:打开网页、识别列表区域、设置翻页循环、定义字段提取规则、循环下一页,全部通过鼠标点选完成。对程序员来说,最大的价值是省掉一次性脚本的开发和调试周期,遇到页面小改动时也不用改代码重新跑流程。
5.2 配置一个大表采集的操作要点
假设要采一个商品列表页,包含名称、价格、销量、店铺名四列,翻页50次。流程拆解如下:
- 新建“自定义采集”任务,输入起始页URL。
- 第一次打开页面后,点击地址栏右侧的“选择区域”按钮,框选第一个商品的区域。
- 软件会自动识别列表循环,接着逐个框选字段对应的文本区域,并给每个字段命名。
- 在第二页识别翻页按钮,设置为自动循环翻页。
- 设置每个页面停留时间,建议在“高级设置”里把页面加载超时调大一点。
- 启动采集,看到数据在预览区滚动的时候确认一下字段没有错位。
实际配置中最大的坑是页面中途弹窗。一些站点滚动到中后段会弹出登录框或推广浮窗,如果你没有在流程里加“关闭浮窗”动作,采集会在同一页面停留到超时。解决方式是配置一个“点击弹窗关闭按钮”的辅助步,放在翻页循环之前,这样每次翻页后都先检查关掉弹窗。
5.3 适用场景和建议心态
八爪鱼的适用场景很清晰:一次性任务、业务人员也能用、改版频繁但结构不复杂的页面。真的到高频、大体量、强反爬的时候,它的短板也明显——处理复杂JS渲染常要依赖关联的浏览器采集方式,运行稳定性赶不上代码方案,字段提取规则在页面改版后需要手动重配。
所以我对程序员的建议是:别排斥这类工具,它们能帮你快速验证数据可行性、跑通业务流程,但核心生产级采集代码仍然要用Scrapy这类框架来沉淀能力。
6. 延伸视野:Go系Colly和Node系Crawlee
6.1 Colly:用并发换速度的轻量选择
如果你的团队是Go技术栈,或者单机想跑到很高的吞吐量,Colly值得认真看。它对标的是类似Scrapy的位置,但设计上更轻,没有内置的完整项目骨架,而是以组件方式调用。一个最基础的示例:
package main import ( "fmt" "github.com/gocolly/colly/v2" ) func main() { c := colly.NewCollector( colly.AllowedDomains("example.com"), colly.MaxDepth(3), ) c.OnHTML(".item-title a", func(e *colly.HTMLElement) { fmt.Println(e.Text, e.Attr("href")) }) c.OnRequest(func(r *colly.Request) { r.Headers.Set("User-Agent", "Mozilla/5.0 ...") }) c.Visit("https://example.com/list") }Colly的响应式回调设计非常简洁:OnRequest改请求、OnHTML匹配解析、OnError处理失败、OnScraped爬完收尾。它天然支持并发,设置c.Limit(&colly.LimitRule{DomainGlob: "*", Parallelism: 8})就能控制并发度。Go的goroutine模型让它在高并发采集时CPU占用和内存控制很优秀。适合那些要高频抓数但又不想引入庞大框架的场景。
6.2 Crawlee:Node全栈爬虫的一站式体验
Node生态的Crawlee(前身是Apify SDK)是给JavaScript开发者准备的爬虫框架。它最大的优势是原生兼容Playwright、Puppeteer这类浏览器自动化工具,同时自带请求队列、自动重试、代理轮换、结果存储。
一个非常实用的场景:你一个项目既是前端又是爬虫,全部用JavaScript。用Crawlee的PuppeteerCrawler启动无头浏览器,自动捕获失败请求并重试,抓取的数据直接通过内置存储落盘:
import { PlaywrightCrawler } from 'crawlee'; const crawler = new PlaywrightCrawler({ maxRequestsPerCrawl: 1000, async requestHandler({ request, page }) { await page.waitForSelector('.item'); const items = await page.locator('.item').allTextContents(); await crawler.pushData({ url: request.url, items, }); }, }); await crawler.run(['https://example.com/list']);Crawlee把重试、超时、队列状态、元数据管理都封装好了,你不需要自己写任务恢复逻辑。如果你的日常技术栈全是JavaScript,它比跳到Python系框架更平顺。
6.3 选哪一系,到底是编程语言的博弈
做技术选型时,经常有人问“Python是不是爬虫唯一选择”。答案显然是否定的。Python的优势是生态丰富、教程多、解析库方便;Go的优势是部署为单个二进制、并发能力强、运行时资源占用低;Node的优势是前端背景平滑过渡、和前端工具链天然衔接。
我的建议是:主力爬虫体系最好固定在一门语言里,吃透它的框架、中间件、部署方式,形成积累。其他语言你可以了解它的差异和适用场景,但不要每换一次任务就换一门语言来写爬虫,那样你的工程化沉淀永远是零散的。
7. 高频问题排查与避坑经验
7.1 状态码和数据异常速查
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 大量403 | 缺Cookie/签名/请求头 | 用浏览器登录态,检查请求头完整性 |
| 大量429 | 频控触发 | 提高下载延迟,降低并发度 |
| 页面无数据 | JS动态渲染 | 改用Playwright或拦截接口 |
| 偶发超时 | 网络不稳定/代理失效 | 开启重试,配置代理池健康检查 |
| 编码乱码 | charset声明与实际不符 | 用content解码,手动指定编码 |
| 字段错位 | 页面结构变化 | 重新定位选择器,加字段空值兜底 |
7.2 去重和增量抓取的工程化思路
数据采集量到一定规模后,“采集完成”不算完,你得保证数据的一致性和可恢复性。经验做法是:
- 以业务主键做去重,不要用URL做去重。同一商品的详情页URL可能带各种不变的参数,但商品ID是稳定的。
- 增量采集时记录抓取时间戳,每次只处理目标时间之后的更新数据。
- 落地数据避免直接覆盖,建议保留原始响应文件或原始记录的哈希值,方便回溯整个采集过程。
我自己维护的一个采集中间层习惯:先把原始HTML/JSON按日期和任务ID落盘,再做解析入库。解析代码出bug时不用重新采集网络,直接在本地重跑解析任务就行。这一步能让你节省大量时间和流量。
7.3 请求频率与封禁预防
爬虫被封不是零一事件,而是概率事件。你无法保证100%不被封,但可以把它控制在可接受范围内。我的基础配置原则:
- 合理限制同一域名的连接数,设置随机延迟区间而不是固定值,例如
DOWNLOAD_DELAY = (1.0, 3.0)能有效降低某些基于统计的反爬系统的风险。 - 分布式或商家自用级别的合规任务标注清楚来源身份,有些站点只要你注明是数据采集用途并提供联系方式,它根本不会封你。
- 不要攻击式采集,单IP多并发、超高频请求是对目标服务器的不公平压力,也更容易进入风控数据库。
写在最后的一个实操心得
如果你问我这6款软件选哪几个先学,我的回答是:Requests和Scrapy是地基,Playwright是攻坚工具,八爪鱼是快速出路,Colly和Crawlee是你团队技术栈的扩展。真正重要的不是工具本身多强大,而是你有没有把采集工程化的习惯——请求头建好、延迟调校、去重持久化、过程可观测。我见过太多人今天用Requests写个脚本成功,明天页面改版就废掉,然后又开始写新脚本,始终处于单点打补丁的循环里。工具只是起点,把每一次采集当成一个可持续运行的数据管道来看待,才是复杂抓取任务里真正的核心竞争力。