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语法提取数据,返回的结果是选择器列表类型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':
这是因为我们在最开始的allowed_domains
里限制了访问的域名:“https://www.gushiwen.org”
而到了第二页的时候,网站偷偷把域名换成.cn
了!
.cn
不是.org
,我们的爬虫没法继续访问,所以就停了。这又是这个网站的一个反爬虫机制,我们只需要在allowed_domains
里添加一个.cn
的域名,这个问题就可以得到妥善的解决:
allowed_domains = ['gushiwen.org','gushiwen.cn']
运行可得到期望结果:
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,
}
大功告成!