我与Scrapy的初次相识,理论+实战入门Scrapy

和Scrapy接触不久,做一个项目学习并记录一下,这个代码倒是写了有段时间了,一直没来写博客,这爬虫集合的更新也耽误好久了。随着疫情的好转,我这也恢复正常写博文(糊脸,疫情不是自己不写博文的理由),大家一起加油呀,加油加油,一起都已经好起来了。
实战项目是爬取简书网(https://www.jianshu.com/) 二级页面信息的Scrapy项目,这也就个入门,大佬看见了一定请指点一下。

一、我对Scrapy的一些浅显的理解

Scrapy就是个爬虫框架,它像个房子的钢筋混凝土框架一样,使得我们可以在这个框架里自由发挥,我们只需要在该安装“门”的地方装上“门”(做填空一样),接下来就可以打开爬虫的大门了,十分的便捷。

我这些浅显的话还是要搭一些科普,不然就太没营养了,我从百*百科cv大法了这个图,下面就我的理解解释一下这图中五大部件:Scrapy Engine(引擎)、Scheduler(调度器)、Downloader(下载器)、Spider(爬虫)、Item Pipeline(管道),及两个中间件:Downloader Middlewares(下载中间件)、Spider Middlewares(爬虫中间件)。
在这里插入图片描述

1.1、五大部件

Scrapy Engine(引擎):顾名思义,看到引擎,我就想到了汽车的发动机,这肯定是一个十分十分重要的东西。引擎关联着其他的所有部件,从图里也看出,引擎起到一个信号塔的作用,传递着各部件之间的信息、数据等。

Scheduler(调度器):调度器调度器,调度两个字尤其突出,那我们可以想到调度些什么呢?调度爬虫下载的请求,它会将从引擎传来的请求加入队列当中,当引擎需要的时候再给回引擎。

Downloader(下载器):下载器,啊哈,简单,就是下载网页嘛,不就是爬虫了嘛,错错错,这个下载器不是爬虫,Scrapy分工十分明确的。下载器是按照引擎给的网页请求,下载网页的内容然后返回给引擎,由引擎交给爬虫,它就专门下载网页,也是后面Spider代码里面的response的由来。

Spider(爬虫):这里就比较熟悉了,自己编写爬虫逻辑,进行网页的解析跳转等等操作,我们使用Scrapy绝大功夫都在这里了。

Item Pipeline(管道):这个啊,就是对Spider获取到的数据进行一些处理,包括(清洗,过滤,储存等等)。

1.2、两个中间件

Downloader Middlewares(下载中间件):可以在这里设置下载的请求头,下载的时间间隔,代理等等操作,在一定程度上使Spider更加纯粹了。

Spider Middlewares(爬虫中间件):看图它位于引擎和spider之间,而经由这条线路上的是request请求和response请求的返回还有items爬取结果(就那三根绿线),这个中间件意味可以自己定义扩展request、response和items。

1.3、项目简说

前面讲的五大部件,两个中间件都太理论了,感觉Scrapy离我们还是有点远,下面就简单讲一下项目的知识,后面还有实战。

1、先要安装呢,我这里不介绍普通python安装Scrapy,太复杂了,要安装好多库,我用的是Anaconda,特别方便,在终端cmd输入conda install scrapy就行,还是有不明白的可以参考Anaconda按照Scrapy

2、安装好后,输入这条指令,会在你输入指令的那个目录下生成自定义项目名的Scrapy项目。

scrapy startproject 项目名

比如这是我在testScrapy目录下执行了scrapy startproject study后指定生成的目录,没有任何改动下,我们就是这么多文件。
在这里插入图片描述
3、而且它会提示你cd到项目目录下,输入指令生成一个spider文件。
在这里插入图片描述
这里我解释一下genspider后接的参数,第一个就是spider的名字,第二个是该爬虫的限定网域,以保证不会爬到别的网页去。

scrapy genspider test www.baidu.com

4、到上面我们这个框架算是完备了,看一下完整的目录。
(这是我的理解,有错误望指点一下)我们要需要修改的也就是test.py(Spider)、item.py、pipelines.py、settings.py。
在这里插入图片描述
而我们完成Scrapy项目大体的步骤如下,在后面实战项目中也有所体现:

  1. 新建爬虫项目
  2. 分析网页确定爬取的内容,修改items.py
  3. 编写spider爬虫,准备开始爬取网页了
  4. 编写pipelines.py,看看数据怎么处理(Scrapy也自带了数据处理)

好了,理论大概都讲完了,下面来个项目感觉一下,在项目里继续学习基础知识。

二、Scrapy简书网项目

1、思路步骤

生成Scrapy项目

scrapy startproject jianshu

生成Spider

scrapy genspider jianshuwang jianshu.com

在这里插入图片描述

1.1、分析网页

打开开发者工具看看,这是第一页的url。
在这里插入图片描述
因为这个网页没有具体的分页,滑动侧边进度条刷新出了新的内容且网页也没变,所以它是个异步加载,去看一下XHR文件。
在这里插入图片描述
我这复制下来了,方便大家看,可以看出它是由多个seen_snote_ids%5B%5D和一个page参数拼接成的,也就是说后面的网页都可以这样来请求,可是seen_snote_ids%5B%5D这个哪来的呢?
https://www.jianshu.com/?seen_snote_ids%5B%5D=60076928&seen_snote_ids%5B%5D=63913898&seen_snote_ids%5B%5D=59873546&seen_snote_ids%5B%5D=63405402&seen_snote_ids%5B%5D=59679766&seen_snote_ids%5B%5D=60717313&seen_snote_ids%5B%5D=62921759&page=2
我们随机取了其中一个全局查找了一下,发现这个id就是文章的data-note-id,ok了,这个问题解决了。
在这里插入图片描述

1.2、解析网页

我本来是想爬这些信息的,但考虑到meta的参数还是比较常用的,想分享给大家,于是改爬取详情页的信息(后面那张截图)。
在这里插入图片描述
在这里插入图片描述

1.3、编写item.py

这个文件会给你一个默认的模板,很方便,跟着修改就行,下面是我们需要的信息。

from scrapy import Item, Field

class JianshuItem(Item):
    title_url = Field()  # 标题链接
    title = Field()  # 标题

    author = Field()  # 作者 
    author_url = Field()  # 作者主页链接

    content = Field()  # 文章内容
    like = Field()  # 点赞数

    # time = Field()  # 发布时间
    # word_number = Field()  # 文章字数
    # reading_volume = Field()  # 阅读量

1.4、编写Spider

我们先获取每个文章的url,分析网页可以发现每个文章都是在一个个的li标签内,那先把li解析出来,在Scrapy中,是直接利用xpath解析response,这个response也就是下载器下载下来的网页(之前有提到,大家没忘记吧)。

这里也可以直接用xpath解析文章的url然后用extract()返回一个url的列表,能够理解的可以自己改改,这里为了演示的易懂些。

item = JianshuItem()

li_xpath = '//li'
for li in response.xpath(li_xpath):
     # 标题链接
     item['title_url'] = 'https://www.jianshu.com' + li.xpath('div/a/@href').extract_first()

这里要讲到这个xpath后面的extract_first(),xpath的语法是没有改变的,可是最后面要跟上extract_first()或者extract(),extract_first() 它会返回xpath解析出的第一个内容,
extract() 会返回一个列表,看具体需求。

那我们有了url,就可以爬取详情页了,这里涉及的知识感觉还是蛮多的,上个代码看看。

item['title_url'] = 'https://www.jianshu.com' + li.xpath('div/a/@href').extract_first()
yield scrapy.Request(url=item['title_url'], headers=self.headers, meta={'item': copy.deepcopy(item)}, callback=self.donwnload_content)

哐哐,敲黑板,重点来了,记笔记,一个个来。
scrapy.Request,可以使得你在一个Reuqest里调用另一个Request,这里和我们以前写的普通爬虫爬取二级页面的原理感觉差不多。url这个参数是必须的,而callback是回调一个函数,就是说这个新的Request将由这个回调函数执行(不要加括号,会报错的)。

meta,这个参数可以以字典的形式将里面的数据等传递给别的Request请求中,我这里是在一级界面的Request请求中获取了文章的url放在item里,然后通过meta这个参数将item传到爬取二级页面的Request请求函数中,最后将获取完所有数据的item一起返回。

这里出现了一个问题:item传过去后,值总是一个,后面知道了是因为使用 Request 函数传递 item 时,使用的是浅复制(对象的字段值被复制时,字段引用的对象不会被复制,所以这里导入copy模块,使用深拷贝copy.deepcopy

yield,可以把它当作一个return,只不过它是一个迭代生成器,每次返回一次,return结束后是直接结束的,而yield结束后会接上之前的代码,可以参考这篇python中yield的用法详解——最简单,最清晰的解释,收益匪浅。


接着着思路来,这里要讲donwnload_content(),获取二级页面的函数。
这里没什么,大体都是解析网页,值得吐槽的是使用xpath无法获取到作者名还有它的链接等信息,我只好用正则匹配返回回来的网页了。

 # 获取详情页信息
def donwnload_content(self, response):
    # 接送传来的item
    item = response.meta['item']
    
    # 获取返回的html
    html = response.body.decode('utf-8')
    # 标题
    item['title'] = response.xpath('//*[@id="__next"]/div[1]/div/div/section[1]/h1/text()').extract_first()

    # 作者主页链接
    item['author_url'] = 'https://www.jianshu.com' + re.findall('<a class="qzhJKO" href="(.*?)"><span', html, re.S)[0]

    # 作者名
    item['author'] = re.findall('<span class="_22gUMi">(.*?)</span></a>', html, re.S)[0]

    # 文章内容
    content = re.findall('<article class="_2rhmJa">(.*?)</article>', html, re.S)[0]
    item['content'] = re.sub('<.*?>', '', re.sub('</p>', '\n', content))

    # 点赞数
    like = re.findall('aria-label="查看点赞列表">(.*?)</span', html, re.S)[0]
    item['like'] = re.sub('<!-- -->|,', '', like)

    # 返回item,这里会结束这个函数,接回到之前调用这个函数的地方,回忆一下之前调用这个函数使用的是yield。
    yield item

等for循环结束,这里爬取完一页的了所有文章的信息了,接着要翻页了。

解析出那些文章的id,获取id作为下一页URL的参数。

data_note_id = response.xpath('//li/@data-note-id').extract()

拼接上url。

for data_id in data_note_id:
       url += 'seen_snote_ids%5B%5D={}&'.format(data_id)
       url += 'page={}'.format(self.page)

有了url后就是使用Request方法回调parse函数,这里本来是可以自动根据start_urls这个url列表爬取的,而我们进行修改了,所以还是使用yield的方法进行操作。

# 测试就爬取五页,自行修改
if self.page < 5:
      yield scrapy.Request(url=url, headers=self.headers, callback=self.parse)

至此,爬虫项目应该就结束了,我们在终端输入以下指令(依旧是要在这个Scrapy项目的目录下)使用默认的数据存储,将数据存到csv文件中。

scrapy crawl jianshuwang -o jianshu.csv

但是我们查看后发现爬取的文章有重复,按照想法,url中的id参数就可以避免重复了,看来是天真了。
查阅资料,要加上cookie值能解决这个问题,所以我这里使用了start_request(),自定义请求头,也可以去settings文件里设置。

start_request()是Spider的默认函数,初始会自动调用,可以用于一些网站一进去就要登入的操作,使用这个这个函数配合FormRequest实现表单登入。

def start_requests(self):
     for url in self.start_urls:
         yield scrapy.Request(url, headers=self.headers, callback=self.parse)

好了,啥问题都解决了,可以正常爬取了。

1.5、编写pipelines.py

接下来就是对数据的存储了,这里使用pipeline管道,我们对数据存储到MongoDB中,一通百通,学会一种其他的就可以模仿着来了。

import pymongo


class JianshuPipeline(object):
    def __init__(self):
        cilent = pymongo.MongoClient('localhost', 27017)
        mydb = cilent['mydb']
        self.post = mydb['jianshuwang']
        
    def process_item(self, item, spider):
        info = dict(item)
        self.post.insert(info)
        return item

此时还应该在settings文件中指定pipeline管理。

ITEM_PIPELINES = {
   'jianshu.pipelines.JianshuPipeline': 300,
}

接下来就正常的执行,在终端里输入。

scrapy crawl jianshuwang

看看下面的截图,是不是很兴奋呢,我每每看到爬取成功的结果,都十分开心呢,你们也快动手做起来吧。
在这里插入图片描述

看完应该可以写些简单的Scrapy的爬虫了,讲的不好,还请多多指正。大家一起加油呀,有问题的话,大家在评论区里留言或者私信我都行呀,没问题也可以唠唠嗑哈。

2、项目代码

Item

最后面注释的三个没有爬到,是个遗憾。scrapy的xpath解析不到,re正则表达式也匹配不到,看了请求回来的网页里没有这三个,不知道靠什么加载的,异步加载的XHR文件也没有这个数据,下次可以尝试seleinum来爬取。

from scrapy import Item, Field

class JianshuItem(Item):
    title_url = Field()  # 标题链接
    title = Field()  # 标题

    author = Field()  # 作者
    author_url = Field()  # 作者主页链接

    content = Field()  # 文章内容
    like = Field()  # 点赞数

    # time = Field()  # 发布时间
    # word_number = Field()  # 文章字数
    # reading_volume = Field()  # 阅读量

Pipelines

import pymongo


class JianshuPipeline(object):
    def __init__(self):
        cilent = pymongo.MongoClient('localhost', 27017)
        mydb = cilent['mydb']
        self.post = mydb['jianshuwang']
        
    def process_item(self, item, spider):
        info = dict(item)
        self.post.insert(info)
        return item

settings

它默认了好多设置,我讲几个我这改过的。
在这里插入图片描述

# 控制台的打印等级,默认的会输出好多东西
# CRITICAL - 严重错误(critical)
# ERROR - 一般错误(regular errors)
# WARNING - 警告信息(warning messages)
# INFO - 一般信息(informational messages)
# DEBUG - 调试信息(debugging messages)
LOG_LEVEL = 'WARNING'

# 睡眠时间 也就是爬取的时间间隔,我就爬取了几页,不会对网站的服务器产生什么压力,如果是大量的爬取建议这里写大一点,开个玩笑,要是一不小心把别人网站搞崩了,小心会有免费衣服和食物还会给配一个亮闪闪的大链子。
DOWNLOAD_DELAY = 0.5

# 设置爬取时规避robots协议
ROBOTSTXT_OBEY = False

# 设置管道管理,如果存在多个管道,后面的数值可以区分先后进行
ITEM_PIPELINES = {
   'jianshu.pipelines.JianshuPipeline': 300,
}

# 下面这几个 我在这个项目里没有在settings文件里设置。
# 可以设置user-agent,也就是模拟浏览器或者其他什么的
USER_AGENT = 'jianshu (+http://www.yourdomain.com)'

# 设置请求头
DEFAULT_REQUEST_HEADERS = {
   'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
   'Accept-Language': 'en',
}

Spider

# -*- coding: utf-8 -*-
import scrapy
import re
import copy

from jianshu.items import JianshuItem


class JianshuwangSpider(scrapy.Spider):
    name = 'jianshuwang'
    allowed_domains = ['jianshu.com']
    start_urls = ['https://www.jianshu.com/']
    page = 1
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
        "X-INFINITESCROLL": "true",
        "X-Requested-With": "XMLHttpRequest",
        "Cookie": "这里放自己的Cookie,可以是不登入简书账号的cookie值"
    }

    def start_requests(self):
        for url in self.start_urls:
            yield scrapy.Request(url, headers=self.headers, callback=self.parse)

    def parse(self, response):
        # 获取信息
        item = JianshuItem()

        li_xpath = '//li'
        for li in response.xpath(li_xpath):
            # 标题链接
            item['title_url'] = 'https://www.jianshu.com' + li.xpath('div/a/@href').extract_first()
            yield scrapy.Request(url=item['title_url'], headers=self.headers, meta={'item': copy.deepcopy(item)}, callback=self.donwnload_content)

	    # 获取数据的id作为下一页URL的参数
        data_note_id = response.xpath('//li/@data-note-id').extract()
        
        self.page += 1

        url = 'https://www.jianshu.com/?'

        for data_id in data_note_id:
            url += 'seen_snote_ids%5B%5D={}&'.format(data_id)
        url += 'page={}'.format(self.page)

        if self.page < 5:
            yield scrapy.Request(url=url, headers=self.headers, callback=self.parse)

    # 获取详情页信息
    def donwnload_content(self, response):
        # 接送传来的item
        item = response.meta['item']
        
        html = response.body.decode('utf-8')
        # 标题
        item['title'] = response.xpath('//*[@id="__next"]/div[1]/div/div/section[1]/h1/text()').extract_first()

        # 作者主页链接
        item['author_url'] = 'https://www.jianshu.com' + re.findall('<a class="qzhJKO" href="(.*?)"><span', html, re.S)[0]

        # 作者名
        item['author'] = re.findall('<span class="_22gUMi">(.*?)</span></a>', html, re.S)[0]

        # 文章内容
        content = re.findall('<article class="_2rhmJa">(.*?)</article>', html, re.S)[0]
        item['content'] = re.sub('<.*?>', '', re.sub('</p>', '\n', content))

        # 点赞数
        like = re.findall('aria-label="查看点赞列表">(.*?)</span', html, re.S)[0]
        item['like'] = re.sub('<!-- -->|,', '', like)

		# 返回item
        yield item
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章