使用Spider提取数据
Scarpy
网络爬虫编程的核心就是爬虫Spider
组件,它其实是一个继承与Spider
的类,主要功能设计封装一个发送给网站服务器的HTTP请求,解析网站返回的网页及提取数据
执行步骤
1、Spider
生成初始页面请求(封装于Request
对象中),提交给引擎
2、引擎通知下载按照Request
的要求,下载网页文档,再将文档封装成Response
对象作为参数传回给Spider
3、Spider
解析Response
中的网页内容,生成结构化数据Item
,或者产生新的请求(比如爬取下一页),再次发送给引擎
4、如果发送给引擎的是新的Request
,就继续第2步。如果发送的是结构化数据Item
,则引擎通知其他组件处理该数据(保存的文件或数据库中)
class DingdianXuanhuanSpider(scrapy.Spider):
# 爬虫名称
name = "dingdian_xuanhuan"
# 允许的域名
allowed_domains = ["www.xiaoshuopu.com"]
# 起始URL列表
start_urls = ["https://www.xiaoshuopu.com/class_1/"]
def parse(self, response):
# 小说列表
novel_list = response.xpath("//table/tr[@bgcolor='#FFFFFF']")
print("小说数量是:", len(novel_list))
# 循环获取小说名称、最新章节、作者、字数、更新、状态
for novel in novel_list:
# 小说名称
name = novel.xpath("./td[1]/a[2]/text()").extract_first()
# 最新章节
new_chapter = novel.xpath("./td[2]/a/text()").extract_first()
# 作者
author = novel.xpath("./td[3]/text()").extract_first()
# 字数
word_count = novel.xpath("./td[4]/text()").extract_first()
# 更新
update_time = novel.xpath("./td[5]/text()").extract_first()
# 状态
status = novel.xpath("./td[6]/text()").extract_first()
# 将小说内容保存到字典中
novel_info = {
"name": name,
"new_chapter": new_chapter,
"author": author,
"word_count": word_count,
"update_time": update_time,
"status": status
}
print("小说信息:",novel_info)
# 使用yield返回数据
yield novel_info
name
:必填项,用于区分不同的爬虫。一个Scrapy
项目中可以有多个爬虫。不同的爬虫,name
值不能相同start_urls
:存放要爬取的模板网页地址的列表start_request()
:爬虫启动时,引擎自动调用该方法,并且只会被调用一次,用于生成初始的请求对象,代码中没有是因为直接使用了基类的功能parse()
:Spider
类的核心方法。引擎将下载好的页面作为参数传递给parse
方法,parse
方法执行从页面中解析数据的功能
重写start_request方法
如何避免爬虫被网站识别出来导致被禁用呢?
通过重写start_request
方法,手动生成一个功能更强大的Request
对象。伪装浏览器、自动登录等功能都是在Request
对象中设置的
- 将爬虫伪装成浏览器
- 设置新的解析数据的回调函数,不使用默认的
parse()
class QdYuepiaoSpider(scrapy.Spider):
name = "qd_yuepiao"
allowed_domains = ["www.qidian.com"]
start_urls = ["https://www.qidian.com/rank/yuepiao/"]
# 设置代理
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0"
}
# 重写请求
def start_requests(self):
for url in self.start_urls:
yield scrapy.Request(url=url, headers=self.headers, callback=self.parse)
def parse(self, response):
print("数据:", response.xpath("//div"))
注:上面简单设置headers
还是会被一些反爬的网站给识别出来。
更好的方式是在settings
中启用并设置user-agent
,这样项目下的所用爬虫都能使用到该设置
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0"
Request对象
request
对象用来描述一个HTTP请求,它通常在Spider
中生成并由下载器执行
class Request(
url: str,
callback: ((...) -> Any) | None = None,
method: str = "GET",
headers: dict | None = None,
body: bytes | str | None = None,
cookies: dict | List[dict] | None = None,
meta: dict | None = None,
encoding: str = "utf-8",
priority: int = 0,
dont_filter: bool = False,
errback: ((...) -> Any) | None = None,
flags: List[str] | None = None,
cb_kwargs: dict | None = None
)
url
:HTTP请求的网址callback
:指定回调函数,即确定页面的解析函数,默认为parse
。在解析期间如果发生异常会调用errback
method
:请求方式。默认为GET
,必须大写英文字母headers
:HTTP请求头body
:HTTP请求体cookies
:请求的Cookie
值,可以实现自动登录的效果meta
:字典类型,用于数据的传递,可以将数据传递给其他组件,也可以传递给Response
对象encoding
:请求的编码方式。默认UTF-8
priority
:请求的优先级,优先级高的优先下载dont_filter
:默认值为False
,避免对同一个url
的重复请求。设置True
,即使是重复的请求也会强制下载errback
:在处理请求时引发任何异常时调用的函数
多页数据爬取
大多数网站都会存在分页条,进行多个页面数据爬取需要:
在解析函数中,提取完本页数据并提交给引擎后,设法提取到下一页的URL
地址,使用这个地址生成新的请求对象,再提交给引擎。
import scrapy
class DangaoSpider(scrapy.Spider):
name = "dangao"
allowed_domains = ["sc.chinaz.com"]
start_urls = ["https://sc.chinaz.com/tupian/dangaotupian.html"]
def parse(self, response):
# 定位到图片的元素,并保存到列表中
img_list = response.xpath("//div[@class='item']/img")
for img in img_list:
name = img.xpath("./@alt").extract_first()
src = img.xpath("./@data-original").extract_first()
img_info = {"name": name, "src": src}
yield img_info
# 获取下一页的url
next_url = response.xpath("//a[@class='nextpage']/@href").extract_first()
if next_url != None:
next_url = "https://sc.chinaz.com/tupian/" + next_url
print("下一页地址是:", next_url)
# 生成新的请求对象,并交给引擎执行
yield scrapy.Request(url=next_url, callback=self.parse)
使用Item封装数据
Item
对象是一个简单的容器,用于收集抓取到的数据,其提供了类似于字典的API,并具有用于声明可用字段的简单语法
定义Item 和 Field
在items.py
中创建对应的类
class DingdianItem(scrapy.Item):
# 小说名称、作者、最新、字数、更新时间、状态
name = scrapy.Field()
author = scrapy.Field()
new_chapter = scrapy.Field()
word_count = scrapy.Field()
update_time = scrapy.Field()
status = scrapy.Field()
在相应爬虫中使用
import scrapy
from qidian_yuepiao.items import DingdianItem
class DingdianXuanhuanSpider(scrapy.Spider):
# 爬虫名称
name = "dingdian_xuanhuan"
# 允许的域名
allowed_domains = ["www.xiaoshuopu.com"]
# 起始URL列表
start_urls = ["https://www.xiaoshuopu.com/class_1/"]
def parse(self, response):
# 小说列表
novel_list = response.xpath("//table/tr[@bgcolor='#FFFFFF']")
print("小说数量是:", len(novel_list))
# 循环获取小说名称、最新章节、作者、字数、更新、状态
for novel in novel_list:
# 小说名称
name = novel.xpath("./td[1]/a[2]/text()").extract_first()
# 最新章节
new_chapter = novel.xpath("./td[2]/a/text()").extract_first()
# 作者
author = novel.xpath("./td[3]/text()").extract_first()
# 字数
word_count = novel.xpath("./td[4]/text()").extract_first()
# 更新
update_time = novel.xpath("./td[5]/text()").extract_first()
# 状态
status = novel.xpath("./td[6]/text()").extract_first()
# 将小说内容保存到Item中
novel_info = DingdianItem()
novel_info["name"] = name
novel_info["new_chapter"] = new_chapter
novel_info["author"] = author
novel_info["word_count"] = word_count
novel_info["update_time"] = update_time
novel_info["status"] = status
print("小说信息:", novel_info)
# 使用yield返回数据
yield novel_info
使用ItemLoader填充容器
在项目很大、提取的字段数很多时,数据提取规则也会越来越多,再加上还要对提取到的数据做转换处理,代码就会变得臃肿,维护起来困难。
为了解决这个问题,Scrapy
提供了项目加载器(ItemLoader)这样一个填充容器。通过填充容器,可以配置Item
中各个字段的提取规则,并通过函数分析原始数据,最后进行赋值
Item 和ItemLoader 的区别在于:
Item
提供了保存数据的容器,需要手动将数据保存于容器中
ItemLoader
提供的是填充容器的机制,提供了3种方法
add_xpath
:使用xpath
选择器提取数据add_css
:使用css
选择器提取数据add_value
:直接传值
import scrapy
from qidian_yuepiao.items import DingdianItem
from scrapy.loader import ItemLoader
class DingdianXuanhuanSpider(scrapy.Spider):
# 爬虫名称
name = "dingdian_xuanhuan"
# 允许的域名
allowed_domains = ["www.xiaoshuopu.com"]
# 起始URL列表
start_urls = ["https://www.xiaoshuopu.com/class_1/"]
def parse(self, response):
# 小说列表
novel_list = response.xpath("//table/tr[@bgcolor='#FFFFFF']")
print("小说数量是:", len(novel_list))
# 循环获取小说名称、最新章节、作者、字数、更新、状态
for novel in novel_list:
# 生成ItemLoader对象
novel_info = ItemLoader(item=DingdianItem(),selector=novel)
# 小说名称
novel_info.add_xpath("name","./td[1]/a[2]/text()")
# 最新章节
novel_info.add_xpath("author","./td[2]/a/text()")
# 作者
novel_info.add_xpath("new_chapter","./td[3]/text()")
# 字数
novel_info.add_xpath("word_count","./td[4]/text()")
# 更新
novel_info.add_xpath("update_time","./td[5]/text()")
# 状态
novel_info.add_xpath("status","./td[6]/text()")
print("小说信息:", novel_info)
处理数据
使用ItemLoader
提取出的数据也是保存于列表中,以前可以通过extract_first()
获取列表数据,现在呢?需要使用输入处理器input_processor
和输出处理器out_processor
import scrapy
from scrapy.loader.processors import TakeFirst
class DingdianItem(scrapy.Item):
# 定义一个转换函数
def change_status(status):
if status[0] == "连载中":
return 1
else:
return 2
# 小说名称、作者、最新、字数、更新时间、状态
# 使用内置函数,获取列表中第一个非空数据
name = scrapy.Field(output_processor=TakeFirst)
author = scrapy.Field(output_processor=TakeFirst)
new_chapter = scrapy.Field(output_processor=TakeFirst)
word_count = scrapy.Field(output_processor=TakeFirst)
update_time = scrapy.Field(output_processor=TakeFirst)
status = scrapy.Field(input_processor=change_status, output_processor=TakeFirst)
使用Pipeline封装数据
当Spider
将收集的数据封装成Item
后,将会被传递到Item Pipeline
项目管道组件中等待进一步处理。Scrapy
犹如一个爬虫流水线,Item Pipeline
是流水线的最后一道工序,它是可选的,默认关闭,使用时需要将它激活。如果需要,也可以定义多个 Item Pipeline
组件,数据会依次访问每个组件,执行相应的数据处理功能
典型应用
- 清理数据
- 验证数据的有效性
- 查重并丢弃
- 将数据按照自定义的格式存储到文件中
- 将数据保存的数据库中
当创建项目后,会字段生成一个pipelines.py
文件,在里面编写自己的Item Pipeline
# 默认生成的
class QidianYuepiaoPipeline:
# process_item 是必须实现的,用于处理每一条数据Item
# item 是待处理的Item对象,spider是爬取此数据的spider对象
def process_item(self, item, spider):
# 编写相应的处理逻辑
if item["status"] == 1:
item["status"] = "连载"
else:
item["status"] = "完结"
return item
# 自定义的
class DingdianPipeline:
def __init__(self):
# 类初始化函数
pass
def process_item(self, item, spider):
# 编写相应的处理逻辑
item["status"] = item["status"].replace("连载", "1").replace("完结", "2")
return item
启用 Item Pipeline
在配置文件settings.py
中启用被注释掉的代码
ITEM_PIPELINES = {
"qidian_yuepiao.pipelines.DingdianItemPipeline": 100,
"qidian_yuepiao.pipelines.QidianYuepiaoPipeline": 300,
}
格式为项目名.pipelines.对应的类:优先级
,数值越小优先级越高。在settings.py
中设置后会对所有爬虫都生效。如果想针对每一个爬虫使用某一个,可以在爬虫内部进行指定,例如
class ScrapyASpider(scrapy.Spider):
name = 'scrapyA'
custom_settings = {
'ITEM_PIPELINES': {
'myproject.pipelines.MyCustomPipelineForScrapyA': 300,
# 其他可能需要的Pipelines...
},
}
# 爬虫的具体逻辑...
保存为其他文件
# 默认生成的
class QidianYuepiaoPipeline:
# 文件名称
file_name = datetime.now().strftime("%Y%m%d%H%M%S") + ".txt"
# 文件对象
file = None
# Spider开启时,执行打开文件操作
def open_spider(self, spider):
# 以追加形式打开文件
self.file = open(self.file_name, "a", encoding="utf-8")
# process_item 是必须实现的,用于处理每一条数据Item
# item 是待处理的Item对象,spider是爬取此数据的spider对象
def process_item(self, item, spider):
# 写入文件
self.file.write("名称:"+item["name"] + "\n")
return item
# 爬虫关闭时,执行关闭文件操作
def close_spider(self, spider):
self.file.close()
案例
还是以获取上面获取蛋糕的案例为基础,上面我们获取了蛋糕图片的名称和地址,我们再次基础上获取图片的简介内容
import scrapy
from scarpy_study.items import DanGaoItem
class DangaoSpider(scrapy.Spider):
name = "dangao"
allowed_domains = ["sc.chinaz.com"]
start_urls = ["https://sc.chinaz.com/tupian/dangaotupian.html"]
def parse(self, response):
# 定位到图片的元素,并保存到列表中
img_list = response.xpath("//div[@class='item']")
for img_item in img_list:
# 获取图片名称和图片地址
name = img_item.xpath("./img/@alt").extract_first()
src = img_item.xpath("./img/@data-original").extract_first()
img_info = {
"name": name,
"url": src
}
# 获取图片详情地址
detail_url = img_item.xpath(
"./div[@class='bot-div']/a/@href").extract_first()
# print("详情地址:", detail_url)
if detail_url != None:
detail_url = 'https://sc.chinaz.com' + detail_url
# 生成新的请求,并使用meta传递信息
yield scrapy.Request(url=detail_url, callback=self.parse_detail,
meta={"img_info": img_info})
# 获取下一页的url
next_url = response.xpath(
"//a[@class='nextpage']/@href").extract_first()
if next_url != None:
next_url = "https://sc.chinaz.com/tupian/" + next_url
print("下一页地址是:", next_url)
# 生成新的请求对象,并交给引擎执行
yield scrapy.Request(url=next_url, callback=self.parse)
# 用来解析详情页
def parse_detail(self, response):
img_info = response.meta["img_info"]
print("img_info:", img_info)
# 记录图片的名称、地址
dangaoItem = DanGaoItem()
dangaoItem["name"] = img_info["name"]
dangaoItem["url"] = img_info["url"]
# 获取描述
desc = response.xpath("//p[@class='all-c']/text()").extract_first()
dangaoItem["desc"] = desc
print("蛋糕图片信息:", dangaoItem)
yield dangaoItem
这里获取详情的核心是,在生成新的请求对象时使用callback
指定详情信息的解析函数,使用meta
来传递之前获取到的图片名称和图片地址
# 生成新的请求,并使用meta传递信息
yield scrapy.Request(url=detail_url, callback=self.parse_detail,
meta={"img_info": img_info})