Scrapy框架爬虫实战——从入门到放弃01

Scrapy框架爬虫实战01——经常被爬的古诗文网

ps. 案例制作时的操作环境是MacOS,如果是windows用户,下文中提到的“终端”指的就是cmd命令行窗口。

pps. 本文省略了安装过程,尚未安装scrapy的用户可以直接在pycharm的preference内搜索安装。

文章目录

  • Scrapy框架爬虫实战01——经常被爬的古诗文网
    • 项目创建
      • 各个文件的作用
    • 爬取第一页的内容并保存
      • 设置`settings.py`
      • 主要工作——编写`gsw_spider.py`
        • 运行的方法
        • 使用xpath提取数据
      • 配置`items.py`
      • 在`gsw_spider.py`里导入`items`
      • 进入`pipelines.py`和`settings.py`
    • 爬取后续内容
    • 针对反爬虫机制的修改完善
    • 最终的古诗文网Scrapy爬虫代码
      • `gsw_spider.py`
      • `items.py`
      • `pipelines.py`
      • `settings.py`

目标网站:传送门

任务:使用Scrapy框架爬虫,爬取“推荐”中共10页的古诗题目、作者、朝代和内容

ps. 各类教程都拿它举例子,古诗文网好惨

项目创建

创建Scrapy爬虫项目需要在终端中进行

先打开一个文件路径,即你希望的爬虫文件存放路径,比如我放在创建好的spidertest 文件夹中:

Scrapy框架爬虫实战——从入门到放弃01_第1张图片

cd /Users/pangyuxuan/spidertest # 这是文件夹路径

使用命令创建项目:

scrapy startproject [项目名称]

创建爬虫:

cd [项目名称] # 先进入项目路径
scrapy genspider [爬虫名称] [目标域名] # 再创建爬虫文件

至此你已经创建好了scrapy爬虫文件,它应该长这样:

Scrapy框架爬虫实战——从入门到放弃01_第2张图片

其中[项目名称]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中打开项目文件:

Scrapy框架爬虫实战——从入门到放弃01_第3张图片

其中各个文件的作用如下:

  1. settings.py:用来配置爬虫的,比如设置User-Agent、下载延时、ip代理。
  2. middlewares.py:用来定义中间件。
  3. items.py:用来提前定义好需要下载的数据字段
  4. pipelines.py:用来保存数据
  5. scrapy.cfg:用来配置项目

爬取第一页的内容并保存

以下内容请按顺序阅读并实现

设置settings.py

先在settings.py中做两项工作:

  1. 设置robots.txt协议为“不遵守”

    robots.txt是一个互联网爬虫许可协议,默认是True(遵守协议),如果遵守的话大部分网站都无法进行爬取,所以先把这个协议的状态设为不遵守

    # Obey robots.txt rules
    ROBOTSTXT_OBEY = False
    

    ps. 所以这个协议的意义是什么。。。

  2. 配置请求头(设置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框架爬虫实战——从入门到放弃01_第4张图片

截至目前为止,我们已经获取了网页源代码,接下来的工作就是从源代码中解析想要的数据了。

无需导入新的库,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)) # 打印标签集中的每个元素的类型

运行结果:

Scrapy框架爬虫实战——从入门到放弃01_第5张图片

使用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函数的结果

输出:

Scrapy框架爬虫实战——从入门到放弃01_第6张图片

我们共提取标题、朝代、作者、内容四部分信息,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.pyspiders文件夹里,也就是说items.pygsw_spiders.py上层目录中:

Scrapy框架爬虫实战——从入门到放弃01_第7张图片

因此导入时,应该这样写:

from ..items import GswTestItem # ..表示上层目录

导入后,我们将对应参数传入,然后使用yield关键字进行返回

item = GswTestItem(title=title,dynasty=dynasty,writer=writer,content=content)
yield item

进入pipelines.pysettings.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,打开以后是这样:

Scrapy框架爬虫实战——从入门到放弃01_第8张图片

至此,第一页爬取成功!(不要在意为什么只爬了一点就结束了,先往下看,最后会有修正)


爬取后续内容

爬取了第一页的内容以后,我们还需要继续往后寻找,先来找一下第二页的url:

右键检查“下一页”按钮以获取下一页的url

Scrapy框架爬虫实战——从入门到放弃01_第9张图片

为了测试寻找下一页的功能,我们暂时忽略之前的代码

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) # 输出以验证

Scrapy框架爬虫实战——从入门到放弃01_第10张图片

找到了!

接下来我们就用一个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,意思是“列表的下标索引超过最大区间”。

为什么会有这样的错误呢?

我们可以在网页上看到,页面上不全是古诗文:

Scrapy框架爬虫实战——从入门到放弃01_第11张图片

除了古诗文外,这种短句子也是在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']

运行可得到期望结果:

Scrapy框架爬虫实战——从入门到放弃01_第12张图片

最终的古诗文网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,
}

大功告成!

你可能感兴趣的:(python,python,爬虫,scrapy)