跳到主要内容

7 篇博文 含有标签「Python」

查看所有标签

Python 操作 Redis

· 阅读需 2 分钟

Python 操作 Redis

Python 有很多的模块都可以实现对 Redis 的操作,常用有 redis 和 pyredis,这两个模块的使用操作是类似的。

这里我们使用 redis 模块来进行演示。

安装 redis 模块:

pip install redis

代码示例:

from redis import Redis
# redis链接
# redis = Redis(host="127.0.0.1",port=6379,db=1)
# 如果设置了密码
redis = Redis(host="127.0.0.1",port=6379,db=1,password="123456")
"""字符串操作"""
# 添加一个字符串数据 set name xiaoming
# redis.set("name","xiaoming")

# 添加一个临时数据, setex title 30 hello
# redis.setex("title",30,"hello")

# 查看一个数据的有效期,-2表示过期,-1表示永久
# time = redis.ttl("title")
# print(time)

# 获取一个字符串
# name = redis.get("name")
# print(name) # 得到的结果是bytes类型的 b'xiaoming'
# print(name.decode()) # 必须要解码,xiaoming

# 删除key,因为del是一个关键词,所以在redis模块,凡是命令如果是一个关键词,全部改成单词的全拼
# redis.delete("name")

# 哈希的操作
# dict1 = {
# "liubei": 28,
# "guanyu": 20,
# "zhangfei": 14,
# }
# redis.hmset("brother",dict1)

# 获取哈希里面的所有成员
dict_data = redis.hgetall("brother")
print(dict_data) # {b'liubei': b'28', b'guanyu': b'20', b'zhangfei': b'14'}
for key,name in dict_data.items():
print(key.decode(),name.decode())
"""
liubei 28
guanyu 20
zhangfei 14
"""
age = dict_data.get("liubei".encode()).decode()
print(age) # b'28'

【置顶】requests 常用方法

· 阅读需 12 分钟

【置顶】requests 常用方法

url = '要爬取的网址'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
}
response = requests(url=url, headers=headers)
response.encoding = 'utf-8'
page_text = response.text # 获取返回的解码后的字符串数据
json_data = response.json() # 获取返回的 json 解析后的数据
img_data = response.content # 获取二进制的返回内容

requests 模块初步使用

requests 是爬虫中一个基于网络请求的模块,安装方式:

pip install requests

不过如果你是用的是 Anaconda 环境,就不需要安装了,Anaconda 默认继承了 requests 模块。

requests 模块作用是模拟浏览器发起请求。

使用 requests 模块获取响应数据的代码编写流程:

  1. 指定 url
  2. 发起请求
  3. 获取响应数据(爬取到的页面源码数据)
  4. 持久化存储

实例:爬取搜狗首页的页面源码数据

import requests
# 1 指定url
url = 'https://www.sogou.com/'
# 2 发起请求get方法的返回值为响应对象
response = requests.get(url=url)
# 3 获取响应数据
#.text:返回的是字符串形式的响应数据
page_text = response.text
# 4 持久化存储
with open('./sougou.html', 'w', encoding='utf-8') as fp:
fp.write(page_text)

运行代码后,会在当前目录下新生成一个 sougo.html 文件。用浏览器打开后如下图所示,只有普通文本,样式都不见了。不过这没关系,因为爬虫往往只在意数据,不计较样式。

![img](/img/requests 模块应对 UA 检测和爬取动态网页/1.png)

参数动态化、UA 检测和 UA 伪装

光拿到搜狗的首页是没有用的 -- 这里什么都没有。如果我们想要拿到指定文本的搜索数据,该怎么办呢?

首先,要了解搜狗等搜索引擎的机制。一般情况下,搜索引擎的搜索请求都是 GET 请求。搜索的关键字放在路径中的查询字符串种传进去。搜狗也是如此。

比如,要搜索 jay,只需在浏览器中输入 https://www.sogou.com/web?query=jay 即可:

![img](/img/requests 模块应对 UA 检测和爬取动态网页/2.png)

要搜索的内容是什么,就把路径中的 jay 改成什么就好了。

现在,让我们来实现一个简易网页采集器,基于搜狗针对指定不同的关键字将其对应的页面数据进行爬取。

这个需求,就是实现参数动态化

如果请求的 url 携带参数,且我们想要将携带的参数进行动态化操作那么我们必须:

  1. 将携带的动态参数以键值对的形式封装到一个字典中
  2. 将该字典作用到 get 方法的 params 参数中即可
  3. 需要将原始携带参数的 url 中将携带的参数删除

写成代码就是:

keyword = input('enter a keyword:')

# 携带了请求参数的url,如果想要爬取不同关键字对应的页面,我们需要将url携带的参数进行动态化
# 实现参数动态化:
params = {
'query': keyword
}
url = 'https://www.sogou.com/web'
#params参数(字典):保存请求时url携带的参数
response = requests.get(url=url, params=params)

page_text = response.text
fileName = keyWord + '.html'
with open(fileName, 'w', encoding='utf-8') as fp:
fp.write(page_text)
print(fileName, '爬取完毕!!!')

比如输入 jay,打开新生成的文件,它长这个样子:

![img](/img/requests 模块应对 UA 检测和爬取动态网页/3.png)

我们发现上面建议的采集器代码会产生两个问题:

  1. 页面乱码了
  2. 页面中的数据明显太少了,我们丢失了数据

首先,让我们解决乱码问题。这很容易,只需要加一行代码,使用一个 encoding 命令即可实现:

keyword = input('enter a keyword: ')

# 携带了请求参数的url,如果想要爬取不同关键字对应的页面,我们需要将url携带的参数进行动态化
# 实现参数动态化:
params = {
'query': keyword
}
url = 'https://www.sogou.com/web'
# params参数(字典):保存请求时url携带的参数
response = requests.get(url=url, params=params)
# 修改响应数据的编码格式
# encoding返回的是响应数据的原始的编码格式,如果给其赋值则表示修改了响应数据的编码格式
response.encoding = 'utf-8'
page_text = response.text
fileName = keyWord + '.html'
with open(fileName, 'w', encoding='utf-8') as fp:
fp.write(page_text)
print(fileName, '爬取完毕!!!')

从结果来看,乱码问题是解决了。而且我们从中也了解到,数据量变少的原因:我们被搜狗的反爬策略限制了。

![img](/img/requests 模块应对 UA 检测和爬取动态网页/4.png)

处理乱码后,页面显示 异常访问请求 导致请求数据的缺失。这是因为网站后台已经检测出该次请求不是通过浏览器发起的请求而是通过爬虫程序发起的请求(不是通过浏览器发起的请求都是异常请求)。

网站的后台主要是通过查看请求的请求头中的 user-agent 判定请求是不是通过浏览器发起的。

什么是 User-Agent

  • 请求载体的身份标识,告诉服务器,使用的是什么工具(浏览器种类,操作系统类型,手机还是电脑,等)
  • 请求载体有且只有两种:
    • 浏览器
      • 浏览器的身份标识是统一固定,身份标识可以从抓包工具中获取。
    • 爬虫程序
      • 身份标识各自不同

这里就涉及到我们的第二种反爬机制,UA 检测:网站后台会检测请求对应的 User-Agent,以判定当前请求是否为异常请求。

UA 检测对应的反反爬策略是 UA 伪装:我们使用一个浏览器的 User-Agent,而不是爬虫的,去访问网页。

伪装流程:

  • 使用抓包工具捕获到某一个基于浏览器请求的 User-Agent 的值,将其伪装作用到一个字典中,将该字典作用到请求方法(get,post)的 headers 参数中即可。
  • 因为 UA 检测机制很多网站都会有,所以一般我们写爬虫代码的时候,都会加上 User-Agent 请求头,有备无患

使用代码表示就是:

keyword = input('enter a keyword:')
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
}
# 携带了请求参数的url,如果想要爬取不同关键字对应的页面,我们需要将url携带的参数进行动态化
# 实现参数动态化:
params = {
'query':keyword
}
url = 'https://www.sogou.com/web'
# params参数(字典):保存请求时url携带的参数
# 实现了UA伪装
response = requests.get(url=url, params=params, headers=headers)
# 修改响应数据的编码格式
# encoding返回的是响应数据的原始的编码格式,如果给其赋值则表示修改了响应数据的编码格式
response.encoding = 'utf-8'
page_text = response.text
fileName = keyWord + '.html'
with open(fileName, 'w', encoding='utf-8') as fp:
fp.write(page_text)
print(fileName, '爬取完毕!!!')

这样,我们就成功拿到搜索页面:

![img](/img/requests 模块应对 UA 检测和爬取动态网页/5.png)

爬取动态页面

现在我们要爬取豆瓣电影中的电影详情数据

url 地址:[https://movie.douban.com/typerank?type_name=%E5%8A%A8%E4%BD%9C&type=5&interval_id=100:90&action=](https://movie.douban.com/typerank?type_name=动作&type=5&interval_id=100:90&action=)

我们想要页面中的电影信息:

![img](/img/requests 模块应对 UA 检测和爬取动态网页/6.png)

如果我们还像之前那样,直接爬取这个网址,把代码写成这样:

url = 'https://movie.douban.com/typerank?type_name=%E5%8A%A8%E4%BD%9C&type=5&interval_id=100:90&action='
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
}
response = requests.get(url=url, headers=headers)
page_text = response.text
with open('movie.html', 'w', encoding='utf-8') as fp:
fp.write(page_text)

执行完上面的代码,我们再打开生成的 movie.html 文件,却发现,里面并没有我们想要的电影详情信息:

![img](/img/requests 模块应对 UA 检测和爬取动态网页/7.png)

这是因为这个网页的数据是动态加载的。

什么是动态加载的数据?

  • 我们通过 requests 模块进行数据爬取无法每次都实现可见即可得。
  • 有些数据是通过非浏览器地址栏中的 url 请求到的数据,而是通过其他请求方式(比如 ajax)请求到的数据。对于这些通过其他请求请求到的数据就是动态加载的数据。

那么该如何检测网页中是否存在动态加载数据呢?

我们当然可以想上面那样,先直接爬取页面看看,如果不能爬取到我们想要的数据,则说明网页很可能是动态加载的。

但是这个办法住农家有点蠢,我们更常用的检测网页是否是动态加载的方式是使用浏览器的抓包工具进行检测。

首先,基于抓包工具进行局部搜索。在当前网页中打开抓包工具,捕获到地址栏的 url 对应的数据包,在该数据包的 response 选项卡搜索我们想要爬取的数据,如果搜索到了结果则表示数据不是动态加载的,否则表示数据为动态加载的。

![browsercapturetoollocal](/img/requests 模块应对 UA 检测和爬取动态网页/8.png)

如果已经确定数据为动态加载,我们该如何捕获到动态加载的数据?

这就要基于抓包工具进行全局搜索。

定位到动态加载数据对应的数据包,从该数据包中就可以提取出

  • 请求的 url
  • 请求方式
  • 请求携带的参数
  • 看到响应数据

![browsercapturetoolglobal](/img/requests 模块应对 UA 检测和爬取动态网页/9.png)

我们找到这个网址的动态请求的链接和各种请求参数,并且知道了请求的方法是 get。有了这些参数,我们就可以实现我们的数据请求:

url = 'https://movie.douban.com/j/chart/top_list?type=5&interval_id=100%3A90&action=&start=0&limit=20'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
}
# 注意data的键值都写成字符串形式,没有值的话,就写成空字符串
data = {
'type': '5',
'interval_id': '100:90',
'action': '',
'start': '0',
'limit': '20',
}
response = requests.get(url=url, headers=headers, data=data)
# .json()将获取的字符串形式的json数据序列化成字典或者列表对象
data_list = response.json()
#解析出电影的名称+评分
for movie in data_list:
title = movie['title']
score = movie['score']
print(title, score)

可以通过修改 data 中的 start 和 limit 等数据,获取不同范围不同数目的结果。

基于抓包工具进行全局搜索不一定可以每次都能定位到动态加载数据对应的数据包,因为有可能动态加载的数据是经过加密的密文数据。这种情况我们后面会有所提及。

分页数据的爬取

需求:爬取肯德基的餐厅位置数据

url:http://www.kfc.com.cn/kfccda/storelist/index.aspx

分析:

  1. 在录入关键字的文本框中录入关键字按下搜索按钮,发起的是一个 ajax 请求。当前页面刷新出来的位置信息一定是通过 ajax 请求请求到的数据
  2. 基于抓包工具定位到该 ajax 请求的数据包,从该数据包中捕获到:
    • 请求的 url
    • 请求方式
    • 请求携带的参数
    • 看到响应数据

首先,我们先爬取第一的内容,注意这次的请求方法是 post,而不是 get 了:

# 爬取第一页的数据
url = 'http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=keyword'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
}
data = {
'cname': '',
'pid': '',
'keyword': '北京',
'pageIndex': '1',
'pageSize': '10',
}
# data参数是post方法中处理参数动态化的参数
response = requests.post(url=url, headers=headers, data=data)
data_list = response.json()
for store in data_list['Table1']:
store_name = store['storeName']
store_addr = store['addressDetail']
print(store_name, store_addr)

很显然,要爬取其他页码的数据,我们只需要讲 pageIndex 的参数修改成需要的页码即可:

url = 'http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=keyword'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
}
for page in range(1, 11):
data = {
'cname': '',
'pid': '',
'keyword': '北京',
'pageIndex': f'{page}',
'pageSize': '10',
}
response = requests.post(url=url, headers=headers, data=data)
data_list = response.json()
for store in data_list['Table1']:
store_name = store['storeName']
store_addr = store['addressDetail']
print(store_name, store_addr)

练习题

任务:爬取药监总局中的企业详情数据

url:http://125.35.6.84:81/xk/

需求:

  • 将首页中每一家企业的详情数据进行爬取。
    • 每一家企业详情页对应的数据
  • 将前 5 页企业的数据爬取即可。

难点:

  • 用不到数据解析
  • 所有的数据都是动态加载出来

提示:先试着将一家企业的详情页的详情数据爬取出来,然后再去爬取多家企业的数据。

完成代码如下:

import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
}
# 爬取前五页,每一家企业的详情
for page in range(1, 6):
url = 'http://125.35.6.84:81/xk/itownet/portalAction.do?method=getXkzsList'
data = {
'on': True,
'page': f'{page}',
'pageSize': '15',
'productName': '',
'conditionType': '1',
'applyname': '',
}
response = requests.post(url=url, headers=headers, data=data)
response_data = response.json()
for enterprise in response_data.get('list'):
url = 'http://125.35.6.84:81/xk/itownet/portalAction.do?method=getXkzsById'
data = {
'id': enterprise.get('ID')
}
enterprise_response = requests.post(url=url, headers=headers, data=data)
enterprise_response_data = enterprise_response.json()
print(enterprise_response_data)

scrapy 简要介绍

· 阅读需 16 分钟

scrapy 简要介绍

我们从前接触到过 DJango,是一个 Web 框架。那么什么是框架呢?

所谓的框架,其实就是一个被集成了很多功能且具有很强通用性的一个项目模板。

对于框架的学习,往往会经历两个阶段:

  • 初级阶段,学习框架中集成好的各种功能的特性及作用,也就是知道怎么用
  • 进阶阶段,逐步的探索框架的底层,知道为什么要这么用,进而知道该如何实现更高级的功能

我们写爬虫代码,需要经常写一些请求发送、数据解析、存储数据的代码。重复写代码当然不是一个好事情。同时,如果我们想要对爬虫代码进行优化,又要付出很大精力写很多代码才行。有时候,受限于自身水平,麻麻烦烦写好的优化代码效率仍然不让人满意。于是,scrapy 应运而生。

scrapy 是一个专门用于异步爬虫的框架,可以实现高性能的数据解析、请求发送、持久化存储、全站数据爬取,中间件、分布式......

scrapy 环境的安装

macOS 和 Linux 系统可以直接 pip 安装:

pip install scrapy

Windows 系统的安装要稍微繁琐些,需要先安装 Twisted,然后才能使用 pip 安装 scrapy。Twisted 是一个网络引擎框架。scrapy 需要依赖 Twisted 环境。对于 macOS 和 Linux 系统来说,会自动安装 Twisted,但是 Windows 系统并不能自动安装,所以需要我们手动下载配置。

  1. 安装 wheel,wheel 可以安装离线的 Python 包:

    pip3 install wheel
  2. 下载 twisted 文件,下载地址:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted

    找到指定版本的 twisted 文件,点击即可下载,比如我的 Python 版本是 3.6.8,操作系统是 64 位的,就选择这个:

  3. 进入下载目录,执行命令安装 Twisted(注意文件名要是刚刚下载的 Twisted 安装文件):

    pip install Twisted‑20.3.0‑cp36‑cp36m‑win_amd64.whl

    Twisted 就是一个异步的架构,被作用在了 scrapy 中。

    如果安装报错,可以尝试更换另一个版本的 Twisted 文件进行安装。

  4. 安装 pywin32:

    pip install pywin32
  5. 安装 scrapy:

    pip install scrapy

测试:cmd 中输入 scrapy 按下回车,如果没有报错说明安装成功。

img

scrapy 工程创建

在终端中输入命令,创建一个 scrapy 的工程项目:

scrapy startproject ProName

输入完上面的命令,会在当前目录下生成 scrapy 项目文件夹。文件夹的目录结构如下:

img

这些文件和文件夹,我们后面用到的时候会详细讨论,先简单介绍两个:

  • spiders 文件夹,是爬虫文件夹,用来存放爬虫源文件。
  • settings.py 文件,是工程项目的配置文件

创建爬虫源文件

进入到项目文件夹中,运行命令创建爬虫源文件:

cd ProNamescrapy genspider spiderName www.xxx.com

运行成功后,将会在 spiders 文件夹中生成一个爬虫源文件,长这样:

img

我们就可以在这个文件中编写爬虫代码了。比如写成这样:

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

class FirstSpider(scrapy.Spider):
# 爬虫文件名称:当前源文件的唯一标识
name = 'first'
# 允许的域名,一般不用,会注释掉
# allowed_domains = ['www.xxx.com']

# 起始的url列表:只可以存储url
# 作用:列表中存储的url都会被进行get请求的发送
start_urls = ['http://www.baidu.com/', 'http://www.sogou.com/']

# 数据解析
# parse方法调用的次数完全取决于请求的次数
# 参数response:表示的就是服务器返回的响应对象
def parse(self, response):
print(response)

爬虫文件 spiderName 内容阐述:

  • name:爬虫文件名称,该文件的唯一标识
  • start_urls:起始 url 列表,存储的都是 url,url 可以被自动进行 get 请求的发送
  • parse 方法:请求后的数据解析操作

执行工程和 settings.py 文件的配置

使用命令执行 scrapy 工程:

scrapy crawl spiderName

工程启动了吗?启动了,因为屏幕有输出显示内容来。但是看不懂,而且太乱:

scrapyruncrawllog-1585313900790

这是因为,执行工程后,默认会输出工程所有的日志信息。我们看到的满屏看不懂的东西,就是项目执行的日志记录。

但是我们往往并不希望看到这些日志。这些 info 级别的日志,不看也罢。

我们可以在 settings.py 文件中加入下面的配置来指定类型日志的输出:

LOG_LEVEL = 'ERROR'

再次执行工程,日志确实没有了,但是我们要打印的东西也没打印出来。事实上,什么都没有显示出来:

img

没有打印结果,说明我们的 parse 方法没有被执行。为什么没有被执行呢?

原来,scrapy 框架默认会遵守网站的 robots 协议。很显然,我们的网址都是在 robots 协议中禁止爬取的。

我们当然可以放弃爬取,但你一定不愿意这么做 -- 如果严格遵守 robots 协议,网络中几乎什么数据都爬取不到了。我们可以通过配置,来无视掉网站的 robots 协议。

在 settings.py 中,修改 ROBOTSTXT_OBEY 的配置位 False:

ROBOTSTXT_OBEY = False

再次执行工程,成功打印出 parse 方法的参数,response 的值,为两个对象:

img

除了配置 robots 协议和日志外,我们还可以指定 user-agent 进行 UA 伪装。在设置中找到 User-Agent,取消注释,然后把 user-agent 配置过去即可:

USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'

小结一下,目前我们可以通过 settings.py 进行三个配置:

  • 无视 robots
  • 指定日志类型:LOG_LEVEL = 'ERROR'
  • UA 伪装

scrapy 数据解析

scrapy 给我们提供了通过 xpath 解析数据的方法,我们只需要在 scrapy 爬虫源文件重点 parse 方法的 response 参数使用 xpath 解析即可:

response.xpath('xpath表达式')

比如,爬取段子王网站的经典段子页面(```````https://duanziwang.com/category/经典段子/`)的段子内容```,可以这样爬取解析:```

class DuanziSpider(scrapy.Spider):
name = 'duanzi'
# allowed_domains = ['https://duanziwang.com/category/经典段子/']
start_urls = ['https://duanziwang.com/category/经典段子/']

# 数据解析
def parse(self, response):
# 数据解析标题和内容
article_list = response.xpath('//main/article')
for article in article_list:
title = article.xpath('./div[1]/h1/a/text()')[0]
content = article.xpath('./div[2]/p/text()')[0]
print(title, content)
break

按照 lxml 模块的 etree 的 xpath 解析的话,title 和 content 应该是字符串。但是从打印出来的结果来看,我们获取到的却是 Selector 对象,字符串数据存放在对象的 data 属性中:

img

若要拿到对象的 data 属性,我们只需调用对象的 extract 方法即可:

title = article.xpath('./div[1]/h1/a/text()')[0].extract()content = article.xpath('./div[2]/p/text()')[0].extract()

img

但是上面那种,拿到 Selector 对象再使用 extract 方法提取数据的方式并不常用。我们更常用的是直接对列表元素进行取值操作。

比如,使用 extract_first 方法可以提取列表中第一个 Selector 对象中的 data 数据:

title = article.xpath('./div[1]/h1/a/text()').extract_first()content = article.xpath('./div[2]/p/text()').extract_first()

img

我们也可以对列表直接使用 extract 方法,提取到里面每一个 Selector 的 data 值。返回的数据也是列表:

title = article.xpath('./div[1]/h1/a/text()').extract()content = article.xpath('./div[2]/p/text()').extract()

img

总结一下 scrapy 封装的 xpath 和 etree 中的 xpath 区别:

  • scrapy 中的 xpath 直接将定位到的标签中存储的值或者属性值取出,返回的是 Selector 对象,且相关 的数据值是存储在 Selector 对象的 data 属性中,需要调用 extract、extract_first () 取出字符串数据

数据持久化存储

我们爬取到的数据,最开始是储存到内存中。我们前面的例子,是将数据打印出来。但是这显然是不够的 -- 如果我们关闭了窗口,数据将不复存在。数据分析也无从谈起。

我们需要通过持久化存储,将数据保存到硬盘中。或是以文件形式,或是存储到数据库中。这样我们将来就可以对数据进行查看和分析了。

基于终端指令的持久化存储

首先,要将我们需要存储的数据在 parse 中整理并返回:

class DuanziSpider(scrapy.Spider):
name = 'duanzi'
# allowed_domains = ['https://duanziwang.com/category/经典段子/']
start_urls = ['https://duanziwang.com/category/经典段子/']

# 将解析到的数据进行持久化存储:基于终端指令的持久化存储
def parse(self, response):
# 数据解析标题和内容
article_list = response.xpath('//main/article')
data = []
for article in article_list:
title = article.xpath('./div[1]/h1/a/text()').extract_first()
content = article.xpath('./div[2]/p/text()').extract_first()
data.append({
'title': title,
'content': content,
})
return data

然后,在执行工程的指令后面加上 -o 参数指定输出的文件位置:

scrapy crawl spiderName -o filePath

需要注意的是,该种方式只可以将 parse 方法的返回值存储到本地指定后缀(json、jsonlines、jl、csv、xml、marshal、pickle)的文本文件中。

基于管道的持久化存储(重点)

基于命令存储文件有两个主要的弊端:首先,文件必须是指定后缀名才可以;第二,只能保存到文件中,无法向数据库中存储。

好在 scrapy 给我们提供了管道,可以对数据进行任意的存储操作。文件可以是任意类型任意后缀,也可以把数据写入到数据库中。

首先,我们要在爬虫文件中进行数据解析,确定我们要保存的数据:

class DuanziSpider(scrapy.Spider):
name = 'duanzi'
# allowed_domains = ['https://duanziwang.com/category/经典段子/']
start_urls = ['https://duanziwang.com/category/经典段子/']

# 基于管道的持久化存储
def parse(self, response):
# 数据解析标题和内容
article_list = response.xpath('//main/article')
for article in article_list:
# title和content是我们解析出来要保存的数据
title = article.xpath('./div[1]/h1/a/text()').extract_first()
content = article.xpath('./div[2]/p/text()').extract_first()

然后,在 items.py 中定义相关属性。注意,我们需要保存哪些数据,就在这里定义对应的属性:

import scrapy

class DuanziproItem(scrapy.Item):
# Filed()定义好的属性当做是一个万能类型的属性
title = scrapy.Field()
content = scrapy.Field()

然后回到爬虫文件,导入我们刚刚写好的 Item 类。先实例化一个 Item 对象,然后把解析到的数据存储封装到 Item 类型的对象中。最后, 使用 yield 把 Item 类型的对象数据提交给管道:

# -*- coding: utf-8 -*-
import scrapy
from duanziPro.items import DuanziproItem

class DuanziSpider(scrapy.Spider):
name = 'duanzi'
# allowed_domains = ['https://duanziwang.com/category/经典段子/']
start_urls = ['https://duanziwang.com/category/经典段子/']

# 基于管道的持久化存储
def parse(self, response):
# 数据解析标题和内容
article_list = response.xpath('//main/article')
for article in article_list:
# title和content是我们解析出来要保存的数据
title = article.xpath('./div[1]/h1/a/text()').extract_first()
content = article.xpath('./div[2]/p/text()').extract_first()

# 实例化一个item类型的对象,将解析到的数据存储到该对象中
item = DuanziproItem()
# 不可以通过.的形式调用属性
item['title'] = title
item['content'] = content

# 将item对象提交给管道
yield item

接下来,在管道文件 pipelines.py 中接收爬虫文件提交过来的 Item 类型对象,且对其进行任意形式的持久化存储操作。比如,将数据写入到本地文件中:

class DuanziproPipeline(object):
fp = None

# 重写父类的两个方法,每次爬虫过程,只打开关闭文件一次,提高效率
def open_spider(self, spider):
print('我是open_spider(),我只会在爬虫开始的时候执行一次!')
self.fp = open('duanzi.txt', 'w', encoding='utf-8')

def close_spider(self, spider):
print('我是close_spider(),我只会在爬虫结束的时候执行一次!')
self.fp.close()

# 该方法是用来接收item对象。一次只能接收一个item,说明该方法会被调用多次
# 参数item:就是接收到的item对象
def process_item(self, item, spider):
# print(item) # item其实就是一个字典
self.fp.write(item['title'] + ':' + item['content'] + '\n')
# 将item存储到本文文件
return item

然后,需要在配置文件中开启管道机制,将下面的代码取消注释:

ITEM_PIPELINES = {
'duanziPro.pipelines.DuanziproPipeline': 300,
}

这里的 300 指代的是管道的优先级。数值越小优先级越高,优先级高的管道类先被执行。

基于管道实现数据的备份

数据备份,就是将数据分别存储到不同的载体中。

需求:将数据一份存储到 MySQL,一份存储到 Redis

问题:管道文件中的一个管道类表示怎样的一组操作呢?

一个管道类对应一种形式的持久化存储操作。如果将数据存储到不同的载体中就需要使用多个管道类。

已经定义好了三个管道类,将数据写入到三个载体中进行存储:

import pymysql
from redis import Redis

class DuanziproPipeline(object):
fp = None

# 重写父类的两个方法
def open_spider(self, spider):
print('我是open_spider(),我只会在爬虫开始的时候执行一次!')
self.fp = open('duanzi.txt', 'w', encoding='utf-8')

def close_spider(self, spider):
print('我是close_spider(),我只会在爬虫结束的时候执行一次!')
self.fp.close()

# 该方法是用来接收item对象。一次只能接收一个item,说明该方法会被调用多次
# 参数item:就是接收到的item对象
def process_item(self, item, spider):
# print(item) # item其实就是一个字典
self.fp.write(item['title'] + ':' + item['content'] + '\n')
# 将item存储到本文文件
return item

# 将数据存储到mysql中
class MysqlPipeline:
conn = None
cursor = None

def open_spider(self, spider):
self.conn = pymysql.Connect(host='127.0.0.1', port=3306, user='root', password='123', database='duanzi')
print(self.conn)

def process_item(self, item, spider):
self.cursor = self.conn.cursor()
sql = 'insert into duanziwang values ("%s", "%s")' % (item['title'], item['content'])
# 事务处理
try:
self.cursor.execute(sql)
self.conn.commit()
except Exception as e:
print(e)
self.conn.rollback()
return item

def close_spider(self, spider):
self.cursor.close()
self.conn.close()

# 将数据写入redis
class RedisPipeline:
conn = None
def open_spider(self, spider):
self.conn = Redis(host='127.0.0.1', port=6379)
print(self.conn)

def process_item(self, item, spider):
# 这一步如果报错,将redis模块的版本指定成2.10.6即可。pip install -U redis==2.10.6
self.conn.lpush('duanziData', item)

运行之前,别忘了在 MySQL 中创建新的数据库,并创建存放数据的数据表:

create database duanzi;
use duanzi;
create table duanziwang (title varchar(100), content varchar(1000));

还要在设置中配置上这三个管道类:

ITEM_PIPELINES = {
'duanziPro.pipelines.DuanziproPipeline': 300,
'duanziPro.pipelines.MysqlPipeline': 301,
'duanziPro.pipelines.RedisPipeline': 302,
}

如果运行过程中, redis 出错,可以尝试更换 redis 的版本为 2.10.6:

pip install -U redis==2.10.6

item 不会依次提交给三个管道类,爬虫文件中的 item 只会被提交给优先级最高的那一个管道类。优先级高的管道类需要在 process_item 中返回 item,这样才能把 item 传递给下一个即将被执行的管道类。

运行 scrapy 项目,可以发现,文件、MySQL 和 Redis 中都有了数据。

scrapy 的手动请求发送实现全站数据爬取

有没有想过这样的问题:为什么 start_urls 列表中的 url 会被自动进行 get 请求的发送?

这是因为列表中的 url 其实是被 start_requests 这个父类方法实现的 get 请求发送。

在爬虫源文件中,可以通过改写父类(scrapy.Spider)的 start_requests 方法,自定义开始请求的方式。默认情况下,启动 scrapy 项目会依次对 start_urls 列表中的 url 发送 requests 请求。用代码表示,大致是这样的(实际实现要稍复杂些,因为要考虑一些条件):

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

yield 后面跟随的是发起的 HTTP 请求。我们可以指定这个方法,来控制发起的是 GET 请求还是 POST 请求:

  • GET 请求要使用 scrapy.Request 方法

    yield scrapy.Request(url,callback)

    callback 指定的是回调的解析函数,用于解析数据

  • POST 请求则使用 scrapy.FormRequest 方法

    yield scrapy.FormRequest(url,callback,formdata)

    callback 的作用同样用于解析数据

    formdata 是一个字典,用来传递请求参数

如何将 start_urls 中的 url 默认进行 post 请求的发送?

重写爬虫源文件中的 start_requests 方法即可:

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

有了这些基础,我们就可以实现手动请求爬取全站数据了。

需求:爬取段子王前 5 页的数据

网址 url:https://duanziwang.com/category/ 经典段子 / 1/```

爬虫源文件实现代码:

import scrapy
from handReqPro.items import HandreqproItem

class DuanziSpider(scrapy.Spider):
name = 'duanzi'
# allowed_domains = ['duanziwang.com/category/经典段子']
start_urls = ['https://duanziwang.com/category/经典段子/1/']

# 通用的url模板
url = 'https://duanziwang.com/category/经典段子/%s/'
page = 2

# 父类方法:这个是该方法的原始实现
def start_requests(self):
for u in self.start_urls:
yield scrapy.Request(url=u, callback=self.parse)

# 将段子网中所有页码对应的数据进行爬取
def parse(self, response):
# 数据解析标题和内容
article_list = response.xpath('//main/article')
for article in article_list:
# title和content是我们解析出来要保存的数据
title = article.xpath('./div[1]/h1/a/text()').extract_first()
content = article.xpath('./div[2]/p/text()').extract_first()

# 实例化一个item类型的对象,将解析到的数据存储到该对象中
item = HandreqproItem()
# 不可以通过.的形式调用属性
item['title'] = title
item['content'] = content

# 将item对象提交给管道
yield item
if self.page <= 5: # 结束递归的条件
new_url = self.url % self.page # 其他页码对应的完整url
self.page += 1
# 对新的页码对应的url进行请求发送(手动请求GET发送)
yield scrapy.Request(new_url, self.parse)

不要忘了在配置文件中,无视 robots,进行 UA 伪装,设置日志级别,配置管道 pipeline:

LOG_LEVEL = 'ERROR'

USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'

ROBOTSTXT_OBEY = False

ITEM_PIPELINES = {
'handReqPro.pipelines.HandreqproPipeline': 300,
}

还要在 items 设置字段信息:

class HandreqproItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
title = scrapy.Field()
content = scrapy.Field()

在管道 pipelines.py 中可以进行数据的存储化操作,这里仅打印出来示例:

class HandreqproPipeline(object):
def process_item(self, item, spider):
print(item)
return item

运行 scrapy 项目,不出意外的话,即可打印出笑话网前五页的内容了。