爬虫是一种模拟浏览器实现,用以抓取网站信息的程序或者脚本。常见的爬虫有三大类:
通用式爬虫:通用式爬虫用以爬取一整个网页的信息。
聚焦式爬虫:聚焦式爬虫可以在通用式爬虫爬取到的一整个网页的信息基础上只选取一部分所需的信息。
增量式爬虫:增量式爬虫每次只爬取网站中更新的信息。
传输协议
我们知道,当我们点进某个页面时,一般我们的客户端会向服务器发送HTTP请求报文。其中报文里面很重要的一个内容就是报文的头信息(包括请求行、首部行等),它们包含了我们请求的一些基本信息。所以我们有必要了解如何找到客户端和服务器端的相互传输的HTTP报文的头信息:先打开任意一个网址,按下键盘的F12
在打开的窗口中点击network栏,再在Name一栏随意找到该网页中的某个对象,点击后选择右端的headers即可看到我们发送的报文一些头信息。
然而为了防止传输的报文被第三方拦截泄露信息,一般情况下我们的传输过程会加上一些保密协议:例如HTTPS就比HTTP更加安全,因为它采用了证书密钥的加密方式。
requests模块
报文请求过程
因为爬虫是模拟浏览器发送报文,所以我们需要先知道怎么模拟浏览器实现向服务器发送请求功能。正好Python中就为我们提供了相应的功能模块–“requests”。
我们需要明确浏览器发送时它共经历了三个步骤:1、获取URL(点击网址);2、向服务器发送请求报文(等待加载);3、得到、处理响应报文(页面展示),所以我们需要利用requests模块也需要模拟出上面三个过程。(下面以获取百度首页文字为例)
1、获取URL:
导入该模块,并且开辟一个变量来存储目标URL。
import requests
url = 'https://www.baidu.com'
2、发送请求:
这里我们模拟的是使用GET方法的报文,当然我们也可以模拟使用POST、DELETE等方法的报文。具体发送请求就调用requests模块里面的get方法,它常用有三个参数:url、params和headers。我们这里先只传入url。因为我们使用的是get方法,所以它会返回一个响应报文。
import requests
url = 'https://www.baidu.com/?tn=44004473_52_oem_dg'
response = requests.get(url)
3、处理响应报文
我们将得到的响应报文存到变量response里面,并且将其字符编码设置为“utf-8”(防止乱码)。然后我们的目标是获取返回的报文里面的文字信息,所以我们将其文章的文字信息以字符串的形式存储到一个文件里面。
import requests
url = 'https://www.baidu.com/?tn=44004473_52_oem_dg'
response = requests.get(url)
response.encoding = 'utf-8'
file = open("./myget.html", "w", encoding='utf-8')
file.write(response.text)
file.close()
这样我们运行生成的html就可以访问到百度的主页文字了
静态网页采集
之前只是获取一个特定的网页的信息,下面我们以实现采集利用关键词搜索出来的网页内容为例子看如何获取我们想要的网页(搜索):
以百度搜索为例,我们利用百度搜索某个关键词时使用的URL如下:
所以一般搜索类的URL结构如下(百度)
域名+s?wd=“我们的问题”
所以我们在爬取网页内容之前需要模仿关键词搜索,实现URL中包含我们需要搜索的内容。为了实现在URL中插入参数变量,get方法提供了一个params参数,我们只需要向其传入一个需要搜索的关键字即可,这个关键字一般存储于一个字典中。以百度搜索为例,我们的问题对应的键就是’wd‘
import requests
url = 'https://www.baidu.com/s'
rec = input()
dic = {
'wd': rec
}
response = requests.get(url= url, params= dic)
response.encoding = 'utf8'
text = response.text
file = open("./output.html", 'w', encoding='utf8')
file.write(text)
file.close()
但是这还不够,我们还需要注意的一点是,我们使用这个方法还不能获取当前的网页,有一网页服务器发现我们不是浏览器发送的请求,会拒绝响应。所以我们需要加上一些伪装信息,绕过一些服务器有反爬机制。所以需要利用get方法的第三个常用参数headers(头信息),假装我们是浏览器发送的报文,而非爬虫程序,一般加入Cookie和Accept即可了。要想知道自己这些信息打开网页按下F12即可看到:
加上我们自己的伪装后的程序可以直接不需要验证就获取网页:
import requests
url = 'https://www.baidu.com/s'
rec = input()
dic = {
'wd': rec
}
headers = {
'Accept': ...
'User-Agent':...
'Cookie':...
}
response = requests.get(url= url, params= dic, headers=headers)
response.encoding = 'utf8'
text = response.text
file = open("./output.html", 'w', encoding='utf8')
file.write(text)
file.close()
动态网页采集
但是并不是所有访问网页上端显示的URL都能获取到对应的完整界面,有时候网页上有一些信息是通过AJAX请求后才返回给我们的,下面是判断信息是否为AJAX请求返回的:
假如我们打开bilibili,找到电影索引的网页,我们目标是想获取所有电影的名字和评分,并将从第一页到第四页的电影名和对应评分都获取下来存到对应的txt文件中,所以需要知道怎么获取其信息。按下F12,我们在All里面找到对应该页面的URL的对象,但是它的response里面没有找到我们需要的数据,那么就证明这些数据不是直接通过访问URL获取的,而是由AJAX请求后再传输给我们的。
我们可以再次定位到XHR一栏,逐个寻找,找到Response里面包含电影名的那个Name,查看头信息获取其URL和返回的文件类型(json类型)以此来模拟浏览器请求过程。
import json
import requests
url ='https://api.bilibili.com/pgc/season/index/result'
headers = {
'User-Agent':...
}
file = open('./output.txt', 'w', encoding='utf8')
for i in range(1,5):
params={
'st': '2',
'order': '2',
'area': '-1',
'style_id': '-1',
'release_date': '-1',
'season_status': '-1',
'sort': '0',
'page': i,
'season_type': '2',
'pagesize': '20',
'type': '1'
}
response = requests.get(url=url, headers=headers, params=params).json()
for p in response['data']['list']:
file.write('电影名:'+p['title']+' '+'评分:'+p['score']+'\n')
file.close()
数据解析
上面使用到的requests模块主要还是用于获取一整张网页,但是我们很多情况下只需要某部分对应的信息,这时候我们就需要对采集的数据进行解析了,解析位于标签之间的文本内容或文本属性。
所以总结我们数据解析的步骤,可以大致分为两步:1、定位到该信息的标签;2、从该标签中提取该信息。
针对上面的数据解析,我们主要利用bs4和xpath的解析方法。
bs4
bs4的数据解析方法共分为两步:
1、将请求到的页面源码赋予给实例化的对象Beautifulsoup
2、调用Beautifulsoup的属性或者方法来从标签中获取我们所需的内容
在我们使用bs4的方法前,需要先导两个包:bs4包和解释源码用到lxml包:
pip install bs4
pip install lxml
第一步实现:
然后我们既可以实例化出一个BeautifulSoup对象bs了,实例化需要向BeautifulSoup里传入两个参数:1、我们获取的网页源码;2、固定是’lxml’的解析方式。其中所需的源码就是我们曾经使用get方法中的text方式获取到的对应网页的数据。
第二步实现:
1、定位
再者就是如何调用实例化出来的对象bs里面的方法和属性来获取对应标签里面的所需信息了。常见的使用方法或者属性有bs.find()、bs.find_all、bs.tagname。
bs.tagname是获取bs对象里面标签名为tagname的第一个标签的内容,相似地,bs.find(‘tagname’)也是找出第一个标签名为tagname的标签里面的内容,如果想找到更深入,还可以加上属性:bs.find(‘tagname’, class_=‘属性’);要想找到全部标签名为tagmane的标签内容,需要使用find_all,传入标签名即可。
而挑选某个符合条件的信息需要使用select方法,我们向它传递的参数是一个选择器,我们可以选择传递类选择器(标志为 . 号)、id选择器(标志位#号)或者标签名选择器等等…(或者我们还可以传递层选择器“上一级标签 > 次级标签 >最低级标签”、如果有非隶属则>换为空格)。
2、获取
当我们利用定位获取到当前信息存储到标签位置时,我么们就需要考虑如何去获取这些信息了,在BeautifulSoup模块中,其为每一个实例出来的对象都提供了两个属性和一个方法来获取这些信息:我们以bs.tagname为定位标签,则获取信息的方式是bs.tagname.text 、 bs.tagname.get_text() 、 bs.tagname.string。但是这三者中前两者可以获取当前标签下的所有内容,而后者只能获取当前标签下的直隶内容。
import json
import requests
import lxml
from bs4 import BeautifulSoup
url = 'https://blog.csdn.net/m0_61151031'
headers ={
'cookie':....、
'user-agent': ...
}
param = {
'spm': '1011.2415.3001.5343'
}
response = requests.get(url=url, params=param, headers= headers)
response.encoding='utf8'
bs = BeautifulSoup(response.text, 'lxml')
title_save = []
URL_save = []
for j in bs.find_all('article' ,class_='blog-list-box'):
URL_save.append(j.find('a')['href'])
for i in bs.find_all('h4'):
title_save.append(i.text)
file= open('./output.txt', 'w', encoding='utf-8')
print(len(title_save))
print(len(URL_save))
for i in range(0,len(title_save)-1):
file.write(title_save[i]+' : '+URL_save[i]+'\n')
file.close()
Xpath
在我们使用Xpath之前需要先安装Xpath所需的解释器 lxml 包:
pip install lxml
安装完后,下面我们就可以使用Xpath来捕获网页中某个标签对应的具体信息。整个获取过程总结起来为两步:1、定位到具体存储的标签2、获取该标签下的信息。实际使用Xpath来操作这两个步骤的过程如下:
1、实例化出一个etree对象,并且将网页源码赋予该对象
2、调用etree对象里面的xpath方法,利用xpath表达式定位到我们所需的标签,捕获其内容信息。
下面是作为例子的html文件,我们可以看到信息都是夹杂在一些尖括号括起来的标签里边(类似这样:信息):
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>测试bs4</title>
</head>
<body>
<div>
<p>百里守约</p>
</div>
<div class="song">
<p>李清照</p>
<p>王安石</p>
<p>苏轼</p>
<p>柳宗元</p>
<a href="http://www.song.com/" title="赵匡胤" target="_self">
<span>this is span</span>
宋朝是最强大的王朝,不是军队的强大,而是经济很强大,国民都很有钱</a>
<a href="" class="du">总为浮云能蔽日,长安不见使人愁</a>
<img src="http://www.baidu.com/meinv.jpg" alt="" />
</div>
<div class="tang">
<ul>
<li><a href="http://www.baidu.com" title="qing">清明时节雨纷纷,路上行人欲断魂,借问酒家何处有,牧童遥指杏花村</a></li>
<li><a href="http://www.163.com" title="qin">秦时明月汉时关,万里长征人未还,但使龙城飞将在,不教胡马度阴山</a></li>
<li><a href="http://www.126.com" alt="qi">岐王宅里寻常见,崔九堂前几度闻,正是江南好风景,落花时节又逢君</a></li>
<li><a href="http://www.sina.com" class="du">杜甫</a></li>
<li><a href="http://www.dudu.com" class="du">杜牧</a></li>
<li><b>杜小月</b></li>
<li><i>度蜜月</i></li>
<li><a href="http://www.haha.com" id="feng">凤凰台上凤凰游,凤去台空江自流,吴宫花草埋幽径,晋代衣冠成古丘</a></li>
</ul>
</div>
</body>
</html>
第一步实现:
要想定位到具体的标签,首先我们需要有一个etree对象。调用etree的parse方法将本地的html文件传入该方法中(如果是网上的html文件,需要调用etree的HTML方法),该函数会返回一个已解释了html对象:
import requests
from lxml import etree
rec_etree = etree.parse('./test.html')
然后我们就可以 调用该对象的方法xpath来定位标签,其中xpath方法有多种使用方式,不过具体还是向其传入一个标签位置。
一个标签位置的格式为 :/ 标签名 / 标签名 / 标签名。由 / 符号表示层级关;但是我们还可以利用 // 表示多层级关系**,/ 标签名 // 标签名**,省略中间的标签名。如果我们以及获取了定位在中间层级的某个对象,那么接下来的位置也可以表示为 ./标签名/标签名,这里的 ./ 就表示从当前位置开始,和Linux系统中用法类似。如果某一层的下一次标签名都相同,那么我们需要在相同的标签后面加上[n](n为数字)来表示是哪一个标签。
例如上图中P[1]是李清照、P[2]是王安石…这里要注意下标从1开始。
如果定位时要加上标签的属性,我们则需要在标签后面加上[@class=“属性”],例如上面的例子要定位到具有属性song的标签地址就需要写为:
//div[@class="song"]
下面以定位信息苏轼的标签p为例可以写出多种定位写法:
print(rec_etree.xpath('/html/body/div/p[3]'))
print(rec_etree.xpath('/html//p[3]'))
print(rec_etree.xpath('//div/p[3]'))
print(rec_etree.xpath('//p[3]'))
从上到下我们依次来看:首先第一个是直接完全每一个层级都标清楚的定位;第二个是省略了中间层级的定位;第三个是省略局部前面层级的定位;第四个是省略全部前面层级的定位。
(或者有时候可以直接用右键点击该信息复制…)
第二步实现:
当我们定位到某个标签后,我们得到的还只是给elements对象,我们要想获取里面对应的信息,最简单的方式就是在标签后再加一层**/text()**,但是这样得出来的还只是一个字典:
为此我们还需要获取其首元素信息
print(rec_etree.xpath('/html/body/div/p[3]/text()')[0])
print(rec_etree.xpath('/html//p[3]/text()')[0])
print(rec_etree.xpath('//div/p[3]/text()')[0])
print(rec_etree.xpath('//p[3]/text()')[0])
但是我们仔细看可以看到有时候有些我们需要的信息没有存在标签之间,而是存在了标签里边,作为标签的属性,这时候我们就需要在定位到标签后加上**…标签名/@属性名**来获取这个属性。
print(rec_etree.xpath('//div[@class="song"]/a[@title="赵匡胤"]/@href')[0])
验证码
验证码是服务器指定的一种反爬机制,当我们登录某些网址时,在输入了账号和密码后系统可能需要我们输入一个随机的验证码,这样可以有效地防止我们的爬虫程序模拟登录,因为程序预先不知道系统会给哪些个验证码,以此来保障反爬。
为此,我们要想爬取需要验证码的网址,需要借助一些第三方验证码识别工具进行验证码破解或者人工识别图片或者用专门的AI图片识别算法。
多线程
现在我们需要考虑一种情况,如果我们一次性需要请求很多的URL,那么系统会如何去执行我们的代码呢?下面是一次模拟请求情况的代码:
import requests
url =[
'url1'
'url2'
'url3'
...
]
headers = {
...
}
def get_status(each_url):
response = requests.get(url=url, headers=headers)
return response.status_code
for each_url in url:
ret = get_status(each_url)
print(ret)
我们将输出每一个请求的URL的返回状态码。系统会将我们的URL通过GET函数一次一次地串行发送过去,这样也就表面,如果GET函数发送请求一个报文并获得响应需要1s,那么100个URL请求将要花费100s,更多情况下花费地时间将更长!
其实我们回过头来看,问题主要还是出在了每一次GET只发送一个URL的请求,下面为串行通信的示意:
为了解决这个问题,人们提出的方案为开辟多个进程进行发送,也就是使用并行的通信方式,这种方式的示意图如下,虽然多线程的通信可以大大减少发送的时间,但是每次调用就开辟一个进程,用完就关掉的往复操作却增大了CPU的工作量!所以每一个方法都会有副作用!
除了上面的两种方法,聪明的人们还想出了另外一种方法:线程池,它在多线程的基础上进行修改,基本原理为一次开辟多个进程,然后用这些有限的线程池来进行数据传输,这样可以有效地避免了多线程情况下线程使用的开关开关操作导致的消耗。下面我们来做两个例子对比一下:
首先是模拟单线程,模拟的例子的思路是用一个函数operation,这个函数将期待我们发送报文的函数,其中每次get函数所导致的阻塞时间就由sleep函数来模拟,每次的url就由一个elements列表来表示。
from time import *
start = time()
def operation(element):
sleep(2)
elements = [1, 2, 3, 4]
for i in elements:
operation(i)
end = time()
print('您一共花费了:'+'%lf'%(end-start)+'秒')
通过测试上面的代码我们可以得到共花费的时间:
然后我们来使用多线程传输。要想实现多线程传输我们首先需要导入一个包,这个包里面的Pool类将帮助我们创建一个多线程对象,其中初始化对象时传进去的参数就是未来我们将开辟的线程池的总线程数:
from time import *
from multiprocessing.dummy import Pool
start = time()
def operation(element):
sleep(2)
elements = [1, 2, 3, 4]
pool = Pool(4)
pool.map(operation, elements)
end = time()
print('您一共花费了:'+'%lf'%(end-start)+'秒')
然后调用实例化出来的多线程对象的map方法,向其传入产生通信阻塞的操作(这里是operation函数)和需要向该操作传递的参数。它就会帮助我们使用多线程的动作来实现这个操作,并且如果该操作本身有返回值那么它也会向我们返回某些数值。 所以使用了多线程那么理论上消耗时间将变为原来的1/4:
当然,如果使用3个线程,那么类似木桶理论,传输时间取最大的值,也就是先用三进程传输3个,再用三进程中其中一个传输第四个数,所以共消耗4s左右:
协程
协程的定义及其实现过程
在我们学习协程之前首先需要理解协程的定义,下面的解释来自百度百科:
协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。
总的来说,协程是运行在线程之上的函数调用操作,之所以选择这么做,是因为有时候即使我们开启了线程池,同时阻塞的情况也是可能存在,线程之间由于阻塞而等待切换的过程会导致时间以及空间上的浪费。所以发明一种协程可以单独运行在堵塞的线程之上,当一个协程完成后另一个可以补上,并且它还不会增加线程的数量。
下面我们来看看如何用Python实现协程:
协程对象及事件循环
因为协程本质是一个函数调用,所以我们首先需要定义一个函数,并且为其加上关键字async,这就表明这个函数不再是一个简单的函数,它会会返回一个协程对象,并且当我们再次调用这个函数时,函数里面的语句不会被执行!
当我们利用async修饰一个函数后,调用该函数会返回一个协程对象,并且如果我们想去使用这个协程对象,需要先创建一个事件循环,我们只有将协程对象注册进事件循环里面其才能被执行,这个过程就像先烧一锅开水(事件循环),然后才能放方便面(协程对象)下去焖(执行)。而创建一个事件循环就是调用asyncio里面的方法,并且还需要启动它和放入协程对象。
import asyncio
async def func1():
print("函数已调用!")
ret = func1()
loop = asyncio.get_event_loop()
loop.run_until_complete(ret)
基于事件循环创建任务对象
关于函数func1返回的协程对象ret,我们还可以将其封装成一个任务对象,它在原来协程对象的基础上会增加一些对象的状态,而封装的方法就是调用loop里面的方法,方法调用完后会返回一个新的任务对象,它具有大部分协程对象的性质:
import asyncio
async def func1():
print("函数已调用!")
ret = func1()
loop = asyncio.get_event_loop()
task = loop.create_task(ret)
loop.run_until_complete(task)
print(task)
下面就是task对象所附带的状态信息的输出
基于asyncio创建任务对象
除了上面利用loop创建任务对象的方法,我们还可以利用一种别的方式来创建任务对象,具体是直接调用asyncio里面的ensure_future方法向其传入协程对象即可创建出一个任务对象:
import asyncio
async def func1():
print("函数已调用!")
ret = func1()
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(ret)
loop.run_until_complete(task)
print(task)
回调函数
如果我们想在函数执行完后绑定一个回调函数,只需要调用下面的方法将task和该回调函数绑定即可,这样在事件循环中凡是执行了task任务对象后它会调用这个回调函数!
import asyncio
async def func1():
print("函数已调用!")
return 'a'
def show_ret(task):
print(task.result())
ret = func1()
loop = asyncio.get_event_loop()
task = loop.create_task(ret)
task.add_done_callback(show_ret)
loop.run_until_complete(task)
协程的优势测试
下面我们就一个具体的例子来看看协程的优势:
下面是一组测试代码,具体的意思是我们先创建一个协程对象,然后在这个协程对象里面写一个睡眠函数,让函数执行停止2秒模仿等待过程(注意这里要用asyncio里面的sleep函数,因为time包里面的sleep函数是基于同步(线程如果堵塞会等待当前任务响应才继续处理下一个任务)实现的,而我们的多任务协程的实现方式是异步(线程如果堵塞将任务挂起直到收到回应才重新处理,不阻碍线程),这里如果调用基于同步实现的函数会失效。同时注意在前面加上一个await的挂起操作)然后使用一个for循环来模仿多任务的情况,每一次循环都会创建一个协程对象并且基于其来创建一个任务对象。最后将存储任务的任务列表注册到事件循环中,这里注意要调用因为是任务列表所以要使用wait。
import asyncio
from time import *
start = time()
async def func():
print("调用该函数!")
await asyncio.sleep(2)
task_list=[]
loop = asyncio.get_event_loop()
for i in range(0,3):
ret = func()
task = loop.create_task(ret)
task_list.append(task)
loop.run_until_complete(asyncio.wait(task_list))
end = time()
print(end-start)
协程的具体实现
接下来我们就可以直接来看看如何使用协程进行多任务异步实现,我们一次性爬取百度搜索的搜狗、360和猎豹页面。按照上面的使用方法,我们先创建一个URL的元组,然后定义一个请求该URL内容的函数,并且将加上async的定义关键字,此时这个函数将会返回一个协程对象。然后创建一个事件循环并且每一次都将协程对象创建出来的任务对象放进事件循环中执行。
import requests
import asyncio
import time
start = time.time()
urls = ['https://www.baidu.com/s?wd=%E6%90%9C%E7%8B%97','https://www.baidu.com/s?wd=360','https://www.baidu.com/s?wd=%E7%8C%8E%E8%B1%B9']
headers = {
'Accept': ...
'User-Agent':'...
'Cookie': ...
}
async def get_content(url):
ret = requests.get(url=url, headers=headers)
print("success!")
loop = asyncio.get_event_loop()
tasks=[]
for url in urls:
ret_ = get_content(url)
task = loop.create_task(ret_)
tasks.append(task)
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print(end-start)
虽然这样看起来好像实现了异步多任务处理,但是实际上requests里面的get函数是一个基于同步处理的函数,也就是说我们根本没有利用协程实现异步多任务的处理!为了解决这个问题我们需要利用基于异步而写的请求函数,为此我们需要使用到aiohttp模块。我们首先需要获取一个请求对象session,这个对象将会帮助我们发送请求报文,我们会调用aiohttp里面的CilentSession方法来创建一个对象,然后类似于requests一样调用对象里面的get方法来获取响应(注意这里获取字符串需要使用到text是一个方法而非属性),而且请求函数和获取响应报文内容等操作是都需要进行挂起(使用await)。
async def get_content(url):
ret = requests.get(url=url, headers=headers)
print("success!")
|
|
V
async def get_content(url):
async with aiohttp.ClientSession() as session:
async with await session.get(url=url, headers=headers) as response:
print('success!')
//ret = await response.text()
使用协程:
不使用协程:
我们可以明显看出使用了协程的速度比不使用要快!
Selenium
因为Selenium的内容有很多,所以我另外写了一篇博客来记录我的Selenium学习过程👉
代理
代理就是我们在客户端和服务器之间构建一个中间桥梁,我们可以通过代理服务器来向目标服务器发送请求。这样可以在我们IP被目标服务器封的时候还能够请求到目标服务器的内容,或者可以让我们不想直接使用自己的以IP的身份直接请求某个服务器而隐藏起自己的IP。
其中代理IP有三种类型:透明、匿名和高匿。
使用透明的代理IP服务器会知道你在使用代理IP并且也知道你的真实IP;
使用匿名的代理IP服务器会知道你在使用代理IP但不知道你的真实IP;
使用高匿的代理IP服务器不知道你在使用代理IP更不会知道你的真实IP。
爬虫是一种模拟浏览器实现,用以抓取网站信息的程序或者脚本。常见的爬虫有三大类:
通用式爬虫:通用式爬虫用以爬取一整个网页的信息。
聚焦式爬虫:聚焦式爬虫可以在通用式爬虫爬取到的一整个网页的信息基础上只选取一部分所需的信息。
增量式爬虫:增量式爬虫每次只爬取网站中更新的信息。
传输协议
我们知道,当我们点进某个页面时,一般我们的客户端会向服务器发送HTTP请求报文。其中报文里面很重要的一个内容就是报文的头信息(包括请求行、首部行等),它们包含了我们请求的一些基本信息。所以我们有必要了解如何找到客户端和服务器端的相互传输的HTTP报文的头信息:先打开任意一个网址,按下键盘的F12
在打开的窗口中点击network栏,再在Name一栏随意找到该网页中的某个对象,点击后选择右端的headers即可看到我们发送的报文一些头信息。
然而为了防止传输的报文被第三方拦截泄露信息,一般情况下我们的传输过程会加上一些保密协议:例如HTTPS就比HTTP更加安全,因为它采用了证书密钥的加密方式。
requests模块
报文请求过程
因为爬虫是模拟浏览器发送报文,所以我们需要先知道怎么模拟浏览器实现向服务器发送请求功能。正好Python中就为我们提供了相应的功能模块–“requests”。
我们需要明确浏览器发送时它共经历了三个步骤:1、获取URL(点击网址);2、向服务器发送请求报文(等待加载);3、得到、处理响应报文(页面展示),所以我们需要利用requests模块也需要模拟出上面三个过程。(下面以获取百度首页文字为例)
1、获取URL:
导入该模块,并且开辟一个变量来存储目标URL。
import requests
url = 'https://www.baidu.com'
2、发送请求:
这里我们模拟的是使用GET方法的报文,当然我们也可以模拟使用POST、DELETE等方法的报文。具体发送请求就调用requests模块里面的get方法,它常用有三个参数:url、params和headers。我们这里先只传入url。因为我们使用的是get方法,所以它会返回一个响应报文。
import requests
url = 'https://www.baidu.com/?tn=44004473_52_oem_dg'
response = requests.get(url)
3、处理响应报文
我们将得到的响应报文存到变量response里面,并且将其字符编码设置为“utf-8”(防止乱码)。然后我们的目标是获取返回的报文里面的文字信息,所以我们将其文章的文字信息以字符串的形式存储到一个文件里面。
import requests
url = 'https://www.baidu.com/?tn=44004473_52_oem_dg'
response = requests.get(url)
response.encoding = 'utf-8'
file = open("./myget.html", "w", encoding='utf-8')
file.write(response.text)
file.close()
这样我们运行生成的html就可以访问到百度的主页文字了
静态网页采集
之前只是获取一个特定的网页的信息,下面我们以实现采集利用关键词搜索出来的网页内容为例子看如何获取我们想要的网页(搜索):
以百度搜索为例,我们利用百度搜索某个关键词时使用的URL如下:
所以一般搜索类的URL结构如下(百度)
域名+s?wd=“我们的问题”
所以我们在爬取网页内容之前需要模仿关键词搜索,实现URL中包含我们需要搜索的内容。为了实现在URL中插入参数变量,get方法提供了一个params参数,我们只需要向其传入一个需要搜索的关键字即可,这个关键字一般存储于一个字典中。以百度搜索为例,我们的问题对应的键就是’wd‘
import requests
url = 'https://www.baidu.com/s'
rec = input()
dic = {
'wd': rec
}
response = requests.get(url= url, params= dic)
response.encoding = 'utf8'
text = response.text
file = open("./output.html", 'w', encoding='utf8')
file.write(text)
file.close()
但是这还不够,我们还需要注意的一点是,我们使用这个方法还不能获取当前的网页,有一网页服务器发现我们不是浏览器发送的请求,会拒绝响应。所以我们需要加上一些伪装信息,绕过一些服务器有反爬机制。所以需要利用get方法的第三个常用参数headers(头信息),假装我们是浏览器发送的报文,而非爬虫程序,一般加入Cookie和Accept即可了。要想知道自己这些信息打开网页按下F12即可看到:
加上我们自己的伪装后的程序可以直接不需要验证就获取网页:
import requests
url = 'https://www.baidu.com/s'
rec = input()
dic = {
'wd': rec
}
headers = {
'Accept': ...
'User-Agent':...
'Cookie':...
}
response = requests.get(url= url, params= dic, headers=headers)
response.encoding = 'utf8'
text = response.text
file = open("./output.html", 'w', encoding='utf8')
file.write(text)
file.close()
动态网页采集
但是并不是所有访问网页上端显示的URL都能获取到对应的完整界面,有时候网页上有一些信息是通过AJAX请求后才返回给我们的,下面是判断信息是否为AJAX请求返回的:
假如我们打开bilibili,找到电影索引的网页,我们目标是想获取所有电影的名字和评分,并将从第一页到第四页的电影名和对应评分都获取下来存到对应的txt文件中,所以需要知道怎么获取其信息。按下F12,我们在All里面找到对应该页面的URL的对象,但是它的response里面没有找到我们需要的数据,那么就证明这些数据不是直接通过访问URL获取的,而是由AJAX请求后再传输给我们的。
我们可以再次定位到XHR一栏,逐个寻找,找到Response里面包含电影名的那个Name,查看头信息获取其URL和返回的文件类型(json类型)以此来模拟浏览器请求过程。
import json
import requests
url ='https://api.bilibili.com/pgc/season/index/result'
headers = {
'User-Agent':...
}
file = open('./output.txt', 'w', encoding='utf8')
for i in range(1,5):
params={
'st': '2',
'order': '2',
'area': '-1',
'style_id': '-1',
'release_date': '-1',
'season_status': '-1',
'sort': '0',
'page': i,
'season_type': '2',
'pagesize': '20',
'type': '1'
}
response = requests.get(url=url, headers=headers, params=params).json()
for p in response['data']['list']:
file.write('电影名:'+p['title']+' '+'评分:'+p['score']+'\n')
file.close()
数据解析
上面使用到的requests模块主要还是用于获取一整张网页,但是我们很多情况下只需要某部分对应的信息,这时候我们就需要对采集的数据进行解析了,解析位于标签之间的文本内容或文本属性。
所以总结我们数据解析的步骤,可以大致分为两步:1、定位到该信息的标签;2、从该标签中提取该信息。
针对上面的数据解析,我们主要利用bs4和xpath的解析方法。
bs4
bs4的数据解析方法共分为两步:
1、将请求到的页面源码赋予给实例化的对象Beautifulsoup
2、调用Beautifulsoup的属性或者方法来从标签中获取我们所需的内容
在我们使用bs4的方法前,需要先导两个包:bs4包和解释源码用到lxml包:
pip install bs4
pip install lxml
第一步实现:
然后我们既可以实例化出一个BeautifulSoup对象bs了,实例化需要向BeautifulSoup里传入两个参数:1、我们获取的网页源码;2、固定是’lxml’的解析方式。其中所需的源码就是我们曾经使用get方法中的text方式获取到的对应网页的数据。
第二步实现:
1、定位
再者就是如何调用实例化出来的对象bs里面的方法和属性来获取对应标签里面的所需信息了。常见的使用方法或者属性有bs.find()、bs.find_all、bs.tagname。
bs.tagname是获取bs对象里面标签名为tagname的第一个标签的内容,相似地,bs.find(‘tagname’)也是找出第一个标签名为tagname的标签里面的内容,如果想找到更深入,还可以加上属性:bs.find(‘tagname’, class_=‘属性’);要想找到全部标签名为tagmane的标签内容,需要使用find_all,传入标签名即可。
而挑选某个符合条件的信息需要使用select方法,我们向它传递的参数是一个选择器,我们可以选择传递类选择器(标志为 . 号)、id选择器(标志位#号)或者标签名选择器等等…(或者我们还可以传递层选择器“上一级标签 > 次级标签 >最低级标签”、如果有非隶属则>换为空格)。
2、获取
当我们利用定位获取到当前信息存储到标签位置时,我么们就需要考虑如何去获取这些信息了,在BeautifulSoup模块中,其为每一个实例出来的对象都提供了两个属性和一个方法来获取这些信息:我们以bs.tagname为定位标签,则获取信息的方式是bs.tagname.text 、 bs.tagname.get_text() 、 bs.tagname.string。但是这三者中前两者可以获取当前标签下的所有内容,而后者只能获取当前标签下的直隶内容。
import json
import requests
import lxml
from bs4 import BeautifulSoup
url = 'https://blog.csdn.net/m0_61151031'
headers ={
'cookie':....、
'user-agent': ...
}
param = {
'spm': '1011.2415.3001.5343'
}
response = requests.get(url=url, params=param, headers= headers)
response.encoding='utf8'
bs = BeautifulSoup(response.text, 'lxml')
title_save = []
URL_save = []
for j in bs.find_all('article' ,class_='blog-list-box'):
URL_save.append(j.find('a')['href'])
for i in bs.find_all('h4'):
title_save.append(i.text)
file= open('./output.txt', 'w', encoding='utf-8')
print(len(title_save))
print(len(URL_save))
for i in range(0,len(title_save)-1):
file.write(title_save[i]+' : '+URL_save[i]+'\n')
file.close()
Xpath
在我们使用Xpath之前需要先安装Xpath所需的解释器 lxml 包:
pip install lxml
安装完后,下面我们就可以使用Xpath来捕获网页中某个标签对应的具体信息。整个获取过程总结起来为两步:1、定位到具体存储的标签2、获取该标签下的信息。实际使用Xpath来操作这两个步骤的过程如下:
1、实例化出一个etree对象,并且将网页源码赋予该对象
2、调用etree对象里面的xpath方法,利用xpath表达式定位到我们所需的标签,捕获其内容信息。
下面是作为例子的html文件,我们可以看到信息都是夹杂在一些尖括号括起来的标签里边(类似这样:信息):
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>测试bs4</title>
</head>
<body>
<div>
<p>百里守约</p>
</div>
<div class="song">
<p>李清照</p>
<p>王安石</p>
<p>苏轼</p>
<p>柳宗元</p>
<a href="http://www.song.com/" title="赵匡胤" target="_self">
<span>this is span</span>
宋朝是最强大的王朝,不是军队的强大,而是经济很强大,国民都很有钱</a>
<a href="" class="du">总为浮云能蔽日,长安不见使人愁</a>
<img src="http://www.baidu.com/meinv.jpg" alt="" />
</div>
<div class="tang">
<ul>
<li><a href="http://www.baidu.com" title="qing">清明时节雨纷纷,路上行人欲断魂,借问酒家何处有,牧童遥指杏花村</a></li>
<li><a href="http://www.163.com" title="qin">秦时明月汉时关,万里长征人未还,但使龙城飞将在,不教胡马度阴山</a></li>
<li><a href="http://www.126.com" alt="qi">岐王宅里寻常见,崔九堂前几度闻,正是江南好风景,落花时节又逢君</a></li>
<li><a href="http://www.sina.com" class="du">杜甫</a></li>
<li><a href="http://www.dudu.com" class="du">杜牧</a></li>
<li><b>杜小月</b></li>
<li><i>度蜜月</i></li>
<li><a href="http://www.haha.com" id="feng">凤凰台上凤凰游,凤去台空江自流,吴宫花草埋幽径,晋代衣冠成古丘</a></li>
</ul>
</div>
</body>
</html>
第一步实现:
要想定位到具体的标签,首先我们需要有一个etree对象。调用etree的parse方法将本地的html文件传入该方法中(如果是网上的html文件,需要调用etree的HTML方法),该函数会返回一个已解释了html对象:
import requests
from lxml import etree
rec_etree = etree.parse('./test.html')
然后我们就可以 调用该对象的方法xpath来定位标签,其中xpath方法有多种使用方式,不过具体还是向其传入一个标签位置。
一个标签位置的格式为 :/ 标签名 / 标签名 / 标签名。由 / 符号表示层级关;但是我们还可以利用 // 表示多层级关系**,/ 标签名 // 标签名**,省略中间的标签名。如果我们以及获取了定位在中间层级的某个对象,那么接下来的位置也可以表示为 ./标签名/标签名,这里的 ./ 就表示从当前位置开始,和Linux系统中用法类似。如果某一层的下一次标签名都相同,那么我们需要在相同的标签后面加上[n](n为数字)来表示是哪一个标签。
例如上图中P[1]是李清照、P[2]是王安石…这里要注意下标从1开始。
如果定位时要加上标签的属性,我们则需要在标签后面加上[@class=“属性”],例如上面的例子要定位到具有属性song的标签地址就需要写为:
//div[@class="song"]
下面以定位信息苏轼的标签p为例可以写出多种定位写法:
print(rec_etree.xpath('/html/body/div/p[3]'))
print(rec_etree.xpath('/html//p[3]'))
print(rec_etree.xpath('//div/p[3]'))
print(rec_etree.xpath('//p[3]'))
从上到下我们依次来看:首先第一个是直接完全每一个层级都标清楚的定位;第二个是省略了中间层级的定位;第三个是省略局部前面层级的定位;第四个是省略全部前面层级的定位。
(或者有时候可以直接用右键点击该信息复制…)
第二步实现:
当我们定位到某个标签后,我们得到的还只是给elements对象,我们要想获取里面对应的信息,最简单的方式就是在标签后再加一层**/text()**,但是这样得出来的还只是一个字典:
为此我们还需要获取其首元素信息
print(rec_etree.xpath('/html/body/div/p[3]/text()')[0])
print(rec_etree.xpath('/html//p[3]/text()')[0])
print(rec_etree.xpath('//div/p[3]/text()')[0])
print(rec_etree.xpath('//p[3]/text()')[0])
但是我们仔细看可以看到有时候有些我们需要的信息没有存在标签之间,而是存在了标签里边,作为标签的属性,这时候我们就需要在定位到标签后加上**…标签名/@属性名**来获取这个属性。
print(rec_etree.xpath('//div[@class="song"]/a[@title="赵匡胤"]/@href')[0])
验证码
验证码是服务器指定的一种反爬机制,当我们登录某些网址时,在输入了账号和密码后系统可能需要我们输入一个随机的验证码,这样可以有效地防止我们的爬虫程序模拟登录,因为程序预先不知道系统会给哪些个验证码,以此来保障反爬。
为此,我们要想爬取需要验证码的网址,需要借助一些第三方验证码识别工具进行验证码破解或者人工识别图片或者用专门的AI图片识别算法。
多线程
现在我们需要考虑一种情况,如果我们一次性需要请求很多的URL,那么系统会如何去执行我们的代码呢?下面是一次模拟请求情况的代码:
import requests
url =[
'url1'
'url2'
'url3'
...
]
headers = {
...
}
def get_status(each_url):
response = requests.get(url=url, headers=headers)
return response.status_code
for each_url in url:
ret = get_status(each_url)
print(ret)
我们将输出每一个请求的URL的返回状态码。系统会将我们的URL通过GET函数一次一次地串行发送过去,这样也就表面,如果GET函数发送请求一个报文并获得响应需要1s,那么100个URL请求将要花费100s,更多情况下花费地时间将更长!
其实我们回过头来看,问题主要还是出在了每一次GET只发送一个URL的请求,下面为串行通信的示意:
为了解决这个问题,人们提出的方案为开辟多个进程进行发送,也就是使用并行的通信方式,这种方式的示意图如下,虽然多线程的通信可以大大减少发送的时间,但是每次调用就开辟一个进程,用完就关掉的往复操作却增大了CPU的工作量!所以每一个方法都会有副作用!
除了上面的两种方法,聪明的人们还想出了另外一种方法:线程池,它在多线程的基础上进行修改,基本原理为一次开辟多个进程,然后用这些有限的线程池来进行数据传输,这样可以有效地避免了多线程情况下线程使用的开关开关操作导致的消耗。下面我们来做两个例子对比一下:
首先是模拟单线程,模拟的例子的思路是用一个函数operation,这个函数将期待我们发送报文的函数,其中每次get函数所导致的阻塞时间就由sleep函数来模拟,每次的url就由一个elements列表来表示。
from time import *
start = time()
def operation(element):
sleep(2)
elements = [1, 2, 3, 4]
for i in elements:
operation(i)
end = time()
print('您一共花费了:'+'%lf'%(end-start)+'秒')
通过测试上面的代码我们可以得到共花费的时间:
然后我们来使用多线程传输。要想实现多线程传输我们首先需要导入一个包,这个包里面的Pool类将帮助我们创建一个多线程对象,其中初始化对象时传进去的参数就是未来我们将开辟的线程池的总线程数:
from time import *
from multiprocessing.dummy import Pool
start = time()
def operation(element):
sleep(2)
elements = [1, 2, 3, 4]
pool = Pool(4)
pool.map(operation, elements)
end = time()
print('您一共花费了:'+'%lf'%(end-start)+'秒')
然后调用实例化出来的多线程对象的map方法,向其传入产生通信阻塞的操作(这里是operation函数)和需要向该操作传递的参数。它就会帮助我们使用多线程的动作来实现这个操作,并且如果该操作本身有返回值那么它也会向我们返回某些数值。 所以使用了多线程那么理论上消耗时间将变为原来的1/4:
当然,如果使用3个线程,那么类似木桶理论,传输时间取最大的值,也就是先用三进程传输3个,再用三进程中其中一个传输第四个数,所以共消耗4s左右:
协程
协程的定义及其实现过程
在我们学习协程之前首先需要理解协程的定义,下面的解释来自百度百科:
协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。
总的来说,协程是运行在线程之上的函数调用操作,之所以选择这么做,是因为有时候即使我们开启了线程池,同时阻塞的情况也是可能存在,线程之间由于阻塞而等待切换的过程会导致时间以及空间上的浪费。所以发明一种协程可以单独运行在堵塞的线程之上,当一个协程完成后另一个可以补上,并且它还不会增加线程的数量。
下面我们来看看如何用Python实现协程:
协程对象及事件循环
因为协程本质是一个函数调用,所以我们首先需要定义一个函数,并且为其加上关键字async,这就表明这个函数不再是一个简单的函数,它会会返回一个协程对象,并且当我们再次调用这个函数时,函数里面的语句不会被执行!
当我们利用async修饰一个函数后,调用该函数会返回一个协程对象,并且如果我们想去使用这个协程对象,需要先创建一个事件循环,我们只有将协程对象注册进事件循环里面其才能被执行,这个过程就像先烧一锅开水(事件循环),然后才能放方便面(协程对象)下去焖(执行)。而创建一个事件循环就是调用asyncio里面的方法,并且还需要启动它和放入协程对象。
import asyncio
async def func1():
print("函数已调用!")
ret = func1()
loop = asyncio.get_event_loop()
loop.run_until_complete(ret)
基于事件循环创建任务对象
关于函数func1返回的协程对象ret,我们还可以将其封装成一个任务对象,它在原来协程对象的基础上会增加一些对象的状态,而封装的方法就是调用loop里面的方法,方法调用完后会返回一个新的任务对象,它具有大部分协程对象的性质:
import asyncio
async def func1():
print("函数已调用!")
ret = func1()
loop = asyncio.get_event_loop()
task = loop.create_task(ret)
loop.run_until_complete(task)
print(task)
下面就是task对象所附带的状态信息的输出
基于asyncio创建任务对象
除了上面利用loop创建任务对象的方法,我们还可以利用一种别的方式来创建任务对象,具体是直接调用asyncio里面的ensure_future方法向其传入协程对象即可创建出一个任务对象:
import asyncio
async def func1():
print("函数已调用!")
ret = func1()
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(ret)
loop.run_until_complete(task)
print(task)
回调函数
如果我们想在函数执行完后绑定一个回调函数,只需要调用下面的方法将task和该回调函数绑定即可,这样在事件循环中凡是执行了task任务对象后它会调用这个回调函数!
import asyncio
async def func1():
print("函数已调用!")
return 'a'
def show_ret(task):
print(task.result())
ret = func1()
loop = asyncio.get_event_loop()
task = loop.create_task(ret)
task.add_done_callback(show_ret)
loop.run_until_complete(task)
协程的优势测试
下面我们就一个具体的例子来看看协程的优势:
下面是一组测试代码,具体的意思是我们先创建一个协程对象,然后在这个协程对象里面写一个睡眠函数,让函数执行停止2秒模仿等待过程(注意这里要用asyncio里面的sleep函数,因为time包里面的sleep函数是基于同步(线程如果堵塞会等待当前任务响应才继续处理下一个任务)实现的,而我们的多任务协程的实现方式是异步(线程如果堵塞将任务挂起直到收到回应才重新处理,不阻碍线程),这里如果调用基于同步实现的函数会失效。同时注意在前面加上一个await的挂起操作)然后使用一个for循环来模仿多任务的情况,每一次循环都会创建一个协程对象并且基于其来创建一个任务对象。最后将存储任务的任务列表注册到事件循环中,这里注意要调用因为是任务列表所以要使用wait。
import asyncio
from time import *
start = time()
async def func():
print("调用该函数!")
await asyncio.sleep(2)
task_list=[]
loop = asyncio.get_event_loop()
for i in range(0,3):
ret = func()
task = loop.create_task(ret)
task_list.append(task)
loop.run_until_complete(asyncio.wait(task_list))
end = time()
print(end-start)
协程的具体实现
接下来我们就可以直接来看看如何使用协程进行多任务异步实现,我们一次性爬取百度搜索的搜狗、360和猎豹页面。按照上面的使用方法,我们先创建一个URL的元组,然后定义一个请求该URL内容的函数,并且将加上async的定义关键字,此时这个函数将会返回一个协程对象。然后创建一个事件循环并且每一次都将协程对象创建出来的任务对象放进事件循环中执行。
import requests
import asyncio
import time
start = time.time()
urls = ['https://www.baidu.com/s?wd=%E6%90%9C%E7%8B%97','https://www.baidu.com/s?wd=360','https://www.baidu.com/s?wd=%E7%8C%8E%E8%B1%B9']
headers = {
'Accept': ...
'User-Agent':'...
'Cookie': ...
}
async def get_content(url):
ret = requests.get(url=url, headers=headers)
print("success!")
loop = asyncio.get_event_loop()
tasks=[]
for url in urls:
ret_ = get_content(url)
task = loop.create_task(ret_)
tasks.append(task)
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print(end-start)
虽然这样看起来好像实现了异步多任务处理,但是实际上requests里面的get函数是一个基于同步处理的函数,也就是说我们根本没有利用协程实现异步多任务的处理!为了解决这个问题我们需要利用基于异步而写的请求函数,为此我们需要使用到aiohttp模块。我们首先需要获取一个请求对象session,这个对象将会帮助我们发送请求报文,我们会调用aiohttp里面的CilentSession方法来创建一个对象,然后类似于requests一样调用对象里面的get方法来获取响应(注意这里获取字符串需要使用到text是一个方法而非属性),而且请求函数和获取响应报文内容等操作是都需要进行挂起(使用await)。
async def get_content(url):
ret = requests.get(url=url, headers=headers)
print("success!")
|
|
V
async def get_content(url):
async with aiohttp.ClientSession() as session:
async with await session.get(url=url, headers=headers) as response:
print('success!')
//ret = await response.text()
使用协程:
不使用协程:
我们可以明显看出使用了协程的速度比不使用要快!
Selenium
因为Selenium的内容有很多,所以我另外写了一篇博客来记录我的Selenium学习过程👉
代理
代理就是我们在客户端和服务器之间构建一个中间桥梁,我们可以通过代理服务器来向目标服务器发送请求。这样可以在我们IP被目标服务器封的时候还能够请求到目标服务器的内容,或者可以让我们不想直接使用自己的以IP的身份直接请求某个服务器而隐藏起自己的IP。
其中代理IP有三种类型:透明、匿名和高匿。
使用透明的代理IP服务器会知道你在使用代理IP并且也知道你的真实IP;
使用匿名的代理IP服务器会知道你在使用代理IP但不知道你的真实IP;
使用高匿的代理IP服务器不知道你在使用代理IP更不会知道你的真实IP。
关于Python技术储备
学好 Python 不论是就业还是做副业赚钱都不错,但要学会 Python 还是要有一个学习规划。最后大家分享一份全套的 Python 学习资料,给那些想学习 Python 的小伙伴们一点帮助!
包括:Python激活码+安装包、Python web开发,Python爬虫,Python数据分析,Python自动化测试学习等教程。带你从零基础系统性的学好Python!
👉[[CSDN大礼包:《python安装包&全套学习资料》免费分享]](安全链接,放心点击)
一、Python大礼包
Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。
二、 Python电子书
三、入门学习视频
四、 Python爬虫秘笈
光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
五、 数据分析全套资源
六、python副业兼职与全职路线
上述这份完整版的Python全套学习资料已经上传CSDN官方,如果需要可以微信扫描下方CSDN官方认证二维码 即可领取
👉[[CSDN大礼包:《python安装包&全套学习资料》免费分享]](安全链接,放心点击)
![](https://i-blog.csdnimg.cn/blog_migrate/1dedcd1c9f33a9f09a49caffaf8b6cfe.png)