Scrapy框架爬虫实战——从入门到放弃01
Posted 昵称竟然不能小于四位
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Scrapy框架爬虫实战——从入门到放弃01相关的知识,希望对你有一定的参考价值。
Scrapy框架爬虫实战01——经常被爬的古诗文网
ps. 案例制作时的操作环境是MacOS,如果是windows用户,下文中提到的“终端”指的就是cmd命令行窗口。
pps. 本文省略了安装过程,尚未安装scrapy的用户可以直接在pycharm的preference内搜索安装。
文章目录
目标网站:传送门
任务:使用Scrapy框架爬虫,爬取“推荐”中共10页的古诗题目、作者、朝代和内容
ps. 各类教程都拿它举例子,古诗文网好惨
项目创建
创建Scrapy爬虫项目需要在终端中进行
先打开一个文件路径,即你希望的爬虫文件存放路径,比如我放在创建好的spidertest
文件夹中:
cd /Users/pangyuxuan/spidertest # 这是文件夹路径
使用命令创建项目:
scrapy startproject [项目名称]
创建爬虫:
cd [项目名称] # 先进入项目路径
scrapy genspider [爬虫名称] [目标域名] # 再创建爬虫文件
至此你已经创建好了scrapy爬虫文件,它应该长这样:
其中[项目名称]
为gsw_test
,[爬虫名称]
为gsw_spider
综上,创建一个基本的scrapy爬虫文件,一共在终端的命令行中输入了4行代码:
cd /Users/pangyuxuan/spidertest # 打开一个文件路径,作为爬虫的存放路径
scrapy startproject gsw_test # 创建scrapy项目,名为gsw_test
cd gsw_test # 打开项目路径
scrapy genspider gsw_spider https://www.gushiwen.org
# 创建scrapy爬虫,爬虫名为gsw_spider,目标域名为 https://www.gushiwen.org
各个文件的作用
后续的编写还是依赖pycharm,所以在pycharm中打开项目文件:
其中各个文件的作用如下:
-
settings.py
:用来配置爬虫的,比如设置User-Agent
、下载延时、ip代理。 -
middlewares.py
:用来定义中间件。 -
items.py
:用来提前定义好需要下载的数据字段。 -
pipelines.py
:用来保存数据。 -
scrapy.cfg
:用来配置项目。
爬取第一页的内容并保存
以下内容请按顺序阅读并实现
设置settings.py
先在settings.py
中做两项工作:
- 设置
robots.txt
协议为“不遵守”
robots.txt
是一个互联网爬虫许可协议,默认是True
(遵守协议),如果遵守的话大部分网站都无法进行爬取,所以先把这个协议的状态设为不遵守
# Obey robots.txt rules ROBOTSTXT_OBEY = False
ps. 所以这个协议的意义是什么。。。 - 配置请求头(设置
user-agent
)
# Override the default request headers: DEFAULT_REQUEST_HEADERS = Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8, Accept-Language: en, user-agent : 我自己的ua
主要工作——编写gsw_spider.py
import scrapy
class GswSpiderSpider(scrapy.Spider): # 我们的代码都写在这个类里面
name = gsw_spider # 爬虫的名字
allowed_domains = [https://www.gushiwen.org] # 目标域名
start_urls = [http://https://www.gushiwen.org/] # 爬虫的起始网页
def parse(self, response):
目前的爬虫起始网页start_urls
是自动生成的,我们把它换成古诗文网的第一页
start_urls = [https://www.gushiwen.cn/default_1.aspx]
为了使打印出来的结果更加直观,我们编写myprint
函数如下:
def myprint(self,value):
print("="*30) # 在输出内容的上、下加一些=,找起来方便
print(value)
print("="*30)
然后我们尝试打印一下当前爬取到的内容,应该为古诗文网第一页的信息。
目前为止,gsw_spider.py
被改成了这样:
import scrapy
class GswSpiderSpider(scrapy.Spider):
name = gsw_spider # 爬虫的名字
allowed_domains = [https://www.gushiwen.org] # 目标域名
start_urls = [https://www.gushiwen.cn/default_1.aspx] # 起始页面
def myprint(self,value):
print("="*30)
print(value)
print("="*30)
def parse(self, response):
self.myprint(response.text) # 打印网页源代码
运行的方法
scrapy爬虫需要在终端里输入命令来运行,输入命令如下:
scrapy crawl gsw_spider # gsw_spider是爬虫名
方便起见,我们在项目目录里新建一个start.py
,通过cmdline
库里的函数来向终端发送命令,这样就不用不停地切换窗口了,而且运行结果可以在pycharm里直接展现,这样就与我们之前学的爬虫一样了。
后续我们无论修改哪个代码,都是运行start.py
这个文件。
from scrapy import cmdline
cmds = [scrapy,crawl,gsw_spider] # 拼接命令语句
cmdline.execute(cmds) # 执行
点击运行,可以在运行窗口中看到结果:
截至目前为止,我们已经获取了网页源代码,接下来的工作就是从源代码中解析想要的数据了。
无需导入新的库,Scrapy框架为我们内置了许多函数,使我们仍可以用之前学习的数据解析知识(xpath、bs4和正则表达式)来完成数据提取。
使用xpath提取数据
使用xpath语法提取数据,返回的结果是选择器列表类型SelectorList
,选择器列表里包含很多选择器Selector
,即:
-
response.xpath
返回的是SelectorList
对象 -
SelectorList
存储的是Selector
对象
我们获取一下所有包含古诗标题的标签,输出返回值类型,以验证上面的结论:
def parse(self, response):
gsw_divs = response.xpath("//div[@class=left]/div[@class=sons]")
self.myprint(type(gsw_divs)) # 打印获取到的div标签集的类型
for gsw_div in gsw_divs :
self.myprint(type(gsw_div)) # 打印标签集中的每个元素的类型
运行结果:
使用get()
或getall()
函数从选择器类型的数据中提取需要的数据:
-
get()
返回选择器的第一个值(字符串类型) -
getall()
返回选择器的所有值(列表类型)
for gsw_div in gsw_divs :
title_get = gsw_div.xpath(".//b/text()").get()
title_getall = gsw_div.xpath(".//b/text()").getall()
self.myprint(title_get) # 打印get函数的结果
self.myprint(title_getall) # 打印getall函数的结果
输出:
我们共提取标题、朝代、作者、内容四部分信息,gsw_spider.py
代码如下:
import scrapy
class GswSpiderSpider(scrapy.Spider):
name = gsw_spider # 爬虫的名字
allowed_domains = [https://www.gushiwen.org] # 目标域名
start_urls = [https://www.gushiwen.cn/default_1.aspx] # 起始页面
def myprint(self,value): # 用于打印的函数
print("="*30)
print(value)
print("="*30)
def parse(self, response):
gsw_divs = response.xpath("//div[@class=left]/div[@class=sons]")
for gsw_div in gsw_divs :
title = gsw_div.xpath(".//b/text()").get() # 题目
source = gsw_div.xpath(".//p[@class=source]/a/text()").getall() # 朝代+作者
# source是getall函数的返回值,是个列表,故可以直接用下标调用
dynasty = source[0] # 朝代
writer = source[1] # 作者
self.myprint(source)
content = gsw_div.xpath(".//div[@class=contson]//text()").getall() # 诗文内容
# 用//text()获取标签下的所有文本
content = .join(content).strip() # 将列表拼接,并用strip()删除前后的换行/空格
你可以在任意地方插入self.myprint(内容)
来进行打印,以验证数据是否被成功提取
接下来就是保存数据,我们先在items.py
中配置好要保存的数据有哪些。
配置items.py
还记得这个文件是干什么用的吗?
items.py
:用来提前定义好需要下载的数据字段。
一共有上述四部分内容需要保存,因此我们的items.py
应该这样写:
import scrapy
class GswTestItem(scrapy.Item):
title = scrapy.Field() # 标题
dynasty = scrapy.Field() # 朝代
writer = scrapy.Field() # 作者
content = scrapy.Field() # 内容
其中Field()
可以理解为一种普适的变量类型,不管是字符串还是列表,都用scrapy.Field()
来接收。
在gsw_spider.py
里导入items
定义完items.py
后,我们在gsw_spiders.py
里导入它。需要注意的是,gsw_spiders.py
在spiders
文件夹里,也就是说items.py
在gsw_spiders.py
的上层目录中:
因此导入时,应该这样写:
from ..items import GswTestItem # ..表示上层目录
导入后,我们将对应参数传入,然后使用yield
关键字进行返回
item = GswTestItem(title=title,dynasty=dynasty,writer=writer,content=content)
yield item
进入pipelines.py
和settings.py
先在settings.py
里把pipelines.py
打开:
# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES =
gsw_test.pipelines.GswTestPipeline: 300,
# 300是这个pipeline的优先级,代表了执行顺序,数值越小优先级越大
再编写pipelines.py
from itemadapter import ItemAdapter
import json # 记得自己导入json库
class GswTestPipeline:
def open_spider(self,spider):
self.fp = open("古诗文.txt",w,encoding=utf-8) # 制定文件名和编码格式
def process_item(self, item, spider):
self.fp.write(json.dumps(dict(item),ensure_ascii=False)+\\n)
# dict函数将item转化为字典
# json.dumps()将字典格式的item转换为json字段
# 参数ensure_ascii=False,用于存储中文
# +\\n用于将保存的内容自动换行
return item
def close_spider(self,spider): # 关闭文件
self.fp.close()
上面的open_spider
函数和close_spider
函数虽然不是自带的,但它是一种模版化的函数(套路),是一种Scrapy框架提供的高效的文件存储形式。
我们自己写的时候,只要按上述样式编(默)写即可,根据自己的需求修改存储文件的文件名、格式和编码方式,但不能改变两个函数名!
现在我们运行start.py
,就会发现路径下多了一个古诗文.txt
,打开以后是这样:
至此,第一页爬取成功!(不要在意为什么只爬了一点就结束了,先往下看,最后会有修正)
爬取后续内容
爬取了第一页的内容以后,我们还需要继续往后寻找,先来找一下第二页的url:
右键检查“下一页”按钮以获取下一页的url
为了测试寻找下一页的功能,我们暂时忽略之前的代码
def parse(self, response):
next_href = response.xpath("//a[@id=amore]/@href").get() # 获取href属性
next_url = response.urljoin(next_href) # 给/default_2.aspx添加前缀域名使其变完整
self.myprint(next_url) # 输出以验证
找到了!
接下来我们就用一个request
来接收scrapy.Request(next_url)
的返回值,并使用yield
关键字来返回即可:
next_href = response.xpath("//a[@id=amore]/@href").get()
next_url = response.urljoin(next_href)
request = scrapy.Request(next_url)
yield request
需要注意的是,我们需要给“寻找下一页”操作设立一个终止条件,当下一页不存在的时候停止访问,所以最后的代码长这个样子:
# 获取下一页
next_href = response.xpath("//a[@id=amore]/@href").get()
if next_href:
next_url = response.urljoin(next_href)
request = scrapy.Request(next_url)
yield request
针对反爬虫机制的修改完善
此时我们的代码是这样的:
gsw_spider.py
import scrapy
from ..items import GswTestItem
class GswSpiderSpider(scrapy.Spider):
name = gsw_spider # 爬虫的名字
allowed_domains = [https://www.gushiwen.org] # 目标域名
start_urls = [https://www.gushiwen.cn/default_1.aspx]
def myprint(self,value):
print("="*30)
print(value)
print("="*30)
def parse(self, response):
gsw_divs = response.xpath("//div[@class=left]/div[@class=sons]")
for gsw_div in gsw_divs :
title = gsw_div.xpath(".//b/text()").get() # 古诗题目
source = gsw_div.xpath(".//p[@class=source]/a/text()").getall() # 朝代+作者
# source是getall函数的返回值,是个列表,直接用下标调用
dynasty = source[0] # 朝代
writer = source[1] # 作者
content = gsw_div.xpath(".//div[@class=contson]//text()").getall() # 诗文内容
# 用//text()获取标签下的所有文本
content = .join(content).strip() # 将列表拼接,并用strip()删除前后的换行/空格
item = GswTestItem(title=title,dynasty=dynasty,writer=writer,content=content)
yield item
# 获取下一页
next_href = response.xpath("//a[@id=amore]/@href").get()
if next_href:
next_url = response.urljoin(next_href)
request = scrapy.Request(next_url)
yield request
运行后,会报这样一个错误:IndexError: list index out of range
,意思是“列表的下标索引超过最大区间”。
为什么会有这样的错误呢?
我们可以在网页上看到,页面上不全是古诗文:
除了古诗文外,这种短句子也是在class=sons
的标签下,按照我们的查找方式:
gsw_divs = response.xpath("//div[@class=left]/div[@class=sons]")
for gsw_div in gsw_divs :
source = gsw_div.xpath(".//p[@class=source]/a/text()").getall()
找到图中蓝色的div
标签以后,它里面是没有p
标签的,也就是说此时的source
是个空表,直接调用source[0]
那必然是要报错的。
这算是网站的一种反爬虫机制,利用格式不完全相同的网页结构来让你的爬虫报错,太狠了!!
为了解决这个问题,我们添加try...except
结构如下:
for gsw_div in gsw_divs :
title = gsw_div.xpath(".//b/text()").get()
source = gsw_div.xpath(".//p[@class=source]/a/text()").getall()
try:
dynasty = source[0]
writer = source[1]
content = gsw_div.xpath(".//div[@class=contson]//text()").getall()
content = .join(content).strip()
item = GswTestItem(title=title,dynasty=dynasty,writer=writer,content=content)
yield item
except:
print(title) # 打印出错的标题以备检查
这样,上面的报错就被完美解决了。
然鹅,一波未平一波又起,bug永远是生生不息源源不绝的
我们发现了一个新的报错:DEBUG: Filtered offsite request to www.gushiwen.cn: <GET https://www.gushiwen.cn/default_2.aspx>
这是因为我们在最开始的allowed_domains
里限制了访问的域名:“https://www.gushiwen.org”
而到了第二页的时候,网站偷偷把域名换成.cn
了!
.cn
不是.org
,我们的爬虫没法继续访问,所以就停了。这又是这个网站的一个反爬虫机制,我们只需要在allowed_domains
里添加一个.cn
的域名,这个问题就可以得到妥善的解决:
allowed_domains = [gushiwen.org,gushiwen.cn]
运行可得到期望结果:
最终的古诗文网Scrapy爬虫代码
gsw_spider.py
import scrapy
from ..items import GswTestItem
class GswSpiderSpider(scrapy.Spider):
name = gsw_spider # 爬虫的名字
# allowed_domains = [https://www.gushiwen.org] # 目标域名
allowed_domains = [gushiwen.org,gushiwen.cn]
start_urls = [https://www.gushiwen.cn/default_1.aspx]
def myprint(self,value):
print("="*30)
print(value)
print("="*30)
def parse(self, response):
gsw_divs = response.xpath("//div[@class=left]/div[@class=sons]")
for gsw_div in gsw_divs :
title = gsw_div.xpath(".//b/text()").get() # 古诗题目
source = gsw_div.xpath(".//p[@class=source]/a/text()").getall() # 朝代+作者
# source是getall函数的返回值,是个列表,直接用下标调用
try:
dynasty = source[0] # 朝代
writer = source[1] # 作者
content = gsw_div.xpath(".//div[@class=contson]//text()").getall() # 诗文内容
# 用//text()获取标签下的所有文本
content = .join(content).strip() # 将列表拼接,并用strip()删除前后的换行/空格
item = GswTestItem(title=title,dynasty=dynasty,writer=writer,content=content)
yield item
except:
print(title)
# 获取下一页
next_href = response.xpath("//a[@id=amore]/@href").get()
if next_href:
next_url = response.urljoin(next_href)
request = scrapy.Request(next_url)
yield request
items.py
# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html
import scrapy
class GswTestItem(scrapy.Item):
# define the fields for your item here like:
title = scrapy.Field()
dynasty = scrapy.Field()
writer = scrapy.Field()
content = scrapy.Field()
pipelines.py
from itemadapter import ItemAdapter
import json
class GswTestPipeline:
def open_spider(self,spider):
self.fp = open("古诗文.txt",w,encoding=utf-8)
def process_item(self, item, spider):
self.fp.write(json.dumps(dict(item),ensure_ascii=False)+\\n) # dict函数将item转化为字典,再转换为json字段进行保存
return item
def close_spider(self,spider):
self.fp.close()
settings.py
为了看起来简洁一点,注释部分我就都删了
BOT_NAME = gsw_test
SPIDER_MODULES = [gsw_test.spiders]
NEWSPIDER_MODULE = gsw_test.spiders
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
# Override the default request headers:
DEFAULT_REQUEST_HEADERS =
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,
Accept-Language: en,
user-agent : 我的user-agent
# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES =
gsw_test.pipelines.GswTestPipeline: 300,
大功告成!
以上是关于Scrapy框架爬虫实战——从入门到放弃01的主要内容,如果未能解决你的问题,请参考以下文章
Python爬虫从入门到放弃之 Scrapy框架整体的一个了解
Python爬虫从入门到放弃 之 Scrapy框架中Download Middleware用法
Python爬虫从入门到放弃(十七)之 Scrapy框架中Download Middleware用法
Python爬虫从入门到放弃(十七)之 Scrapy框架中Download Middleware用法