Scrapy 爬虫
Scrapy 是一个 Python 爬虫框架,用起来很方便。
如果打开网页源代码就能看到数据,用 Scrapy 很合适;如果数据必须等浏览器执行 JavaScript、滚动、点击之后才出现,那就先别硬上 Scrapy,Selenium 或 Playwright 会省事一点
先看 Scrapy 到底帮我们做了什么
很多入门文章都会放 Scrapy 的流程图。腾讯云开发者社区里也有类似图,画得比较直观:

正常的运行顺行是这样的
1
| Spider 产生请求 -> Engine 调度 -> Downloader 下载页面 -> Spider 解析响应 -> Pipeline 处理数据
|
也就是说,我们刚开始真正要写的主要是两块:
spider:页面从哪里进来,数据怎么取,下一页怎么跟;
pipeline:取出来的数据要不要清洗、去重、保存。
中间那些调度、下载、去重、并发控制,Scrapy 已经帮我们做了一大截。这也是它比临时写 requests.get() 更适合长期维护的地方。
创建项目
先建虚拟环境:
这类关于python虚拟环境的创建其实有很多种方式:比如
- uv
- conda
- pyenv
- virtualenv
- venv
等等,不过我喜欢uv多一点
安装:
创建项目:
1 2
| scrapy startproject quote_spider cd quote_spider
|
创建完成后,目录大概是这样:

常用文件先记这几个:
1 2 3 4 5
| quote_spider/ items.py # 定义数据字段 pipelines.py # 清洗、保存数据 settings.py # 并发、下载延迟、管道开关等配置 spiders/ # 放具体爬虫
|
用 shell 先试页面
一般来说,先用 Scrapy shell 可以简单快捷的定位到页面的元素用于调试,我自己用的时候感觉非常方便,先到对应页面有Xpath或者CSS选择到对应的元素路径,然后放到Shell做测试,因为有时候会有一定的兼容问题
1
| scrapy shell https://quotes.toscrape.com/
|
进去后先试:
1
| response.css("div.quote").get()
|
能拿到一段 HTML,说明页面内容在服务端返回的 HTML 里,Scrapy 可以直接抓。
再取第一条 quote:
1 2 3 4
| quote = response.css("div.quote")[0] quote.css("span.text::text").get() quote.css("small.author::text").get() quote.css("div.tags a.tag::text").getall()
|
这里最常用的就三个写法:
1 2 3
| ::text 取文本 ::attr(href) 取属性 .getall() 取全部结果
|
选择器这一步别省。很多“爬虫跑不出来”的问题,最后都不是 Scrapy 的问题,而是 CSS 或 XPath 写偏了。
第一个 spider
在 quote_spider/spiders/quotes.py 新建文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import scrapy
class QuotesSpider(scrapy.Spider): name = "quotes" allowed_domains = ["quotes.toscrape.com"]
async def start(self): yield scrapy.Request("https://quotes.toscrape.com/", self.parse)
def parse(self, response): for quote in response.css("div.quote"): yield { "text": quote.css("span.text::text").get(), "author": quote.css("small.author::text").get(), "tags": quote.css("div.tags a.tag::text").getall(), }
|
运行:
如果终端里能看到一条条 quote 数据,第一步就过了。
网上很多旧教程会写:
1
| start_urls = ["https://quotes.toscrape.com/"]
|
这个写法简单项目当然能用。但 Scrapy 新版官方教程已经在示例里使用 async def start(),后面要接参数、登录、动态入口时会更顺手,所以这里直接用新版写法。
保存成文件
先别急着写数据库。入门阶段最适合先导出 JSONL:
1
| scrapy crawl quotes -O quotes.jsonl
|
-O 是覆盖输出,-o 是追加输出。开发调试时我更喜欢 -O,不容易把旧数据混进去。
JSONL 的好处是每行一条数据:
1
| {"text": "The world as we have created it...", "author": "Albert Einstein", "tags": ["change", "deep-thoughts", "thinking", "world"]}
|
后面要导入 MySQL、MongoDB,或者丢给脚本二次处理,都方便。
把下一页也跟上
只抓第一页没什么意义。先在 shell 里看下一页链接:
1
| response.css("li.next a::attr(href)").get()
|
返回的是:
改 spider:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import scrapy
class QuotesSpider(scrapy.Spider): name = "quotes" allowed_domains = ["quotes.toscrape.com"]
async def start(self): yield scrapy.Request("https://quotes.toscrape.com/", self.parse)
def parse(self, response): for quote in response.css("div.quote"): yield { "text": quote.css("span.text::text").get(), "author": quote.css("small.author::text").get(), "tags": quote.css("div.tags a.tag::text").getall(), }
next_page = response.css("li.next a::attr(href)").get() if next_page: yield response.follow(next_page, callback=self.parse)
|
这里用 response.follow(),别自己拼域名。它会把 /page/2/ 转成完整 URL,并交给 Scrapy 调度。
再次运行:
1
| scrapy crawl quotes -O quotes.jsonl
|
看到日志里出现 /page/2/、/page/3/,说明翻页已经跑起来了。
什么时候需要 Item
前面直接 yield dict 没问题。项目小的时候,这样最快。
但如果字段多了,建议在 items.py 里写清楚:
1 2 3 4 5 6 7
| import scrapy
class QuoteItem(scrapy.Item): text = scrapy.Field() author = scrapy.Field() tags = scrapy.Field()
|
spider 里改成:
1 2 3 4 5 6 7 8 9 10
| from quote_spider.items import QuoteItem
def parse(self, response): for quote in response.css("div.quote"): yield QuoteItem( text=quote.css("span.text::text").get(), author=quote.css("small.author::text").get(), tags=quote.css("div.tags a.tag::text").getall(), )
|
Item 不是为了显得“正规”,它主要解决一个问题:字段边界清楚。后面加 pipeline、保存数据库时,不容易把字段名写散。
Pipeline 放清洗逻辑
Spider 里别塞太多处理逻辑。Spider 负责抓,Pipeline 负责处理。
比如去掉 quote 两边的中文引号,顺手清一下空标签:
1 2 3 4 5 6
| class CleanQuotePipeline: def process_item(self, item, spider): item["text"] = item["text"].strip("“”") item["author"] = item["author"].strip() item["tags"] = [tag.strip() for tag in item["tags"] if tag.strip()] return item
|
在 settings.py 打开:
1 2 3
| ITEM_PIPELINES = { "quote_spider.pipelines.CleanQuotePipeline": 300, }
|
后面的数字是执行顺序,数字越小越早执行。比如你可以先清洗,再去重,最后写数据库。
问题
数据是不是接口返回的
现在很多网站都是前后端分离。直接请求页面 HTML,可能只有空壳,真正数据来自接口。
这时别急着模拟浏览器,先打开浏览器开发者工具,看 Network 里有没有 JSON 接口。能直接请求接口,Scrapy 反而更舒服。
频率有没有太高
开发阶段建议先保守一点:
1 2 3
| CONCURRENT_REQUESTS = 8 DOWNLOAD_DELAY = 1 ROBOTSTXT_OBEY = True
|
别一开始就把并发开很大。爬虫不是压测工具,爬太快的后果说实话可以自己去试一下(我已经被封了两个号)
日志是不是太吵
调试时可以看详细日志,跑稳定后再降级:
如果要写到文件:
数据会不会重复
Scrapy 默认会对请求 URL 去重,但不会帮你判断两条 Item 是否重复。如果一个详情页从多个入口都能进去,就要在 Pipeline 里用唯一键去重,比如详情页 URL、业务 ID 或内容 hash。