创建项目
在要创建项目的文件夹下,使用python -m scrapy startproject tutorial命令创建项目,其中python -m scrapy startproject为固定的写法,tutorial为项目的名字,这个命令会在当前目录下创建出tutorial文件夹,目录结构如下
tutorial
| scrapy.cfg # 部署配置文件
|
\---tutorial # 项目的Python模块,从这里导入代码
| items.py # 项目项定义文件
| middlewares.py # 项目中间件文件
| pipelines.py # 项目管道文件
| settings.py # 项目设置文件
| __init__.py
|
\---spiders # 稍后放置spiders的目录
__init__.py
创建spider
进入项目目录,例如这里的tutorial目录(注意是第一层的目录),然后执行python -m scrapy genspider quotes_spider quotes.toscrape.com, 命令行会进行类似这样的输出
C:\tutorial>python -m scrapy genspider quotes_spider quotes.toscrape.com
Created spider 'quotes_spider' using template 'basic' in module:
tutorial.spiders.quotes_spider
现在tutorial/tutorial/spiders目录下面会多出来一个quotes_spider.py的文件,
内容如下,每行的含义可以看注释
import scrapy
class QuotesSpiderSpider(scrapy.Spider):
# 爬虫的名字
name = "quotes_spider"
# 当前爬虫允许访问的域名,不符合的会自动屏蔽掉
allowed_domains = ["quotes.toscrape.com"]
# 起始域名,第一个访问的url
start_urls = ["https://quotes.toscrape.com"]
# 解析数据的方法
def parse(self, response):
pass
修改spider
将quotes_spider.py的内容替换为下面的内容
from pathlib import Path
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
def start(self):
urls = [
"https://quotes.toscrape.com/page/1/",
"https://quotes.toscrape.com/page/2/",
]
for url in urls:
yield scrapy.Request(url=url, callback=self.parse)
def parse(self, response):
# 从URL中提取页码(如 "page/1/" → "1")
page = response.url.split("/")[-2]
# 生成文件名(如 quotes-1.html)
filename = f"quotes-{page}.html"
# 将响应内容保存为HTML文件
Path(filename).write_bytes(response.body)
# 日志记录(输出到控制台)
self.log(f"Saved file {filename}")
注意,这里的爬虫名和类名都改变了,还需要注意的是start方法,这个方法在2.13之前为start_requests,默认实现与代码中的实现一致,区别在于默认读取的urls列表为start_urls,所以这里的写法等效于不写start方法,直接声明数组start_urls = [ "https://quotes.toscrape.com/page/1/", "https://quotes.toscrape.com/page/2/", ],两种方式相较而言,实现start方法会更加灵活,更容易定制;使用start_urls会更简洁,适合不需要特殊处理的脚本
等效于:
from pathlib import Path
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
"https://quotes.toscrape.com/page/1/",
"https://quotes.toscrape.com/page/2/",
]
def parse(self, response):
page = response.url.split("/")[-2]
filename = f"quotes-{page}.html"
Path(filename).write_bytes(response.body)
在start方法中使用了yield关键词,这个是python中的生成器,生成器是一种特殊的迭代器,可以自动实现__iter__() 和 __next__()两个方法,yield 暂停函数执行,保留局部变量
运行spider
在项目的顶层目录下,运行python -m scrapy crawl quotes执行代码,这个命令中quotes为spider中对应子类中的name的值,其他部分为固定写法
方法执行完毕后,在顶层目录下会出现quotes-1.html,quotes-2.html两个文件,即请求的结果
解析结果
交互式解析
scrapy提供了一个shell的命令,提供了交互式的操作,同时也便于进行开发和调试
命令格式为python -m scrapy shell <url>,这里的url可以是网络地址,如https://xxxx.xx这种,也可以是本地文件,例如./quotes-1.html,也可以是完整地址,例如file:///C:/tutorial/quotes-1.html。当为网络地址时,需要用双引号包裹
运行之后进入交互终端, 使用--nolog可以跳过不需要的日志输出,如果是在项目目录中运行这个命令,会以项目中的setting.py作为配置
可用快捷键
shelp()- 打印包含可用对象列表的帮助fetch(url[, redirect=True])- 从给定的 URL 获取新响应 并相应地更新所有相关对象。可以选择redirect=False要求HTTP 3xx重定向后不传递fetch(request)- 从给定的请求中获取新响应并更新 所有相关对象。view(response)- 在本地web浏览器中打开给定的响应以供检查。这将在响应体中添加一个标签,以便外部链接(如图像和样式表)正确显示。但是请注意,这将在您的计算机中创建一个临时文件,该文件不会自动删除。
可用的 Scrapy 对象
Scrapy shell会自动从下载的页面创建一些方便的对象,如Response对象和Selector对象(用于HTML和XML内容)。
这些对象是:
crawler- 当前爬虫对象。spider- 已知处理该URL的Spider,或者如果当前URL没有找到Spider,则为Spider对象request- 最后一个提取页面的Request对象。您可以使用replace()修改此请求,也可以使用shortcut.fetch获取新请求(无需离开shell)response- 一个包含最后提取页面的Response对象settings- 当前的 Scrapy 设置
常见的操作
css表达式
可以使用response.css("title")获取title标签,这会返回一个 SelectorList 对象(类似列表),包含所有匹配的 <title> 标签
使用response.css("title::text").getall()获取title标签的文本,其中getall()表示获取所有符合要求的文本;
使用get()来获取第一个, 等效于[0].get(),例如response.css("title::text")[0].get()。但是使用get()如果元素不存在,会返回None,而不会报错
除了getall()和get()之外,还可以用正则表达式进行操作,例如
>>> response.css("title::text").re(r"Quotes.*")
['Quotes to Scrape']
>>> response.css("title::text").re(r"Q\w+")
['Quotes']
>>> response.css("title::text").re(r"(\w+) to (\w+)")
['Quotes', 'Scrape']
xpath表达式
可以使用response.xpath("//title")获取title标签
使用response.xpath("//title/text()").get()来获取文本,大致操作与css选择器相似,但是表达式的写法与方法不同
除了getall(),get()之外,在1.x的早期版本中还有一组对应的方法extract()和extract_first(),新版本中也可以用,但是推荐用新的api
脚本解析
在交互式中的操作,在脚本中同样可以执行,例如response.css("div.quote")等,但是脚本中需要使用变量来接受,修改quotes_spider.py的内容如下
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
"https://quotes.toscrape.com/page/1/",
"https://quotes.toscrape.com/page/2/",
]
def parse(self, response):
# 找到所有class为quote的div并进行遍历
for quote in response.css("div.quote"):
yield {
# 找到div中class为text的span,并获取第一个标签的文本
"text": quote.css("span.text::text").get(),
# 找到div中class为author的small标签,并获取第一个的文本
"author": quote.css("small.author::text").get(),
# 找到div中所有class为tags的div下面的所有class为tag的a标签,并获取所有a标签的文本
"tags": quote.css("div.tags a.tag::text").getall(),
}
保存,然后使用python -m scrapy crawl quotes命令运行,在输出的日志中可以看到类似{'text': '“It is not a lack of love, but a lack of friendship that makes unhappy marriages.”', 'author': 'Friedrich Nietzsche', 'tags': ['friendship', 'lack-of-friendship', 'lack-of-love', 'love', 'marriage', 'unhappy-marriage']} 的输出,证明我们写的表达式确实正常解析出来了想要的内容
关于表达式的内容,这个要分析html的文档结构,然后编写对应的表达式来获取
比如打开网页的的源码进行分析,通过class或者id或者属性等来定位元素,然后进行处理
存储结果
简单存储
解析之后的结果可以使用python -m scrapy crawl quotes -O quotes.json命令进行存储,运行后在项目根目录会生成一个quotes.json的文件,打开之后就是解析到的数据
这个命令中python -m scrapy crawl quotes为运行命令,-O为覆盖写入的意思,quotes.json为写入文件的文件名,除了-O之外,还有-o参数,为追加写入的意思,当使用-O、-o参数时,会激活内置的导出器,将parse方法yield的数据保存到文件,具体的导出器由扩展名决定,除了json,还支持.json, .jsonl, .csv, .xml, .pickle, .marshal 等(根据 Scrapy 版本支持情况),还可以使用-t参数,用来指定输出格式,优先级高于文件扩展名,例如 -t json
管道存储
管道是一个类,通常会命名为XxxPipeline,当项目比较简单的时候,类可以直接被定义在pipelines.py中;当项目很复杂,或者管道类很复杂需要拆分的时候,则可以将类单独用一个文件存储,只需要在settings.py文件中指定清楚即可
在spider的parse方法中返回的对象,会被传递到管道中,按照管道的顺序依次处理,管道的典型用途包括:
- 清理 HTML 数据
- 验证抓取的数据(检查项目是否包含某些字段)
- 检查重复项(并删除它们)
- 将抓取的项目存储在数据库中
在项目的
pipelines.py文件中,可以看到一个默认创建出来的TutorialPipeline
创建一个管道需要在类中实现process_item(self, item, spider)方法,这个方法中对每个对象进行处理,item是spider的parse()方法返回的数据,spider则是处理数据的类,也就是之前写的QuotesSpider
当对象被处理完毕之后则使用return返回,如果数据不需要进一步的处理直接丢弃,则使用raise返回,例如下面这个例子
from itemadapter import ItemAdapter
from scrapy.exceptions import DropItem
class PricePipeline:
vat_factor = 1.15
def process_item(self, item, spider):
adapter = ItemAdapter(item)
if adapter.get("price"):
if adapter.get("price_excludes_vat"):
adapter["price"] = adapter["price"] * self.vat_factor
return item
else:
raise DropItem("Missing price")
DropItem会被标记为丢弃,不会被传递到后续的管道,日志中会记录传递的信息,例如这里的Missing price
除了process_item方法外,还可以重写open_spider(self, spider)方法和close_spider(self, spider)方法,这两个方法分别在spider被打开和关闭时调用,参数中的spider即spider实例
例如下面这个 存储数据到文件 的例子
import json
from itemadapter import ItemAdapter
class JsonWriterPipeline:
def open_spider(self, spider):
self.file = open("items.jsonl", "w")
def close_spider(self, spider):
self.file.close()
def process_item(self, item, spider):
line = json.dumps(ItemAdapter(item).asdict()) + "\n"
self.file.write(line)
return item
这个管道将数据存入items.jsonl中
创建管道
这里使用创建文件的方式,而不是直接在pipelines.py中新增类
在项目的spiders目录的同级中新建目录pipelines,然后创建json_writer_pipeline.py文件,将上面的 存储数据到文件 的例子粘贴进去
要使管道生效,还需要在settings.py中配置,配置项为ITEM_PIPELINES,例如
ITEM_PIPELINES = {
"tutorial.pipelines.json_writer_pipeline.JsonWriterPipeline": 300
}
前面的key为类在项目中的定位,后面的数字为运行顺序,一般为从0-1000范围之内,数字越小的越先执行
运行python -m scrapy crawl quotes,运行结束后在项目根目录下可以看到items.jsonl文件
如果有多个管道需要执行,在配置中配置多个即可
这个例子仅用于展示管道的使用,如果需要将数据转化为json文件保存,参考之前的简单存储代码
获取全部数据
通过递归,不停的获取下一页,一直到获取全部数据
修改quotes_spider.py的内容为如下代码
import scrapy # 导入Scrapy框架
class QuotesSpider(scrapy.Spider):
# 爬虫的基本属性
name = "quotes" # 爬虫的唯一标识名(通过命令运行:scrapy crawl quotes)
start_urls = [
"https://quotes.toscrape.com/page/1/", # 初始爬取的URL列表(可多个)
]
def parse(self, response):
"""
处理网页响应的核心方法,会被自动调用
Args:
response: 下载器返回的网页响应对象(包含HTML内容)
"""
# 遍历页面中所有class="quote"的div元素
for quote in response.css("div.quote"):
# 提取每条名言的信息,生成字典格式数据
yield {
"text": quote.css("span.text::text").get(), # 提取名言文本(CSS选择器)
"author": quote.css("small.author::text").get(), # 提取作者(::text表示文本内容)
"tags": quote.css("div.tags a.tag::text").getall(), # 提取所有标签(getall()返回列表)
}
# 查找"下一页"的链接(通过CSS选择器获取href属性)
next_page = response.css("li.next a::attr(href)").get()
# 如果存在下一页
if next_page is not None:
# 将相对URL转为绝对URL(如/page/2/ → https://quotes.toscrape.com/page/2/)
next_page = response.urljoin(next_page)
# 创建新的请求,指定回调函数为parse(实现自动翻页)
yield scrapy.Request(next_page, callback=self.parse)
这段代码的的变化就是下面多了个获取下一页,如果存在下一页,就将相对url转化为绝对url,然后调用当前方法继续获取,核心代码yield scrapy.Request(next_page, callback=self.parse),实现递归获取数据的操作
follow方式获取
如果需要请求的地址不是相对路径,或者和当前主域名不一致,则可以修改下面部分的代码为
next_page = response.css("li.next a::attr(href)").get()
if next_page is not None:
yield response.follow(next_page, callback=self.parse)
response.follow是用来生成新的请求,可以自动处理 相对/绝对 url,并且可以直接传入Selector ,并且代码更简洁,需要注意的是去重:Scrapy 默认会过滤重复 URL,若分页参数不同但内容相同(如 ?page=2 和 ?page=2#top),需设置 dont_filter=True。例如yield response.follow(a, callback=self.parse, dont_filter=True)
只有页码的获取
如果分页栏只有页数,而没有下一页,或者希望同时请求页码而不是一页一页的获取,可以使用下面循环的代码进行爬取
for href in response.css("ul.pager a::attr(href)"):
yield response.follow(href, callback=self.parse)
更简洁的写法,这种写法不必获取href,而是直接传入Selector对象,但是限制条件是a标签的链接必须放在href属性中
for a in response.css("ul.pager a"):
yield response.follow(a, callback=self.parse)
还可以直接生成全部,而不是一个个的遍历
anchors = response.css("ul.pager a")
yield from response.follow_all(anchors, callback=self.parse)
更精简的写法
yield from response.follow_all(css="ul.pager a", callback=self.parse)
传递参数
在运行时动态传递的参数,可以使用-a参数,例如python -m scrapy crawl quotes -O quotes-humor.json -a tag=humor
在spider中,可以使用self.tag来获取,例如
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
async def start(self):
url = "https://quotes.toscrape.com/"
tag = getattr(self, "tag", None)
if tag is not None:
url = url + "tag/" + tag
yield scrapy.Request(url, 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(),
}
next_page = response.css("li.next a::attr(href)").get()
if next_page is not None:
yield response.follow(next_page, self.parse)
getattr() 是一个 内置函数,用于 动态获取对象的属性或方法。如果属性存在,则返回属性,否则返回默认值,这里的默认值为None