写爬虫这么多年,我越来越觉得“程序员必备的爬虫软件”这个说法容易误导人。市面上根本没有一款软件能包打天下:有的适合快速写一次性脚本,有的适合跑大规模分布式采集,有的专门处理动态渲染页面,有的则是给非程序员准备的零代码工具。我在自己的模拟项目里把这些方案挨个跑过一遍之后发现,真正高效的做法不是选“最好”的,而是按任务类型把工具拆成几档,各干各的活。这篇就围绕我个人实际用过的6款软件,把每个工具适合什么场景、有哪些坑、怎么做选型,一次性说清楚。
1. 先用一张“作战地图”梳理六款软件的真实分工
先别急着看代码,我把6款软件按“运行形态”分成三大类,这样后面选型时思路会清晰很多。
| 类别 | 软件 | 核心定位 | 上手门槛 | 典型任务 |
|---|---|---|---|---|
| 轻量脚本型 | Requests + BeautifulSoup | 静态页面解析、快速原型 | 低 | 文章列表、接口JSON、简单翻页 |
| 异步框架型 | Scrapy | 大规模采集、管道化处理 | 中 | 全站抓取、多级页面、增量更新 |
| 浏览器自动化型 | Playwright | 动态渲染、模拟登录、截图 | 中 | 前端渲染站点、复杂交互 |
| 浏览器自动化型 | Selenium | 兼容老环境、已有WebDriver资产 | 中 | 遗留系统、维护中的老脚本 |
| 任务调度型 | PySpider | 定时抓取、小集群管理 | 中 | 定时监控、多个站点统一管理 |
| 零代码可视化型 | Octoparse | 无代码抓取、业务人员自助 | 低 | 快速建表、可视化点选提取 |
注意我特意没有列那些“需要配合境外服务器才能稳定访问”的工具和方案,因为正常业务里绝大多数抓取任务根本用不到那种东西。先分清“静态内容”和“动态内容”是选好工具的第一步,这个后面会反复提到。
再补一句:Requests + BeautifulSoup严格来说是“组合”,但大家平时都直接当一套工具用,我也就按一个完整工具来讲。如果你只是想从某个页面把表格数据抠出来,用这个组合比上任何框架都快,杀鸡不用牛刀。
2. 为什么没有“万能神器”:选型要看数据形态与目标网站结构
很多初学者会问:我直接学Scrapy不就行了?我的回答是——如果你抓的页面全是前端渲染、数据靠XHR异步加载,Scrapy的基础能力基本帮不上忙。这不是Scrapy不行,而是它的定位就不是浏览器。选型前必须回答三个问题。
2.1 数据藏在HTML里还是藏在JavaScript里
把目标URL用浏览器的“查看源代码”打开,如果数据明明白白在HTML里,那Requests + BeautifulSoup或Scrapy就能处理。如果HTML只有一堆空壳标签,数据是页面加载后通过异步请求填进去的,你就需要Playwright这类能执行JavaScript的工具。
我自己的模拟项目里有个典型的例子:某公开数据页面,初次请求返回的HTML只有几百字节,真实数据都在一个JSON接口里,而且这个接口的URL是JavaScript动态拼接的。后来我用Playwright的page.on("response")直接拦截网络响应,把JSON拿下来,比解析DOM还干净。这就是形态决定工具。
2.2 抓取规模决定框架复杂度
只抓几百个页面,用Requests循环就够了;要抓几十万甚至上百万个页面,就要考虑并发、失败重试、请求去重、数据落库。Scrapy在这方面的价值是内置了这些机制:它的调度器、下载器中间件、Item Pipeline都是为大规模生产而生。如果你用Requests自己写并发,得同时处理线程池、超时重试、请求去重,工作量会大不少。
2.3 站点结构决定了“自动化”还是“写代码”
有些目标站点的页面结构高度规范化,比如每个详情页都有统一的URL规律,这时候写代码最有效率。但如果目标是一个结构混乱、经常改版的小网站,或者你只是业务方需要临时拉一批数据,那零代码工具Octoparse反而更省力。技术选型永远是被需求和站点结构推着走的,不是哪个工具名字响亮就选哪个。
3. 六款软件的实战操作要领,以及各自“不为人知”的细节
接下来逐个讲实战。每个工具我都只挑最关键的操作路径和最容易踩的坑来讲,完整文档大家可以去官方看,我这里讲的是文档里通常不会写的经验。
3.1 Requests + BeautifulSoup:轻量任务最稳妥的组合
这是最朴素的方案:先用Requests拿HTML,再用BeautifulSoup解析。适合目标明确、结构简单的页面。
import requests from bs4 import BeautifulSoup url = "https://example.org/list" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" } resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() soup = BeautifulSoup(resp.text, "html.parser") items = soup.select(".item-list a") for a in items: print(a.get_text(strip=True), a.get("href"))这套方案最大的坑在于编码。有些老站点返回的HTML没有明确charset,Requests会按响应头猜测编码,很容易乱码。稳妥做法是:
if resp.encoding is None or resp.encoding.lower() == "iso-8859-1": resp.encoding = resp.apparent_encoding再说一下“接口优先”原则。能抓到JSON接口就绝不解析HTML。JSON结构清晰、字段稳定、解析成本低。你在浏览器开发者工具里打开Network面板,过滤XHR,往往就能发现页面真实的数据来源。直接请求这个接口,解析JSON,比在HTML里折腾正则表达式舒服得多。
3.2 Scrapy:大规模采集时的可靠选择
Scrapy是异步框架,不是“爬虫脚本”,它改变的是整个编程模型。首次使用者最容易不适应的是“你不用自己去写循环”。
scrapy startproject myspider cd myspider scrapy genspider example example.org然后编辑spiders/example.py:
import scrapy class ExampleSpider(scrapy.Spider): name = "example" start_urls = ["https://example.org/list"] def parse(self, response): for item in response.css(".item"): yield { "title": item.css("a::text").get(), "url": response.urljoin(item.css("a::attr(href)").get()), } next_page = response.css(".pagination .next::attr(href)").get() if next_page: yield scrapy.Request(response.urljoin(next_page), callback=self.parse)跑起来只有一句话:scrapy crawl example。并发、重试、去重都自动处理。这里我说一个很多人忽略的配置:
# settings.py ROBOTSTXT_OBEY = True CONCURRENT_REQUESTS = 16 DOWNLOAD_DELAY = 0.5 AUTOTHROTTLE_ENABLED = TrueAUTOTHROTTLE_ENABLED是Scrapy最值得开的功能,它会根据服务器响应速度自动调整请求频率,比手动固定延迟更安全也更高效。我在模拟项目中开着它,整体采集时间反而比纯固定延迟少了很多,因为慢的站点自动给了更长间隔、快的站点自动加速。
Scrapy的第二个坑是重复配置。数据清洗、去重、入库逻辑应该放进Pipeline,而不是在Spider里写一堆if else。比如:
class DuplicatesPipeline: def __init__(self): self.seen = set() def process_item(self, item, spider): if item["url"] in self.seen: raise DropItem(f"Duplicate item: {item['url']}") self.seen.add(item["url"]) return item这样才能让Spider只负责“抓”,Pipeline管“存”。如果你把“存”写在Spider里,后期维护就是噩梦。
3.3 Playwright:动态页面自动化里的最省心选择
Playwright是我处理动态页面的首选,原因有三:API统一、自动等待、自带浏览器下载。它支持Chromium、Firefox和WebKit,一套代码三种浏览器跑。
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.org", timeout=30000) page.wait_for_selector(".data-list") items = page.locator(".data-list .row").all_text_contents() browser.close()Playwright一个很大的好处是wait_for_selector会自动等待元素出现,不需要像Selenium那样写一堆time.sleep。这看起来是个小差异,实际体验天差地别。我以前用Selenium写动态页面,因为等待时间不准,脚本隔三差五就挂一次;用Playwright之后这类问题几乎消失了。
处理异步接口时,可以拦截响应:
def on_response(response): if "/api/data" in response.url: print(response.json()) page.on("response", on_response) page.goto("https://example.org")这样你不用等页面渲染完成,直接在网络层就把数据拿走了。拦截响应比解析DOM更可靠,因为JSON结构比HTML稳定得多。
另一个技巧是处理登录态。很多目标站点的数据需要登录才能看到,这时候别每次都重新登录,把登录后的context存下来复用:
context = browser.new_context(storage_state="state.json") page = context.new_page()首次登录成功后执行context.storage_state(path="state.json"),之后每次启动直接用这个文件恢复Cookie和LocalStorage,能省掉大量重复登录的时间。
3.4 Selenium:老当益壮,但只在特定场景里选它
Selenium目前最大的问题是维护成本偏高,WebDriver要对应浏览器版本。但它有两个场景目前仍有价值:一是你所在团队已有大量基于Selenium的遗留脚本,迁移没必要;二是一些极老的系统只认特定浏览器,Playwright兼容性反而受限。
from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver = webdriver.Chrome() driver.get("https://example.org") data = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, ".data-list")) ) print(data.text) driver.quit()如果一定要用Selenium,我建议用WebDriverWait,不要用time.sleep()。前者会在条件满足的瞬间继续执行,后者无论页面是否就绪都会傻等,慢而且容易翻车。
给还在维护Selenium脚本的人一个建议:把WebDriver的初始化、浏览器版本检查、异常截图统一封装在基类里。这样浏览器升级导致脚本崩的时候,你只需要改一处。我见过太多项目,一个Chrome自动升级就把全部脚本集体送走,就是因为初始化代码散落各处,没人统一管理。
3.5 PySpider:定时监控与小规模多站点采集
PySpider是一个带Web UI的爬虫框架,支持在浏览器里调试、查看抓取结果、定时运行。虽然近年更新频率不高,但它的“Web UI + 定时调度 + 单机多任务”设计放到今天依然有竞争力,特别适合“每天固定抓一遍十几个小站”的场景。
PySpider的代码结构是Handler类的on_message方法:
from pyspider.libs.base_handler import * class Handler(BaseHandler): @every(minutes=60) def on_message(self, project, msg): pass def crawl_config(self): return {"delay": 0.5, "retries": 3} def on_start(self): self.crawl("https://example.org", callback=self.index_page) def index_page(self, response): for item in response.doc.items(".item"): self.crawl(item.find("a").attr.href, callback=self.detail_page) def detail_page(self, response): return { "title": response.doc("h1").text(), "content": response.doc(".content").text(), }PySpider最吸引人的是那个可视化调度面板:你可以直观看到每个任务的抓取状态、错误堆栈,还能直接调整定时策略。对中小型团队来说,这种“开箱即用的调度能力”能省掉很多DevOps工作。
需要注意的是,PySpider的依赖库很多,Python版本兼容性也不是最好。我一般用虚拟环境单独装一份,避免污染日常开发环境。如果你只需要定时任务,不想引入重框架,也可以考虑自己用cron或系统计划任务跑Scrapy,但就没有Web UI可看了。
3.6 Octoparse:不会写代码也能抓数据的可视化工具
Octoparse这类可视化工具的定位不是取代程序员,而是让业务人员在没有开发资源的情况下自助完成数据收集。它通过“点选网页元素”的方式设定提取规则,运行后在云端或本地采集。
对程序员来说,它的价值是两个:一是作为原型验证工具,业务方提需求时你用Octoparse先跑一批样例数据,确认字段是否完整,再决定是否写正式爬虫;二是作为交付给非技术同事的“自助工具”,让他们日常自己采集简单数据,不必天天来排队找你。
用Octoparse抓数据时,建议先选“向导模式”,它会自动识别列表页和详情页的规律。对大部分常见站点,向导模式能覆盖七八成的需求;遇到识别不了的复杂站点,再切换到“自定义模式”,手动框选需要提取的区域。
零代码工具的共同局限也很明显:规则调整不灵活、正则和正则表达式支持有限、对强反爬站点几乎没有还手之力。所以我的态度一直是:**它适合“快速拿一批数据”,不适合“做一个长期稳定的数据管道”。**这两者要拎清楚。
4. 真正决定抓取成败的五个隐形细节
工具学得再熟,如果下面这些环节没处理好,爬虫照样三天两头挂。这五件事是我认为比选哪个库更影响交付质量的关键。
4.1 User-Agent与请求头:不是伪装,是基本的网络礼仪
很多站点会拦截无UA的请求,不是因为它反爬严格,而是这类请求看起来不像正常浏览器。至少要设置一个合理的User-Agent,最好随机轮换。
import random USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...", "Mozilla/5.0 (X11; Linux x86_64) ...", ] headers = {"User-Agent": random.choice(USER_AGENTS)}注意我这里没有让你做任何绕过措施,只是保持一个正常请求的呈现方式。如果一个站点明确在robots里禁止抓取,或者服务协议里声明不允许,那就别抓,这跟技术无关。
4.2 重试策略与超时设置:稳定性靠的是异常处理
网络请求失败是常态,不是意外。所有抓取任务都必须考虑超时、连接失败、HTTP 5xx这三种情况。基础模板:
def fetch_with_retry(url, retries=3, timeout=10): for attempt in range(retries): try: resp = requests.get(url, timeout=timeout) if resp.status_code >= 500: continue return resp except requests.RequestException as exc: if attempt == retries - 1: raise exc return None还有一个容易忽略的点:不要把重试和固定延迟混在一起写死。Scrapy的AUTOTHROTTLE会根据服务器响应情况自动调整访问频率,这是很多人在真实环境里忽略的“免费安全性能”。手动延迟写死,既慢又不安全。
4.3 Cookie与Session:保持会话一致性的底层逻辑
很多站点的列表页和详情页需要同一会话才能访问,或者登录态依赖Cookie。用Requests时要注意Session对象能自动保存Cookie,但重定向时的Header丢失是常见坑:
session = requests.Session() resp = session.get(url, allow_redirects=True)如果请求过程中出现“被重定向到登录页”但代码没有感知,通常是因为Cookie在重定向过程中未随请求传递。排查办法:打印每次响应的history和最终URL,确认重定向链条里是否混入了登录页。这个排查思路比学多少代码技巧都实用。
4.4 请求频率:比“反爬策略”更重要的是不扰民
我发现很多人一谈到频率管理就默认是反爬对抗,其实不是。**站在目标网站的角度考虑,一个正常的采集任务应该像一个正常用户访问一样有节奏。**固定间隔太短会让服务器CPU飙升,日志刷屏,还可能影响站点的正常用户体验。
这里有必要区分“速率控制”和“绕过限制”的差别:前者是让爬虫更礼貌、更稳定地运行,后者是突破网站的访问限制。我在这篇里只讨论前者。正确做法是给请求设置合理的间隔,比如每抓20个页面休息2到5秒。这样的抓取速度对绝大多数业务采集需求来说已经足够,而且不容易给自己和对方惹麻烦。
4.5 数据去重与增量更新:长期跑下去的核心保障
很多爬虫脚本能跑,但重复数据越来越多。Scrapy有内置的RFPDupeFilter,但如果你用Requests自己写,就得手动维护一个“已见集合”。可以用Redis做去重,但最简单粗暴的方案是记录已处理的URL到一个本地文件,每次启动时加载:
seen = set() if os.path.exists("seen.txt"): seen = set(open("seen.txt").read().splitlines()) with open("seen.txt", "a") as f: for url in new_urls: if url not in seen: process(url) f.write(url + "\n")这个最初级的方案能解决80%的去重需求,等数据量大到文件读不动了,再考虑上Bloom Filter这类高级方案。
5. 我的最终排序建议与一点实践体会
如果让我给一个“程序员必备”的推荐顺序,我会这么说:
- Requests + BeautifulSoup:必须会,因为它是最小可用的地基,代码简单、调试容易、适合理解爬虫的本质。
- Scrapy:建议早学。它能逼着你理解下载器、解析器、管道、中间件分层,这个思维模式对处理任何复杂任务都有益。
- Playwright:当遇到动态站点时,它是效率最高的解法。这年头纯静态页面越来越少,Playwright应该排到Selenium前面学。
- Selenium:只在遇到需要维护的老项目时再回头补。
- PySpider / Octoparse:按需。前者适合定时任务调度,后者适合快速原型或交付给非技术同事。
最后再分享一个“百试百灵”的小技巧:**任何爬虫项目,先抓一个页面,用命令行打印出原始响应,确认数据结构后再写解析代码。**我见过太多人一上来就写完整爬虫,结果跑了十分钟才发现字段选错,白跑一趟。先拿单页验证解析逻辑,再启动全量任务,能节省大量调试时间。
爬虫这行,真正拉开差距的不是工具本身,而是你对待细节的态度:请求头有没有写对,超时有没有设置,去重有没有维护,频率有没有控制。这些做好了,哪怕只用最基础的Requests,也能稳定跑完一个任务;这些没做好,再高级的框架也是三天两头报错。