'
>img_src_list=re.findall(ex,page_text,re.M)
># 创建一个文件夹,用来保存所有的图片数据\
>file_dir='spider_img_folder'
>if not os.path.exists(file_dir):
> os.mkdir(file_dir)
>os.chdir(file_dir)
>for img_src in img_src_list:
> img_src='https:'+img_src # 获得了完整的图片地址
> print(img_src)
> # 请求得到图片的二进制数据
> img_data=requests.get(img_src,headers=headers).content
> # 生成图片名称
> name=img_src.split('/')[-1]
> with open(name,'wb') as fp:
> fp.write(img_data)
>```
**下面实现分页爬取功能**
观察不同分页之间的url的区别
https://www.qiushibaike.com/imgrank/page/3/
只有page后面的数字发生了变化
>```python
>import requests
>import json
>import re
>import os
># UA伪装
>headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36'
>}
>
># 观察不同分页之间的url的区别
># https://www.qiushibaike.com/imgrank/page/3/
># 只有page后面的数字发生了变化
>
>file_dir='spider_img_folder'
>if not os.path.exists(file_dir):
> os.mkdir(file_dir)
>os.chdir(file_dir)
># 设定一个通用的url模板
>url='https://www.qiushibaike.com/imgrank/page/%d/'
>
>for page_num in range(1,3):
> # 获得对应页码的url
> url_src=format(url%page_num)
> # 也可以url='https://www.qiushibaike.com/imgrank/page/{0}/'.format(page_num)
> # 先使用通用爬虫对无聊图的整张页面进行爬取
> response = requests.get(url_src,headers=headers)
> page_text=response.text # 一整张页面的源码数据
>
> ex = '
.*?
)
'
> img_src_list=re.findall(ex,page_text,re.S)
> # 创建一个文件夹,用来保存所有的图片数据\
>
> for img_src in img_src_list:
> img_src='https:'+img_src # 获得了完整的图片地址
> print(img_src)
> # 请求得到图片的二进制数据
> img_data=requests.get(img_src,headers=headers).content
> # 生成图片名称
> name=img_src.split('/')[-1]
> with open(name,'wb') as fp:
> fp.write(img_data)
>```
## 5.4. 使用beautifulsoup4来进行数据解析
beautifulsoup4的解析方式只能在python中进行使用,而正则表达式是跨语言的。
- 数据解析的原理
- 1. 标签定位
- 2. 提取标签、标签属性中存储的数据值
- bs4实现数据解析的原理
- 1. 实例化一个BeautifulSoup对象,将页面源码数据加载到对象中
- 2. 调用BeautifulSoup对象中相关的属性或方法进行标签定位和数据提取
- 环境配置
- pip install bs4
- pip install lxml 安装lxml或者其他解析器
- 如何实例化BeautifulSoup对象
- from bs4 import BeautifulSoup
- 对象实例化:
- 将本地的html文件中的数据加载到该对象中
- 将互联网上获取的页面源码加载到该对象中
**第一种形式的实例化**
例子:将“菜鸟教程”网站的原始html代码拷贝下来保存到本地
>```python
>from bs4 import BeautifulSoup
># 将本地的html文档中的数据加载到该对象中
>fp=open('./file.html','r',encoding='utf-8')
>soup = BeautifulSoup(fp,'lxml')
>print(soup) # 发现打印出来的soup对象的内容其实就是file.html本身的内容
>```
**第二种形式的实例化**
>```python
>page_text = respone.text
>soup = BeautifulSoup(page_text,'lxml')
>```
- bs4中提供的用于数据解析的方法和属性
- 属性:
- soup.tagName:
- 标签名称,可以是head、div、a、p、li等具体的标签名称。(tagName是一个总的代指)
- 返回的是文档中第一次出现的标签
- 方法
- find系列函数
- soup.find(tagName) 返回找到的第一个匹配的标签,等同于soup.tagName
- 属性定位,定位到特定位置的标签:soup.find('a')) # 和soup.a 是一样的
- soup.find('div',class_='col search row-search-mobile')) 使用class_来进行定位,这里的class_也可以是其他的属性,例如id啥的
- soup.find_all()
- 找到所有满足条件的标签(返回的是列表)
- select函数
- soup.select('某种css选择器[id选择器:#id_name,class选择器:.class_name]') 可以传入各种css选择器来进行对应选择,关于css选择器可见 https://www.runoob.com/cssref/css-selectors.html
- **最终返回的是一个列表(
返回满足要求的所有标签)**
- select使用 **层级选择器**
- 单层选择器:>表示的是一个层级
>```python
>a_list=soup.select('.container.navigation > .row > .col.nav > #index-nav > li > a') # 解决空格的方法:用'.'代替空格
># 在bs4中值支持属性定位,而不支持索引定位
>for each in a_list:
> print(each)
>```
得到结果如下所示
>```html
>
首页
>
菜鸟笔记
>
菜鸟工具
>
参考手册
>
用户笔记
>
测验/考试
>
本地书签
>```
- 多个层级选择器(跨过中间某个标签)
- **用空格省略标签**
- 以上面的'.container.navigation > .row > .col.nav > #index-nav > li > a'为例,可以写成'.container.navigation li > a',过中间的一系列标签。最终得到的结果是一样的
- 获取标签之中的文本数据
- soup.a.text或者soup.a.string或者soup.a.get_text()
- text/get_text():可以获取某一个标签中所有的文本内容(就算文本内容不属于该标签的直系文本)
- string: 只可以获取该标签下的直系文本内容,例如div标签下的ul标签下的a标签中的文本内容则无法获取。而get_text()可以获取。
>```python
>a_list=soup.select('.container.navigation #index-nav > li')[0] # 解决空格的方法:用'.'代替空格
># 在bs4中只支持属性定位,而不支持索引定位
>print(a_list)
>print(a_list.string)
>print(a_list.get_text())
>```
得到结果如下
>```
>
首页
>首页
>首页
>```
而若是如下代码
>```python
>a_list=soup.select('.container.navigation #index-nav')[0] # 解决空格的方法:用'.'代替空格
># 在bs4中值支持属性定位,而不支持索引定位
>print(a_list.string)
>print(a_list.get_text())
>```
则得到结果如下
>```
>None
>
>首页
>菜鸟笔记
>菜鸟工具
>参考手册
>用户笔记
>测验/考试
>
>本地书签
>```
- 获取标签中属性值
- 直接中括号加属性名称即可,例如soup.a['href']
>```python
>a_list=soup.select('.container.navigation li') # 解决空格的方法:用'.'代替空格
># 在bs4中值支持属性定位,而不支持索引定位
>for each in a_list:
> print(each.a['href'])
>```
得到的情况如下所示
>```
>//www.runoob.com///www.runoob.com/w3cnote/https://c.runoob.com/
>javascript:void(0);
>//www.runoob.com/commentslist
>javascript:void(0);
>//www.runoob.com/browser-history
>```
**beautifulsoup解析网站数据实际案例**
爬取三国演义小说所有的章节标题和对应章节的内容 https://www.shicimingju.com/book/sanguoyanyi.html
>```python
>import requests
>import os
>from bs4 import BeautifulSoup
>
># 获得三国演义小说所有的章节标题和标题内容 https://www.shicimingju.com/book/sanguoyanyi.html
># 通用爬虫进行整张网页的爬取
>url='https://www.shicimingju.com/book/sanguoyanyi.html'
>#UA伪装
>headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36'
>}
># 得到整张页面数据
>page_text = requests.get(url=url,headers=headers).text
># bs4解析html源码
>soup = BeautifulSoup(page_text,'lxml')
># 定位标签位置
>a_list = soup.select('.book-mulu > ul > li > a') # 选择中了所有满足 .book-mulu > ul > li > a的标签,哪怕这两个标签是来自于不同的li
># 建立对应文件夹
>file_path='./三国演义'
>if not os.path.exists(file_path):
> os.mkdir(file_path)
>os.chdir(file_path)
># 爬取对应章节内容
>for each in a_list: # a_list里面的内容本身就是a标签
> title_name=each.string
> title_url='https://www.shicimingju.com'+each['href']
> print(title_url)
> chapter_page_text=requests.get(title_url,headers=headers).text
> # bs4进行数据解析
> soup = BeautifulSoup(chapter_page_text,'lxml')
> chapter_content = soup.find('div',class_='chapter_content').get_text()
> with open(title_name+'.txt','w',encoding='utf8') as fp:
> fp.write(chapter_content)
> print('章节 '+title_name+' 爬取成功')
>```
## 5.5. xpath进行数据解析
建议首选xpath进行数据解析式(跨编程语言)
- xpath的解析原理
- 1. 实例化一个etree对象,需要将被解析的页面源码数据加载到该对象中
- 2. 调用etree对象中的xpath方法结合xpath表达式实现标签定位和内容捕获
- 环境配置
- pip install lxml
- 如何实例化一个etree对象: from lxml impirt etree
- 1. 将本地的html文档中的源码数据加载到etree对象中
- etree.path(filepath)
- 2. 可以将从互联网上获取的页面源码数据加载到该对象中
- etree.HTML('page_text')
- xpath('xpath表达式')
- xpath表达式
- 在xpath中就是根据层级关系来进行标签定位的,也只能根据层级关系来进行定位
- /:表示的是从根节点开始定位,一个/表示的是一个层级
- //:表示多个层级,例如'/html//div'表示在html下的div标签(不管中间有多少层级)。写成'//div'表示从任意位置开始定位,定位到div。(将源码中所有的div都定位到)
- 属性定位://div[@class='song'] ,通用写法是 tag[@attrName='attrValue']
- 索引定位(位置定位):tree.xpath('//ul[@id="index-nav"]/li[3]')得到了第三个li标签,**注:索引是从1开始的,而不是0开始的**
- 获取标签对应的文本数据:
- '/text()' 取得的是某一个标签当中存储的直系文本内容
- '//text()' 标签中非直系的文本内容(所有的文本内容)
- 取得标签对应的属性值:
- '/@attrName'
- 节点缩写:
- . 表示当前节点
- .. 表示当前节点前一节点
>```python
>from lxml import etree
>
># 传入本地html文件,实例化一个etree对象
>parser=etree.HTMLParser(encoding='utf8') # 原来的html写的不够规范,所以需要指定parser
>tree = etree.parse('file.html',parser=parser)
>
># 假如想定位title标签
>r =tree.xpath('/html/head/title')
># /html前的'/'表示的是从根节点开始遍历的
># 返回的是一个列表,其中储存的是Element类型的对象,定位到了title对应的标签内容
>print(r)
>r = tree.xpath('//div/h1/a')
># 假如'/html/body/div'则,body下面有几个div就会返回多少个对象
>print(r)
>r = tree.xpath('//div[@class="col logo"]') # @后面进行属性定位,表示定位到class='col logo'的div
>print(r)
>r = tree.xpath('//ul[@id="index-nav"]/li[3]')
>print(r)
># 获取文本数据
>r = tree.xpath('//ul[@id="index-nav"]/li[3]/a/text()')
>print(r)
># 取得非直系文本
># 错误方式
>r = tree.xpath('//ul[@id="index-nav"]/li[3]/text()')
>print(r)
># 正确方式
>r = tree.xpath('//ul[@id="index-nav"]/li[3]//text()')
>print(r)
># 获取标签下的所有文本内容,但返回的是对应的列表
>r = tree.xpath('//ul[@id="index-nav"]/li//text()')
>print(r)
># 获取属性
>r = tree.xpath('//ul[@id="index-nav"]/li[3]/a/@href')
>print(r)
>```
得到的结果如下图所示
>```
>[
]
>[]
>[]
>[]
>['菜鸟工具']
>[]
>['菜鸟工具']
>['首页', '菜鸟笔记', '菜鸟工具', '参考手册', '用户笔记', '测验/考试', '本地书签']
>['https://c.runoob.com/']
>```
**实际案例**
例子:爬取58二手房的房源信息
>```python
>from lxml import etree
>import requests
>import os
># 首先爬取单张页面的数据
># https://hz.58.com/ershoufang/
># 解析房源名称
>url='https://hz.58.com/ershoufang/'
>headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36'
>}
># 获得整张页面的源码数据
>page_text=requests.get(url=url,headers=headers).text
># 进行数据解析
>tree = etree.HTML(page_text)
>xpath_express='//div[@class="content-side-left"]//ul[@class="house-list-wrap"]/li'
>li_list = tree.xpath(xpath_express) # 得到的是所有符合xpath_express条件下的div标签
>file_name='房源信息.txt'
>if os.path.exists(file_name):
> os.remove(file_name)
>fp=open(file_name,'w+',encoding='utf8')
>for li in li_list:
> # 局部数据的解析
> title=li.xpath('./div[@class="list-info"]//h2[@class="title"]/a/text()')[0] # 这边的“./”表示从当前节点开始查询
> # 不能够写成“//div[@class="list-info"]//h2[@class="title"]/a/text()”或者“li/div[@class="list-info"]//h2[@class="title"]/a/text()”
> # 也不可以写成“/div[@class="list-info"]//h2[@class="title"]/a/text()”,这样的话表示从根节点开始查找
> # 因此上述xpath表达式表示的是,当循环执行的时候获取了第一个li标签,然后在当前节点下解析到title文本
> # 循环执行,获取每个li标签下的文本数据
> company = li.xpath('./div[@class="list-info"]//div[@class="jjrinfo"]/span[@class="anxuan-qiye-text"][1]/text()')[0]
> person_in_charge = li.xpath('./div[@class="list-info"]//div[@class="jjrinfo"]/a/span[@class="jjrname-outer"]/text()')[0]
> base_info = li.xpath('./div[@class="list-info"]//p[@class="baseinfo"][1]/span/text()') # 遇到一个问题,每个房源的base_info信息是分三行显示的
> fp.writelines(title+'\n')
> fp.writelines(base_info)
> fp.write('\n')
> fp.writelines(company)
> fp.write('-')
> fp.writelines(person_in_charge)
> fp.write('\n\n')
> break
>fp.close()
>```
得到的文件内容如下所示
>```
>电梯房 125万 南北通透 精装修 3室2厅 8
>3室2厅2卫89.0㎡ 南北高层(共13层)
>上海泽恺企业营销策划中心-张俊杰
>```
----------------------------------
下面实现对各个分页进行爬取的功能
-----------------------------------
例子2:解析下载图片数据
爬取彼岸4k图片网站 http://pic.netbian.com/4kdongman/
>```python
>from lxml import etree
>import os
>import requests
>
>url='http://pic.netbian.com/4kqiche/'
>headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36'
>}
># 获得整张页面源码数据
>page_text = requests.get(url=url,headers=headers).text
># 数据解析
>tree = etree.HTML(page_text)
>li_list = tree.xpath('//div[@class="slist"]/ul[@class="clearfix"]/li')
>
>folder_path='图片信息爬取'
>if not os.path.exists(folder_path):
> os.mkdir(folder_path)
>os.chdir(folder_path)
>
>for li in li_list:
> src_url = li.xpath('./a/img/@src')[0]
> img_rul = 'http://pic.netbian.com'+ src_url
> # 访问图片源url发出请求
> img_data = requests.get(url=img_rul,headers=headers).content # 获取二进制数据
> file_name = li.xpath('./a/img/@alt')[0]+'.jpg' # 发现名字是乱码,是由于原始html的编码造成的
> with open(file_name,'wb') as fp:
> fp.write(img_data)
>```
乱码问题是由于原始html的编码造成的。**解决编码乱码问题:在请求响应的时候进行重新编码**,请求响应部分改成如下形式
>```python
> response = requests.get(url=url,headers=headers)
> response.encoding ='gbk' # 该网页采用gbk编码
> page_text = response.text
>```
也可以先转化为成iso编码然后解码成gbk编码, 不需要前面的response.encoding = 'utf-8'这段
>```python
> file_name = file_name.encode('iso-8859-1').decode('gbk')
>```
**但是!但是!按照以上方式爬取到额只能是预览图,并不是4k的格式的图片数据**
要下载4k图片需要登录才能实现,登录操作后面再讲解
案例3:解析出所有城市的名称 https://www.aqistudy.cn/historydata/
>```python
>from lxml import etree
>import os
>import requests
>
>url='https://www.aqistudy.cn/historydata/'
>headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36'
>}
>
>page_text = requests.get(url=url,headers=headers).text
>tree = etree.HTML(page_text)
># div = tree.xpath('/html/body/div[3]/div/div[1]')[0]
># div_list = tree.xpath('//div[@class="container"]/div[@class="row"]/div[1]') 两种方式均可
># 热门城市
>hot_city_li_list= tree.xpath('//div[@class="hot"]/div[@class="bottom"]/ul/li')
>hot_city_info=[]
>for li in hot_city_li_list:
> hot_city_name = li.xpath('./a/text()')[0]
> hot_city_data_url ='https://www.aqistudy.cn/historydata/'+ li.xpath('./a/@href')[0]
> hot_city_info.append([hot_city_name,hot_city_data_url])
># 全部城市
>all_city_info=[]
>all_city_li_list = tree.xpath('//div[@class="all"]/div[@class="bottom"]/ul/div[2]/li')
>for li in all_city_li_list:
> city_name = li.xpath('./a/text()')[0]
> city_data_url = 'https://www.aqistudy.cn/historydata/'+ li.xpath('./a/@href')[0]
> all_city_info.append([city_name,city_data_url])
>
>for each in all_city_info:
> print(each)
>```
但是发现上面的代码有很多重复的地方
>```python
> url='https://www.aqistudy.cn/historydata/'
> headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36'
> }
>
> page_text = requests.get(url=url,headers=headers).text
> tree = etree.HTML(page_text)
> # 想要解析到热门城市和所有城市对应的a标签
> # 发现热门城市对应的a标签和所有城市对应的a标签的层级是不一样的
> # 热门城市://div[@class="bottom"]/ul/li/a
> # 所有城市://div[@class="bottom"]/ul/div[2]/li/a
> a_list = tree.xpath('//div[@class="bottom"]/ul/li/a | //div[@class="bottom"]/ul/div[2]/li/a')
> # 表示将左边的xpath表达式或者右边的xpath表达式作用到xpath表达式中
> for a in a_list:
> city_name = a.xpath('./text()')[0]
> print(city_name)
>```
使用xpath的逻辑运算'|'可以定位到两个表达式匹配的标签
作业:爬取站长素材中免费简历素材 http://sc.chinaz.com/jianli/free.html
点击进入具体简历,进入下载地址,会让你选择一个下载途径,“福建电信下载”、“浙江联通下载”等,下载保存二进制数据rar格式的文件。并实现分页操作。
# 6. 验证码的识别
验证码与爬虫之间的关系:
- 验证码是网站采取的一种反爬机制
- 需要我们识别验证码图片中的数据用于模拟登录
- 识别验证码的操作
- 第三方软件自动识别
- 百度智能云平台 ,详细可参考 这个链接 https://www.baidu.com/link?url=uxeR26rCLVnNuX2KpqfFM4Lzb5JFpYC6GMTgQePtqrP8yq6PTWZt11YH4F7Ghvjy-j32IHuXCE6Z79biGgXvaa&wd=&eqid=98e9636b00017307000000055f86a8ac ,以sdk方式进行安装,详细文档如下:http://ai.baidu.com/ai-doc/OCR/3k3h7yeqa 。百度的这个api不仅可以实现验证码识别,还可以进行各种文字的识别。
- 验证码识别时,遇到图片是动态加载的链接时 https://blog.csdn.net/qq_49077418/article/details/108274944 ,则最好的办法是用截图进行图片保存,再来识别。
- 使用muggle_ocr库来进行识别;https://pypi.org/project/muggle-ocr/ 但是需要依赖tensorflow环境
案例:古诗文网登陆页面中的验证码的识别
- 获取网站的验证码图片所在的url:发现验证码是动态加载的图片链接
- 使用百度aip在线识别图片验证码
本案例只考虑保存的图片的文字识别
>```python
># 使用百度aip进行文字识别
>from lxml import etree
>import os
>import requests
>from aip import AipOcr
>from PIL import Image
>from io import BytesIO
>
>url='https://so.gushiwen.cn/user/login.aspx?from=http://so.gushiwen.cn/shiwenv.aspx?id=34710e44f31f'
>headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36'
>}
>#发起请求
>page_text = requests.get(url=url,headers=headers).text
># 数据解析
>tree = etree.HTML(page_text)
>code_img_url = 'https://so.gushiwen.cn' + tree.xpath('//*[@id="imgCode"]/@src')[0]
>#下载保存图片(由于这个验证码url并不是一个图片的格式,而百度aip智能实现图片格式的识别,所以需要先下载图片)
>img_data = requests.get(url=code_img_url,headers=headers).content
>
>img=Image.open(BytesIO(img_data)) # 以二进制的方式打开图片
>
>img.save('img.png')
># 使用百度aip进行文字识别
>APP_ID = '22828804'
>API_KEY = 'efw9OcLaGyL3q6fwkQweUzXV'
>SECRET_KEY = 'cGo2xvHKxDNwZDt7zA15GowlQZBoAjNR'
>
>client = AipOcr(APP_ID, API_KEY, SECRET_KEY)
>def get_img_data(file):
> with open(file,'rb') as fp:
> return fp.read()
>
>image = get_img_data('img.png')
>""" 如果有可选参数 """
>options = {}
>options["language_type"] = "CHN_ENG"
>options["detect_direction"] = "true"
>options["detect_language"] = "true"
>result = client.basicGeneral(image,options=options)
>
>print(result)
>```
注意这里使用了BytesIO打开了图片文件,假如不使用BytesIO则百度aip会返回格式错误的信息。(具体原因还不清楚,但转换成BytesIO对象不会出错)
BytesIO是以二进制的形式读取一张图片。io.BytesIO打开文件和open()打开文件的区别 https://stackoverflow.com/questions/42800250/difference-between-open-and-io-bytesio-in-binary-streams ,但是这里在用BytesIO打开后必须保存为png或者jpg格式,再进行读入,这是因为直接resopnse.content得到的图片格式是gif的,百度aip无法识别这种格式
最终得到result的信息如下
>```python
>{"words_result": [{'words': 'ZESNE'}],
> 'log_id': 1318377407022891008,
> 'words_result_num': 1,
> 'language': 0,
> 'direction': 0
>}
>```
# 7. 模拟登录实现流程整理
模拟登陆:
- 爬取基于某些用户的用户信息。(需要经过登录之后才能跳转到对应的页面当中)
需求:对古诗文网进行模拟登录
- 使用浏览器抓包工具,勾选preserve log选项。发现在点击登录按钮后,出现了一个名字以login开头的数据包。发现这个数据包是一个post请求,post请求一般都是携带参数的(data选项),而get请求最多会携带param参数
- 因此模拟登录只需要对对应的url发起post请求,传入对应的参数(用户名、密码、验证码...)
- 登录成功之后会得到一张页面,只需要对这张页面进行持久化存储即可。打开这张页面,假如和网页中的一样,则说明登录爬取成功
- 验证码:每次请求都会动态变化。要保证验证码和post请求是一一对应的
编码流程:
- 验证码识别:获取验证码图片的文字数据
- 对post请求进行发送(处理请求参数)
- 对响应数据进行持久化存储
>```python
>from lxml import etree
>import os
>import requests
>from aip import AipOcr
>from PIL import Image
>from io import BytesIO
>
>"""验证码的捕获和识别"""
>url = 'https://so.gushiwen.cn/user/login.aspx?from=http://so.gushiwen.cn/user/collect.aspx'
>headers = {
> 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36'
>}
># 发起请求
>page_text = requests.get(url=url, headers=headers).text
># 数据解析
>tree = etree.HTML(page_text)
>code_img_url = 'https://so.gushiwen.cn' + tree.xpath('//*[@id="imgCode"]/@src')[0]
># 获得参数
>__VIEWSTATE = tree.xpath('//*[@id="__VIEWSTATE"]/@value')[0]
>__VIEWSTATEGENERATOR = tree.xpath('//*[@id="__VIEWSTATEGENERATOR"]/@value')[0]
>print(__VIEWSTATE)
>print(__VIEWSTATEGENERATOR)
># 下载保存图片(由于这个验证码url并不是一个图片的格式,而百度aip智能实现图片格式的识别,所以需要先下载图片)
>img_data = requests.get(url=code_img_url, headers=headers).content
>img = Image.open(BytesIO(img_data)) # 以二进制的方式打开图片
>img.save('img.png')
># 使用百度aip进行文字识别
>APP_ID = '22828804'
>API_KEY = 'efw9OcLaGyL3q6fwkQweUzXV'
>SECRET_KEY = 'cGo2xvHKxDNwZDt7zA15GowlQZBoAjNR'
>client = AipOcr(APP_ID, API_KEY, SECRET_KEY)
>def get_img_data(file):
> with open(file, 'rb') as fp:
> return fp.read()
>image = get_img_data('img.png')
># result = client.basicGeneral(image)
>""" 如果有可选参数 """
>options = {}
>options["language_type"] = "CHN_ENG"
>options["detect_direction"] = "true"
>options["detect_language"] = "true"
>result = client.basicGeneral(image, options=options)
>verification_code = result['words_result'][0]['words']
>print(verification_code)
># 发起post请求(模拟登录)
># 抓包工具找到login数据包
>post_url = 'https://so.gushiwen.cn/user/login.aspx?from=http://so.gushiwen.cn/user/collect.aspx'
># 进行参数处理
>data={
> '__VIEWSTATE':__VIEWSTATE,
> '__VIEWSTATEGENERATOR': __VIEWSTATEGENERATOR,
> # 'from': 'http://so.gushiwen.cn/user/collect.aspx',
> 'email': '160********940@qq.com',
> 'pwd': 'Zho**********2',
> 'code': verification_code,
> 'denglu': '登录'
>}
>response = requests.post(url=post_url,data=data,headers=headers)
>page_text_login = response.text
>with open('page_login.html','w',encoding='utf-8') as fp:
> fp.write(page_text_login)
># 使用通用方式来查看post请求是否成功。查看post的状态码
>print(response.status_code) # 状态码200表示请求成功,但是请求成功和登录成功是两个概念
>```
不知道为啥这个保存的html始终显示提交的html验证码错误。。。。。。
# 8. 使用cookie进行模拟登录
模拟登录的目的就是为了爬取用户的用户信息数据
需求:爬取人人网登录后的用户个人主页的信息
**http/https协议特性**:无状态,当客户端向服务器端发出请求之后,**服务器端并不会记录当前客户端的用户状态。** 也就是说,服务器端不知道你这个帐号是否是出于登录状态。
cookie: 用来让服务器端记录客户端的相关状态。(是由服务器端创建,保存在客户端上的)
- 手动处理cookie:通过抓包工具获取cookie值,封装入headers中
- 自动处理cookie:
- 思考问题:网站的cookie值是从哪里来的?
- 模拟登录post请求后,由服务器端创建
- session会话对象
- 作用:
- 1. 可以进行请求的发送
- 2. 如果请求过程中产生了cookie,则该cookie会被自动存储/携带在该session对象中
- 步骤:
- 创建一个session对象:request.Session()
- 使用session对象进行模拟登陆post请求的发送(cookie会被存储在session对象中)
- 使用session对象对个人主页对应的get请求进行发送(携带了cookie进行的一次发送)
**手动处理cookie**
以人人网为例,登录后对用户头像进行点击(网址为 http://www.renren.com/436271706/profile ),发送请求,可以在浏览器抓包工具中找到profile文件。发现profile文件的request headers里面有个参数是cookie。也就是说在登录后,对这个url发请求,是携带了cookie值的。这组值就是服务器端用来记录客户端状态的一组数据值。
>```python
># 获取当前用户的个人信息
>detail_url = 'http://www.renren.com/436271706/profile'
>headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36',
> 'Cookie':'anonymid=khohpriq-dcldl; depovince=GW; _r01_=1; taihe_bi_sdk_uid=1a165c66e7fa501b12f2d762822267d5; _de=CE7251CCBC54A589D1308F00DED45C956DEBB8C2103DE356; jebecookies=b82e82e2-3329-40d6-b88e-f5e3117092f3|||||; JSESSIONID=abcbqDqKaYfS98qxuUJxx; ick_login=a13897f6-f616-4a87-a285-e7f35e280277; taihe_bi_sdk_session=97102f6b6d2813b549fd0900bd30152d; p=2b9a300502ebbe6620954397eee0e1e56; first_login_flag=1; ln_uact=1603527940@qq.com; ln_hurl=http://hdn.xnimg.cn/photos/hdn421/20130926/0650/h_main_uUeG_547a000005be113e.jpg; t=58ba71f0abd76eab57f13df9a212cc046; societyguester=58ba71f0abd76eab57f13df9a212cc046; id=436271706; xnsid=1c469d86; ver=7.0; loginfrom=null'
>}
>detail_page= requests.get(url=detail_url,headers=headers).text # 获得详情页面信息
>with open('file.html','w',encoding='utf8') as fp:
> fp.write(detail_page)
>```
上述代码中,因为已经获取了cookie,服务器已经知道了客户端处于登录状态,因此不需要再登录。
模拟登录后爬取到的网页信息结果如下:
**不过不推荐使用上述方式,上述是一种手动获取cookie的方式**
有些网站的cookie具有有效时长,超出cookie的生命周期就会失效;有些网站的cookie是动态变化的cookie,不是静态cookie。因此不推荐使用手动写入的方式进行cookie的设置。
**自动处理cookie**
>```python
>import requests
>from lxml import etree
>import muggle_ocr
>
># -------------------------为了识别验证码----------------------------------------
>url='http://www.renren.com/' # 人人网链接
>headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36'
>}
># 爬取整张页面信息
>page_text = requests.get(url=url,headers=headers).text
>tree = etree.HTML(page_text)
># 找到并保存验证码
>code_img_url= tree.xpath('//*[@id="verifyPic_login"]/@src')[0]
>data = requests.get(url=code_img_url,headers=headers).content
># code_img = Image.open(BytesIO(data))
>file_name='img.png'
># code_img.save(file_name)
># 存入图片
>with open(file_name,'wb') as fp:
> fp.write(data)
># 读入验证码图片
>def get_code_img_data(file_name):
> with open(file_name,'rb') as fp:
> return fp.read()
>image = get_code_img_data(file_name)
># 初始化模型
>sdk=muggle_ocr.SDK(model_type=muggle_ocr.ModelType.Captcha)
>verification_code=sdk.predict(image_bytes=image)
>print('识别到的验证码是:',verification_code)
>
>#----------------------------进行模拟登陆-------------------------------------------
># 创建一个session对象
>session = requests.Session()
>url = 'http://www.renren.com/ajaxLogin/login?1=1&uniqueTimestamp=2020931648440'
>data = {
> 'email': '160*****40@qq.com',
> 'icode': verification_code,
> 'origURL': 'http://www.renren.com/home',
> 'domain': 'renren.com',
> 'key_id': '1',
> 'captcha_type': 'web_login',
> 'password': '7e0300ddbb5e1821e98e92581cf845559ba30170f699f6c8a79450acf893e2f3',
> 'rkey': '1675e02911435a4867105858c862a18c',
> 'f': 'http%3A%2F%2Fwww.renren.com%2F436271706'
>}
># 对模拟登录的对象使用session进行请求发送
>r = session.post(url=url,headers=headers,data=data)
>print('响应状态码:',r.status_code)
># ----------------------------进行个人主页信息的爬取--------------------------
>detail_url = 'http://www.renren.com/436271706/profile'
># 使用session进行get请求的发送
>detail_page= session.get(url=detail_url,headers=headers).text # 获得详情页面信息
>with open('file.html','w',encoding='utf8') as fp:
> fp.write(detail_page)
>```
最终实现了自动对个人主页进行爬取。由于使用muggle_ocr进行验证码的识别,需要加载tensorflow因此速度比较慢。当验证码比较简单时muggle_ocr能够准确识别,当验证码之间有重叠或者验证码扭来扭去的这种,muggle_ocr比较难识别。
# 9. 代理理论(破解封IP的反爬机制)
- 什么是代理
- 代理服务器:使用代理,我们就是将请求发送给代理服务器,再用代理服务器将请求发送给对应的IP
- 代理的作用:
- 突破自身IP限制
- 可以隐藏自身真实的IP,防止自身IP被攻击
- 代理相关的网站:
- 快代理
- 西祠代理
- www.goubanjia.com
- https://www.zdaye.com/dayProxy.html
- 代理ip的类型
- http: 只能应用到HTTP对应的url中
- https: 只能应用到HTTPS对应的url中
- 代理IP的匿名度
- 透明:服务器知道该次请求使用了代理,也知道请求对应的真实ip
- 匿名:服务器知道使用了代理,但是不知道真实的ip
- 高匿:服务器不知道使用了代理,更不知道你的真实ip
## 9.1. 代理在爬虫中的应用
访问网址查看本机IP地址
>```python
>import requests
>
># 下面这个网址可以查看访问的IP
>url='https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&ch=14&tn=98010089_dg&wd=ip'
>headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36'
>}
>
>page_text = requests.get(url=url,headers=headers).text
>with open('ip.html','w',encoding='utf8') as fp:
> fp.write(page_text)
>```
得到结果如下所示:
**使用代理IP**
>```python
>import requests
>
># 下面这个网址可以查看访问的IP
>url='https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&ch=14&tn=98010089_dg&wd=ip'
>headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36'
>}
>headers_proxies={
> 'https':'171.35.173.30:9999'
>}
>
>r = requests.get(url=url,headers=headers,proxies=headers_proxies)
>print(r.status_code)
>page_text = r.text
>with open('ip.html','w',encoding='utf8') as fp:
> fp.write(page_text)
> ```
得到结果如下
但是在使用代理时,很多免费代理IP是失效的,但是收费代理IP又很贵,因此需要我们自己构建有效的免费代理IP。详情可见:https://blog.csdn.net/weixin_44517301/article/details/103393145 ,里面有手动构建代理IP池和使用“开源ip代理池—ProxyPool”构建代理IP的方法。
**在编写爬虫过程中,如果遇到了封IP的反爬机制,则使用代理ip进行反反爬**
# 10. 高性能异步爬虫
- 目的:在爬虫中使用异步实现高性能的数据爬取操作,
## 10.1. 单线程下的串行数据爬取
>```python
>import requests
>import time
>headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36'
>}
>
>urls = [
> 'https://www.runoob.com/sql/sql-tutorial.html',
> 'https://www.runoob.com/sql/sql-intro.html',
> 'https://www.runoob.com/sql/sql-syntax.html',
> 'https://www.runoob.com/sql/sql-select.html'
>]
>
>def get_content(url):
> print('正在爬取:',url)
> # get方法是一个阻塞的方法,只有将阻塞的方法运行完之后才会继续运行其他的代码
> response = requests.get(url=url,headers=headers)
> if response.status_code == 200: # 说明请求响应成功
> return response.content
> else:
> print('数据爬取失败:',url)
> return None
>
>def parse_content(content): # 解析数据
> print('响应数据的长度为:',len(content)) # 模拟解析数据操作
>
>start_time = time.time()
># 遍历所要访问的url
>for url in urls:
> content = get_content(url)
> parse_content(content)
>end_time=time.time()
>print('一共耗时 %f seconds'%(end_time-start_time))
>```
得到的结果如下:
>```python
>正在爬取: https://www.runoob.com/sql/sql-tutorial.html
>响应数据的长度为: 57430
>正在爬取: https://www.runoob.com/sql/sql-intro.html
>响应数据的长度为: 58411
>正在爬取: https://www.runoob.com/sql/sql-syntax.html
>响应数据的长度为: 59382
>正在爬取: https://www.runoob.com/sql/sql-select.html
>响应数据的长度为: 59186
>一共耗时 0.787958 seconds
>```
单线程下的串行数据爬取,会在每次爬取数据时将程序阻塞,只有当阻塞的方法/函数执行完之后才会继续执行后面的代码,比较耗时。
## 10.2. 异步爬虫
- 异步爬虫的方式
- 多线程或者多进程(不建议使用)
- 好处:可以为相关阻塞的操作单独开启线程/进程,阻塞操作就可以异步执行
- 弊端:无法无限制地开启多线程或者多进程。
- 线程池、进程池(适当使用)
- 好处:可以降低系统对进程/线程创建和销毁的频率,从而降低系统的开销
- 弊端:池中进程/线程的数量是有上限的。当爬取的数据进程/线程远远大于池中进程/线程数量的时候,对于爬取速度的提升就没那么明显了。
- 单线程+异步协程(推荐)
- event_loop:事件循环,相当于一个无限循环,我们可以把一些函数注册到事件循环上,当满足某些条件时,函数会被循环执行
- coroutine:协程对象,我们可以将协程对象注册到事件循环中,它会被事件循环调用,我们可以用async关键字定义一个方法,这个方法在调用时不会立刻被执行,而是返回一个协程对象。
- task:任务,它是协程对象的进一步封装,包含了任务的各个状态
- future:代表将来执行或还没有被执行的任务,实际上和task没有本质区别
- async:定义一个协程
- await:用来挂起阻塞方法的执行
### 10.2.1. 线程池
#### 10.2.1.1. 使用multiprocessing模块
- from multiprocessing.dummy import Pool 导入线程池
- pool.map(func,iterable) 这个map()和python标准的map()函数的功能其实是一样的
>```python
>from multiprocessing.dummy import Pool
>p=Pool(4)
>help(p.map)
>Help on method map in module multiprocessing.pool:
>
>map(func, iterable, chunksize=None) method of multiprocessing.pool.ThreadPool instance
> Apply `func` to each element in `iterable`, collecting the results
> in a list that is returned.
> ```
展示代码
>```python
>import requests
>import time
>from multiprocessing.dummy import Pool # 导入线程池
>
>headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36'
>}
>
>urls = [
> 'https://www.runoob.com/sql/sql-tutorial.html',
> 'https://www.runoob.com/sql/sql-intro.html',
> 'https://www.runoob.com/sql/sql-syntax.html',
> 'https://www.runoob.com/sql/sql-select.html'
>]
>
>def get_content(url):
> print('正在爬取:',url)
> time.sleep(2)
> # get方法是一个阻塞的方法,只有将阻塞的方法运行完之后才会继续运行其他的代码
> response = requests.get(url=url,headers=headers)
> if response.status_code == 200: # 说明请求响应成功
> return response.content
> else:
> print('数据爬取失败:',url)
> return None
>
>def parse_content(content): # 解析数据
> print('响应数据的长度为:',len(content)) # 模拟解析数据操作
>
>def process(url):
> parse_content(get_content(url))
>
>start_time = time.time()
># 实例化一个线程池对象
>pool = Pool(4) # 开辟4个线程对象
>content_list = pool.map(process,urls) # pool.map()函数,第一个参数传入的是一个函数,第二个是需要传入一个可迭代对象
>end_time=time.time()
>print('使用多线程一共耗时 %f seconds'%(end_time-start_time))
>
># 假如不使用多线程
>start_time = time.time()
>for each in urls:
> process(each)
>end_time = time.time()
>print('单线程线程一共耗时 %f seconds'%(end_time-start_time))
>```
得到结果如下;
>```python
>正在爬取: https://www.runoob.com/sql/sql-tutorial.html
>正在爬取: https://www.runoob.com/sql/sql-intro.html
>正在爬取: https://www.runoob.com/sql/sql-syntax.html
>正在爬取: https://www.runoob.com/sql/sql-select.html
>响应数据的长度为: 57430
>响应数据的长度为: 59186
>响应数据的长度为: 59382
>响应数据的长度为: 58411
>使用多线程一共耗时 2.640870 seconds
>
>正在爬取: https://www.runoob.com/sql/sql-tutorial.html
>响应数据的长度为: 57430
>正在爬取: https://www.runoob.com/sql/sql-intro.html
>响应数据的长度为: 58411
>正在爬取: https://www.runoob.com/sql/sql-syntax.html
>响应数据的长度为: 59382
>正在爬取: https://www.runoob.com/sql/sql-select.html
>响应数据的长度为: 59186
>单线程线程一共耗时 8.423171 seconds
>```
#### 10.2.1.2. 线程池实际案例
需求:爬取梨视频网站上的视频
**原则**:线程池处理的是**阻塞且耗时**的操作
定位到梨视频网站的“新知”板块
>```python
>import requests
>import time
>from multiprocessing.dummy import Pool # 导入线程池
>from lxml import etree
>
>headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36'
>}
>
># 对下属url发请求,解析出视频详情页的url和视频的名称
>url = 'https://www.pearvideo.com/category_10'
>
>page_text = requests.get(headers=headers,url=url).text
>
>tree = etree.HTML(page_text)
>li_list=tree.xpath('//ul[@id="listvideoListUl"]/li')
>for li in li_list:
> detail_url ='https://www.pearvideo.com/' + li.xpath("./div[@class='vervideo-bd']/a/@href")[0]
> video_name = li.xpath('.//div[@class="vervideo-title"]/text()')[0] + '.mp4'
> # 对详情页的url发出请求
> detail_page_text = requests.get(url=detail_url,headers=headers).text
> tree = etree.HTML(detail_page_text)
> # 找到存储视频对应的url
> video_url = tree.xpath('//div[@class="main-video-box"]//video/@src')
> print(video_url) # 发现解析出来的@src下面的值为[]
>```
得到结果如下:
>```
>[]
>[]
>[]
>[]
>```
发现解析出来的@src下面的值为[],为什么会这样?
这是因为对url进行访问得到的数据,不一定是对本url请求得到的,也有可能是动态加载得到的。打开抓包工具,只有本url的response里面的数据才是通过本url请求得到的(如下图所示)。其他的数据都是通过加载得到的。
对其进行"video"标签的搜索,发现搜索不到对应的标签。因此,当前源码中并没有video所对应标签,得出结论,视频是动态加载的。在抓包工具XHR中发现了响应得到的json数据。(假如是在网页的js代码中的,则要考虑正则表达式来解析了,因为xpath和bs4都不能解析js代码)
但是发现这个ajax请求的request url信息如下,
>```
>Request URL: https://www.pearvideo.com/videoStatus.jsp?contId=1707286&mrd=0.9042121874588889
>Request Method: GET
>Status Code: 200 OK
>Remote Address: 203.107.32.197:443
>Referrer Policy: no-referrer-when-downgrade
>```
那我们怎么获得request url中的contId和mrd这两个参数的信息呢? 参考了一些资料:https://www.jb51.net/article/199219.htm
mrd是我们不需要的信息,网站里面一些参数是不需要的,是为了防止被爬虫爬取的。
假如我们直接按如下形式发送请求,是不行的,会得到文章已下线的信息,这是一种反爬措施。
>```
>url = 'https://www.pearvideo.com/videoStatus.jsp'
>params={
> 'contId':1707286
>}
>page_text = requests.get(url=url,headers=headers,params=params).text
>```
以上代码会得到如下结果:
>```
>{
> "resultCode":"5",
> "resultMsg":"该文章已经下线!",
> "systemTime": "1606654920664"
>}
>```
我们来关注request headers 中的信息,发现有一个Referer参数
>```
>Host: www.pearvideo.com
>Referer: https://www.pearvideo.com/video_1707286
>```
#### 10.2.1.3. 关于referer
HTTP Referer是header的一部分,当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器该网页是从哪个页面链接过来的,服务器因此可以获得一些信息用于处理。
**有些反爬机制就会识别referer**,看看是否正常(一般检查是否为空)。
那么什么时候referer会为空呢?
1.你直接从浏览器的地址栏中输入网址时(或者像Chrome的书签栏中);
2.你写python爬虫,没有指定referer时
因此,这种情况下需要我们给headers里面指定Referer值。
**只要是Request Headers里面的信息,都有可能是反爬措施。**
改造后的代码如下
>```python
>import json
>import requests
>url = 'https://www.pearvideo.com/videoStatus.jsp'
>params={
> 'contId':1707286
>}
>
>headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36',
> 'Referer':'https://www.pearvideo.com/video_'+str(params['contId'])
>}
>
>r = requests.get(url=url,headers=headers,params=params)
>with open('file.json','w',encoding='utf8') as fp:
> json.dump(r.json(),fp=fp,ensure_ascii=False)
> ```
得到结果如下:
>```json
>{
>"resultCode":"1",
>"resultMsg":"success",
>"reqId":"650b3bb4-7ca5-4211-a054-d2f240b7f710",
>"systemTime":"1606656222653",
>"videoInfo":{
> "playSta":"1",
> "video_image":"https://image2.pearvideo.com/cont/20201116/cont-1707286-12508784.png",
> "videos":{
> "hdUrl":"",
> "hdflvUrl":"",
> "sdUrl":"",
> "sdflvUrl":"",
> "srcUrl":"https://video.pearvideo.com/mp4/adshort/20201116/1606656222653-15486338_adpkg-ad_hd.mp4"
> }
> }
>}
>```
发现在headers中加入referer参数后就能够正常请求到响应的数据了。而在返回的json数据中的“srcUrl”中则包含了视频对应的下载地址。
因此,我们只需要获得这个视频的contId值,即可得到下载链接。而contId值又是在最初爬取到的html中可以定位到的。因此整理后,我们的代码如下所示:
>```python
>import requests
>from lxml import etree
>
>headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36'
>}
>
># 对下属url发请求,解析出视频详情页的url和视频的名称
>url = 'https://www.pearvideo.com/category_10'
>
>page_text = requests.get(headers=headers,url=url).text
>
>tree = etree.HTML(page_text)
>li_list=tree.xpath('//ul[@id="listvideoListUl"]/li')
>for li in li_list:
> detail_url ='https://www.pearvideo.com/' + li.xpath("./div[@class='vervideo-bd']/a/@href")[0]
> video_name = li.xpath('.//div[@class="vervideo-title"]/text()')[0] + '.mp4'
>
> # 定位得到contId值
> contId = li.xpath("./div[@class='vervideo-bd']/a/@href")[0].replace('video_','')
> video_info_url = 'https://www.pearvideo.com/videoStatus.jsp?contId='+contId
> headers['Referer'] = detail_url
> # 重新发起请求
> r = requests.get(url=video_info_url,headers=headers)
> video_download_url = r.json()['videoInfo']['videos']['srcUrl']
> print(video_download_url)
> ```
得到的结果如下所示
>```
>https://video.pearvideo.com/mp4/short/20180227/1606658812124-11620765-hd.mp4
>https://video.pearvideo.com/mp4/adshort/20201116/1606658812252-15486338_adpkg-ad_hd.mp4
>https://video.pearvideo.com/mp4/adshort/20201126/1606658812391-15503995_adpkg-ad_hd.mp4
>https://video.pearvideo.com/mp4/adshort/20201127/1606658812561-15505726_adpkg-ad_hd.mp4
>```
我们得到了视频的下载地址,下面就可以对视频实施下载了。为了保证下载速度,我们使用多线程/进程下载。
但是如果按照上述链接进行爬取,则会发现爬取下来的文件是不可用的,打开上述链接只能得到404页面。**这是因为网站采用了反爬措施,ajax请求得到的srcurl数据不是真正的视频对应的url,而是经过“伪装”的**,在Element界面下,我们发现视频播放地址如下所示。
>```
>Element界面下的实际视频链接:
> https://video.pearvideo.com/mp4/adshort/20201116/cont-1707286-15486338_adpkg-ad_hd.mp4
>ajax请求加载得到的json数据中的视频链接:
> https://video.pearvideo.com/mp4/adshort/20201116/1606658812252-15486338_adpkg-ad_hd.mp4
>```
故,我们需要将json数据中srcurl中的“systemTime”替换成“cont-”+contId的形式。
最终代码如下:
>```python
>import requests
>from multiprocessing.dummy import Pool # 导入线程池
>from lxml import etree
>import re
>
>headers={
> 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36'
>}
>
># 对下属url发请求,解析出视频详情页的url和视频的名称
>url = 'https://www.pearvideo.com/category_10'
>
>page_text = requests.get(headers=headers,url=url).text
>
>tree = etree.HTML(page_text)
>li_list=tree.xpath('//ul[@id="listvideoListUl"]/li')
>video_info_list = []
>for li in li_list:
> detail_url ='https://www.pearvideo.com/' + li.xpath("./div[@class='vervideo-bd']/a/@href")[0]
> video_name = li.xpath('.//div[@class="vervideo-title"]/text()')[0] + '.mp4'
> # 定位得到contId值
> contId = li.xpath("./div[@class='vervideo-bd']/a/@href")[0].replace('video_','')
> video_info_url = 'https://www.pearvideo.com/videoStatus.jsp?contId='+contId
> headers['Referer'] = detail_url
> # 重新发起请求
> r = requests.get(url=video_info_url,headers=headers)
> url = r.json()['videoInfo']['videos']['srcUrl']
> # 获取正确的url。(使用正则表达式进行替换)
> pattern = r'/\d{13}-'
> repl = '/cont-'+contId+"-"
> video_download_url = re.sub(pattern,repl,url)
> # 将视频的信息按照字典的形式打包到列表中
> video_info_list.append({'name':video_name,'url':video_download_url})
># 下面进行下载操作
># 使用线程池对视频数据进行请求(较为耗时的操作)
>pool = Pool(4)
>def get_video(dic):
> url = dic['url']
> name = dic['name']
> print('正在爬取:',url)
> r = requests.get(url=url,headers=headers)
> print('正在储存:',url)
> with open(name,'wb') as fp:
> fp.write(r.content)
>
>pool.map(get_video,video_info_list)
>```
### 10.2.2. 协程回顾
在异步协程中如果出现了同步模块相关的代码,则无法实现异步。因此不能用time.sleep()而是要用ayncio.sleep()方法。遇到阻塞代码,需要用await来进行手动挂起。
### 10.2.3. aiohttp模块
由于在异步操作中出现同步模块无法实现异步操作,因此在异步爬虫爬取过程中,不能直接使用requests模块。(requests发起的请求是基于同步的)必须使用基于异步的网络请求模块,进行指定url的请求发送。
#### 10.2.3.1. aiohttp+协程实现异步爬虫
>```python
>import asyncio
>import aiohttp # 使用该模块中的ClientSsiion
>
>urls=[]
>
>async def get_page(url):
> async with aiohttp.ClientSession() as session: # 这个是asyncio的上下文管理
> # get()、post()
> # 对get()或者post()添加headers参数
> # 添加请求参数params/data,proxy='http://ip:port' 代理ip
> async with await session.get(url) as resopnse: # 这里的get()是一个耗时操作,所以需要用await进行手动挂起
> # text()方法可以返回字符串形式的响应数据
> # read()方法返回的是二进制形式的响应数据
> # json()方法返回的就是json对象
> # 在使用aiohttp模块时,在获取响应数据前一定要对响应数据进行挂起
> page_text=await response.text()
> print(page_text)
>
>tasks=[]
>for url in urls:
> c=get_page(url)
> task =asyncio.ensure_future(c)
> tasks.append(task)
>
>loop = asyncio.get_event_loop()
>loop.run_until_complete(asyncio.wait(tasks))
>```
# 11. selenium 简介
问题:selenium模块和爬虫之间是什么样的关系?
- 便捷的获取网站中动态加载的数据
- 便捷地实现模拟登陆
什么是selenium?
- 基于浏览器自动化的模块。
**1. selenium的使用流程:**
- pip install selenium
- 下载一个浏览器的驱动程序(要有对应版本浏览器的驱动程序)
- 实例化一个浏览器对象:
- 编写基于浏览器自动化的操作代码
- 发起请求:get(url)方法
- 标签定位:find系列的方法
- 标签交互(键盘输入):send_keys('xxxx')
- 执行js程序:execute_script('js_code')
- 前进、后退:back()、forward()
- 关闭浏览器:quit()
注意:在vscode中自动调试selenium时,会自动关闭浏览器窗口,这是由于IDE的垃圾回收机制引起的。直接右键在终端运行,则不会自动关闭
实例:使用selenium爬取食品药品总局的网站。http://scxk.nmpa.gov.cn:81/xk/
>```python
>from selenium import webdriver
>from lxml import etree
># 实例化一个浏览器对象,传入浏览器驱动
>broser = webdriver.Chrome(executable_path='./chromedriver.exe') # 传入浏览器的驱动所在的目录
># 让浏览器发起一个指定url的请求
>broser.get('http://scxk.nmpa.gov.cn:81/xk/')
># ----------------爬取网站中动态加载的企业数据-----------------------------
># 获取浏览器当前页面的源码数据
>page_text = broser.page_source # 该属性返回页面源码数据
>
># 解析企业数据
>tree = etree.HTML(page_text)
>li_list = tree.xpath('//ul[@id="gzlist"]/li')
>for li in li_list:
> name = li.xpath('./dl/@title')[0]
> print(name)
># 可以time.sleep() 进行停留
>broser.quit() # 关闭浏览器
>```
发现这样操作之后能够获取得到对应企业的数据。这是因为selenium是通过模拟浏览器的行为来进行的,因此所得到的结果和直接用浏览器是一样的。而request模块则不同。
>```
>河南波斯坦生物科技有限公司
>谢馥春(江苏)美妆实业股份有限公司
>江苏博后智造生物科技有限公司
>吉林省塔姆化妆品有限公司
>福建省梦娇兰日用化学品有限公司
>桂林市高乐医药保健品有限公司
>昆明锐斯得科技有限公司
>重庆小丸生物科技股份有限公司
>广州中科佰氏健康产业有限公司
>广州瑞美堂生物科技有限公司
>广东优品生物科技有限公司
>广州欧特丽美容生物科技有限公司
>广州鑫蕊生物科技有限公司
>广州全亚化妆品国际实业有限公司
>广州市锦致精细化工有限公司
>```
**2. selenium的其他操作**
>```python
>from selenium import webdriver
>from lxml import etree
>import time
># 实例化一个浏览器对象,传入浏览器驱动
>broser = webdriver.Chrome(executable_path='./chromedriver.exe') # 传入浏览器的驱动所在的目录
># 让浏览器发起一个指定url的请求
>broser.get('https://www.taobao.com/')
># 往当前页面的搜索款中录入一个词
># 首先找到搜索框,实现标签定位.发现搜索框是一个input标签下id=q的标签
>search_input = broser.find_element_by_id('q')
># 标签的交互
>search_input.send_keys('Iphone') # 模拟键盘输入
># 需要点击搜索按钮
># 搜索按钮定位
>search_button = broser.find_element_by_class_name('btn-search')
># search_button = broser.find_element_by_css_selector('.bin-search')
>search_button.click() # 点击搜索按钮
>time.sleep(3)
>broser.quit()
>```
下面实现滚轮拖动等效果。
可以让浏览器在consle面板中执行一段js代码
>```javascript
> window.scrollTo(0,document.body.scrollHeight) // 滚动一屏幕的高度
>```
>```python
># -------------------------------------
># 执行一组js代码
>broser.execute_script('window.scrollTo(0,document.body.scrollHeight)')
>time.sleep(2)
>```
综合案例如下:
>```python
>from selenium import webdriver
>from lxml import etree
>import time
># 实例化一个浏览器对象,传入浏览器驱动
>broser = webdriver.Chrome(executable_path='./chromedriver.exe') # 传入浏览器的驱动所在的目录
># 让浏览器发起一个指定url的请求
>broser.get('https://www.taobao.com/')
># 往当前页面的搜索款中录入一个词
># 首先找到搜索框,实现标签定位.发现搜索框是一个input标签下id=q的标签
>search_input = broser.find_element_by_id('q')
># 标签的交互
>search_input.send_keys('Iphone')
># -------------------------------------
># 执行一组js代码
>broser.execute_script('window.scrollTo(0,document.body.scrollHeight)')
>time.sleep(2)
># 需要点击搜索按钮
># 搜索按钮定位
>search_button = broser.find_element_by_class_name('btn-search')
># search_button = broser.find_element_by_css_selector('.bin-search')
>search_button.click() # 点击搜索按钮
>
># 再对其他的url发起请求
>broser.get('https://www.baidu.com')
>time.sleep(2)
>broser.back() # 当前浏览器进行页面回退
>time.sleep(1)
>broser.forward() # 实现页面的前进
>
>time.sleep(2)
>broser.quit()
>```
**3. selenium处理iframe+动作链**
- iframe
- 在一张页面中可以嵌套一个子页面,这个子页面就可以用iframe来实现
- 案例网址: https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable
- selenium处理iframe
- 如果定位的标签存在于iframe标签之中,则必须使用switch_to.frame('frame_id')
- 动作链
- from selenium.webdeiver import ActionChains
- 实例化一个动作链对象:action = ActionChains(browser)
- click_and_hold(div): 长按且点击
- move_by_offset(xoffset,yoffset): 移动操作
- perform()让动作链立即执行
- action.release()释放动作链对象
>```python
>from selenium import webdriver
># 实例化一个浏览器对象,传入浏览器驱动
>broser = webdriver.Chrome(executable_path='./chromedriver.exe') # 传入浏览器的驱动所在的目录
># 让浏览器发起一个指定url的请求
>broser.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')
>div = broser.find_element_by_id('draggable')
>print(div)
>```
发现结果报错NoSuchElementException。原因是我们所定位的div标签处于一个iframe标签中,是处于一个子页面的标签中
具体实现拖动的代码如下
>```python
>from selenium import webdriver
>from selenium.webdriver import ActionChains # 导入动作链
>from lxml import etree
>import time
># 实例化一个浏览器对象,传入浏览器驱动
>broser = webdriver.Chrome(executable_path='./chromedriver.exe') # 传入浏览器的驱动所在的目录
>broser.maximize_window()
># 让浏览器发起一个指定url的请求
>broser.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')
># --------------------------iframe 定位----------------------------------------
># 如果存在的标签是存在于iframe标签之中的,则必须通过如下方法进行标签定位
># 切换浏览器标签的作用域
>broser.switch_to.frame('iframeResult') # 传入iframe对应的id
>div = broser.find_element_by_id('draggable')
># ---------------------------动作链------------------------------------------
># 下面我们实现拖动标签。
># 实现拖动的逻辑:按下鼠标(长按)不放,移动,松开鼠标
>action =ActionChains(broser)
>action.click_and_hold(div) # 点击且长按指定的标签
># action.drag_and_drop() 方法也可以实现类似功能,不过是定位标签
>for i in range(5):
> # perform()表示立即执行动作连操作
> # move_by_offset(xoffset,yoffset)
> action.move_by_offset(17,0).perform() # 移动偏移
> time.sleep(0.3)
># 室放动作链
>action.release()
>broser.quit()
>```
**4. selenium模拟登录**
案例:模拟登录qq空间 https://qzone.qq.com/
注意:在安全验证的前必须等待几秒,不然会报错。因为安全验证的代码是后面动态加载的,需要等它加载完毕。
>```python
>from selenium import webdriver
>from selenium.webdriver import ActionChains # 导入动作链
>import time
># 实例化一个浏览器对象,传入浏览器驱动
>broser = webdriver.Chrome(executable_path='./chromedriver.exe') # 传入浏览器的驱动所在的目录
>broser.maximize_window()
>broser.get('https://qzone.qq.com/')
># 标签定位
># 找到iframe位置
>broser.switch_to.frame('login_frame')
># 选择账号密码登录
>account_login_button = broser.find_element_by_id('switcher_plogin')
>account_login_button.click()
># 找到账号密码输入框
>account_input = broser.find_element_by_id('u')
>account_input.send_keys('160*****940')
>password_input = broser.find_element_by_id('p')
>password_input.send_keys('Zhon**********###')
>button = broser.find_element_by_class_name('login_button')
>button.click()
># 滑动验证
># 定位到新的iframe
>time.sleep(2) # 必须等待几秒,不然会报错
>iframe = broser.find_element_by_xpath('//iframe[@id="tcaptcha_iframe"]')
>broser.switch_to.frame(iframe)
># 定位滑动条
>drag_bar = broser.find_element_by_xpath('//img[@id="slideBlock"]')
>action = ActionChains(broser)
>action.click_and_hold(drag_bar)
>action.move_by_offset(xoffset=180,yoffset=0)
>action.release()
>
>time.sleep(20)
>```
**5. 无头浏览器 + 规避检测**
无头浏览器,也叫做无可视化界面浏览器。即显式的弹出浏览器窗口。
>```python
>from selenium import webdriver
>from selenium.webdriver import ActionChains # 导入动作链
>from selenium.webdriver.chrome.options import Options # 带入设置参数
>import time
># 设置无头浏览器参数
>chrome_options = Options()
>chrome_options.add_argument('--headless')
>chrome_options.add_argument('--disable-gpu')
>
># 实例化一个浏览器对象,传入浏览器驱动
>broser = webdriver.Chrome(executable_path='./chromedriver.exe',options=chrome_options) # 传入浏览器的驱动所在的目录
># 无可视化界面(无头浏览器)phantomJs
>broser.get('https://www.baidu.com')
>print(broser.page_source)
>
>time.sleep(2)
>broser.quit()
>```
由于API的更新,webdriver.Chrome()中chrome_options参数即将被剔除。
>```python
>from selenium import webdriver
>from selenium.webdriver import ActionChains # 导入动作链
>from selenium.webdriver import ChromeOptions # 用来实现规避检测
>import time
>
>## 实现规避selenium被检测到的风险
>options = ChromeOptions()
>options.add_experimental_option('excludeSwitches',['enable-automation'])
># 设置无头浏览
>options.add_argument('--headless')
>options.add_argument('--disable-gpu')
>
># 实例化一个浏览器对象,传入浏览器驱动
>broser = webdriver.Chrome(executable_path='./chromedriver.exe',options=options) # 传入浏览器的驱动所在的目录
># 无可视化界面(无头浏览器)phantomJs
>broser.get('https://www.baidu.com')
>print(broser.page_source)
>
>time.sleep(2)
>broser.quit()
>```
http://www.python66.com/seleniumjiaocheng/156.html