爬虫基础
1.爬虫简介
抓取互联网上的数据,为我所用。
有了大量的数据,就如同有了一个数据银行一样。
下一步做的就是如何将这些爬取的数据,产品化、商业化。
1.1.爬虫合法性探究
1.1.1.爬虫究竟是违法还是合法的?
- 在法律中不被禁止
- 具有违法风险
- 区分为善意爬虫和恶意爬虫
1.1.2.爬虫带来的风险
- 爬虫干扰了被访问网站的正常运营
- 爬虫抓取了受到法律保护的特定类型的数据或信息
1.1.3.如何避免进局子喝茶
- 时常优化自己的程序,避免干扰被访问网站的正常运行
- 在使用、传播爬取到的数据时,审查抓取的内容,如果发现了涉及到用户以及商业机密等敏感内容时,需要及时停止爬取或传播。
1.2.爬虫初始深入
1.2.1.爬虫在使用场景中的分类
- 通用爬虫
- 抓取系统的重要组成部分。
- 抓取的是一整张页面数据。
- 聚焦爬虫
- 是建立在通用爬虫的基础之上。
- 抓取的是页面中特定的局部内容。
- 增量式爬虫
- 检测网站中数据更新的情况。
- 只会抓取网站中最新更新出来的数据。
1.2.2.爬虫的矛与盾
- 反爬机制
- 门户网站,可以通过指定相应的策略或者技术手段,防止爬虫程序进行网站数据的爬取。
- 反反爬策略
- 爬虫程序,可以通过指定相关的策略或者技术手段,破解门户网站中具备的反爬机制,从而可以获取门户网站的和数据。
1.2.3.robots.txt协议
君子协议,规定了网站中哪些数据可以被爬虫爬取,哪些数据不可以被爬取。
http://www.baidu.com/robots.txt
2.http&https协议
详细请点击http&https协议
2.1.http协议
- 概念:就是服务器和客户端,进行数据交互的一种形式。
- 常用请求头信息:
- User-Agent:请求头的身份标识。
- Connection:请求完毕后,是断开连接还是保持连接。
- 常用响应头信息:
- Content-Type:服务器响应回客户端的数据类型。
2.2.https协议
- 概念:安全的超文本传输协议。
- 加密方式:
- 对称密钥加密
- 非对称密钥加密
- 证书密钥加密
3.Requests模块
requests模块:python中原生的一款基于网络请求的模块,功能非常强大,简单便捷,效率极高。
作用:模拟浏览器发送请求。
如何使用:(requests模块的编码流程):
- 指定url
- 发起请求
- 获取响应数据
- 持久化存储
环境安装:pip install requests
实战:获取sogou首页的数据
1 | import requestsif __name__ == '__main__': # 指定url url = 'https://wwww.sogou.com/' # 发送get请求,获取响应数据 response = requests.get(url = url) # 将响应数据转化为字符串 response_text = response.text # 本地持久化存储 with open('sogou.html','w',encoding='utf-8') as fw: fw.write(response_text) |
3.1.Requests巩固
3.1.1.深入案例介绍
- 爬取搜狗指定词条,对应的搜索结果页面(简易网页采集器)。
- 破解百度翻译。
- 爬取豆瓣电影分类排行榜中的电影详情数据。
- 爬取肯德基餐厅查询中,指定地点的餐厅数。
- 爬取国家药品监督管理总局基于中华人民共和国化妆品生产许可证相关数据。
3.1.2.简易网页采集器
- UA检测
- UA伪装
1 | import requestsif __name__ == "__main__": url = 'https://www.sogou.com/web' headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3947.100 Safari/537.36' } # 处理url携带的参数:封装到字典中 kw = input('enter a keyword:') param = { 'query': kw } response = requests.get(url = url,params = param,headers = headers) page_text = response.text with open(kw+'html','w',encoding='utf-8')as fw: fw.write(page_text) # print(page_text) |
3.1.3.百度翻译
- post请求(携带了参数)
- 响应是一组Json数据
resposne.json()
直接返回的是obj
1 | import requests import timeimport jsonif __name__ =='__main__': post_url = 'https://fanyi.baidu.com/sug' headers ={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3947.100 Safari/537.36' } # post请求参数处理(同get请求一致) input_string = input('enter a key:') data = { 'kw': input_string } response = requests.post(url = post_url,data = data,headers=headers) dict_obj = response.json() # 持久化存储 with open(input_string +'.json','w',encoding='utf-8') as fw: json.dump(dict_obj,fp=fw,ensure_ascii=False) print(dict_obj) |
3.1.4.豆瓣电影
1 | import requests import timeimport json if __name__ =='__main__': url = 'https://movie.douban.com/j/chart/top_list' headers ={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3947.100 Safari/537.36' } param = { 'type': '24', 'interval_id': '100:90', 'action': '', 'start': '1', 'limit': '20', } response = requests.get(url = url,params = param,headers=headers) print(response) list_data = response.json() # 持久化存储 with open('movie.json','w',encoding='utf-8') as fw: json.dump(list_data,fp=fw,ensure_ascii=False) print(list_data) |
备注:会被限制ip,response返回的是403
3.1.5.作业
肯德基餐厅
观测地址栏的url有没有发生变化,如果没有发生变化,但是数据发生了更新,表示发送了ajax请求。
1 | import requests import timeimport jsonif __name__ =='__main__': url = 'http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=keyword' headers ={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3947.100 Safari/537.36' } data = { 'cname':'', 'pid':'', 'keyword': '北京', 'pageIndex': '1', 'pageSize': '10', } response = requests.post(url = url,data = data,headers=headers) response_text = response.text with open('kfc.txt','w',encoding='utf-8') as fw: fw.write(response_text ) |
备注:这里请求的url,不要去掉后面的参数
3.2.综合练习
3.2.1.药监总局01
- url的域名都是一样的,只有携带的参数(id)不一样
- id值可以从首页对应的ajax请求到的json串中获取
settings.py
1 | START_URL = { 'home': 'http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsList', 'detail': 'http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsById',}DATA = { 'home': { "on": "true", "page": "1", "pageSize": "15", "productName": "", "conditionType": "1", "applyname": "", "applysn": "", }, 'detail': { "id": "ef765ed313ec457dbecd434aa08b0e93", },}HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3947.100 Safari/537.36'} |
utils.py
1 | import jsondef json_local_storage(response, outfile = 'local_storage.json'): response_json = response.json() with open(outfile,'w',encoding='utf-8') as fw: json.dump(response_json,fp=fw,ensure_ascii=False)def txt_local_storage(response, outfile = 'txt_strage.txt'): response_text = response.text with open(outfile, 'w', encoding='utf-8') as fw: fw.write(response_text)def redis_conn(host,port,password): pass |
spider_task.py
1 | import requests import timeimport jsonfrom settings import START_URL, HEADERS, DATAfrom utils import json_local_storage,txt_local_storageif __name__ =='__main__': response = requests.post(url = START_URL['home'],data = DATA['home'],headers=HEADERS) # json_local_storage('home.json') response_json = response.json() company_array = response_json['list'] for ele in company_array: time.sleep(1) # print(ele['ID']) DATA['detail']['id'] = ele['ID'] print(DATA['detail']) response_detail = requests.post(url = START_URL['detail'],data = DATA['detail'],headers=HEADERS) json_local_storage(response_detail, ele['EPS_NAME'] + '.json') # print(response_detail) time.sleep(1) |
4.数据解析概述
聚焦爬虫:爬取页面中指定的页面内容
编码流程:
- 指定url
- 发起请求
- 获取响应数据
- 数据解析
- 持久化存储
数据解析分类:
- 正则
- bs4
- xpath
数据解析原理概述:
- 解析的局部的文本内容都会在标签之间或者标签对应的属性中进行存储
- 进行指定标签的定位
- 标签或者标签对应的属性中存储的数据值进行提取(解析)
响应数据的类型:
response.text
,处理字符串类型的响应数据response.content
,处理二进制类型的响应数据response.json()
,处理Json类型(对象类型)的响应数据
4.1.图片数据爬取
单张图片数据爬取
spider_task.py
1 | import requests import timeimport jsonfrom settings import START_URL, HEADERS, DATAfrom utils.utils_strorage import content_local_storageif __name__ =='__main__': response = requests.get(url=START_URL['pic_demo'],headers=HEADERS) content_local_storage(response,'qiutu.jpg') |
utils_storage.py
1 | # 二进制类型响应对象的本地存储def content_local_storage(response, outfile = 'content_strage.jpg'): response_content = response.content # 二进制存储不需要指定编码格式 with open(outfile, 'wb') as fw: fw.write(response_content) |
4.2.正则解析
单页面的多张图片爬取
1 | import requests import timeimport jsonfrom settings import START_URL, HEADERS, DATAfrom utils.utils_strorage import content_local_storage,txt_local_storagefrom utils.utils_os import my_mkdirimport reif __name__ =='__main__': my_mkdir('./qiutu') response = requests.get(url=START_URL['pic_demo'],headers=HEADERS) response_text = response.text ex = '<div class="thumb">.*?<img src="(.*?)" alt.*?</div>' src_list = re.findall(ex,response_text,re.S) for src in src_list: src = 'https:' + src time.sleep(1) response = requests.get(url=src,headers=HEADERS) img_name = src.split('/')[-1] img_path = './qiutu/'+ img_name content_local_storage(response,img_path) time.sleep(1) |
多页面,多图片爬取
- 设置url请求的通用模板
1 | import requests import timeimport jsonfrom settings import START_URL, HEADERS, DATAfrom utils.utils_strorage import content_local_storage,txt_local_storagefrom utils.utils_os import my_mkdirimport refrom utils.utils_parse import regex_parseif __name__ =='__main__': def single_parse(): my_mkdir('./qiutu') response = requests.get(url=START_URL['pic_demo']['index_url'],headers=HEADERS) ex = '<div class="thumb">.*?<img src="(.*?)" alt.*?</div>' src_list = regex_parse(ex,response,re.S) for src in src_list: src = 'https:' + src time.sleep(1) response = requests.get(url=src,headers=HEADERS) img_name = src.split('/')[-1] img_path = './qiutu/'+ img_name content_local_storage(response,img_path) time.sleep(1) def multi_parse(): template_url = START_URL['pic_demo']['template_url'] my_mkdir('./qiutu') for i in range(1, 3): next_url = template_url % i response = requests.get(url=next_url,headers=HEADERS) ex = '<div class="thumb">.*?<img src="(.*?)" alt.*?</div>' src_list = regex_parse(ex,response,re.S) for src in src_list: src = 'https:' + src time.sleep(1) response = requests.get(url=src,headers=HEADERS) img_name = src.split('/')[-1] img_path = './qiutu/'+ img_name content_local_storage(response,img_path) time.sleep(1) print(next_url+'start') multi_parse() |
4.3.bs4解析
4.3.1.bs4解析概述
bs4只可以用在python中
数据解析的原理
- 1.标签定位
- 2.提取标签、标签属性中存储数据
bs4数据解析的原理:
- 1.实例化一个BeautifulSoup对象,并且将页面元数据加载到该对象中
- 2.通过调用BeautifulSoup对象中的相关的属性或者方法进行标签定位和数据提取
环境安装
pip install bs4
pip install lxml
如何实例化BeautifulSoup
导包:
from bs4 import BeautifulSoup
对象的实例化
1.将本地的html加载到BeautifulSoup对象中
1
# 将本地的html文档中的数据加载到该对象中with open('aa.html','r',encoding='utf-8') as fr: soup = BeautifulSoup(fr,'lxml')
2.将互联网上获取的页面源码加载到该对象中
1
page_text = response.textsoup = BeautifulSoup(page_text,'lxml')
4.3.2.bs4解析具体使用讲解
- 提供的用于数据解析的方法和属性
soup.tagName
,等价于soup.find('tagName')
- 返回的是html中第一次出现的tagName标签
- 属性定位
soup.find('div',class_='song')
,返回类名为song对应的div,记得class要加下划线soup.findAll('tagName')
,返回所有的tag标签,返回值为数组
- 标签选择器(和CSS选择器一致),返回的是列表
select('.song')
,类选择器select('#song')
,id选择器select('a')
,标签选择器- 层级选择器
select('.tag > ul > li > a')
,单个多层级用>
表示select('.tang > ul a)
,多个层级用空格表示
- 层级选择器中,不支持索引定位
- 获取标签之间的文本数据
soup.a.text/string/get_text()
- 区别
text/get_text()
,可以获取某一个标签中的所有文本内容string
,只可以获取该标签下面直系的文本内容
4.3.3.bs4解析案例实战
响应数据乱码解决方案
1 | response_text = response.text.encode('ISO-8859-1') |
1 | def sanguo(): response = requests.get(url=START_URL['sanguo'],headers=HEADERS) response_text = response.text.encode('ISO-8859-1') soup = BeautifulSoup(response_text,'lxml') li_list = soup.select('.book-mulu > ul > li') for li in li_list: title = li.a.string detail_url = 'https://www.shicimingju.com' + li.a['href'] print(detail_url) detail_response = requests.get(url=detail_url,headers=HEADERS) detail_response_text = detail_response.text.encode('ISO-8859-1') soup_detail = BeautifulSoup(detail_response_text,'lxml') detail_tag = soup_detail.find('div', class_='chapter_content') detail_content = detail_tag.text print(detail_content) break time.sleep(1)sanguo() |
4.4.xpath解析
4.4.1.xpath解析基础
xpath解析:最常用且最便捷高效的一种解析方法,通用性。
xpath解析原理
- 1.实例化一个etree对象,且需要将被解析的页面源码数据加载到该对象中
- 2.调用etree对象中的xpath方法,结合着xpath表达式是实现标签的定位和内容的捕获
环境的安装
pip install lxml
如何实例化一个etree对象
1.将本地的html文档中的源码数据加载到etree对象中
etree.parse(filepath)
2.可以将从互联网上获取到的源码数据加载到该对象中
etree.HTML('page_text')
3.
xpath('xpath表达式')
xpath表达式
- / 表示的是从根节点开始定位。表示的是一个层级
- // 表示的是多个层级。可以表示从任意位置开始定位
- 属性定位://div[@class=”song]
- 索引定位://div[@class=”song”]/p[3,索引是从1开始的
- 获取属性://img/@href
4.4.2.xpath实战
4.4.2.1.xpath-58二手房
1 | def ershoufang(): response = requests.get(url=START_URL['ershoufang'],headers=HEADERS) response_text = response.text tree = etree.HTML(response_text) # txt_local_storage(response) div_list = tree.xpath('//*[@id="__layout"]/div/section/section[3]/section[1]/section[2]/div') for div in div_list: title = div.xpath('.//div[@class="property-content-title"]/h3/text()')[0] print(title) ershoufang() |
4.4.2.2.xpath-4k图片解析下载
解析出的字段为乱码解决方案
1 | # 1response.encoding = 'utf-8# 2image_name.encode('iso-8859-01').decode('gbk')# 3response = response.text.encode('ISO-8859-1') |
1 | def meitu(): my_mkdir('./meitu') response = requests.get(url=START_URL['img_02'],headers=HEADERS) response_text = response.text tree = etree.HTML(response_text) li_list = tree.xpath('//ul[@class="clearfix"]/li') detail_url_list = [] for li in li_list: detail_url = li.xpath('./a/@href')[0] detail_url_list.append('https://pic.netbian.com' + detail_url) print(detail_url_list) for detail_ele in detail_url_list: response_detail = requests.get(url=detail_ele,headers=HEADERS) response_detail_text = response_detail.text.encode('ISO-8859-1') tree_detail = etree.HTML(response_detail_text) img_src = tree_detail.xpath('//div[@class="photo-pic"]/a/img/@src')[0] img_src = 'https://pic.netbian.com' + img_src img_response = requests.get(url=img_src,headers=HEADERS) img_name = tree_detail.xpath('//div[@class="photo-pic"]/a/img/@title')[0] + '.jpg' filepath = './meitu/' + img_name content_local_storage(img_response,filepath) print(img_name+'下载完成') time.sleep(1)meitu() |
4.4.2.3.xpath-全国城市名称爬取
xpath中,多个选择语句,可以用|
符号连接
4.4.2.4.作业
爬取站长素材的简历模板
5.验证码识别
5.1.验证码识别简介
反爬机制:验证码,识别验证码图片中的数据,用于模拟登录操作。
识别验证码的操作:
- 人工肉眼识别(不操作)
- 第三方自动识别
5.2.云打码使用流程
5.2.1.云打码平台
http://www.ttshitu.com/docs/index.html?spm=null
5.2.2.Tesseract-OCR完成验证码训练
(时间成本、学习成本过高,略过)
5.3.古诗文网验证码识别
1 | def base64_api(uname, pwd, img, typeid): with open(img, 'rb') as f: base64_data = base64.b64encode(f.read()) b64 = base64_data.decode() data = {"username": uname, "password": pwd, "typeid": typeid, "image": b64} result = json.loads(requests.post("http://api.ttshitu.com/predict", json=data).text) if result['success']: return result["data"]["result"] else: return result["message"] return ""def parse_ocr(img_path,uname,pwd): result = base64_api(uname, pwd, img=img_path, typeid=3) print(result) def query_account_nfo(username, password): url = 'http://api.ttshitu.com/queryAccountInfo.json?username='+username+'&password='+password result = requests.get(url=url,headers=HEADERS).text print(result) |
6.模拟登陆
6.1.模拟登陆实现流程梳理
爬取基于某些用户的用户信息
需求:模拟登录
- 点击登录之后,会发起一个post请求
- post请求中会携带登录之前录入的相关的登录信息(用户名、密码、验证码…)
- 验证码:每次请求都会变化
6.2模拟登陆
1 | def gsw(): response_index = requests.get(url=START_URL['gsw_index'],headers=HEADERS).text tree = etree.HTML(response_index) img_src = tree.xpath('//img[@id="imgCode"]/@src')[0] img_src = 'https://so.gushiwen.cn' + img_src response_code = requests.get(url=img_src,headers=HEADERS).content with open('code.jpg','wb') as fw: fw.write(response_code) code_parse= parse_ocr('./code.jpg',OCR_USERNAME,OCR_PASSWORD) data = { "__VIEWSTATE": "qehW4Za32V5Uek9jdmRmASRMZhBh28aebNG9fOHgl5F5cv4MfUrmJcSloFlvQV6QRmbmpt7oAiql3jOVGoYwEN88jjGokFQcptQt9NeH3BwYJn6MZe5PGbBE05c=", "__VIEWSTATEGENERATOR": "C93BE1AE", "from": "http", # 账号密码自行注册 "email": "********", "pwd": "********", "code": code_parse, "denglu": "登录", } response_login = requests.post(url=START_URL['gsw_login'],data=data,headers=HEADERS) print(response_login.status_code) txt_local_storage(response_login,'login.html')gsw() |
6.3.模拟登陆cookie操作
需求:爬取当前用户的相关的用户信息(个人主页中显示的信息)
http/https协议特性:无状态
没有请求到对应页面数据的原因:
- 发起的第二次基于个人主页页面请求的时候,服务器端并不知道该次请求时基于登录状态下的请求。
cookie:用来让服务器端记录客户端的相关状态
手动处理:通过抓包工具获取cookie的值,将该值封装到headers中(不建议)
自动处理:
- cookie的值来源是哪里?
- 模拟登录post请求后,由服务器端创建
- session会话对象:
- 作用:
- 1.可以进行请求的发送
- 如果请求过程中产生了cookie,则该cookie会被自动存储在该session对象中
- 作用:
- 创建一个session对象:
session = requests.Session()
- 使用session对象进行模拟登录post请求的发送(cookie就会被存储在session对象中)
- session对象对个人主页的get请求进行发送(携带了cookie)
- cookie的值来源是哪里?
7.代理
7.1.代理理论讲解
代理:破解封IP这种反爬机制
什么是代理:
- 代理服务器
代理的作用:
- 突破自身IP访问的限制
- 隐藏自身真是IP
代理相关的网站:
7.2.代理在爬虫中的应用
代理ip的类型:
- http:应用到http协议对应的url中
- https:应用到https协议对应的url中
代理ip的匿名度:
- 透明,服务器知道该次请求使用了代理,也知道请求对应的真是ip
- 匿名,知道使用了代理,不知道真是ip
- 高匿,不知道使用了代理,更不知道真实的ip
代码:
response = requests.get(url=url,headers=headers,proxies={"http":"1.127.0.1:8080})
1 | import requests# 根据协议类型,选择不同的代理proxies = { "http": "http://12.34.56.79:9527", "https": "http://12.34.56.79:9527",}response = requests.get("http://www.baidu.com", proxies = proxies)print response.text |
8.异步爬虫
8.1.异步爬虫概述
在爬虫中使用异步,实现高性能的数据爬取操作
异步爬虫的方式:
- 多线程,多进程:
- 好处:可以为相关阻塞的操作单独开启线程或者进程,阻塞操作就可以异步执行。
- 弊端:无法无限制的开启多线程或者多进程
8.2.异步爬虫-多进程&多线程
- 进程池、线程池(适当地使用)
- 可以降低系统对进程和线程创建和销毁的一个频率,从而很好的降低系统的开销。
- 弊端:池中线程的数量是有上限的。
8.3.异步爬虫-进程池&线程池
单线程模拟
1 | def get_page(str): print('downloading...') time.sleep(2) print('don=wnloaded successed!',str)name_list= ['a','b','c','d']strat_time = time.time()for i in range(0,len(name_list)): get_page(name_list[i])end_time = time.time()print('%d seconds' % (end_time - strat_time)) |
结果
1 | downloading...donwnloaded successed! adownloading...donwnloaded successed! bdownloading...donwnloaded successed! cdownloading...donwnloaded successed! d8 seconds |
线程池
1 | def get_page(str): print('downloading...') time.sleep(2) print('donwnloaded successed!',str)name_list= ['a','b','c','d']strat_time = time.time()# 实例化一个线程池对象pool = Pool(4)# 将列表的每一个列表元素传递给get_page依次处理pool.map(get_page, name_list)end_time = time.time()print('%d seconds' % (end_time - strat_time))# 关闭线程池pool.close()pool.join() |
结果
1 | downloading...downloading...downloading...downloading...donwnloaded successed! bdonwnloaded successed! adonwnloaded successed! cdonwnloaded successed! d2 seconds |
8.4.异步爬虫-线程池案例应用
https://www.pearvideo.com/category_5
该网站视频的src链接,是放在ajax的相应对象中的,其对应的url有两个参数,其中一个参数是动态加载的,需要用selenium
9.1.协程相关概念
9.1.1.异步编程
为什么要讲?
- 异步非阻塞、asyncio等等概念,或多或少的听说过
- tornado、fastapi、django3.x asgi、aiohttp等框架或者模块中,都在使用异步(提升性能)
如何讲解?
- asyncio模块进行异步编程
- 实战案例
9.1.2.协程
协程不是计算机提供,而是程序员人为创造的
协程(Coroutine),也可以被称为微线程、是一种用户态的上下文切换技术
简而言之,其实就是通过一个线程实现代码块(不同函数之间)相互切换执行
1 | def func1(): print('1') ... print('2n-1') def func2(): print('2') ... print('2n') func1()func2() |
如上两个函数,协程可以让函数中的不同语句交错执行
实现协程的方法:
greenlet
,比较早期的模块。yield关键字
asyncio装饰器
- python3.4引入的
async/await关键字
- python3.5引入的
- 目前比较主流
9.1.2.1.greenlet实现协程
安装pip install greenlet
1 | # -*- coding: utf-8 -*-from greenlet import greenletdef func1(): print('1') # 第2步,输出1 gr2.switch() # 第3步,切换到func2函数 print('2') # 第6步,输出2 gr2.switch() # 第7步,切换到func2函数,从上一次执行的位置继续向后执行 def func2(): print('3') # 第4步,输出3 gr1.switch() # 第5步,切换到func1函数,从上一次的位置继续向后执行 print('4') # 第8步,输出4 gr1 = greenlet(func1)gr2 = greenlet(func2)gr1.switch() # 第1步,去执行func1 |
9.1.2.2.yield关键字
1 | # -*- coding: utf-8 -*-def func1(): yield 1 yield from func2() yield 2 def func2(): yield 3 yield 4 f1 = func1()for item in f1: print(item) |
9.1.2.3.asyncio
在python3.4以及之后的版本
1 | # -*- coding: utf-8 -*-import asyncio@asyncio.coroutinedef func1(): print('1') yield from asyncio.sleep(2) # 遇到IO耗时操作,自动切换到task中的其他任务 print('2') @asyncio.coroutinedef func2(): print('3') yield from asyncio.sleep(2) # 遇到IO耗时操作,自动切换到task中的其他任务 print('4')tasks = [ asyncio.ensure_future(func1()), asyncio.ensure_future(func2()) ]loop = asyncio.get_event_loop()loop.run_until_complete(asyncio.wait(tasks)) |
注意:注意IO自动切换
9.1.2.3.async & await关键字
自python3.5及以后的版本
和上面的本质上类似,可以理解为语法糖
1 | # -*- coding: utf-8 -*-import asyncioasync def func1(): print('1') await asyncio.sleep(2) # 遇到IO耗时操作,自动切换到task中的其他任务 print('2') async def func2(): print('3') await asyncio.sleep(2) # 遇到IO耗时操作,自动切换到task中的其他任务 print('4')tasks = [ asyncio.ensure_future(func1()), asyncio.ensure_future(func2()) ]loop = asyncio.get_event_loop()loop.run_until_complete(asyncio.wait(tasks)) |
9.1.3.协程意义
在一个线程中如果遇到IO等待时间,线程不会傻傻等,利用空闲时间再去干点其他事
案例:去下载三站图片(网络IO)
普通方式
1
import requestsheaders ={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3947.100 Safari/537.36'}def download_images(url): print('正在下载',url) response = requests.get(url=url,headers=headers).content file_name = url.split('/')[-1] with open(file_name,'wb') as fw: fw.write(response) print('下载结束',url)urls = [ "https://pic.netbian.com/uploads/allimg/210812/230003-1628780403b213.jpg", "https://pic.netbian.com/uploads/allimg/210718/001826-16265387066216.jpg", "https://pic.netbian.com/uploads/allimg/210812/225733-16287802533d30.jpg",]for url in urls: download_images(url)
基于协程的异步
1
import aiohttpimport asyncioasync def fetch(session, url): print('发送请求',url) async with session.get(url,verify_ssl=False) as response: content = await response.content.read() filename = url.rsplit('/')[-1] with open(filename,'wb') as fw: fw.write(content)async def main(): async with aiohttp.ClientSession() as session: urls = [ "https://pic.netbian.com/uploads/allimg/210812/230003-1628780403b213.jpg", "https://pic.netbian.com/uploads/allimg/210718/001826-16265387066216.jpg", "https://pic.netbian.com/uploads/allimg/210812/225733-16287802533d30.jpg", ] tasks = [asyncio.create_task(fetch(session,url)) for url in urls] await asyncio.wait(tasks)if __name__=="__main__": asyncio.run(main())
9.1.4.异步编程
9.1.4.1asyncio事件循环
理解为一个死循环,去检测并执行某些代码
1 | # 伪代码任务列表 = [任务1, 任务2, 任务3...]while True: 可执行的任务列表, 已完成的任务列表 = 去任务列表中检查所有的任务,将'可执行'和'已完成'的任务返回 for 就绪任务 in 可执行的任务列表: 在任务列表中移除 已完成的任务 如果 任务列表 中的任务都已完成,则终止循环 |
1 | import asyncio# 生成(获取)一个事件循环loop = asyncio.get_event_loop()# 将任务放到'任务列表'loop.run_until_complete(任务) |
9.1.4.2.快速上手
协程函数:定义函数的时候,加上修饰符async
协程对象:执行协程函数得到的协程对象
1 | async def func(): passresult = func() |
注意:执行协程函数创建爱你协程对象,函数内部代码不会执行
如果想要运行协程函数内部代码,必须要将协程对象交给事件循环来处理
1 | import asyncioasync def func(): print('aa') result = func()# loop = async.get_event_loop()# loop.run_untl_complete(result)asyncio.run(result) # python3.7 |
9.1.4.3.await关键词
await + 可等待对象(协程对象、Future、Task对象),如IO等待
示例一:
1 | import asyncioasync def func(): print('aa') response = await asyncio.sleep(2) # 只有等待结束,有结果了,才会继续向下执行 print('end',response) asyncio.run(func()) |
示例二:
1 | import asyncioasync def others(): print('start') await asyncio.sleep(2) print('end') return '返回值'async def func(): print("执行协程函数内部代码") # 遇到IO操作挂起当前协程(任务),等IO操作完成之后,再继续往下执行,当前协程对象挂起时,事件循环可以去执行其他协程(任务) response = await others() print('IO请求结束,结果为:',response) asyncio.run(func()) |
示例三:
1 | import asyncioasync def others(): print('start') await asyncio.sleep(2) print('end') return '返回值'async def func(): print("执行协程函数内部代码") # 遇到IO操作挂起当前协程(任务),等IO操作完成之后,再继续往下执行,当前协程对象挂起时,事件循环可以去执行其他协程(任务) response1 = await others() print('IO请求结束,结果为:',response1) response2 = await others() print('IO请求结束,结果为:',response2) asyncio.run(func()) |
await就是等待对象的值得到结果之后,再继续向下走
9.1.4.4.task对象
Tasks are used to schedule coroutines concurrently.
When a coroutine is wrapped into a Task with functions like asyncio.create_task()
the coroutine is automatically scheduled to the soon.
白话:在事件循环中添加多个任务的
Tasks用于并发调度协程,通过asynio.create_task(协程对象)的方式创建Task对象,这样可以让协程加入事件循环中等待被调度执行。除了使用asyncio.create_task()
函数以外,还可以用更低层级的loop.create_task()
和ensure_future()
函数。不建议手动实例化Task对象。
注意:asyncio.create_task()
函数在Python3.7中被加入。在Python3.7之前,可以改用低层级的loop.create_task()
和ensure_future()
函数。
示例一:
1 | import asyncioasync def func(): print('1') await asyncio.sleep(2) print('2') return '返回值'async def main(): print('main start') # 创建Task对象,将当前执行func函数添加到事件循环 task1 = asyncio.create_task(func()) task2 = asyncio.create_task(func()) print('main end') # 当执行某协程遇到IO操作时,会自动化切换执行其他任务 # 此处的await是等待相对应的协程,全部执行完毕后,然后获取结果 ret1 = await task1 ret2 = await task2 print(ret1, ret2) asyncio.run(main()) |
执行结果
1 | main startmain end1122返回值 返回值 |
示例二:
1 | import asyncioasync def func(): print('1') await asyncio.sleep(2) print('2') return '返回值'async def main(): print('main start') task_list=[ asyncio.create_task(func(),name='n1'), asyncio.create_task(func(),name='n2') ] print('main end') # done默认提供的是集合 # 如果timeout=1,执行时还没有完成,pending就是那个没有完成的东西 # 默认timeout=None,等待全部完成 done,pending = await asyncio.wait(task_list,timeout=None) print(done)asyncio.run(main()) |
结果:
1 | main startmain end1122{<Task finished name='n1' coro=<func() done, defined at F:\workspace\test.py:4> result='返回值'>, <Task finished name='n2' coro=<func() done, defined at F:\workspace\test.py:4> result='返回值'>} |
示例三:
1 | import asyncioasync def func(): print('1') await asyncio.sleep(2) print('2') return '返回值'task_list=[ func(), func(),]done,pending = asyncio.run(asyncio.wait(task_list))print(done) |
9.1.4.5.async的future对象
A Future
is a special low-level
awaitable object that represents an eventual result of an asynchronous operation.
Task继承Future,Task对象内部await结果的处理,基于Future对象来的。
示例一:
1 | async def main(): # 获取当前事件循环 loop = asyncio.get_running_loop() # 创建一个任务(future对象),这个任务什么都不干 fut = loop.create_future() # 等待任务最终结束(Future),没有结果会一直等下去 await fut |
示例二:
1 | import asyncioasync def set_after(fut): await asyncio.sleep(2) fut.set_result('aaa')async def main(): # 获取当前事件循环 loop = asyncio.get_running_loop() # 创建一个任务(Future)对象,没绑定任何行为,则这个任务永远不知道什么时候结束 fut = loop.create_future() # 创建一个任务(Task对象),绑定了set_after函数,函数内在2秒之后,会给fut赋值 # 即手动设置future任务的最终结果,那么future就可以结束了 await loop.create_task(set_after(fut)) # 等待Future对象获取最终结果,否则一直等待下去 data = await fut print(data)asyncio.run(main()) |
9.1.4.6.concurrent的future对象
concurrent.futures.Future
使用线程池、进程池实现异步操作时用到的对象
1 | import timefrom concurrent.futures import Futurefrom concurrent.futures.thread import ThreadPoolExecutorfrom concurrent.futures.process import ProcessPoolExecutordef func(value): time.sleep(1) print(value) return 123# 创建线程池pool = ThreadPoolExecutor(max_workers=5)# 创建进程池# pool = ProcessPoolExecutor(max_workers=5)for i in range(10): fut = pool.submit(func,i) print(fut) |
以后写代码,可能会存在交叉使用。
例如,crm项目80%都是基于协程和异步编程 + MySQL(不支持)【线程、进程做异步编程】
1 | import timeimport asyncioimport concurrent.futuresdef func1(): # 某个耗时操作 time.sleep(2) return 123async def main(): loop = asyncio.get_running_loop() # 1. Run in the default loop's executor(默认ThreadPoolExecutor) # 第一步:内部会先调用 ThreadPoolExcutor的submit方法去线程池中申请一个线程去执行func函数,并返回一个concurrent.futures.Future对象 # 第二步,调用asyncio.wrap_future将concurrent.futures.Future对象包装为asyncio.Future对象 # 因为concurrent.futures.Future对象不支持await语法,所以需要包装为asyncio.Future才能使用 fut = loop.run_in_executor(None,func1) result = await fut print('default thread pool',result) # 2.Run in a custom thread pool; # with concurrent.futures.ThreadPoolExecutor() as pool: # result = await loop.run_in_executor(pool, func1) # print('custom thread pool',result) # 3.Run in a custom process pool; # with concurrent.futures.ProcessPoolExecutor() as pool: # result = await loop.run_in_executor(pool, func1) # print('custom process pool',result)asyncio.run(main()) |
9.1.4.7.异步和非异步混合案例
案例,asyncio+不支持异步的模块
1 | import requestsimport asyncioasync def download_image(url): # 发送网络请求,下载图片(遇到网络下载图片的IO请求,自动切换到其他任务) print('download start',url) loop = asyncio.get_event_loop() # reqeusts模块不支持异步操作,所以就使用线程池来配合实现了 future = loop.run_in_executor(None,requests.get,url) resposne = await future print('download end') # 图片保存到本地 filename = url.split('/')[-1] with open(filename,'wb') as fw: fw.write(resposne.content)if __name__ =='__main__': url_list = [ 'https://pic.netbian.com/uploads/allimg/210817/235554-162921575410ce.jpg', 'https://pic.netbian.com/uploads/allimg/210816/234129-162912848931ba.jpg', 'https://pic.netbian.com/uploads/allimg/210815/233459-16290416994668.jpg', ] tasks = [download_image(url) for url in url_list] loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait(tasks)) |
9.1.4.8.异步迭代器
什么是异步迭代器
实现饿了__aiter__()
和__anext__()
方法的对象,__anext__()
必须返回一个awaitable
对象,async_for
会处理异步迭代器的__anext()__
方法所返回的可等待都对象,直到其引发一个StopAsyncIteration
异常。
什么时异步可迭代对象
可在async_for
语句中被使用的对象,必须通过它的__aiter__()
方法返回一个asynchronous iterator
1 | import asyncioclass Reader(object): # """自定义异步迭代器(同时也是异步可迭代对象)""" def __init__(self): self.count = 0 async def readline(self): # await asyncio.sleep(2) self.count += 1 if self.count == 100: return None return self.count def __aiter__(self): return self async def __anext__(self): val = await self.readline() if val == None: raise StopAsyncIteration return valasync def func(): obj = Reader() async for item in obj: print(item) asyncio.run(func()) |
9.1.4.9.异步上下文管理器
此种对象通过定义__aenter__()
和aexit__()
方法来对async with
语句中的环境进行控制。
1 | import asyncioclass AsyncContextMannager: def __init__(self): self.conn = conn async def do_something(self): # 异步操作数据库 return 123 async def __aenter__(self): # 异步链接数据库 # self.conn = await asyncio.sleep(1) return self async def __aexit__(self,exc_type,tb): # 异步关闭数据库链接 await asyncio.sleep(1)async def func(): async with AsyncContextMannager() as f: resutl = await f.do_something() print(resutl)asyncio.run(func()) |
9.1.4.10.uvloop
是asyncio的事件循环的替代方案。事件循环 > 默认asyncio的事件循环
运行速度堪比go
1 | pip install uvloop |
注意:不支持windows
1 | import asyncioimport uvloopasyncio.set_event_loop_policy(uvloop.EventLoopPolicy())# 编写asyncio的代码,与之前的代码一致# 内部的事件循环,会由uvloop替代asyncio.run(...) |
asgi中的uvcorn,使用的就是uvloop
9.1.4.11.案例-异步操作redis
在使用python操作redis时,链接/操作/断开都是网络IO
pip install aioredis
示例一:
1 | import asyncioimport aioredisasync def execute(address,passward): print('start',address) redis = await aioredis.create_redis(address,passward=passward) # 网络IO操作 result = await redis.hmset_dict('car', key1=1, key2=2, key3=3) print(result) redis.close() # 网络IO操作,关闭redis链接 await redis.wait_closed() print('end',address)asyncio.run(execute('redis://127.0.0.2:6379','123')) |
示例二:
1 | import asyncioimport aioredisasync def execute(address,passward): print('start',address) redis = await aioredis.create_redis(address,passward=passward) # 网络IO操作 result = await redis.hmset_dict('car', key1=1, key2=2, key3=3) print(result) redis.close() # 网络IO操作,关闭redis链接 await redis.wait_closed() print('end',address)task_list = [ execute('redis://127.0.0.1:6379','123'), execute('redis://127.0.0.2:6379','123'),]asyncio.run(asyncio.wait(task_list)) |
9.1.4.12.案例-异步操作mysql
9.1.4.13.FastApi框架异步
9.1.4.14.异步爬虫
pip install aiohttp
1 | import aiohttpimport asyncioasync def fetch(session, url): print('start', url) async with session.get(url, verify_ssl = False) as response: text = await response.text() print('result:', url, len(text)) async def main(): async with aiohttp.ClientSession() as session: url_list =[ 'https://www.baidu.com', 'https://www.qq.com', 'https://pic.netbian.com/4kmeinv/' ] tasks = [asyncio.create_task(fetch(session, url)) for url in url_list] done, pending = await asyncio.wait(tasks)if __name__ == '__main__': asyncio.run(main()) |
9.1.4.15.总结
9.2.协程相关操作
9.3.多任务异步协程,实现异步爬虫
10.aiohttp模块
10.1.aiohttp模块引出
10.2.aiohttp+多任务异步协程,实现异步爬虫
11.selenium
11.1.selenium简介
selenium模块和爬虫之间有怎样的关联?
- 便捷的获取网站中动态加载的数据
- 便捷实现模拟登录
什么是selenium模块?
- 基于浏览器自动化的一个模块
11.2.selenium初试
selenium使用流程
环境安装:
pip install selenium
下载浏览器的驱动程序
编写基于浏览器自动化的操作代码
1
from selenium import webdriverfrom lxml import etreefrom time import sleep# 实例化一个浏览器对象(传入浏览器的驱动程序)bro = webdriver.Chrome(executable_path = './chromedriver.exe')# 让浏览器发起一个指定url对应请求bro.get('http://npm.taobao.org/mirrors/chromedriver/92.0.4515.107/')# 获取浏览器当前页面的源码数据page_text = bro.page_source# 解析字段tree = etree.HTML(page_text)src = tree.xpath('//div[@class="container"]/pre/a/@href')for i in range(0,len(src)): print(src[i])sleep(5)bro.quit()
注意点:谷歌中文官网上的浏览器,默认安装在c盘,安装之后不要移动目录,否则驱动无法检测。
11.3.selenium其他自动化操作
- 编写基于浏览器自动化的操作代码
- 发起请求:
get(url)
- 标签定位:find系列的方法
- 标签交互:
send_keys('xxx')
- 执行js程序:
excuted_script('jsCode')
- 前进、后退:
back()
、forward()
- 关闭浏览器:
quit()
- 发起请求:
1 | from selenium import webdriverfrom lxml import etreefrom time import sleepbro = webdriver.Chrome(executable_path='./chromedriver.exe')bro.get('https://www.taobao.com')search_input = bro.find_element_by_id('q')search_input.send_keys('Iphoe')bro.execute_script('window.scrollTo(0.document.body.scrollHeight)')sleep(2)btn =bro.find_element_by_css_selector('.btn-search')btn.click()bro.get('https://www.baidu.com')sleep(2)bro.back()sleep(5)bro.quit() |
11.4.iframe处理+动作链
1 | from selenium import webdriverfrom time import sleep# 导入动作链对应的类from selenium.webdriver import ActionChainsbro = webdriver.Chrome(executable_path='./chromedriver.exe')bro.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')# 如果定位的标签是存在ifram标签之中的,则必须通过如下操作,再进行标签定位bro.switch_to.frame('iframeResult') # 切换浏览器标签定位的作用域div = bro.find_element_by_id('draggable')# 动作链action = ActionChains(bro)# 点击长按指定的标签action.click_and_hold(div)for i in range(5): # perform()表示立即执行动作链操作 action.move_by_offset(7,0).perform() sleep(0.3)# 释放动作链action.release()print(div) |
11.5.selenium的模拟登陆
1 | from selenium import webdriverfrom time import sleepbro = webdriver.Chrome(executable_path='./chromedriver.exe')bro.get('https://qzone.qq.com/')bro.switch_to.frame('login_frame')a_tag = bro.find_element_by_id('switcher_plogin')a_tag.click()sleep(1)user_name = bro.find_element_by_id('u')sleep(1.1)password = bro.find_element_by_id('p')user_name.send_keys('1123123')password.send_keys('123123')sleep(1.2)btn = bro.find_element_by_id('login_button')btn.click()sleep(3)bro.quit() |
11.6.无头浏览器+规避检测
现在不少大网站对selenium采取了检测机制。比如正常情况下我们用浏览器访问淘宝等网站的window.navigator.webdriver
的值为undefined
。而使用selenium访问该值为true。那么如何解决这个问题呢?
只需要设置Chromedriver的启动参数即可。在启动Chromedriver之前,为Chrome开启实验性功能参数excludeSwitches
,它的值为['enable-automation']
,完整代码如下:
1 | from selenium import webdriverfrom time import sleep# 实现无可视化界面from selenium.webdriver.chrome.options import Options# 实现规避检测from selenium.webdriver import ChromeOptions# 实现无可视化界面的操作chrome_options = Options()chrome_options.add_argument('--headless')chrome_options.add_argument('--disable-gpu')# 实现规避检测option = ChromeOptions()option.add_experimental_option('excludeSwitches',['enable-automation'])# 如何实现让selenium规避被检测到的风险# bro = webdriver.Chrome(executable_path='./chromedriver.exe',chrome_options=chrome_options,options=option)bro = webdriver.Chrome(executable_path='./chromedriver.exe',options=option)# 五可视化界面(无头浏览器)bro.get('https://www.baidu.com')print(bro.page_source)sleep(2)bro.quit() |
11.7.超级鹰的基本使用
12.scrapy
12.1.scrapy框架初识
- 什么是框架
- 就是一个集成了很多的功能,并且 有很强通用性的一个项目模板。
- 如何学习框架
- 专门学习框架封装的各种功能的详细用法
- 什么是scrapy
- 爬虫中封装好的一个明星框架。
- 功能:高性能的持久化存储,异步的数据下载,高性能的数据解析,分布式。
12.2.scrapy环境安装
环境安装
linux或mac系统
pip install scrapy
windows系统
pip install scrapy
测试:在终端里录入
scrapy
命令,没有报错即表示安装成功。
12.3.scrapy基本使用
scrapy使用流程
创建工程
scrapy startproject ProName
进入工程目录
cd ProName
创建爬虫文件
scrapy genspider SpiderName www.xxx.com
1
import scrapyclass FirstSpider(scrapy.Spider): # 爬虫文件的名称,就是爬虫文件的唯一标识 name = 'first' # 允许的域名:用来限定start_urls中,哪些url可以进行请求发送 allowed_domains = ['www.baidu.com'] # 起始的url列表:该列表中存放的url会被scrapy自动的进行请求的发送,可以有多个 start_urls = ['http://www.baidu.com/','http://www.sogou.com'] # 作用于数据解析:response参数表示的就是,请求成功后对应的响应对象 # 会被调用多次,由start_urls中的元素个数决定的 def parse(self, response): pass
设置
ROBOTSTXT=False
设置
LOG_LEVEL='ERROR'
1
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3947.100 Safari/537.36'# Obey robots.txt rulesROBOTSTXT_OBEY = FalseLOG_LEVEL = 'ERROR'
编写相关操作代码
执行工程
scrapy crawl SpiderName
12.4.scrapy数据解析
爬取糗事百科https://www.qiushibaike.com/text/
1 | import scrapyclass QiushiSpiderSpider(scrapy.Spider): name = 'qiushi_spider' # allowed_domains = ['www.xxx.com'] start_urls = ['https://www.qiushibaike.com/text/'] def parse(self, response): # 解析作者的名称+段子内容 div_list = response.xpath('//div[@class="col1 old-style-col1"]/div') # print(div_list) for div in div_list: # xpath返回的是列表,但是列表元素一定是selector类型的对象 # extract可以将selector对象中,data参数的存储的字符串提取出来 # author = div.xpath('./div[1]/a[2]/h2/text()')[0].extract() author = div.xpath('./div[1]/a[2]/h2/text()').extract_first() # 如果列表调用了extract之后,则表示将列表中的每一个selecor对象中data对应的字符串提取了出来 content = div.xpath('./a[1]/div/span//text()').extract() content = ''.join(content) print(author,content) break |
设置USER_AGENT
运行scrapy crawl qiushi_spider
12.5.持久化存储
12.5.1.基于终端指令的持久化存储
只可以将parse方法的返回值存储到本地的文本文件中。
注意:持久化存储对应的文本文件的类型,只可以为:json
、jsonlines
、jl
、csv
、xml
、marshal
、pickle
指令:scrapy crawl qiushi_spider -o ./qiushi.csv
好处:简洁高效便捷
缺点:局限性比较强(数据只可以存储到指定后缀的文本文件中)
12.5.2.基础管道持久化存储
编码流程:
数据解析
在iem类中定义相关的属性
将解析的数据,封装存储到item类型的对象中
1
# items.pyimport scrapyclass QiushiItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() author = scrapy.Field() content = scrapy.Field() # pass
将item类型的数据,提交给管道进行持久化存储的操作
在管道类的process_item中,要将其接收到的item对象中存储的数据,进行持久化存储操作
process_item
- 专门用来处理item类型的对象
- 该方法可以接收爬虫文件提交过来的item对象
- 该方法没接收一个item,就会被调用一次
1
# pipelines.pyfrom itemadapter import ItemAdapterclass QiushiPipeline: fp = None # 重写父类的方法,该方法只会在开始爬虫的时候,被调用一次 def open_spider(self, spider): print('爬虫开始...') self.fp = open('./qiushi.txt','w',encoding='utf-8') # 专门用来处理item类型对象 def process_item(self, item, spider): author = item['author'] content = item['content'] self.fp.write(author + ':' + content + '\n') return item # 这里如果写了return,则item会传递给下一个即将执行的管道类,默认都是加上 # 结束爬虫时,会被调用一次 def close_spider(self,spider): print('爬虫结束...')
在配置文件中开启管道
1
ITEM_PIPELINES = { # 数值表示优先级,数值越小,优先级越高 'qiushi.pipelines.QiushiPipeline': 300,}
备注:如果有匿名用户,则会报错
完善author的xpath:
1
autho = div.xpath('./div[1]/a[2]/h2/text() | ./div[1]/span/h2/text()').extarct_first()
好处:通用性强
面试题:将爬取到的 数据一份存储到本地,一本存储到数据库,如何实现?
在管道文件中定义多个管道类:
1
# 管道文件中,一个管道类对应将一组数据存储到一个平台或一个载体中class mysqlPipeline(object): conn = None pool = None value = '' def open_spider(self, spider): self.pool = ConnectionPool(host='127.0.0.1',port=6379,password='foobared', decode_responses=True) def process_item(self, item, spider): self.conn = Redis(connection_pool=self.pool) self.conn.set('k1','v1') value = self.conn.get('k1') def close_spider(self, spider): print(self.value)
在
ITEM_PIPELINES
中配置:1
ITEM_PIPELINES = { 'qiushi.pipelines.QiushiPipeline': 300, 'qiushi.pipelines.redisPipeline': 301,}
爬虫文件提交的item,只会给管道文件中第一个被执行的管道类接受
process_item中的return item表示将item传递给下一个即将执行的管道类
12.6.全站数据爬取
基于Spider的全站数据爬取
就是将网站中某板块下的全部页码,对应的页面数据进行爬取
需求:爬取校花网中的照片的名称
实现方式:
将所有页面的url添加到start_urls列表(不推荐)
自行手动进行请求发送
1
import scrapyclass XiaohuaSpider(scrapy.Spider): name = 'xiaohua' # allowed_domains = ['www.xx.com'] start_urls = ['http://www.521609.com/tuku/index.html'] # 生成一个通用的url模板(不可变的) url = 'http://www.521609.com/tuku/index_%d.html' page_num = 2 def parse(self, response): li_list = response.xpath('/html/body/div[4]/div[3]/ul/li') for li in li_list: img_name = li.xpath('./a/p/text()').extract_first() print(img_name) if self.page_num <= 3: new_url = format(self.url % self.page_num) self.page_num += 1 # 手动发送请求,callback回调函数专门用作数据解析 yield scrapy.Request(url = new_url, callback = self.parse)
12.7.五大核心组件
- 引擎(scrapy)
- 用来处理整个系统的数据流处理,触发事务(核心)
- 调度器(Scheduler)
- 用来接受引擎发过来的请求,压入队列中,并在引擎再次请求的时候返回。可以想象成要给URL(抓取网页的网址或者是链接)的优先队列,由它来决定下一个要抓取的网址是什么,同时去除重复的网址。
- 下载器(Downloader)
- 用于下载网页内容,并将网页内容返回给引擎,下载器是建立在twisted这个高效的异步模型上的。
- 爬虫(Spiders)
- 爬虫是用来干活的,用于从特定网页中提取自己需要的信息,即所谓的实体(item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面。
- 项目管理(Pipeline)
- 负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体,验证实体信息有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管理,并经过几个特定的次序处理数据。
12.7.1.请求传参
- 使用场景:如果爬取解析的数据不在同一张页面中。我们就需要用到请求传参(深度爬取)
- 需求:爬取boss的岗位名称,岗位描述
如果要使用管道进行持久化存储,需要先在item.py中定义item:
1 | class BossproItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() # pass title = scrapy.Field() describe = scrapy.Field() |
然后导入item中的类:
1 | from bosspro.items import BossproItem |
然后在for循环中实例化item对象,把需要的字段,存到item类型的字段中:
1 | item = BossproItem()item['title'] = titleyield scrapy.Request(title_href,callback=self.parse_detail,meta={'item':item}) |
在回调的解析方法中,接受item对象,并传入该解析方法特有的值,最后返回item给管道:
1 | item = response.meta['item']item['describe'] = describeyield item |
分页爬取
定义url模板
1
template_url = 'http://news.longhoo.net/njxw/%d.html'page_num = 2
在首个url的parse方法中,进行分页操作
1 | if self.page_num <= 3: next_url = format(self.template_url % self.page_num) self.page_num += 1 yield scrapy.Request(next_url, callback=self.parse) |
练习:
12.7.2.scrapy图片爬取
基于scrapy爬取字符串类型的数据,和爬取图片类型的数据的区别?
- 字符串:只需要进行xpath进行解析,且提交到管道进行持久化存储。
- 图片:xpath解析出图片的src的属性值。单独的对图片地址发起请求获取图片二进制类型的数据。
ImagePipeline:
- 只需要将Img的src的属性值进行解析,提交到管道,管道就会对图片的src进行请求发送获取图片的二进制类型数据,且还会帮我们进行持久化存储。
- 需求:爬取图片网的图片
使用流程:
- 数据解析(获取图片地址)
- 在管道文件中,自定义一个基于ImagesPipeline的一个管道类
- get _media_request
- file_path
- item_completed
- 在配置文件中:
- 指定图片存储路径:
IMAGES_STORE='./imgs'
- 指定开启的管道:自定义管道类
- 指定图片存储路径:
注意点:
默认的管道类,是不处理图片格式的数据的。
ImagesPipeline专门用于文件下的管道类,下载过程支持异步和多线程
重写父类的三个方法
1
# Define your item pipelines here## Don't forget to add your pipeline to the ITEM_PIPELINES setting# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html# useful for handling different item types with a single interfacefrom itemadapter import ItemAdapterfrom scrapy.pipelines.images import ImagesPipelineimport scrapy# class GirlpicPipeline:# def process_item(self, item, spider):# return itemfrom girlpic.items import GirlpicItemclass girlPipeline(ImagesPipeline): # 对item中的图片进行请求操作 def get_media_requests(self, item, info): # return super().get_media_requests(item, info) yield scrapy.Request(item['src']) # 定制图片的名称 # def file_path(self, request, response, info, *, item): # return super().file_path(request, response=response, info=info, item=item) def file_path(self, request, response=None, info=None): item = GirlpicItem() image_name = item['image_name'] return image_name # def item_completed(self, results, item, info): # return super().item_completed(results, item, info) # 返回给下一个即将被执行的管道类中 return item
在settings.py中,定义图片的存储目录
IMAGES_STORE = './imgs'
如果路径不存在,则会自行创建
在settings.py中,开启指定的管道类
图片懒加载
- 有些图片的src属性,写成src2之类的,只有元素被滑动到可视窗口中是,才会切换成src属性。
- 在xpath提取的时候,得提取src2属性。
- 有的图片懒加载,变化的是是src的值,这时候得用其他属性,直接用src属性,可能会有问题。
12.7.3.中间件
12.7.3.1.中间件初始
- 下载中间件
- 位置:引擎和下载器之间
- 作用:批量拦截到整个工程中所有的请求和响应
- 拦截请求:
- UA伪装
- 代理IP
- 拦截响应:
- 篡改响应数据,响应对象
12.7.3.2.中间件-处理请求
爬虫中间件
下载中间件
process_request
用来拦截请求
UA伪装
1
def process_request(self, request, spider): request.headers['User-Agent'] = random.choice(self.user_agent_list) request.meta['proxy'] = 'http://183.151.202.233:9999' return None
process_response
- 用来所有的拦截响应
- 需求:爬取网易新闻中的新闻数据(标题和内容)
- 1.通过网易新闻
process_exception
用来拦截发生异常的请求对象
代理ip:process_exception:return request
1
def process_exception(self, request, exception, spider): # 代理 if request.url.split(':')[0] == 'http': request.meta['proxy'] = 'http:' + random.choice(self.PROXY_http) else: request.meta['proxy'] = 'https:' + random.choice(self.PROXY_https) # 将修正后的请求对象,重新发送 return request
settings.py中开启中间件
12.7.4.中间件-处理响应
案例:网易新闻
通过网易新闻的首页解析五大板块对应的详情页的url(没有动态加载)
每一个板块对应的新闻标题都是动态加载的出来的(动态加载)
使用下载中间件的process_response,篡改响应对象
1
def process_response(self, request, response, spider): bro = spider.bro # 挑选指定的响应对象进行篡改 # 通过传入的url和爬虫中的存的url进行判断 # 匹配上的话,就篡改对应的response对象 if request.url in spider.category_urls: # 传入目标板块的response # 针对定位的response进行篡改 # 实例化一个新的响应对象(符合需求,包含动态加载出来的新闻数据),替代原来旧的响应对象 # 基于selenium获取动态加载数据 bro.get(request.url) sleep(2) page_text = bro.page_source new_response = HtmlResponse(url=request.url, body=page_text, encoding='utf-8', request=request) return new_response else: return response
在爬虫文件的init方法中,实例化浏览器对象
1
def __init__(self): self.bro = webdriver.Chrome(executable_path='./chromedriver.exe')
通过解析出每一新闻详情页的url获取详情页的页面源码,解析出新闻内容
12.7.5.crawlspider的全站数据爬取
Crawlspider:Spider的一个子类
- 全站数据爬取的方式
- 基于Spider:手动请求
- 基于CrawlSpider
- CrawlSpider的使用:
- 创建一个工程
- cd xxx
- 创建爬虫文件(CrawlSpider)
scrapy genspider -t crawl xxx www.xxx.com
- 启动爬虫文件
scrapy crawl 文件名
- 链接提取器
- 根据指定规则
(allow="正则")
进行执行链接的提取 - 注意特殊字符,在正则表达式中的转义,比如
?
- 根据指定规则
- 规则解析器
- 将链接提取器提取到的链接,进行指定规则
(callback)
的解析操作 - follow=True,则意味着对解析出来的url页面,再重复按照相同的规则进行提取,并去重。
- 将链接提取器提取到的链接,进行指定规则
12.7.5.1.练习
- https://wz.sun0769.com/political/index/politicsNewest?id=1&page=0
- 爬取编号,新闻标题,内容
- 二级页编号要多爬取一次
- 可以设置多个链接提取器
12.8.分布式
12.8.1.分布式概述
分布式爬虫
- 概念:我们需要搭建一个分布式的集群,让其对一组资源进行分布联合爬取。
- 作用:提升爬取效率
如何实现分布式
- 安装一个scrapy-redis组件
- 原生的scrapy是不可以实现分布式爬虫的,必须要让scrapy结合着scrapy-redis组件,一起实现分布式爬虫。
12.8.2.分布式搭建
创建一个工程
创建一个基于CrawlSpider的爬虫文件
修改当前的爬虫文件
- 导包:
from scrapy_redis.spiders import RedisCrawlSpider
- 将
strat_urls
和allowed_domains
进行注释 - 添加一个新属性:
redis_key = 'sun'
,表示可以被共享的调度器队列的名称 - 编写数据解析相关的操作
- 将当前爬虫类的父类修改成RedisCrawlSpider
- 导包:
修改配置文件,末尾添加
1
# 指定管道ITEM_PIPELINES = { 'scrapy_redis.pipelines.RedisPipeline' : 400}# 指定调度器# 增加了一个去重容器类的配置,作用使用Redis集合来存储请求的指纹数据,从而实现请求去重的持久化DUPEFILTER_CLASS ='scrapy_redis.dupefilter.RFPDupeFilter'# 使用scrapy_redis组件自己的调度器SCHEDULER = 'scrapy_redis.scheduler.Scheduler'# 配置调度器是否要持久化,也就是当爬虫结束了,要不要清空Redis中请求队列和去重指纹的set。如果是,TrueSCHEDULER_PERSIST = True# 指定redisREDIS_HOST = '127.0.0.1'REDIS_PORT = '6379'REDIS_PARAMS = { 'password': '123123', }
redis相关配置操作
若是云主机,在控制台开启对应端口
注释 bind
关闭保护模式:protected mode = no
后台启动redis
启动客户端
执行工程
- 进入到爬虫文件所在的目录,执行
scrapy runspider xxx.py
- 向调度器的队列中,放入起始url
- 调度器的队列在redis的客户端中
lpush key url
- 调度器的队列在redis的客户端中
- 进入到爬虫文件所在的目录,执行
爬取到的数据存储在了redis的proName:items这个数据结构中
12.8.3.增量式爬虫
- 概念:监测网站数据实时更新的情况,只会爬取网站最新更新出来的数据
- 分析:
- 指定一个起始url
- 基于CrawlSpider获取其他页码链接
- 基于Rule将其他页码链接进行请求
- 从每一个页码对应的页面源码中,解析出每一个电影详情页的url
- 核心:
- 检测电影详情页的url之前有没有被请求过
- 将爬取过的电影详情页的url存储
- 对详情页的url发起请求,然后解析出电影名称和简介
- 进行持久化存储