# 网络爬虫学习笔记 **Repository Path**: HaiXiuDeDXianSheng/web-crawler-learning-notes ## Basic Information - **Project Name**: 网络爬虫学习笔记 - **Description**: 记录python爬虫相关内容 - **Primary Language**: Python - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 2 - **Created**: 2022-05-23 - **Last Updated**: 2025-05-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: Python, Spider ## README - [1. 爬虫基本分类](#1-爬虫基本分类) - [2. 爬虫和反爬虫](#2-爬虫和反爬虫) - [3. Http和Https协议](#3-http和https协议) - [3.1. http传输协议](#31-http传输协议) - [3.2. https协议](#32-https协议) - [4. requests模块](#4-requests模块) - [5. 数据解析](#5-数据解析) - [5.1. 数据解析的方法:](#51-数据解析的方法) - [5.2. 数据解析的原理概述](#52-数据解析的原理概述) - [5.3. 正则表达式](#53-正则表达式) - [5.4. 使用beautifulsoup4来进行数据解析](#54-使用beautifulsoup4来进行数据解析) - [5.5. xpath进行数据解析](#55-xpath进行数据解析) - [>```](#) - [6. 验证码的识别](#6-验证码的识别) - [7. 模拟登录实现流程整理](#7-模拟登录实现流程整理) - [8. 使用cookie进行模拟登录](#8-使用cookie进行模拟登录) - [9. 代理理论(破解封IP的反爬机制)](#9-代理理论破解封ip的反爬机制) - [9.1. 代理在爬虫中的应用](#91-代理在爬虫中的应用) - [10. 高性能异步爬虫](#10-高性能异步爬虫) - [10.1. 单线程下的串行数据爬取](#101-单线程下的串行数据爬取) - [10.2. 异步爬虫](#102-异步爬虫) - [10.2.1. 线程池](#1021-线程池) - [10.2.1.1. 使用multiprocessing模块](#10211-使用multiprocessing模块) - [10.2.1.2. 线程池实际案例](#10212-线程池实际案例) - [10.2.1.3. 关于referer](#10213-关于referer) - [10.2.2. 协程回顾](#1022-协程回顾) - [10.2.3. aiohttp模块](#1023-aiohttp模块) - [10.2.3.1. aiohttp+协程实现异步爬虫](#10231-aiohttp协程实现异步爬虫) - [11. selenium 简介](#11-selenium-简介) # 1. 爬虫基本分类 * 通用爬虫:抓取系统重要组成部分,抓取的是一整张页面数据 * 聚焦爬虫:建立在通用爬虫基础上,抓取的是页面中特定的局部内容 * 增量式爬虫:检测网站中数据更新的情况,只会抓取网站中最新更新出来的数据 # 2. 爬虫和反爬虫 * 反爬机制 * 反反爬策略 robots.txt协议:反爬协议(君子协定)。(规定了网站中那些内容可以被爬虫爬取,哪些不行) # 3. Http和Https协议 http和https表示的都是超文本传输协议 ## 3.1. http传输协议 * 概念:服务器和客户端进行数据交互的一种形式。 * 常用的请求头信息: * User-Agent:表示请求载体的身份标识(例如我在浏览器中输入网址并回车,则当前的浏览器就是一个请求载体) * Connection:请求完毕后,是断开连接还是保持连接。(有两种值,一个是close另一个是keep_alive) * 常用响应头信息 * Content-Type: 表示服务器响应回客户端的数据类型 ## 3.2. https协议 * https表示安全的http协议(涉及到数据加密) * 加密方式: * **对称密钥加密**:客户端向服务器端同时传递密钥和密文(容易被第三方拦截) * **非对称密钥加密**:服务器端设定一种加密方式,将这种加密方式发送给客户端(公钥)。客户端就用这种加密方式对即将要发送给服务器端的数据进行加密,把加密后的密文发给服务器端,服务器端用密钥进行解密(私钥)。非对称加密方式会涉及到两把钥匙,一把是公钥,一把是私钥。(这种方式可以避免密文和密钥同时在网络中传输,但是这种加密方式效率较低,影响通信速度。第二,公钥在发送给客户端时易被第三机构篡改) * **证书密钥加密**:(这种方式是https采用的方式)可以解决非对称密钥加密的缺陷。服务器端提供公钥給信任的第三方机构(客户端也认可),第三方机构对公钥进行数字签名,传给服务端。服务端将数字签名后的公钥给客户端,客户端用数字签名来验证密钥真伪。一般数字签名很难被伪造。 # 4. requests模块 用来模拟浏览器发请求。 浏览器是如何发起请求的?(requests模块的编码流程) * 指定url * 发起请求, * 获取响应数据 * 将爬取到的数据进行持久化存储 案例:爬取搜狗首页的页面数据 >```python > # 终极简单版本的程序 >import requests >url='https://www.sogou.com/' >response = requests.get(url) >page_text=response.text # requests.get()得到的网页源码的信息会储存在response.text中。是字符串形式的响应数据 >print(page_text) >with open('./sogou.html','w',encoding='utf-8') as f: > f.write(page_text) >``` 案例2:爬取搜狗指定词条对应的搜索结果页面(简易网页采集器) >```python > import requests > #url='https://www.sogou.com/web?query=小甲鱼' # 这里query指的就是我们需要搜索的内容 > # 下面对url进行一个封装 ># kw=input('输入一个关键词:') >kw='小甲鱼' >param ={ > 'query': kw >} > # 对url进行拆分 >url='https://www.sogou.com/web' ># 对指定的url发起的请求是携带参数的,并且请求过程中处理了参数 >response = requests.get(url,params=param) ># 获取响应数据 >page_text=response.text >with open('./sogou.html','w',encoding='utf-8') as f: > f.write(page_text) >``` **介绍一种反爬机制:User-Agent伪装** **UA检测**:门户网站的服务器会检测对应请求的载体身份标识,如果检测到请求的载体身份标识为某一款浏览器,则意味着当前请求是一个正常请求(即用户通过浏览器发起的请求),服务器端一定不会拒绝正常请求。但是如果检测到请求的身份标识不是基于某款浏览器,则会判断为不正常请求(爬虫),很有可能会拒绝该请求。 **UA伪装**:让爬虫对应的请求载体身份标识伪装乘某一款浏览器 >```python >import requests >kw='小甲鱼' >param ={ > 'query': kw >} ># 将对应的一个User-Agent封装到一个字典中 >headers={ > 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3775.400 QQBrowser/10.6.4208.400' >} > # 拆分url >url='https://www.sogou.com/web' ># 对指定的url发起的请求是携带参数的,并且请求过程中处理了参数 >response = requests.get(url,params=param,headers=headers) ># 获取响应数据 >page_text=response.text >with open('./sogou.html','w',encoding='utf-8') as f: > f.write(page_text) >``` **案例3**:破解百度翻译 * 请求是一个post请求(携带了参数) * 响应数据是一组json数据 >```python >import requests >import json ># 需要的是爬取到的页面的局部数据 ># 当我们录入一个单词后,当前页面会进行一个局部刷新(这是ajax干的) ># ajax是通过原生的XMLHttpRequest(缩写xhr)对象发出HTTP请求, >post_url=r'https://fanyi.baidu.com/sug' > ># 封装需要传递给post的请求参数 >data={ > 'kw':'dog' >} > ># 进行UA伪装 >headers={ > 'User-Agent':r'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3775.400 QQBrowser/10.6.4208.400' >} > >response=requests.post(post_url,data=data,headers=headers) > ># 获取响应数据 ># response.text获取的是一组字符串数据 >result=response.json() # 用来处理json格式的数据,返回一个dict对象。只能处理json格式数据 >print(result) >file=open('./file.json','w',encoding='utf-8') >json.dump(result,fp=file,ensure_ascii=False) >file.close() >``` **案例4**:爬取豆瓣电影分类排行榜 https://movie.douban.com/ 也涉及到局部数据的爬取。(不用数据解析的方式来做) **在网页中假如点击查询后,网页的url不变,则说明这个查询请求是一个ajax请求,否则则不是** 对于ajax请求,数据包不是来自于当前的url,而是来自于另一个url >```python >import requests >import json > ># 打开网页,检查元素,选择network抓包工具,选择XHR发现request请求的地址如下 >url=r'https://movie.douban.com/j/chart/top_list?type=24&interval_id=100%3A90&action=&start=0&limit=1' ># 来仔细观察上述url,发现url中是携带参数的,“type=24&interval_id=100%3A90&action=&start=0&limit=1”均是参数 ># 下面我们进行url拆分 >request_url=r'https://movie.douban.com/j/chart/top_list' >params={ > 'type': '24', > 'interval_id': '100:90', > 'action': '', > 'start': '0', #从数据库中的第几部电影开始取 > 'limit': '20' #sql语句中的limit >} ># UA伪装 >headers={ > 'User-Agent':r'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3775.400 QQBrowser/10.6.4208.400' >} ># 发起请求 >response=requests.get(request_url,params=params,headers=headers) ># 获得json数据 >list_data=response.json() ># json数据持久化存储 >with open('./file.json','w',encoding='utf-8') as fp: > json.dump(list_data,fp,ensure_ascii=False) >``` **案例5**:爬取肯德基餐厅查询 http://www.kfc.com.cn/kfccda/storelist/index.aspx 发现网页元素中的信息如下 >``` >General > Request URL: http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=keyword > Request Method: POST > Status Code: 200 OK > Remote Address: 139.224.15.100:80 > Referrer Policy: no-referrer-when-downgrade >``` 查询参数如下所示 >``` >Query String Parameter > op: keyword >``` 发现其中From Data的信息如下所示 >``` >From Data > cname: > pid: > keyword: 杭州 > pageIndex: 2 > pageSize: 10 >``` 且发现点击查询后,网页的url不变。故该请求是一个ajax请求。 故按照这个网页结构编写代码 >```python >import requests >import json > >url='http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=keyword' >data={ > 'cname':'', > 'pid': '', > 'keyword': '杭州', > 'pageIndex': '1', > 'pageSize': '10' >} ># 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' >} ># 发起请求 >response=requests.post(url,data=data,headers=headers) ># 获取数据 >list_data=response.json() ># 数据持久化 >with open('./file.json','w',encoding='utf-8') as fp: > json.dump(list_data,fp,ensure_ascii=False) >``` **似乎是只有post请求才会有From Data选项,而Get请求只有Query String Parameter选项。在编写post请求时,response.post()里面的data选项传入From Data里面的内容即可** **综合案例** **案例6**:抓取国家药品监督管理总局基于中华人民共和国化妆品生产许可证相关数据 http://scxk.nmpa.gov.cn:81/xk/ 发现网页中公司名称是一个超链接,点击超链接后才有该公司的详情数据,我们需要爬取每个的详情数据。 我们先尝试对网址 http://scxk.nmpa.gov.cn:81/xk/ 进行请求响应,在浏览器中按F12可以看到它是get请求 >```python >import requests >import json > >url='http://scxk.nmpa.gov.cn:81/xk/' ># 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' >} ># 发起请求 >response=requests.get(url,headers=headers) ># 获取数据 >page_text=response.text ># 数据持久化 >with open('./file.html','w',encoding='utf-8') as fp: > fp.write(page_text) >``` 让我们用浏览器打开保存的file.html文件(**这是我们通过requests模块发请求得到的页面数据**),如下图所示。
对比我们通过浏览器发请求得到的页面情况
**发现浏览器发出的请求是有数据显示的,而requests发出的请求得到的页面是没有数据显示的** 这就说明对应的数据信息并不是通过浏览器对应的地址栏 http://scxk.nmpa.gov.cn:81/xk/ 这个网址请求到的,而是和前面“肯德基餐厅查询”、“豆瓣电影爬取”的案例一样,很有可能 **是通过其他url对应的ajax动态请求到的** 用google浏览器打开 http://scxk.nmpa.gov.cn:81/xk/ ,右键检查,点击抓包工具,点击xk/文件,发现request url是 http://scxk.nmpa.gov.cn:81/xk/ ,请求方式是get,点击response发现response里面的信息和file.html里面是一样的。这也就是说,对http://scxk.nmpa.gov.cn:81/xk/ 发出请求,只能得到file.html里面的信息 得出结论,**公司详情数据是通过动态加载出来的数据** 在网站搜索框中搜索一个公司信息,例如“广东天姿化妆品科技有限公司”,发现页面网址不变。XHR中确实捕捉到了数据包。是一个post请求,且对应response里面找到了对应数据。 再回顾一下我们的需求,我们是想获取所有企业对应的数据所在url,爬取详情数据。 通过对详情页url的观察: * 发现url的域名都是一样的,只有携带的参数不同 * id值可以从对应的ajax请求到的json数据中获取 * 可以用域名和id值拼接出一个完整的网址。 进入详情页: * 发现该页面的数据也是通过动态加载得到的。(因为直接对详情页url发请求得到的html信息中不包含目标数据) * 发现ajax访问的企业对应的url都一样。http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsById * 只有ajax请求的参数id不一样 * 如果我们可以获取多家企业的id值,就可以将id和url形成一个完整的详情页对应详情数据的ajax请求的url * 有了url就可以得到json串数据,从而解析出想要的数据 >```python >import requests >import json > ># 批量获取不同企业的id值 ># url是首页对应的ajax访问的url >url='http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsList' >data={ > 'on': 'true', > 'page': '1', # 可以批量处理页码操作 > 'pageSize': '15', > 'productName': '', > 'conditionType': '1', > 'applyname': '', > 'applysn': '' >} > ># 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' >} > ># 发出请求 ># 得到一组包含id的json数据。(dict类型数据) >json_ids = requests.post(url=url,data=data,headers=headers).json() >id_list=[] # 存储企业的id >all_data_list=[] # 储存企业的详情数据 >for each in json_ids['list']: > id_list.append(each['ID']) > ># 获取企业id是为了得到详情页的ajax请求的数据 >post_url='http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsById' >for each in id_list: > data={ > 'id':each > } > detail_json=requests.post(post_url,headers=headers,data=data).json() > all_data_list.append(detail_json) ># 持久化存储 >with open('./file.json','w',encoding='utf-8') as fp: > json.dump(all_data_list,fp,ensure_ascii=False) >``` 以上代码只爬取了第一页的企业信息,假如需要把所有页的企业信息全部爬取到则代码改成如下: >```python >import requests >import json > ># 批量获取不同企业的id值 ># url是首页对应的ajax访问的url >url='http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsList' ># 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' >} >id_list=[] # 存储企业的id >all_data_list=[] # 储存企业的详情数据 >for i in range(1,6): > data={ > 'on': 'true', > 'page': str(i), # 可以批量处理页码操作 > 'pageSize': '15', > 'productName': '', > 'conditionType': '1', > 'applyname': '', > 'applysn': '' > } > # 发出请求 > # 得到一组包含id的json数据。(dict类型数据) > print('================page %d start================'%i) > json_ids = requests.post(url=url,data=data,headers=headers).json() > for each in json_ids['list']: > id_list.append(each['ID']) > print('================page %d end================'%i) ># 获取企业id是为了得到详情页的ajax请求的数据 >post_url='http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsById' >for each in id_list: > data={ > 'id':each > } > detail_json=requests.post(post_url,headers=headers,data=data).json() > all_data_list.append(detail_json) ># 持久化存储 >with open('./file.json','w',encoding='utf-8') as fp: > json.dump(all_data_list,fp,ensure_ascii=False) >``` # 5. 数据解析 数据解析是用在 **聚焦爬虫**中,聚焦爬虫抓取页面中的 **局部内容**。我们首先使用通用爬虫对互联网上的整张页面进行爬取,然后对局部内容进行提取。 ## 5.1. 数据解析的方法: * 正则表达式 * beautifulsoup4 * **xpath (比较通用)** ## 5.2. 数据解析的原理概述 * 解析的局部的文本内容都会在标签之间或者标签对应的属性中进行存储 * 1. 进行指定标签的定位 * 2. 标签或者标签对应的属性中存储的数据值进行提取(解析) ## 5.3. 正则表达式 **案例7**:爬取糗图百科中热图板块下所有的图片 (使用正则表达式来解析) 测试案例 >```python >import requests >import json > ># 对图片数据进行爬取。图片对应的是一个二进制的数据 >url='http://wx3.sinaimg.cn/mw600/00893JKXly1gj8kn823fij30u0156apq.jpg' ># 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' >} > ># 使用content属性来获得请求到的二进制数据 ># text得到的是字符串形式的数据 ># json()得到的是对象类型的响应数据 >img = requests.get(url).content >with open('./img.jpeg','wb') as fp: > fp.write(img) #假如使用pickle来保存,则保存的是矩阵形式的数据,图片浏览器无法打开 >``` 进入正文程序,先爬取当前第一页的图片数据 >```python >import requests >import json >import re >import os ># 对图片数据进行爬取。图片对应的是一个二进制的数据 >url='https://www.qiushibaike.com/imgrank/' ># 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' >} > ># 先使用通用爬虫对无聊图的整张页面进行爬取 >response = requests.get(url,headers=headers) >page_text=response.text # 一整张页面的源码数据 > ># 使用聚焦爬虫将页面中所有的图片进行提取 ># 对page_text中的源码满足要求的的img中的src属性发送一次请求,就得到了图片数据 ># 观察源码结构,发现图片数据在一层层嵌套的子标签里面,
下面的标签中 ># 下面我们只需要对里面内容进行正则解析提取即可 > ># 编写正则 ># ex = '
.*?' >#假设是对的 > >ex = '
.*?' >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