跳到主要内容

Linux 修行之路 · Blog

Linux修行之路 - 技术博客

分享Kubernetes、Linux、Python、网络安全等技术文章

文章数量169
技术分类9
查看分类
19

获取 Cookie

· 阅读需 11 分钟

Cookie 是存储在客户端浏览器中的一组键值对。

web 中 Cookie 的典型应用为免密登录。

Cookie 和爬虫之间也有千丝万缕的关联:

  • 有时候,对一张页面进行请求的时候,如果请求的过程中不携带 Cookie 的话,那么我们是无法请求到正确的页面数据。因此 Cookie 是爬虫中一个非常典型且常见的反爬机制。

接下来,我们看一个需要用到 Cookie 的爬虫案例。

需求:爬取雪球网中的咨询信息。

网址 url:https://xueqiu.com/

分析:

  1. 判定爬取的咨询数据是否为动态加载的
    • 相关的更多咨询数据是动态加载的,滚轮滑动到底部的时候会动态加载出更多咨询数据。
  2. 定位到 ajax 请求的数据包,提取出请求的 url,响应数据为 json 形式的咨询数据

尝试使用常规方法抓取数据:

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'
}
url = 'https://xueqiu.com/v4/statuses/public_timeline_by_category.json?since_id=-1&max_id=20369485&count=15&category=-1'
response = requests.get(url=url, headers=headers)
print(response.json())

数据拿到了吗?拿到了。但是为什么不开心?因为拿到的数据是这么一串玩意儿:

{'error_description': '遇到错误,请刷新页面或者重新登录帐号后再试', 'error_uri': '/v4/statuses/public_timeline_by_category.json', 'error_data': None, 'error_code': '400016'}

数据没写错,但是却没拿到正确的响应。我们没有请求到我们想要的数据

原因:我们没有严格意义上模拟浏览器发请求,比如没有携带 Cookie。

处理:可以将浏览器发请求携带的请求头,全部粘贴在 headers 字典中,将 headers 作用到 requests 的请求操作中即可。

Cookie 有两种处理方式

方式 1:手动处理

  • 将抓包工具中的 cookie 粘贴在 headers 中
  • 弊端:cookie 如果过了有效时长则该方式失效。

方式 2:自动处理

  • 基于 Session 对象实现自动处理。
    1. 获取一个 session 对象:requests.Session() 返回一个 session 对象。
    2. 使用 session 对象可以像 requests 一样调用 get 和 post 发起指定的请求。只不过如果在使用 session 对象发请求的过程中产生了 cookie,cookie 会被自动存储到该 session 对象中。在下次再次使用 session 对象发起请求时,该次请求就是携带 cookie 进行的请求发送。

问:在爬虫中使用 session 的时候,session 对象至少会被使用几次?

答:两次。第一次使用 session 是为了将 cookie 捕获且存储到 session 对象中。下次的时候就是携带 cookie 进行的请求发送。

一般情况下,直接访问首页就可以获得浏览器返回的 Cookie。使用 Cookie 即可进行下一步的操作。

上面雪球网的例子,我们就可以修改成这样:

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'
}
session = requests.Session() # 创建sesion对象
# 第一次使用session捕获且存储cookie,猜测对雪球网的首页发起的请求可能会产生cookie
session.get(url='https://xueqiu.com/', headers=headers) # 捕获并存储cookie
url = 'https://xueqiu.com/v4/statuses/public_timeline_by_category.json?since_id=-1&max_id=20369485&count=15&category=-1'
response = session.get(url=url, headers=headers) # 携带cookie发起的请求
print(response.json())

顺利拿到想要的数据。

代理操作

在爬虫中,所谓的代理指的就是代理服务器。代理服务器的作用是用来转发请求和响应。

在爬虫中为什么会需要使用代理服务器?

  • 如果我们的爬虫在短时间内对服务器发起了高频的请求,那么服务器一旦检测到这样的一个异常的行为请求,就有可能将该请求对应设备的 ip 禁掉,这样这台 client 设备就无法对服务器端再次进行请求发送(ip 被禁掉了)。
  • 如果 ip 被禁,我们可以使用代理服务器进行请求转发,破解 ip 被禁的反爬机制。因为使用代理后,服务器端接受到的请求对应的 ip 地址就是代理服务器而不是我们真正的客户端的。

代理服务器分为不同的匿名度:

  • 透明代理:如果使用了该形式的代理,服务器端知道你使用了代理机制也知道你的真实 ip。
  • 匿名代理:知道你使用代理,但是不知道你的真实 ip
  • 高匿代理:不知道你使用了代理也不知道你的真实 ip

代理的类型

  • https:代理只能转发 https 协议的请求
  • http:转发 http 的请求

常用的代理服务器

接下来,我们先通过高频次的爬取西刺代理的网页,让其封禁我们的 IP(尽管如此,也不要写死循环,温柔一点。我们的目的是让他把我们禁掉,而不是把人家搞崩)。然后通过 IP 代理的方式,继续爬取。

我们使用的代理时代理精灵,网站上面有给。买一个最低配的 3 元套餐,用作练习测试。

点击 API 提取,选择我们刚刚支付的套餐,然后选择要生产的数量,还有协议格式之类的。最后点击 生成 API 链接 即可。

img

首先使用生成好的链接封装一个代理池:

# 刚刚生成的API链接
proxy_url = 'http://ip.11jsq.com/index.php/api/entry?method=proxyServer.generate_api_url&packid=1&fa=0&fetch_key=&groupid=0&qty=10&time=1&pro=&city=&port=1&format=html&ss=5&css=&dt=1&specialTxt=3&specialJson=&usertype=15'
page_text = requests.get(url=proxy_url, headers=headers).text
tree = etree.HTML(page_text)
http_proxy = [{'https': ip} for ip in tree.xpath('//body//text()')] # 代理池

接下来,对西祠代理发起一个高频的请求,让其将我本机 ip 禁掉。将下面的代码多执行几次,直到报错。

url = 'https://www.xicidaili.com/nn/%s'
ip_list = []
for page in range(1, 11):
new_url = url % page
page_text = requests.get(url=new_url, headers=headers).text
tree = etree.HTML(page_text)
# 在xpath表达式中不可以出现tbody标签
tr_list = tree.xpath('//table[@id="ip_list"]//tr')[1:]
for tr in tr_list:
ip = tr.xpath('./td[2]/text()')
ip_list.append(ip)
len(ip_list)

然后浏览器访问 ````https://www.xicidaili.com`,访问不到```,说明 IP 已经被禁掉。

img

然后,使用代理,尝试继续访问:

# 生成代理池,注意代理的API地址要及时更新
proxy_url = 'http://t.11jsq.com/index.php/api/entry?method=proxyServer.generate_api_url&packid=1&fa=0&fetch_key=&groupid=0&qty=53&time=1&pro=&city=&port=1&format=html&ss=5&css=&dt=1&specialTxt=3&specialJson=&usertype=15'
page_text = requests.get(url=proxy_url, headers=headers).text
tree = etree.HTML(page_text) # 代理池
http_proxy = [{'https': ip} for ip in tree.xpath('//body//text()')]

# 代码不需要大改动,只需要在requests请求中加上代理即可
url = 'https://www.xicidaili.com/nn/%s'
ip_list = []
for page in range(1, 11):
new_url = url % page
# 让当次的请求使用代理机制,就可以更换请求的ip地址
page_text = requests.get(url=new_url, headers=headers, proxies=random.choice(http_proxy)).text
tree = etree.HTML(page_text)
# 在xpath表达式中不可以出现tbody标签
tr_list = tree.xpath('//table[@id="ip_list"]//tr')[1:]
for tr in tr_list:
ip = tr.xpath('./td[2]/text()')
ip_list.append(ip)
print(len(ip_list))

就又可以爬取到数据了

验证码的识别

我们往往通过基于线上的打码平台识别验证码,而不是自己用机器学习写一个。虽然能够实现功能,但是自己写的毕竟没有大的数据量支撑,且不是一朝一夕能搞定的。

常用的打码平台有:

  • \1. 超级鹰(使用):

    http://www.chaojiying.com/about.html

    • 注册(用户中心的身份)
    • 登录(用户中心的身份)
      1. 查询余额,请充值
      2. 创建一个软件 ID(例如:899370)
      3. 下载示例代码
  • \2. 云打码

  • \3. 打码兔

超级鹰打码平台 SDK 整理封装成一个函数,传入验证码地址和图片的验证码类型数字,即可得到验证码识别结果。

#!/usr/bin/env python
# coding:utf-8

import requests
from hashlib import md5

class Chaojiying_Client(object):

def __init__(self, username, password, soft_id):
self.username = username
password = password.encode('utf8')
self.password = md5(password).hexdigest()
self.soft_id = soft_id
self.base_params = {
'user': self.username,
'pass2': self.password,
'softid': self.soft_id,
}
self.headers = {
'Connection': 'Keep-Alive',
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
}

def PostPic(self, im, codetype):
"""
im: 图片字节
codetype: 题目类型 参考 http://www.chaojiying.com/price.html
"""
params = {
'codetype': codetype,
}
params.update(self.base_params)
files = {'userfile': ('ccc.jpg', im)}
r = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files, headers=self.headers)
return r.json()

def ReportError(self, im_id):
"""
im_id:报错题目的图片ID
"""
params = {
'id': im_id,
}
params.update(self.base_params)
r = requests.post('http://upload.chaojiying.net/Upload/ReportError.php', data=params, headers=self.headers)
return r.json()


def get_img_code(img_path, img_type):
chaojiying = Chaojiying_Client('liushuo', 'liushuo', '904154') # 用户中心>>软件ID 找到或生成软件ID
im = open(img_path, 'rb').read()
return chaojiying.PostPic(im, img_type)['pic_str'] # 验证码类型 官方网站>>价格体系


print(get_img_code('a.jpg', 1902))

模拟登录

有些网站的信息,是需要我们登录之后,才能拿到的。要想爬取这些网站的内容,我们首先要进行模拟登录。

模拟登录的流程:

  1. 对点击登录按钮对应的请求进行发送(post 请求)
  2. 处理请求参数:
    • 用户名
    • 密码
    • 验证码
    • 其他的防伪参数

需求:登录古诗文网,获取登陆成功后的页面

登陆页面 url:https://so.gushiwen.org/user/login.aspx?from=http://so.gushiwen.org/user/collect.aspx

首先,我们猜测,需要输入用户名密码和验证码信息即可登录:

# 识别验证码
url = 'https://so.gushiwen.org/user/login.aspx?from=http://so.gushiwen.org/user/collect.aspx'
# 解析验证码图片的地址
page_text = requests.get(url=url, headers=headers).text
tree = etree.HTML(page_text)
img_code_url = 'https://so.gushiwen.org' + tree.xpath('//img[@id="imgCode"]/@src')[0]
img_data = requests.get(url=img_code_url, headers=headers).content
# 将验证码图片保存到本地
with open('b.jpg', 'wb') as fp:
fp.write(img_data)
# 识别验证码
code = get_img_code('b.jpg', 1902)
print(code)
login_url = 'https://so.gushiwen.org/user/login.aspx?from=http://so.gushiwen.org/user/collect.aspx'
data = {
'__VIEWSTATE': 'zLcWU1Ihz+ZP28Jlu5xMxY8NMnEHb6jYi3hkBbQemydBVHdU8VMEyRw7jL0QOdTEnWM6aGEQMYQiKLb0fccd11U/MqxMMEpMBNI6pF8mr9Xzi0ARS/O3rvKz+vM=',
'__VIEWSTATEGENERATOR': 'C93BE1AE',
'from': 'http://so.gushiwen.org/user/collect.aspx',
'email': 'liushuo432@outlook.com',
'pwd': 'liushuo',
'code': code, # 动态变化
'denglu': '登录',
}
# 对点击登录按钮发起请求:获取了登录成功后对应的页面源码数据
page_text = requests.post(url=login_url, headers=headers, data=data).text
with open('gushiwen.html', 'w', encoding='utf-8') as fp:
fp.write(page_text)

打开生成的 gushiwen.html 文件,并没有成功登录。

对比我们的请求和浏览器发送的请求,可以获得这样两个信息:

  1. 浏览器的请求有携带 Cookie,或许是因为我们没有携带 Cookie 信息,从而导致登录失败
  2. 从打印出来的验证码数据来看,验证码识别是正确的,所以不可能是验证码的问题

所以接下来我们尝试携带 Cookie 进行模拟登录:

# 携带 Cookie 模拟登录
session = requests.Session()
url = 'https://so.gushiwen.org/user/login.aspx?from=http://so.gushiwen.org/user/collect.aspx'
page_text = session.get(url=url, headers=headers).text
tree = etree.HTML(page_text)
img_code_url = 'https://so.gushiwen.org' + tree.xpath('//img[@id="imgCode"]/@src')[0]
img_data = session.get(url=img_code_url, headers=headers).content
with open('b.jpg', 'wb') as fp:
fp.write(img_data)
code = get_img_code('b.jpg', 1902)
print(code)
login_url = 'https://so.gushiwen.org/user/login.aspx?from=http://so.gushiwen.org/user/collect.aspx'
data = {
'__VIEWSTATE': 'zLcWU1Ihz+ZP28Jlu5xMxY8NMnEHb6jYi3hkBbQemydBVHdU8VMEyRw7jL0QOdTEnWM6aGEQMYQiKLb0fccd11U/MqxMMEpMBNI6pF8mr9Xzi0ARS/O3rvKz+vM=',
'__VIEWSTATEGENERATOR': 'C93BE1AE',
'from': 'http://so.gushiwen.org/user/collect.aspx',
'email': 'liushuo432@outlook.com',
'pwd': 'liushuo',
'code': code,
'denglu': '登录',
}
page_text = session.post(url=login_url, headers=headers, data=data).text
with open('gushiwen.html', 'w', encoding='utf-8') as fp:
fp.write(page_text)

这样就成功访问到我们需要的页面了。

其实我们还有一个东西没有用到,就是 data 中的两个无规律的字符串。在请求参数中如果看到了一组乱序的请求参数,最好去验证这组请求参数是否为动态变化。如果这组参数不变化,直接在请求中写死就行了。但如果请求参数是动态变化的,我们就可能需要对这些数据进行处理:

  1. 常规来讲一半动态变化的请求参数会被隐藏在前台页面中,那么我们就要去前台页面源码中取找。
  2. 如果前台页面没有的话,我们就可以基于抓包工具进行全局搜索。

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 项目,不出意外的话,即可打印出笑话网前五页的内容了。

scrapy 的五大核心组件

· 阅读需 8 分钟

scrapy 的五大核心组件

学习 scrapy 核心组件的目的:

  1. 大概了解 scrapy 的运行机制
  2. 为我们后面学习分布式爬虫做铺垫

五大核心组件及其作用:

  • 引擎(Scrapy):用来处理整个系统的数据流,触发事务(框架核心)
  • 调度器(Scheduler):用来接收引擎发过来的请求,压入队列中,并在引擎再次请求的时候返回。可以想像成一个 URL(抓取网页的网址或者说是链接)的优先队列,由它来决定下一个要抓取的网址是什么,同时去除重复的网址。事实上,调度器可分为两个部分:过滤器和队列。过滤器用来将请求去重,队列用来调整网址抓取的顺序
  • 下载器(Downloader):用于下载网页内容,并将网页内容返回给蜘蛛引擎(Scrapy 下载器是建立在 twisted 这个高效的异步模型上的)
  • 爬虫(Spiders):爬虫是主要干活的,用于从特定的网页中提取自己需要的信息,即所谓的实体(Item)。用户也可以从中提取出链接,让 Scrapy 继续抓取下一个页面
  • 项目管道(Pipeline):负责处理爬虫从网页中抽取的实体(Item),主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。

![scrapy-architecture](/img/scrapy 高级用法/scrapy-architecture.png)

如上图所示,scrapy 五大核心组件的请求调度流程为:

  1. 请求最先从爬虫中发出,发送给引擎。
  2. 引擎将请求交给调度器。
  3. 调度器分为两部分,过滤器和队列。请求经过滤器进行去重后,交给队列。调度器将按照队列中元素的次序,将请求返回给引擎。
  4. 引擎将调度好的请求交给下载器下载资源。
  5. 下载器从网络中把资源下载好后,把响应结果交给引擎。
  6. 引擎把响应结果交给爬虫,进行数据解析
  7. 爬虫将解析好的数据放到实体(Item)中,交给引擎
  8. 引擎根据爬虫返回的结果进行不同的处理。如果返回的是实体对象,则交给管道进行数据持久化操作;如果返回的是请求对象,则交给调度器,重复步骤 2。

请求传参实现深度爬取

深度爬取指的爬取的数据没有在同一张页面中,比如首页数据和详情页数据。

在 scrapy 中如果没有请求传参我们是无法持久化存储数据的。

请求传参的实现方式:

scrapy.Request(url, callback, meta)

meta 是一个字典,可以将 meta 传递给 callback。我们可以在回调函数 callback 中取出 meta:

response.meta

需求:爬取 2345 电影网电影前五页中所有的电影标题和电影简介信息

网址 url:https://www.4567kan.com/index.php/vod/show/id/5.html

分析:首页只能看见电影标题,电影简介要在电影的详情页才能看见。这就涉及了网页的深度爬取,需要结合请求传参来实现。

爬虫源文件的写法为:

import scrapyfrom moviePro.items import MovieproItemclass MovieSpider(scrapy.Spider):    name = 'movie'    # allowed_domains = ['www.xxx.com']    start_urls = ['https://www.4567kan.com/index.php/vod/show/id/5.html']    url = 'https://www.4567kan.com/index.php/vod/show/id/5/page/%s.html'    page = 2    def parse(self, response):        li_list = response.xpath('//ul[@class="stui-vodlist clearfix"]/li')        for li in li_list:            url = 'https://www.4567kan.com' + li.xpath('./div/a/@href').extract_first()            name = li.xpath('./div/a/@title').extract_first()            item = MovieproItem()            item['name'] = name            # 对详情页url发起请求            # meta作用:可以将meta字典传递给callback            yield scrapy.Request(url, self.parse_detail, meta={'item': item})        if self.page <= 5:            new_url = self.url % self.page            self.page += 1            yield scrapy.Request(new_url, self.parse)    # 被用作于解析详情页的数据    def parse_detail(self, response):        desc = response.xpath('//span[@class="detail-content"]/text()').extract_first()        if not desc:            desc = response.xpath('//span[@class="detail-sketch"]/text()').extract_first()        # 接收传递过来的meta        item = response.meta['item']        item['desc'] = desc        yield item

settings 里面要配置 UA 伪装,要在 items 里面写好字段,还要再管道中写好数据持久化代码,这里就不列举了。

中间件

在 Django 中我们已经学习过中间件了,scrapy 的中间件与 Django 的中间件类似,也是用来批量拦截处理请求和响应。

scrapy 的中间件有两种:

  • 爬虫中间件
  • 下载中间件(推荐)

爬虫中间件和下在中间件的作用是类似的。稍微有点区别是,下在中间件处理的请求是经过调度器调度去重了的。

通过中间件,我们可以在三个方面进行处理:拦截请求、拦截响应和拦截异常的请求

拦截请求可以做到:

  • 篡改请求 url(可以,但是没必要)
  • 伪装请求头信息(通常在 settings 里面配置)
    • UA 伪装
    • Cookie 伪装

拦截响应可以用来:

  • 篡改响应数据(一般不这么做)

拦截异常的请求通常用来:

  • 代理操作必须使用中间件才可以实现(重点)

    process_exception:    request.meta['proxy'] = 'http://ip:port'

中间件的使用示例:

class MiddleproDownloaderMiddleware(object):    # 拦截所有(正常&异常)的请求    # 参数:request就是拦截到的请求,spider就是爬虫类实例化的对象    def process_request(self, request, spider):        print('process_request()')        request.headers['User-Agent'] = 'xxx'        # request.headers['Cookie'] = 'xxxxx'        return None #or request    # 拦截所有的响应对象    # 参数:response拦截到的响应对象,request响应对象对应的请求对象    def process_response(self, request, response, spider):        print('process_response()')        return response    # 拦截异常的请求    # 参数:request就是拦截到的发生异常的请求    # 作用:想要将异常的请求进行修正,将其变成正常的请求,然后对其进行重新发送    def process_exception(self, request, exception, spider):        # 如果请求的ip被禁掉,该请求就会变成一个异常的请求        request.meta['proxy'] = 'http://ip:port' #设置代理        print('process_exception()')        return request #将异常的请求修正后将其进行重新发送

不要忘了在 settings 中,把下载中间件的代码取消注释:

DOWNLOADER_MIDDLEWARES = {   'middlePro.middlewares.MiddleproDownloaderMiddleware': 543,}

大文件(图片视频等)下载

大文件数据是在管道中请求到的。下载管道类是 scrapy 封装好的我们直接用即可:

from scrapy.pipelines.images import ImagesPipeline    # 提供了数据下载功能

创建一个管道类,继承自 ImagesPipeline(类似地,还有 MediaPipeline 和 FilePipeline,用法大同小异),重写该管道类的三个方法:

  • get_media_requests:对图片地址发起请求
  • file_path:返回图片名称即可
  • item_completed:返回 item,将其返回给下一个即将被执行的管道类

在配置文件中指定文件下载后存放的文件夹:

IMAGES_STORE = 'dirName'

需求:使用 scrapy 爬取校花网的图片

网址 url:http://www.521609.com/daxuexiaohua/

管道类代码:

# 该默认管道无法帮助我们请求图片数据,因此该管道我们就不用# class ImgproPipeline(object):#     def process_item(self, item, spider):#         return item# 管道需要接受item中的图片地址和名称,然后再管道中请求到图片的数据对其进行持久化存储from scrapy.pipelines.images import ImagesPipeline    # 提供了数据下载功能# from scrapy.pipelines.media import MediaPipeline# from scrapy.pipelines.files import FilesPipelineimport scrapyclass ImgsPipiLine(ImagesPipeline):    # 根据图片地址发起请求    def get_media_requests(self, item, info):        # print(item)        yield scrapy.Request(url=item['src'],meta={'item':item})    # 返回图片名称即可    def file_path(self, request, response=None, info=None):        # 通过request获取meta        item = request.meta['item']        filePath = item['name']        return filePath    # 只需要返回图片名称    # 将item传递给下一个即将被执行的管道类    def item_completed(self, results, item, info):        return item

爬虫源文件代码:

import scrapyfrom imgPro.items import ImgproItemclass ImgSpider(scrapy.Spider):    name = 'img'    # allowed_domains = ['www.xxx.com']    start_urls = ['http://www.521609.com/daxuexiaohua/']    def parse(self, response):        li_list = response.xpath('//div[@id="content"]/div[2]/div[2]/ul/li')        for li in li_list:            name = li.xpath('./a/img/@alt').extract_first() + '.jpg'            src = 'http://www.521609.com' + li.xpath('./a/img/@src').extract_first()            item = ImgproItem()            item['name'] = name            item['src'] = src            yield item

items 和 settings 也要进行常规配置,就不一一列举了。

settings.py 中的常用配置

增加并发。默认 scrapy 开启的并发线程为 32 个,可以适当进行增加。在 settings 配置文件中修改 CONCURRENT_REQUESTS 的值即可:

CONCURRENT_REQUESTS = 100

降低日志级别。在运行 scrapy 时,会有大量日志信息的输出,为了减少 CPU 的使用率,可以设置 log 输出信息为 INFO 或者 ERROR。在配置文件中编写:

LOG_LEVEL = 'INFO'# 或者LOG_LEVEL = 'ERROR'

禁止 cookie。如果不是真的需要 cookie,则在 scrapy 爬取数据时可以禁止 cookie 从而减少 CPU 的使用率,提升爬取效率。在配置文件中编写(如果要启用 cookie,将这个值改成 True 即可):

COOKIES_ENABLED = False

禁止重试。对失败的 HTTP 进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:

RETRY_ENABLED = False

减少下载超时。如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:

DOWNLOAD_TIMEOUT = 10    # 超时时间为10s