ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Scrapy入门:从零创建第一个爬虫的完整指南

Scrapy入门:从零创建第一个爬虫的完整指南

写这篇笔记的起因很简单:我第一次用 Scrapy 创建爬虫时,真的卡了很久。不是卡在写代码上,而是卡在一堆“很基础但没人细讲”的事情上——环境装完为什么跑不起来、目录里一堆文件到底哪些要动、parse 方法里到底该 return 还是 yield。当时满脑子只有一个念头:如果有人能把这一步一步掰开揉碎讲清楚就好了。所以这篇就聊最纯粹的切入点:Scrapy 入门之创建爬虫。我会从环境准备讲到项目骨架,再从写第一个爬虫讲到常见报错排查,全程保持“边做边说”的笔记风格。适合刚接触 Scrapy 的初学者,也适合之前试过但没跑通的读者。你不需要懂框架原理,跟着操作一遍,跑通第一个爬虫就赢了。

1. 环境准备:先把 Python 环境收拾干净

1.1 为什么一定要用虚拟环境

很多人刚开始学 Scrapy,第一反应是直接pip install scrapy装到系统 Python 里。短期看没什么问题,但 Scrapy 的依赖链非常长,它要装 Twisted、lxml、pyOpenSSL、cryptography 这一堆,每个包的版本还互相有要求。只要某个依赖和你系统里已有的包版本冲突,你就等着排查依赖地狱吧。我见过太多人卡在这一步,最后也不知道是哪个包把环境搞坏的。

所以我的建议很朴素:每个项目单独建一个虚拟环境。虚拟环境做的事其实很简单,就是把当前项目的依赖安装到一个独立目录里,和系统全局环境彻底隔离。这样做的好处很多——项目之间互不干扰,某个项目升级依赖不会影响另一个,环境坏了直接删掉目录重建就行。

创建和激活虚拟环境用 Python 自带的 venv 就够了,不需要额外装东西:

python -m venv venv source venv/bin/activate

Windows 上激活命令不太一样:

venv\Scripts\activate

激活成功的话,终端前面会出现(venv)前缀,这时候你再执行 pip,装的东西都会进这个环境。想退出就执行deactivate。这个习惯看起来多了一步,但真实项目里能帮你避免七成以上的依赖问题,非常值。

1.2 安装 Scrapy 并验证版本

环境激活后,安装就很直接了:

pip install scrapy

如果网络不好或安装很慢,可以把下载源换成国内镜像,命令大致是pip install scrapy -i 镜像地址,这里不展开。装完之后一定要做一次验证:

scrapy version

能打印出版本号,说明安装成功。这一步看似多余,但能直接暴露很多奇怪的问题。

常见的安装失败原因有这么几类:虚拟环境没激活就执行 pip,结果装到了别的环境;系统里存在多个 Python,pip 对应的解释器和你当前用的不是一个;Python 版本太老,新版 Scrapy 不支持。我个人建议 Python 3.8 以上,装完先which python确认当前解释器路径,再执行 pip 安装。环境这关过了,后面就顺了。

2. 创建项目:一张图看懂 Scrapy 的骨架

2.1 一行命令创建项目

进入你准备存放代码的目录,执行:

scrapy startproject demo_spider

这个命令会生成一个叫demo_spider的文件夹,里面的内容就是 Scrapy 工程的完整骨架。这里有个小细节:项目名建议全小写,单词之间用下划线连接,不要用连字符。因为项目名在 Python 里是要作为包名导入的,连字符在 Python 标识符里不合法,后面会出现各种奇怪的报错。

执行完命令后,先不要急着写代码,花几分钟把生成的文件看清楚。你会发现 Scrapy 帮你把很多东西都安排好了,你只需要往对的位置填你自己的逻辑。这个“对的位置”很多人一开始搞不清楚,后面我会逐个讲。

2.2 目录结构拆解:每个文件是干什么的

生成的目录结构大概是这样的:

demo_spider/ ├── scrapy.cfg └── demo_spider/ ├── __init__.py ├── items.py ├── middlewares.py ├── pipelines.py ├── settings.py └── spiders/ └── __init__.py

里外两层目录都叫demo_spider,新手经常被绕晕。简单说,外层是项目目录,里面的同名目录是 Python 包,真正的代码都在内层。

逐个文件说:

  • scrapy.cfg:项目配置文件,命令行工具靠它定位项目根目录。平时基本不用动。
  • items.py:定义数据结构的文件。比如你要提取书名、链接、发布时间,就在这里声明对应的字段。数据量小的项目,甚至可以不定义 Item 直接返回字典。
  • middlewares.py:中间件文件。请求和响应在经过下载器前后都会过一遍中间件,你可以在里面统一改 User-Agent、加代理、处理重试。入门阶段先不用碰。
  • pipelines.py:管道文件。提取出来的数据会一条条经过这里,你可以在这里做去重、清洗、存数据库。同样是后面的内容。
  • settings.py:全局配置文件。请求延迟、并发数、是否遵守 robots.txt、日志级别,这些影响爬虫全局行为的参数都在这里改。
  • spiders/目录:爬虫本体所在的位置。一个网站通常对应一个爬虫文件,你的抓取逻辑都写在这里。

初学者的典型误区是在 projects 刚创建时,就把 settings.py、middlewares.py、pipelines.py 全改一遍,结果项目根本跑不起来。其实默认骨架本身已经能支撑一个最简爬虫了,先保持原样跑通,再一点点加东西,才是稳妥的路线。

3. 创建爬虫文件:从模板开始最快

3.1 用 genspider 生成爬虫模板

进入项目根目录,注意是外层那个demo_spider:

cd demo_spider scrapy genspider books books.example.com

命令格式是genspider 爬虫名 域名。爬虫名是这个爬虫的唯一标识,后面运行爬虫时要用这个名字。域名会写进allowed_domains,这个参数的作用是限定爬虫只在指定域名下活动,防止爬着爬着跑到外站去了。

不过实战中,allowed_domains是个比较容易踩坑的点。如果起始 URL 里的域名和它不一致,请求会被直接过滤掉,页面都不请求就报错。所以如果目标站的页面来自其他域名,趁早把allowed_domains改对,或者改成和start_urls里的域名一致。

生成完,spiders目录下会多出books.py,内容大致是这样:

import scrapy class BooksSpider(scrapy.Spider): name = "books" allowed_domains = ["books.example.com"] start_urls = ["https://books.example.com/"] def parse(self, response): pass

这就是 Scrapy 的最小运行单元。name是爬虫名;start_urls是起始 URL 列表,爬虫启动后会依次请求这些地址;parse是默认回调函数,请求完成后 Scrapy 会把响应对象传给这个函数。我们要做的事情,就是在parse里把需要的数据提取出来。

3.2 写第一个提取规则:XPath 与 CSS 选择器

Scrapy 的响应对象response自带两个查询接口:response.xpath()和response.css()。你不需要额外引入解析库,直接用就行。它们的返回值是 SelectorList,可以用.get()取第一个匹配结果,用.getall()取所有匹配结果。

最简单的提取逻辑是这样:

def parse(self, response): title = response.css("h1::text").get() links = response.xpath("//a/@href").getall() yield {"title": title, "links": links}

这里有个容易搞混的细节:为什么用yield而不是return。因为 Scrapy 的设计中,parse是一个生成器,每yield一条数据,框架就会把它交给后续的管道处理。如果数据量很大,yield能做到边抓边吐,而不是等全部抓完才一次性处理。这个习惯一开始就要养成,后面改用 Item 和 Pipeline 时会顺畅很多。

来个更贴近现实的例子。假设目标页面结构是:

<div class="book"> <h2>Python实战</h2> <a href="/detail/123">查看详情</a> </div>

你想提取每本书的书名和详情链接:

def parse(self, response): for book in response.css("div.book"): name = book.css("h2::text").get() detail_url = book.css("a::attr(href)").get() yield {"name": name, "detail_url": detail_url}

需要留意两点。第一,CSS 选择器提取文本要写::text,提取属性要写::attr(属性名),这两个后缀最容易漏写,漏了返回的就是空结果。第二,页面里的链接往往是相对路径,比如/detail/123,如果之后要请求这个链接,得拼成完整 URL,用response.urljoin(detail_url)就能自动处理。这个函数会把相对路径和当前页面地址拼接好,比手动写字符串拼接靠谱。

3.3 运行爬虫并输出数据

在项目根目录执行:

scrapy crawl books

终端会先打出启动日志,然后逐条输出你提取到的数据。这些数据默认只是打印在终端里,想存到文件,加一个-o参数:

scrapy crawl books -o books.json

支持 json、jsonl、csv、xml 等格式,一般看文件后缀自动识别。第一次跑通时,你会直观感受到 Scrapy 和手写 requests 的区别:请求调度、并发控制、重试机制、数据输出,框架全给你安排好了。

这里有一个很实用的建议:调试选择器的时候,不要反复去请求目标站,特别是不确定规则时。可以在本地搭一个测试页面,或者用scrapy shell手动请求一次再慢慢试选择器。这样既不会给对方服务器造成压力,也避免自己 IP 被临时限制。

4. settings.py 核心参数:跑得稳比跑得快重要

4.1 必须了解的几个配置项

爬虫能跑起来之后,下一步就是学会调参。settings.py里绝大多数参数都有默认值,但有几个是入门阶段就应该理解的。

配置项默认值含义与建议
ROBOTSTXT_OBEYTrue是否遵守目标站的 robots.txt。为 True 时,Scrapy 请求前会先读取 robots 文件,里面禁止的链接不会去抓。建议保持开启。
USER_AGENT默认 Scrapy 标识请求头里的浏览器标识。默认 UA 很容易被目标站识别为脚本,建议改成常见浏览器的 UA。强烈建议必改。
DOWNLOAD_DELAY0两次请求之间的间隔秒数。设成 1 或 2,能显著降低请求频率,减少被限流概率。
CONCURRENT_REQUESTS16全局并发请求数。刚调试时调成 4 更稳,跑通了再往上加。
CONCURRENT_REQUESTS_PER_DOMAIN8单个域名下的并发请求上限。多域名抓取时这个参数比全局并发更实用。
LOG_LEVELINFO日志级别。调成 DEBUG 能看到更详细的请求与重试信息,排查问题很有用。
DOWNLOAD_TIMEOUT180单个请求的超时时间。网络不稳定时可以适当调低,避免长时间卡在某个请求上。

这些参数里,我第一推荐改的是USER_AGENT。默认的 Scrapy UA 长得很像机器人,稍微有点反爬意识的站点都会拦截。改成一个常见的浏览器 UA,能解决大量 403 问题。第二推荐改的是DOWNLOAD_DELAY,很多新手不在乎请求频率,几毫秒内发出成百上千个请求,这不叫抓取,这叫自爆。

4.2 调试三板斧:shell、日志、浏览器预览

遇到数据为空或者请求被拦截时,别急着改代码,用这三招定位。

第一招是scrapy shell。在项目目录下执行:

scrapy shell "https://books.example.com/"

Scrapy 会像爬虫一样请求这个地址,然后把你丢进一个交互式环境。你可以直接输入response.css(...)、response.xpath(...)现场试选择器,返回值立刻可见。这一步比“写好代码再跑一遍”快得多。如果试出来结果为空,先输入response.text看看页面正文在不在,判断是不是根本没有抓到内容。

第二招是调整日志级别。运行爬虫时加-L DEBUG:

scrapy crawl books -L DEBUG

DEBUG 级别会打印每个请求的细节、重试原因、丢弃原因,排查 403、404 和跳转问题特别好用。默认的 INFO 级别看不到这些信息。

第三招是用 Scrapy 自带的浏览器预览功能。在 parse 里临时加一行:

from scrapy.utils.response import open_in_browser def parse(self, response): open_in_browser(response)

运行后,Scrapy 会把抓到的响应用系统浏览器打开。你可以直接看到“Scrapy 眼里的页面”长什么样,和你在浏览器里看到的是不是同一个版本。这个方法对排查页面被重定向、UA 被识别、内容靠 JS 动态渲染等问题非常直观。

5. 常见问题与排查实录

5.1 请求被拒绝:403、406 和空响应

入门阶段遇到最多的问题就是访问被拒绝。状态码 403 是最典型的,含义是服务器理解请求但拒绝执行。常见原因有三类:User-Agent 被识别成脚本、请求频率太高触发防爬、缺少必要请求头。

排查流程可以这么走:先看日志里报的 HTTP 状态码;再用scrapy shell手动请求同一地址;如果浏览器能打开而 Scrapy 打不开,那基本可以断定是反爬策略在起作用。解决手段从简单到复杂排序:改 UA、加下载延迟、用中间件轮换更多 UA、补全常见请求头。

406 状态码也比较常见,通常和 Accept 头、Cookie 校验有关。遇到时先检查目标站期望什么样的请求头,用scrapy shell抓一下响应头信息,往往能发现线索。空响应则要区分是超时、连接被重置还是返回了空内容,日志里都有记录,照着看就行。

5.2 提取不到数据:选择器为空

代码没报错,但提取结果是空列表,这是每个爬虫新手都会撞上的坑。原因一般有三个:

第一,页面内容由 JS 动态渲染。Scrapy 本身不执行 JavaScript,它拿到的是服务器返回的原始 HTML。如果你的目标数据是 JS 执行后才出现的,那用选择器必然抓不到。这时候要看页面有没有对应的数据接口,或者考虑渲染工具。判断方法还是那招:response.text里有没有你要的内容。

第二,选择器写错了。CSS 里漏了::text,XPath 里漏了/text(),属性名拼错,这类小错误很常见。解决方式是用scrapy shell逐步验证,先从大范围匹配,再逐步缩小到目标节点。

第三,数据藏在嵌套结构里。比如 iframe 里的内容,Scrapy 不会自动展开,你得先提取 iframe 地址,再单独发一次请求。处理这类问题,先打印response.text看看有没有相关关键字,是最快的定位方式。

5.3 运行时报错:学会读报错信息

新手遇到报错容易慌,其实大部分错误都很直接。这里列几个高频问题:

  • ModuleNotFoundError: No module named 'scrapy':虚拟环境没激活,或者 scrapy 没装到当前环境里。先检查which python。
  • Spider not found:检查爬虫名和name属性是否一致,还要确认你的执行目录是不是项目根目录。
  • Spider already exists:同名爬虫已经存在,genspider拒绝覆盖。换个名字,或者删掉旧文件再生成。
  • Twisted 相关报错:一般是依赖版本和 Python 版本不匹配,重装依赖,或者升级 Python 版本。

排查的思路永远是从最后几行报错开始看,往前翻调用栈,看到底是哪一步出的问题。不要一上来就怀疑框架,绝大多数情况是环境和路径问题。

6. 进阶方向与我的个人体会

6.1 从入门到进阶:Item、Pipeline、中间件和 CrawlSpider

一个爬虫跑通之后,接下来的学习路径基本是清晰的。

先是用 Item 代替字典。字典适合快速验证,但项目变大后,字段拼写错误、数据结构不统一都是隐患。Item 允许你提前声明字段,配合 Pipeline 可以做统一的数据清洗、去重、入库。Pipeline 的机制很像流水线,每个process_item方法都处理经过的 item,你可以把不同的处理逻辑拆到多个 Pipeline 里,按顺序执行。

再往上是中间件。UA 轮换、代理池、自定义请求头、重试策略,这些涉及全局请求行为的功能都在中间件里做。入门阶段可以先理解“请求在进入下载器之前,会经过一系列中间件”这个模型,后面自然就明白该往哪里加代码。

如果目标站点的结构是“列表页 -> 详情页 -> 下一页”,可以换用 CrawlSpider。它提供了一套规则机制,你只要声明链接提取规则和对应的回调函数,框架就会自动帮你遍历整个站点的页面。相比手写“请求下一页”的循环,省力得多。

6.2 关于“礼貌爬取”这件事

最后想多说一句。很多人把爬虫理解为“想抓就抓”,但实际做项目时,尊重目标站的规则才是持久之道。我自己的习惯是:先看目标站的 robots.txt,尊重对方的限制;控制请求频率,能慢则慢;只爬公开数据,不碰需要登录才能访问的内容;抓到的数据不用于骚扰用户或商业滥用。

这不是说教,是长期实战的自我保护。爬虫的本质是模拟人类访问,如果访问频率比人类快几十倍,被封是必然的。给自己加一个DOWNLOAD_DELAY,不是示弱,是让你跑得更久。

我个人用 Scrapy 最深的体会是:入门并不难,难在把“能跑”变成“稳定”。第一次跑通爬虫的兴奋感过去之后,你会开始关注延迟、反爬、数据质量、异常重试,这些才是爬虫工程的核心。所以我给新手的建议很简单:别急着把框架全部学完,先动手把第一个爬虫跑起来,跑通了再回来研究 Pipeline 和中间件。那时候你再回头看这些代码,会有完全不同的理解。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进