和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项目大体的步骤如下,在后面实战项目中也有所体现:
- 新建爬虫项目
- 分析网页确定爬取的内容,修改items.py
- 编写spider爬虫,准备开始爬取网页了
- 编写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