系列文章目录
爬虫学习笔记(总)
爬虫学习笔记(第一章)爬虫简介
爬虫学习笔记(第二章)requests模块
爬虫学习笔记(第三章)数据解析
爬虫学习笔记(第四章)验证码识别
爬虫学习笔记(第五章)requests进阶
爬虫学习笔记(第六章)高性能异步爬虫
爬虫学习笔记(第七章)动态加载数据处理
爬虫学习笔记(第八章)Scrapy框架[上]
爬虫学习笔记(第八章)Scrapy框架[下]
爬虫学习笔记(第九章)增量式爬虫
爬虫学习笔记(补充内容)异步编程
Log
2021.07.14开始系统地学习爬虫:2020年Python爬虫全套课程(学完可做项目)
2021.07.15接着肝,感觉按章节分篇来写会比较好,最后再弄吧,一个汇总的,一系列分章节的。
2021.07.16小学期的课设和实验室的事占用了一些时间,目前学习进度还是有点慢,昨晚交流完了之后要更改数据库MySQL改成MongoDB,主要还是爬下来的数据存储时对长度的限定要求。
2021.07.17今天有点水,只写了几篇爬虫代码
2021.07.20学了两天正则,还弄了其他的事,今天继续
2021.07.21琐事
2021.07.22utf-8编码下文字仍为乱码应该怎么办
2021.07.27放假了,这两天在忙其它事,这几天回来继续学
2021.07.28学完了第三章;搞完了第三章
2021.07.29第四章似乎不是很多,后面几章好像也不多。
2021.07.31战线似乎拖得有点长,争取在未来5天之内学完剩下的内容。
2021.08.01弄完第五章。干就完了。协程听得有点蒙,弄完第六章
2021.08.06肝下去,坚持不弃坑。
2021.08.08弄完第七章。
2021.08.13最近在学车、准备数模国赛以及ACM竞赛,抽时间学点,完结的时间要延后了。。。
2021.08.22断断续续
2021.08.30完成第八章的学习,感觉学的有些迷糊
2021.09.01完成第九章的学习,回头还要学一下协程的基础内容,听得有点懵。
完结撒花~~~
第一章
一、爬虫简介
- 概念:模拟&抓取
- 价值:实际应用&就业
- 合法性:
法律上不禁止
具有违法风险 (干扰网络正常运营;爬取法律保护的数据)
应对:
优化代码;审查爬取内容
二、爬虫分类
- 通用爬虫: 抓取一整张页面内容;
- 聚焦爬虫: 抓取页面的局部内容;
- 增量式爬虫: 抓取网站更新内容。
三、反爬&反反爬
- 反爬机制: 门户网站防止被爬虫程序爬取数据
- 反反爬策略: 破解反爬机制
- robots.txt协议(君子协议):
规定网页内容哪些数据可以被爬取(君子——无强制机制,靠自觉)
四、http协议&https协议
- http协议: 服务器与客户端进行数据交换的协议
- 常用请求头信息:
- User-Agent: 请求载体的身份标识
- Connection: 请求完毕后,断开连接||保持连接
- 常用响应头信息:
- Cntent-Type: 服务器响应回客户端的数据类型
- https协议: 安全的超文本传输协议(http协议)(s——security,进行了数据加密)
- 加密方式:
- 对称密钥加密: 同时传输密钥和密文
- 非对称密钥加密: 服务器传输密钥(可能会被中间拦截,然后进行恶意篡改),客户端返回相应密文
- 证书密钥加密: 非对称基础上,认证机构确认后给密钥签名(https采用)
(三种方式的详细介绍:路飞学城-学习文档——http&https协议)
第二章
一、request模块
简介
- urllib模块: 古老,用法繁琐
- requests模块: 功能强大(爬虫半壁江山),用法简洁且高效
Python中原生的基于网络请求的模块 - 作用: 模拟浏览器发请求
- 如何使用: (requests模块的编码流程)
- 指定url
- UA伪装
- 请求参数的处理(没有可以不处理)
- 发起请求
- 获取响应数据
- 持久化存储
- 指定url
- 环境安装:
- pip install requests
实战编码
①爬取搜狗首页的页面数据
- 需求:爬取搜狗首页的页面数据
# 需求:爬取搜狗首页的页面数据
import requests
from bs4 import BeautifulSoup
if __name__ == "__main__":
# step1 :指定url
url = 'https://www.sogou.com/'
# step2 :发起请求
# get方法会返回一个相应对象
response = requests.get(url=url)
# step3 :获取响应数据.text返回的是字符串形式的响应数据
page_text = response.text
# print(page_text)
x = BeautifulSoup(page_text, 'lxml') # 使用 lxml 解析器作为底层解析引擎
print(x.prettify()) # 变成规整的 html 格式
# step4 :持久化存储
with open('./sogou.html', 'w', encoding='utf-8') as fp:
fp.write(page_text)
print('数据爬取结束')
遇到的问题及解决: Python网络爬虫之:如果请求url 返回的 response 中代码是一行而不是规范的 html 分段格式,处理方法
实战巩固
- 需求:爬取搜狗指定词条对应的搜索结果页面(简易网页采集器)
- 需求:破解百度翻译
- 需求:爬取豆瓣电影分类排行(https://movie.douban.com/)中的电影详情数据
- 需求:爬取肯德基餐厅查询(http://www.kfc.com.cn/kfccda/storelist/index.aspx)中指定地点的餐厅数
- 需求:爬取国家药品监督管理总局中基于中华人民共和国化妆品生产许可证相关数据
②网页采集器
UA
- UA:User-Agent (请求载体的身份标识)
- UA检测:门户网站的服务器会检测对应请求的载体身份标识,如果检测到的载体身份标识唯一正常浏览器说明该请求是一个正常的请求。但是,如果检测到请求的载体身份标识不是基于某一浏览器的,则表示该请求为不正常的请求(爬虫),则服务器端就很有可能拒绝该次请求
- UA伪装:让爬虫对应的请求载体身份标识伪装成某一浏览器
import requests
if __name__ == '__main__':
# UA伪装:将对应的User-Agent封装到一个字典中
headers = {
'User-Agent':'别想看到我的相关信息/doge'
}
url = 'https://www.sogou.com/web'
# 处理url携带的参数:封装到字典中
kw = input('enter a word:')
param = {
'query': kw
}
# 对指定的url发起的请求时携带参数的,并且请求过程中处理了参数
response = requests.get(url=url, params=param, headers=headers)
page_text = response.text
filename = kw + '.html'
with open(filename, 'w', encoding='utf-8') as fp:
fp.write(page_text)
print(filename, '保存成功')
③破解百度翻译
import requests
import json
if __name__ == '__main__':
# 1.指定url
post_url = 'https://fanyi.baidu.com/sug'
# 2.进行UA伪装
headers = {
'User-Agent': '别想看到我的相关信息/doge'
}
# 3.post请求参数处理(同get请求一致)
word = input('enter a word:')
data = {
'kw': word
}
# 4.请求发送
response = requests.post(url=post_url, data=data, headers=headers)
# 5.获取响应数据,json方法返回的是obj(对象)(如果确认响应数据是json类型的,才可以使用json())
# response.text
dic_obj = response.json()
print(dic_obj)
# 进行持久化存储
fileName = word + '.json'
fp = open(fileName, 'w', encoding='utf-8')
json.dump(dic_obj,fp=fp, ensure_ascii=False)
print('over')
④豆瓣电影爬取
- 阿贾克斯: ajax 即Asynchronous javascript and xml,中文意为异步的javascript和xml,用于与后台进行异步交互
- 在线Json格式化校验工具
以下代码已经经过了优化
import requests
import json
if __name__ == '__main__':
url = 'https://movie.douban.com/j/chart/top_list?'
type = input('(纪录片——1;传记——2;犯罪——3;历史——4;动作——5;情色——6;歌舞——7;\n'
'儿童——8;悬疑——10;剧情——11;灾难——12;爱情——13;音乐——14;\n'
'冒险——15;奇幻——16;科幻——17;运动——18;惊悚——19;恐怖——20;\n'
'战争——22;短片——23;喜剧——24;动画——25;同性——26;西部——27;\n'
'家庭——28;武侠——29;古装——30;黑色电影——31)\n'
'(PS:缺少9,21可能是用于反爬,防止循环数据时进行爬取)\n'
'enter a type:')
start = input('enter start(first one corresponds with number 0):')
limit = input('enter the amount you want:')
param = {
'type': type, # 可以进行修改,喜剧:24, 动作片:5 爱情片:13 etc.
'interval_id': '100:90',
'action': '',
'start': start, # 从库中的第几部电影去取 0对应第一部电影
'limit': limit, # 一次取出的个数
}
headers = {
'User-Agent': '别想看到我的相关信息/doge'
}
response = requests.get(url=url, params=param, headers=headers)
list_data = response.json()
fp = open('./douban.json', 'w', encoding='utf-8')
json.dump(list_data, fp=fp, ensure_ascii=False)
print('over')
⑤肯德基餐厅位置爬取
import requests
import json
if __name__ == '__main__':
url = 'http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=keyword'
keyword = input('enter a word:')
param = {
'cname': '',
'pid': '',
'keyword': keyword,
'pageIndex': '1',
'pageSize': '10',
}
headers = {
'User-Agent': '找到你自己的信息输入就行'
}
response = requests.get(url=url, params=param, headers=headers)
list_data = response.text
fp = open('./{}KFC.txt'.format(keyword), 'w', encoding='utf-8')
json.dump(list_data, fp=fp, ensure_ascii=False)
# print(list_data)
print('over')
⑥药监总局相关数据爬取
- 动态加载数据
- 首页中对应的企业信息数据是通过Ajax动态请求到的
- 通过对详情页url的观察发现:
http://scxk.nmpa.gov.cn:81/xk/itownet/portal/dzpz.jsp?id=b552e60bdce74f8abbd3c496bd8926e7
http://scxk.nmpa.gov.cn:81/xk/itownet/portal/dzpz.jsp?id=c6c0a187bdba47abb243a4ddcd0d1ff0- url的域名都是一样的,只有携带的参数(ID)不一样
- ID值可以从首页对应的Ajax请求到的json串中获取
- 域名和ID可以拼接成一个完整的企业对应的详情页url
- 详情页的数据也是动态加载出来的
- http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsById
- http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsById
- 观察后发现:
- 所有的post请求的url都是一样的,只有参数的ID值是不同的
- 如果我们可以批量获取多家企业的ID后,就可以将ID和url形成一个完整的详情页数据的Ajax请求的url
import requests
import json
if __name__ == '__main__':
headers = {
'User-Agent': '该内容不可见qwq'
}
id_list = [] # 存储企业的id
all_data_list = [] # 存储所有企业的详情数据
# 批量获取不同企业的ID值
url = 'http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsList'
# 参数的封装
for page in range(1, 6):
# 理论上来说,这里把6改成379(截止20210717)就可以爬取所有的页面内容了
# 但是由于该网站采取了反爬策略,50之后(含50)的内容都不可以爬取了
# (也不能是说反爬吧,正常浏览也只能看到49页,之后的页面就显示数据异常,不能加载)
page = str(page)
data = {
'on': 'true',
'page': page,
'pageSize': '15',
'productName': '',
'conditionType': '1',
'applyname': '',
'applysn': '',
}
json_ids = requests.post(url=url, headers=headers, data=data).json()
for dic in json_ids['list']:
id_list.append(dic['ID'])
# print(id_list)
# 获取企业详情数据
post_url = 'http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsById'
for id in id_list:
data = {
'id': id
}
detail_json = requests.post(url=post_url, headers=headers, data=data).json()
# print(detail_json, '-----------------ending-----------------')
all_data_list.append(detail_json)
# 持久化存储
fp = open('./allData.json', 'w', encoding='utf-8')
json.dump(all_data_list, fp=fp, ensure_ascii=False)
print('over')
- 数据解析:
- 聚焦爬虫
- 正则
- bs4
- xpath
第三章
一、理论知识
- 聚焦爬虫:爬取页面中指定的页面内容。
- 编码流程:
- 1.指定url
- 2.发起请求
- 3.获取响应数据
- 4.数据解析
- 5.持久化存储
- 编码流程:
- 数据解析分类:
- 正则
- bs4
- xpath(重点,因为通用性强)
- 数据解析原理概述:
- 解析的局部文本内容都会在标签之间或标签对应的属性中进行存储
- 1.进行指定标签的定位
- 2.标签或者标签对应的属性中存储的数据值进行提取(解析)
二、图片爬取(爬一张)
- 需求:爬取糗事百科中糗图板块下所有的糗图图片
# 需求:爬取糗事百科中糗图板块下所有的糗图图片
import requests
if __name__ == "__main__":
# 如何爬取图片数据
url = 'https://pic.qiushibaike.com/system/pictures/12449/124493985/medium/LN0LAJL8MW92B3VB.jpg'
# content 返回的是二进制形式的图片数据
# text (字符串) conten (二进制) json() (对象)
img_data = requests.get(url=url).content
with open('./qiutu.jpg', 'wb') as fp:
fp.write(img_data)
三、正则解析(爬一页)
分析:
<div class="thumb">
<a href="/article/124546781" target="_blank">
<img src="//pic.qiushibaike.com/system/pictures/12454/124546781/medium/I19I57DDXMNZ8QFD.jpg" alt="糗事#124546781" class="illustration" width="100%" height="auto">
</a>
</div>
使用正则获取图片对应的url:
ex = '<div class="thumb">.*?<img src="(.*?)" alt.*?</div>'
最终代码如下:
# 需求:爬取糗事百科中糗图板块下所有的糗图图片
import requests
import re
import os
if __name__ == "__main__":
# 创建一个文件夹,保存所有的图片
if not os.path.exists('./qiutuLibs'):
os.mkdir('./qiutuLibs')
url = 'https://www.qiushibaike.com/imgrank/'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
}
# 使用通用爬虫对url对应的一整站页面进行爬取
page_text = requests.get(url=url, headers=headers).text
# 使用聚焦爬虫将页面中所有的糗图进行解析/提取
ex = '<div class="thumb">.*?<img src="(.*?)" alt.*?</div>'
# re.S 单行匹配 re.M 多行匹配
img_src_list = re.findall(ex, page_text, re.S) # findall 返回的是列表
print(img_src_list)
for src in img_src_list:
# 拼接出一个完整的图片url
src = 'https:' + src
# 请求到了图片的二进制数据
img_data = requests.get(url=src, headers=headers).content
# 生成图片名称
img_name = src.split('/')[-1]
# 图片存储的路径
imgPath = './qiutuLibs/' + img_name
with open(imgPath, 'wb') as fp:
fp.write(img_data)
print(img_name, '下载成功')
四、正则解析(分页爬)
# 需求:爬取糗事百科中糗图板块下所有的糗图图片
import requests
import re
import os
if __name__ == "__main__":
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
}
# 创建一个文件夹,保存所有的图片
if not os.path.exists('./qiutuLibs'):
os.mkdir('./qiutuLibs')
# 设置一个通用的url模板
url = 'https://www.qiushibaike.com/imgrank/page/%d/'
# pageNum = 1
for pageNum in range(1, 3): # 修改第二个数字改变爬取页数
# 对应页码的url
new_url = format(url%pageNum) # 返回字符串
# 使用通用爬虫对url对应的一整站页面进行爬取
page_text = requests.get(url=new_url, headers=headers).text
# 使用聚焦爬虫将页面中所有的糗图进行解析/提取
ex = '<div class="thumb">.*?<img src="(.*?)" alt.*?</div>'
# re.S 单行匹配 re.M 多行匹配
img_src_list = re.findall(ex, page_text, re.S) # findall 返回的是列表
print(img_src_list)
for src in img_src_list:
# 拼接出一个完整的图片url
src = 'https:' + src
# 请求到了图片的二进制数据
img_data = requests.get(url=src, headers=headers).content
# 生成图片名称
img_name = src.split('/')[-1]
# 图片存储的路径
imgPath = './qiutuLibs/' + img_name
with open(imgPath, 'wb') as fp:
fp.write(img_data)
print(img_name, '下载成功')
五、bs4解析基础
- bs4进行数据解析
- 数据解析的原理:
- 1.标签定位
- 2.提取标签、标签属性中存储的数据值
- bs4数据解析的原理:
- 1.实例化一个BeautifulSoup对象,并且将页面源码数据及加载到该对象中
- 2.通过调用BeautifulSoup对象中相关的属性或者方法进行签定位和数据解析
- 环境安装:
pip install bs4
pip install lxml
- 如何实例化BeautifulSoup对象:
from bs4 import BeautifulSoup
- 对象的实例化:
- 1.将本地的html文档中的数据加载到该对象中
fp = open('./test.html', 'r', encoding='utf-8')
soup = BeautifulSoup(fp, 'lxml')
- 2.将互联网上获取的页面源码加载到对象中
page_text = requests.text
soup = BeautifulSoup(page_text, 'lxml')
- 1.将本地的html文档中的数据加载到该对象中
- 提供的用于数据解析的方法和属性:
soup.tagName
: 返回的是文档中第一次出现的tagName 对应的标签soup.find()
:find('tagName')
: 等同于soup.div- 属性定位:
soup.find('div', class_/id/attr='song')
soup.find_all('tagName')
: 返回符合要求的所有标签(列表)
- select:
select('某种选择器(id,class,标签。。。选择器)')
: 返回的是一个列表- 层级选择器:
soup.select('.tang > ul > li > a')
:>表示的是一个层级soup.select('.tang > ul a')
:空格表示的是多个层级
- 获取标签之间的文本数据:
soup.a.text/string/get_text()
两个属性一个方法text/get_text()
: 可以获取某一个标签中所有的文本内容(不属于直系的也可以)string
:只可以获取该标签下面直系的文本内容
- 获取标签中属性值:
soup.a['href']
各种用法的例子代码:
- 数据解析的原理:
from bs4 import BeautifulSoup
if __name__ == "__main__":
# 将本地的html文档中的数据加载到该对象中
fp = open('./test.html', 'r', encoding='utf-8')
# 第一个参数为文件路径,第二个为固定的lxml解析器
soup = BeautifulSoup(fp, 'lxml')
# print(soup)
print(soup.a) # soup.tagName 返回的是html中第一次出现的tagName标签
print(soup.div)
# find('tagName'):等同于soup.div
print(soup.find('div')) # print(soup.div)
print(soup.find('div', class_='song')) # 注意'class'为关键字,加上下划线'class_'为参数名称
print(soup.find_all('a')) # 返回是一个列表类型
print(soup.select('.tang')) # '.'表示class select返回列表
print(soup.select('.tang > ul > li > a')[0]) # 1.层级选择器'>'表示一个层级 2.bs4支持属性定位,不支持索引定位 3.返回的是列表
print(soup.select('.tang > ul > li > a')[0].text)
print(soup.select('.tang > ul > li > a')[0].string)
print(soup.select('.tang > ul > li > a')[0].get_text())
print(soup.select('.tang > ul > li > a')[0]['href'])
六、bs4解析案例实战
- 需求:爬取三国演义小说所有的章节标题和章节内容
代码如下:
import requests
from bs4 import BeautifulSoup
# 需求:爬取三国演义小说所有的章节标题和章节内容http://www.cdjianxin.com/book/sanguoyanyi.html
if __name__ == "__main__":
# 对首页的页面数据进行爬取
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
}
url = 'http://www.cdjianxin.com/book/sanguoyanyi.html'
page = requests.get(url=url, headers=headers)
page.encoding = 'utf-8'
# 在首页中解析出章节的标题和详情页的url
# 1.实例化BeautifulSoup对象,需要将页面源码数据加载到该对象中
soup = BeautifulSoup(page.text, 'lxml')
# 解析章节标题和详情页url
li_list = soup.select('.book-mulu > ul > li')
fp = open('./sanguo.txt', 'w', encoding='utf-8')
for li in li_list:
title = li.a.string
detail_url = 'http://www.cdjianxin.com' + li.a['href']
# 对详情页发起请求,解析出章节内容
detail_page = requests.get(url=detail_url, headers=headers)
detail_page.encoding = 'utf-8'
# 解析出详情页中相关的章节内容
detail_soup = BeautifulSoup(detail_page.text, 'lxml')
div_tag = detail_soup.find('div', class_='chapter_content')
# 解析到了章节的内容
content = div_tag.text
# 持久化存储
fp.write(title + ':' + content + '\n')
print(title, "爬取成功")
七、xpath解析基础
- xpath解析:最常用且最便捷高效的一种解析方式,同时也是通用性最强的一种方式。
- xpath解析原理:
- 1.实例化一个etree的对象,且需要将被解析的页面源码数据加载到该对象中。
- 2.调用etree对象中的xpath方法结合着xpath表达式实现标签的定位和内容的捕获。
- 环境的安装:
pip install lxml
- 如何实例化一个etree对象:
from lxml import etree
- 1.将本地的html文档中的源码数据加载到etree对象中:
etree.parse(filePath)
- 2.可以将从互联网上获取的源码数据加载到该对象中:
etree.HTML('page_text')
xpath('xpath表达式')
:/
:表示的是从根节点开始定位。表示的是一个层级//
:表示的是多个层级;可以表示从任意位置开始定位。- 属性定位:
//div[@class="song"]
语法:tag[@attrName="attrValue"]
- 索引定位:
//div[@class="song"]/p[3]
索引是从1开始的 - 取文本:
/text()
获取的是标签中的直系文本内容//text()
获取标签中非直系文本的内容(所有的文本内容)
- 取属性:
/attrName ==>img/src
- 1.将本地的html文档中的源码数据加载到etree对象中:
- xpath解析原理:
- 备注:python3.5之后的 lxml 模块中不能再直接引入etree模块
from lxml import etree
变为from lxml import html
etree = html.etree
样例代码:
# from lxml import etree
from lxml import html
etree = html.etree
if __name__ == "__main__":
# 实例化好了一个etree对象,且将被解析的源码加载到了该对象中
tree = etree.parse('test.html')
# r = tree.xpath('/html/body/div')
# r = tree.xpath('/html//div')
# r = tree.xpath('//div')
# r = tree.xpath('//div[@class="song"]')
# r = tree.xpath('//div[@class="song"]/p[3]') # 索引从1开始,不是从0开始
# r = tree.xpath('//div[@class="tang"]//li[5]/a/text()')[0]
# r = tree.xpath('//li[7]//text()')
# r = tree.xpath('//div[@class="tang"]//text()')
r = tree.xpath('//div[@class="song"]/img/@src')
print(r)
八、xpath实战
①案例一:58二手房
- 需求:爬取58二手房中的房源信息 (2021.7.27版58同城。网站的html代码架构已经改变,和教程里的版本不一样)
代码如下:
import requests
from lxml import html
etree = html.etree
# 需求:爬取58二手房中的房源信息 (2021.7.27版58同城。网站的代码已经改变,和教程里的版本不一样)
if __name__ == "__main__":
# 爬取到页面源码数据
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
}
url = 'https://bj.58.com/ershoufang/'
page_text = requests.get(url=url, headers=headers).text
# 数据解析
tree = etree.HTML(page_text)
div_list = tree.xpath('//div[@class="property-content"]')
print(div_list)
fp = open('58.txt', 'w', encoding='utf-8')
for div in div_list:
# 局部解析
title = div.xpath('./div[@class="property-content-detail"]/'
'div[@class="property-content-title"]/h3/text()')[0] # 若开头用'/'则表示html根目录,'./'表示当前div指向的
print(title)
price = div.xpath('./div[@class="property-price"]/p[@class="property-price-total"]//text()')[0]
print(price)
# print(div.xpath('./h3/text()'))
fp.write('标题:' + title + '\n')
fp.write('价格:' + price + '万\n')
②案例二:下载图片数据
- 需求:解析下载图片数据
- 爬取内容中文乱码问题:
- 原代码:
page_text = requests.get(url=url, headers=headers).text
- 方法一:手动设定响应数据的编码格式
response = requests.get(url=url, headers=headers)
response.encoding = 'utf-8'
page_text = response.text
- 方法二:通用处理中文乱码的解决方案——iso编码,gbk解码
img_name = img_name.encode('iso-8859-1').decode('gbk')
- 原代码:
案例代码如下:
# 需求:解析下载图片数据 https://pic.netbian.com/4kdongwu/
import requests
from lxml import html
etree = html.etree
import os
if __name__ == "__main__":
url = 'https://pic.netbian.com/4kdongwu/'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
}
# page_text = requests.get(url=url, headers=headers).text # 中文乱码
response = requests.get(url=url, headers=headers)
# 手动设定响应数据的编码格式
# response.encoding = 'utf-8'
page_text = response.text
# 数据解析:src属性值 alt属性值
tree = etree.HTML(page_text)
li_list = tree.xpath('//div[@class="slist"]/ul/li')
# 创建一个文件夹
if not os.path.exists('./picLibs'):
os.mkdir('./picLibs')
for li in li_list:
img_src = 'https://pic.netbian.com' + li.xpath('./a/img/@src')[0]
img_name = li.xpath('./a/img/@alt')[0] + '.jpg'
# 通用处理中文乱码的解决方案
img_name = img_name.encode('iso-8859-1').decode('gbk')
# print(img_src)
# print(img_name)
# 请求图片进行持久化存储
img_data = requests.get(url=img_src, headers=headers).content
img_path = 'picLibs/' + img_name
with open(img_path, 'wb') as fp:
fp.write(img_data)
print(img_name, '下载成功')
③案例三:解析出所有城市名
- 需求:解析出所有城市名称
- 要点:使用或运算符
|
:
a_list = tree.xpath('//div[@class="bottom"]/ul/li/a | //div[@class="bottom"]/ul/div[2]/li/a')
案例代码如下:
# 需求:解析出所有城市名称 https://www.aqistudy.cn/historydata/
import requests
from lxml import html
etree = html.etree
if __name__ == "__main__":
# # version1.0
# headers = {
# 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
# }
# url = 'https://www.aqistudy.cn/historydata/'
# page_text = requests.get(url=url, headers=headers).text
#
# tree = etree.HTML(page_text)
# hot_li_list = tree.xpath('//div[@class="bottom"]/ul/li')
# all_city_names = []
# # 解析到了热门城市的城市名称
# for li in hot_li_list:
# hot_city_name = li.xpath('./a/text()')[0]
# all_city_names.append(hot_city_name)
#
# # 解析的是全部城市的名称
# city_names_list = tree.xpath('//div[@class="bottom"]/ul/div[2]/li')
# for li in city_names_list:
# city_name = li.xpath('./a/text()')[0]
# all_city_names.append(city_name)
# print(all_city_names)
# print(len(all_city_names))
# # version2.0
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
}
url = 'https://www.aqistudy.cn/historydata/'
page_text = requests.get(url=url, headers=headers).text
tree = etree.HTML(page_text)
# 解析到热门城市和所有城市对应的a标签
# div[@class="bottom"]/ul/li/a 热门城市a标签的层级关系
# div[@class="bottom"]/ul/div[2]/li/a 所有城市a标签的层级关系
a_list = tree.xpath('//div[@class="bottom"]/ul/li/a | //div[@class="bottom"]/ul/div[2]/li/a')
all_city_names = []
for a in a_list:
city_name = a.xpath('./text()')[0]
all_city_names.append(city_name)
print(all_city_names)
print(len(all_city_names))
④案例四:爬取站长素材中免费简历模板
- 需求:爬取站长素材中免费简历模板
- 进阶功能:实现了多页的爬取
练习代码如下:
# 需求:解析下载站长素材中免费简历模板 https://sc.chinaz.com/jianli/free.html
import requests
from lxml import html
etree = html.etree
import os
if __name__ == "__main__":
# url = 'https://sc.chinaz.com/jianli/free.html'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
}
for i in range(1,3):
if i == 1:
url = 'https://sc.chinaz.com/jianli/free.html'
else:
url = 'https://sc.chinaz.com/jianli/free_' + str(i) + '.html'
response = requests.get(url=url, headers=headers)
response.encoding = 'utf-8'
page_text = response.text
# print(page_text)
tree = etree.HTML(page_text)
div_list = tree.xpath('//div[@class="box col3 ws_block"]')
# print(div_list)
if not os.path.exists('./resumeLibs'):
os.mkdir('./resumeLibs')
for div in div_list:
resume_url = 'https:' + div.xpath('./a/@href')[0]
# print(resume_url)
response = requests.get(url=resume_url, headers=headers)
response.encoding = 'utf-8'
page_text = response.text
tree = etree.HTML(page_text)
li_list = tree.xpath('//div[@class="clearfix mt20 downlist"]/ul/li')
li = li_list[0]
download_url = li.xpath('./a/@href')[0]
download_name = div.xpath('./a/img/@alt')[0] + '.rar'
# print(download_name)
download_path = 'resumeLibs/' + download_name
down_res = requests.get(download_url)
with open(download_path, 'wb') as file:
file.write(down_res.content)
print(download_name, '保存成功')
print('爬取结束')
第四章
验证码识别
验证码和爬虫之间的关系
- 反爬机制:验证码。识别验证码图片中的数据,用于模拟登陆操作。
- 识别验证码的操作:
- 人工肉眼识别。(不推荐)
- 第三方自动识别。(推荐)
- 云打码(http://www.yundama.com/demo.html 支持的类型最全)
(教程里显示要花钱,一次识别花几分钱,咱学的时候url已经失效了)
(现在我找到的可以使用的是超级鹰)
- 云打码(http://www.yundama.com/demo.html 支持的类型最全)
- 云打码的使用流程:(看着玩就行了,个人认为选学内容,因为网站已经进不去了,但是可以做个参考,大体的操作流程都差不多)
- 1.注册:普通和开发者用户(两个都要注册)
- 2.登录:
- 普通用户的登录:查询该用户是否还有剩余的题分
- 开发者用户登录:
- 创建一个软件:
- 下载示例代码:
备注:如果使用超级鹰的话暂时不用充钱,先关注公众号然后再绑定微信就能获得1000题分,标准的4位英文数字验证码识别成功只花费10题分,1000题分用来练练手已经足够了。
实战:识别古诗文网登录页面中的验证码。
- 使用打码平台识别验证码的编码流程:
- 将验证码图片进行本地下载
- 调用平台提供的示例代码进行图片数据识别
练习代码如下:
import requests
from lxml import html
etree = html.etree
from hashlib import md5
class Chaojiying_Client(object):
def __init__(self, username, password, soft_id):
self.username = username
password = password.encode('utf8')
self.password = md5(password).hexdigest()
self.soft_id = soft_id
self.base_params = {
'user': self.username,
'pass2': self.password,
'softid': self.soft_id,
}
self.headers = {
'Connection': 'Keep-Alive',
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
}
def PostPic(self, im, codetype):
"""
im: 图片字节
codetype: 题目类型 参考 http://www.chaojiying.com/price.html
"""
params = {
'codetype': codetype,
}
params.update(self.base_params)
files = {'userfile': ('ccc.jpg', im)}
r = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files, headers=self.headers)
return r.json()
def ReportError(self, im_id):
"""
im_id:报错题目的图片ID
"""
params = {
'id': im_id,
}
params.update(self.base_params)
r = requests.post('http://upload.chaojiying.net/Upload/ReportError.php', data=params, headers=self.headers)
return r.json()
if __name__ == '__main__':
# 将验证码图片下载到本地
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/83.0.4103.97 Safari/537.36'
}
page_text = requests.get(url=url, headers=headers).text
# 解析验证码图片img中src属性值
tree = etree.HTML(page_text)
code_img_src = 'https://so.gushiwen.cn' + tree.xpath('//*[@id="imgCode"]/@src')[0]
img_data = requests.get(url=code_img_src, headers=headers).content
# 将验证码图片保存到本地
with open('./code.jpg', 'wb') as fp:
fp.write(img_data)
# 调用打码平台的示例程序进行验证码图片数据识别
chaojiying = Chaojiying_Client('你的账号qwq', '你的密码(不给你咱的)', '96001')
# 用户中心>>软件ID 生成一个替换 96001
im = open('code.jpg', 'rb').read()
# 本地图片文件路径 来替换 a.jpg 有时WIN系统须要//
print(chaojiying.PostPic(im, 1902))
# 1902 验证码类型 官方网站>>价格体系 3.4+版 print 后要加()
第五章
1.模拟登录
- 模拟登录:
- 爬取基于某些用户的用户信息。
2.实战
①人人网模拟登录(src反爬待解决)
- 需求:对人人网进行模拟登录(人人网,注册不了,可以跳了)。
- 点击登录按钮之后会发起一个post请求;
- post请求中会携带登录之前录入的相关的登录信息(用户名、密码、验证码等);
- 验证码:每次请求都会变化;
- 编码流程:
- 1.验证码识别,获取验证码图片的文字数据
- 2.对post请求进行发送
- 3.对相应数据进行持久化存储
练习代码如下:
[备注:教程中目前不可使用或访问的部分已经进行了提换和修改,以下内容使用的是超级鹰进行图片识别]
# 编码流程:
# 1.验证码识别,获取验证码图片的文字数据
# 2.对post请求进行发送
# 3.对相应数据进行持久化存储
import requests
from lxml import html
etree = html.etree
from bs4 import BeautifulSoup
from CodeClass import Chaojiying_Client
# 1.对验证码图片进行捕获和识别
if __name__ == "__main__":
url = 'https://www.renren.com/login'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
}
page_text = requests.get(url=url, headers=headers).text
tree = etree.HTML(page_text)
# 这里图片的src为 data:image/jpg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD/.../.../.../.../.../.../.../.../.../...
# 但是xpath解析到的数据为 data:image/jpg;base64,
# 逗号后面的数据都没有了,不知道是什么原因,所以这里我换一种方法
# code_img_src = tree.xpath('/html/body/div/div[3]/div/div[1]/div[2]/div[2]/div[3]/div/img/@src')[0]
soup = BeautifulSoup(page_text, 'lxml')
code_img_src = soup.find('div', class_='rr-login_code input')
# 好吧,人家验证码图片的src咱爬不全,不是解析的问题
# 以后如果能解决的话再回来改
print(code_img_src)
code_img_data = requests.get(url=code_img_src, headers=headers)
with open('./code.jpg', 'wb') as fp:
fp.write(code_img_data)
# 使用超级鹰提供的示例代码对验证码图片进行识别
chaojiying = Chaojiying_Client('qwq', 'qwq', 'qwq')
im = open('code.jpg', 'rb').read()
result = chaojiying.PostPic(im, 1902)
print(result)
# 以下代码正确性以及适用性未知
login_url = ''
data = {
}
response = requests.post(url=login_url, headers=headers, data=data)
print(response.status_code) # 如果是200,则请求成功
# login_page_text = requests.post(url=login_url, headers=headers, data=data).text
# with open('renren.html', 'w', encoding='utf-8') as fp:
# fp.write(login_page_text)
②爬取用户信息(咱没有人人网账号,代码仅供参考)
- 需求:爬取当前用户的相关用户信息(个人主页中显示的用户信息)
import requests
from lxml import html
etree = html.etree
from bs4 import BeautifulSoup
from CodeClass import Chaojiying_Client
# 1.对验证码图片进行捕获和识别
if __name__ == "__main__":
url = 'https://www.renren.com/login'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
}
page_text = requests.get(url=url, headers=headers).text
tree = etree.HTML(page_text)
code_img_src = tree.xpath('/html/body/div/div[3]/div/div[1]/div[2]/div[2]/div[3]/div/img/@src')[0]
code_img_data = requests.get(url=code_img_src, headers=headers)
with open('./code.jpg', 'wb') as fp:
fp.write(code_img_data)
# 使用超级鹰提供的示例代码对验证码图片进行识别
chaojiying = Chaojiying_Client('qwq', 'qwq', 'qwq')
im = open('code.jpg', 'rb').read()
result = chaojiying.PostPic(im, 1902)
print(result)
login_url = ''
data = {
}
response = requests.post(url=login_url, headers=headers, data=data)
print(response.status_code) # 如果是200,则请求成功
# 爬取当前用户的个人主页对应的页面数据
detail_url = 'http://www.renren.com/*********/profile'
detail_page_text = requests.get(url=detail_url, headers=headers).text
with open('detailPage.html', 'w', encoding='utf-8') as fp:
fp.write(detail_page_text)
3.模拟登录Cookie操作
- http/https协议特性:无状态。
- 没有请求到对应数据的原因:
- 发起的第二次基于个人主页页面请求的时候,服务器端并不知道该请求是基于登录状态下的请求。
- cookie:用来让服务器端记录客户端的相关状态。
- 处理cookie的形式:
- 手动处理:通过抓包工具获得cookie值,将该值封装到headers中(不推荐)
(通用性不强,处理麻烦;有的页面cookie是动态变化的,有的具有有效时长) - 自动处理:
- cookie值的来源是哪里?
- 模拟登录post请求后,由服务器端创建。
- session会话对象:
- 作用:
- 1.可以进行请求的发送
- 2.如果请求过程中产生了cookie,则该cookie会被自动存储/携带在该session对象中
- 作用:
- 操作流程:
- 1.创建一个session对象:
session = requests.Session()
- 2.使用session对象进行模拟登录post请求发送(cookie会被存储在session中)
- 3.session对象对个人主页对应的get请求进行发送(携带了cookie)
- 1.创建一个session对象:
- cookie值的来源是哪里?
- 手动处理:通过抓包工具获得cookie值,将该值封装到headers中(不推荐)
爬取用户信息修改版(使用session对象):
import requests
from lxml import html
etree = html.etree
from bs4 import BeautifulSoup
from CodeClass import Chaojiying_Client
# ①创建一个session对象
session = requests.Session()
# 1.对验证码图片进行捕获和识别
if __name__ == "__main__":
url = 'https://www.renren.com/login'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
}
page_text = requests.get(url=url, headers=headers).text
tree = etree.HTML(page_text)
code_img_src = tree.xpath('/html/body/div/div[3]/div/div[1]/div[2]/div[2]/div[3]/div/img/@src')[0]
code_img_data = requests.get(url=code_img_src, headers=headers)
with open('./code.jpg', 'wb') as fp:
fp.write(code_img_data)
# 使用超级鹰提供的示例代码对验证码图片进行识别
chaojiying = Chaojiying_Client('qwq', 'qwq', 'qwq')
im = open('code.jpg', 'rb').read()
result = chaojiying.PostPic(im, 1902)
print(result)
login_url = ''
data = {
}
# ②使用session进行请求的发送
response = session.post(url=login_url, headers=headers, data=data)
print(response.status_code) # 如果是200,则请求成功
# 爬取当前用户的个人主页对应的页面数据
detail_url = 'http://www.renren.com/*********/profile'
# 手动cookie处理
# 通用性不强,处理麻烦
# 有的页面cookie是动态变化的,有的具有有效时长
# headers = {
# 'Cookie': '*****'
# }
# ③使用携带cookie的session进行get请求的发送
detail_page_text = session.get(url=detail_url, headers=headers).text
with open('detailPage.html', 'w', encoding='utf-8') as fp:
fp.write(detail_page_text)
4.代理
- 代理:破解封IP这种反爬机制。
- 什么是代理:
- 代理服务器。
- 代理的作用:
- 突破自身IP访问的限制。
- 隐藏自身真实的IP。
- 代理相关的网站:
- IP查询网站:
- 代理IP的类型:
- http:应用到http协议对应的url中
- https:应用到https协议对应的url中
- 代理IP的匿名度:
- 透明:服务器它知道该次请求使用了代理,也知道请求对应的真实IP
- 匿名:服务器它知道该次请求使用了代理,不知道请求对应的真实IP
- 高匿:服务器不知道该次请求使用了代理,不知道请求对应的真实IP
- 反爬机制:封IP
- 反反爬策略:使用代理进行请求发送
测试代码如下:
import requests
if __name__ == "__main__":
url = 'https://www.baidu.com/s?wd=ip'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
}
# page_text = requests.get(url=url, headers=headers, proxies={"https": '免费的代理暂时没找到好用的,付费的还没试,使用的时候把:后面的去掉'}).text
# page_text = requests.get(url=url, headers=headers, proxies={"https": ''}).text
page_text = requests.get(url=url, headers=headers).text
with open('ip.html', 'w', encoding='utf-8') as fp:
fp.write(page_text)
# 反爬机制:封IP
# 反反爬策略:使用代理进行请求发送
第六章
1.知识点
高性能异步爬虫
- 目的:在爬虫中使用异步实现高性能的数据爬取操作。
- 异步爬虫的方式:
- 1.多线程、多进程(不建议):
- 好处:可以为相关阻塞的操作单独开启线程或进程,阻塞操作就可以异步执行。
- 弊端:无法无限制的开启多线程或者多进程。
- 2.线程池、进程池(适当的使用):
- 好处:我们可以降低系统对进程或者线程创建和销毁的频率,从而很好地降低系统的开销。
- 弊端:池中线程或进程的数量是有上限的。
- 原则:线程池处理的是阻塞且较为耗时的操作
- 3.单线程 + 异步协程(推荐):
- 一些概念和两个关键字:
- ①event_loop:事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足某些条件时,函数就会被循环执行。
- ②coroutline:协程对象,我们可以将协程对象注册到事件循环中,它会被事件循环调用。我们可以使用async关键字来定义一个方法,这个方法在调用时不会被立即被执行,而是返回一个协程对象。
- ③task:任务,它是对协程对象的进一步封装,包含了任务的各个状态。
- ④future:代表将来执行或还没有执行的任务,实际上和task没有本质区别。
- ⑤async:定义一个协程。
- ⑥await:用来挂起阻塞方法的执行。
- 一些概念和两个关键字:
- 1.多线程、多进程(不建议):
2.代码实现
①单线程爬取数据
使用单线程爬取数据案例展示:
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
}
def get_content(url):
print('正在爬取:', url)
# get方法是一个阻塞的方法
response = requests.get(url=url, headers=headers)
print(response.status_code)
if response.status_code == 200:
return response.content
def parse_content(content):
print('相应数据的长度为:', len(content))
if __name__ == "__main__":
urls = [
'https://downsc.chinaz.net/Files/DownLoad/jianli/202107/jianli15646.rar',
'https://downsc.chinaz.net/Files/DownLoad/jianli/202107/jianli15647.rar',
'https://downsc.chinaz.net/Files/DownLoad/jianli/202107/jianli15651.rar',
]
for url in urls:
content = get_content(url)
parse_content(content)
②线程池爬取数据
使用线程池爬取数据案例展示:
# import time
#
#
# # 使用单线程串行方式执行
#
# def get_page(str):
# print("正在下载:", str)
# time.sleep(2)
# print("下载成功", str)
#
#
# if __name__ == "__main__":
# name_list = ['xiaozi', 'aa', 'bb', 'cc']
#
# start_time = time.time()
#
# for i in range(len(name_list)):
# get_page(name_list[i])
#
# end_time = time.time()
# print('%d second' % (end_time - start_time))
import time
# 导入线程池对应的类
from multiprocessing.dummy import Pool
# 使用线程池方式执行
def get_page(str):
print("正在下载:", str)
time.sleep(2)
print("下载成功", str)
if __name__ == "__main__":
start_time = time.time()
name_list = ['xiaozi', 'aa', 'bb', 'cc']
# 实例化一个线程池对象
# (优化后耗时2秒,试了一下,把4改成3耗时4秒,推测是有一条为单线)
pool = Pool(4)
# 将列表中每一个列表元素传递到gert_page进行处理
# map的返回值是get_page函数的返回值,为一个列表,此处不需要进行处理
pool.map(get_page, name_list)
end_time = time.time()
print('%d second' % (end_time - start_time))
③爬虫中应用线程池(动态加载的video标签待解决)
爬虫中应用线程池:
import requests
from lxml import html
etree = html.etree
import re
from multiprocessing.dummy import Pool
# 需求:爬取视频的视频数据
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
}
# 原则:线程池处理的是阻塞且较为耗时的操作
def get_video_data(dic):
url = dic['url']
print(dic['name'], 'downloading...')
data = requests.get(url=url, headers=headers).content
# 持久化存储操作
with open(dic['name'], 'wb') as fp:
fp.write(data)
print(dic['name'], '下载成功')
if __name__ == "__main__":
# 对下述url发起请求解析出视频详情页的url和视频名称
url = 'https://www.pearvideo.com/category_5'
page_text = requests.get(url=url, headers=headers).text
tree = etree.HTML(page_text)
li_list = tree.xpath('//ul[@id="listvideoListUl"]/li')
urls = [] # 存储所有视频的名称和链接
for li in li_list:
detail_url = 'https://www.pearvideo.com/' + li.xpath('./div/a/@href')[0]
name = li.xpath('./div/a/div[2]/text()')[0] + '.mp4'
print(detail_url, name)
# 对详情页的url发起请求
detail_page_text = requests.get(url=detail_url, headers=headers).text
# 从详情页中解析出视频的地址(url)
# 用作案例的网站在教程中的那个时间段(一几年,大概是18年)
# 没有video标签,视频的地址在js里面,所以不能用bs4和xpath
# 但是网站已经更新(2021.08.01),我现在学的时候src的Url已经在video标签中了
# 因此此处正则不再适用,咱用xpath修改一下
# 教程代码:
# ex = 'srcUrl="(.*?)",vdoUrl'
# video_url = re.findall(ex, detail_page_text)[0]
# 咱的代码:
# detail_tree = etree.HTML(detail_page_text)
# print(detail_page_text)
# video = detail_tree.xpath('//*[@id="JprismPlayer"]/video')
# print(video)
# video_url = video[0]
# 哈哈,放video标签之后不知道是人家反爬了还是咱的爬取有问题
# 应该是弄得Ajax动态加载
# 爬到的:
# <div class="main-video-box" id="drag_target1">
# <div class="img prism-player" style="height:100% !important;" id="JprismPlayer">
# </div>
# </div>
# F12看到的:
# <div class="main-video-box" id="drag_target1">
# <div class="img prism-player play" style="height: 100% !important; width: 100%;" id="JprismPlayer"
# x-webkit-airplay="" playsinline="" webkit-playsinline="" >
# < video webkit-playsinline="" playsinline="" x-webkit-airplay="" autoplay="autoplay"
# src="https://video.pearvideo.com/mp4/third/20210801/cont-1737209-12785353-091735-hd.mp4"
# style="width: 100%; height: 100%;">
# < / video >
# ...
# </div>
# </div>
# 又挖了一个坑,以后再填qwq
video_url = ''
dic = {
'name': name,
'url': video_url
}
urls.append(video_url)
# 使用线程池对视频数据进行请求(较为耗时的阻塞操作)
pool = Pool(4)
pool.map(get_video_data, urls)
pool.close()
pool.join()
④协程
代码如下:
import asyncio
async def request(url):
print('正在请求的url是', url)
print('请求成功', url)
return url
# async修饰的函数,调用之后返回的一个协程对象
c = request('www.baidu.com')
# # 创建一个事件循环对象
# loop = asyncio.get_event_loop()
#
# # 将协程对象注册到loop中,然后启动loop
# loop.run_until_complete(c)
# # task的使用
# loop = asyncio.get_event_loop()
# # 基于loop创建task任务对象
# task = loop.create_task(c)
# print(task)
#
# loop.run_until_complete(task)
#
# print(task)
# # future的使用
# loop = asyncio.get_event_loop()
# task = asyncio.ensure_future(c)
# print(task)
# loop.run_until_complete(task)
# print(task)
def callback_func(task):
# result返回的是任务对象中封装的协程对象对应函数的返回值
print(task.result())
# 绑定回调
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(c)
# 将回调函数绑定到任务对象中
task.add_done_callback(callback_func)
loop.run_until_complete(task)
⑤多任务异步协程01
代码如下:
import asyncio
import time
async def request(url):
print('正在下载', url)
# 在异步协程中如果出现了同步模块相关的代码,那么就无法实现异步
# time.sleep(2)
# 当在asyncio中遇到阻塞操作必须进行手动挂起
await asyncio.sleep(2)
print('下载完毕', url)
start = time.time()
urls = [
'www.baidu.com',
'www.sogou.com',
'www.goubanjia.com'
]
# 任务列表:存放多个任务对象
stasks = []
for url in urls:
c = request(url)
task = asyncio.ensure_future(c)
stasks.append(task)
loop = asyncio.get_event_loop()
# 需要将任务列表封装到wait中
loop.run_until_complete(asyncio.wait(stasks))
print(time.time()-start)
⑥多任务异步协程02
搭建一个简单的web服务器:
from flask import Flask
import time
app = Flask(__name__)
@app.route('/john')
def index_john():
time.sleep(2)
return 'Hello john'
@app.route('/smith')
def index_smith():
time.sleep(2)
return 'Hello smith'
@app.route('/tom')
def index_tom():
time.sleep(2)
return 'Hello tom'
if __name__ == "__main__":
app.run(threaded=True)
异步协程实现代码(此处仍为单线):
import requests
import asyncio
import time
start = time.time()
urls = [
'http://127.0.0.1:5000/john',
'http://127.0.0.1:5000/smith',
'http://127.0.0.1:5000/tom',
]
async def get_page(url):
print('正在下载', url)
# requests.get是基于同步的,必须使用基于异步的网络请求模块进行指定url的请求发送
# aiohttp:基于异步网络请求的模块
response = requests.get(url)
print('下载完毕', response.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))
end = time.time()
print('总耗时:', end-start)
⑦aiohttp实现多任务异步协程
- aiohttp:基于异步网络请求的模块
- 环境安装:
pip install aiohttp
- 使用环境中的ClientSession
- 与requests模块的区别,调用的不是属性,而是方法:
- text()返回字符串形式的响应数据
- read()返回二进制形式的响应数据
- json()返回的是json对象
代码如下:
import requests
import asyncio
import time
import aiohttp
# 环境安装:pip install aiohttp
# 使用环境中的ClientSession
start = time.time()
urls = [
'http://127.0.0.1:5000/john',
'http://127.0.0.1:5000/smith',
'http://127.0.0.1:5000/tom',
]
async def get_page(url):
async with aiohttp.ClientSession() as session:
# 使用不同的请求类型:
# get()、post()
# 添加相关的参数:
# headers,params/data proxy='http://ip:port'
# 例如:
# async with await session.get(url, params, proxy) as response:
# async with await session.post(url, data, proxy) as response:
async with await session.get(url) as response:
# text()返回字符串形式的响应数据
# read()返回二进制形式的响应数据
# json()返回的是json对象
# 注意: 获取响应数据操作之前一定要使用await进行手动挂起
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))
end = time.time()
print('总耗时:', end-start)
第七章
一、selenium模块的基本使用
- 问题:selenium模块和爬虫之间具有怎样的关联?
- 1.便捷的获取网站中动态加载的数据
- 2.便捷的实现模拟登录
- 什么是selenium模块?
- 基于浏览器自动化的一个模块。
1.selenium使用流程
知识点部分
- selenium使用流程:
-
环境的安装:
pip install selenium
-
下载一个浏览器的驱动程序
-
实例化一个浏览器的对象
-
编写基于浏览器自动化的操作代码
- 发起请求:
get(url)
- 标签定位:
find系列方法
- 标签交互:
send_keys('***')
- 执行js程序:
excute_script('jsCode')
- 前进:
forward()
- 后退:
back()
- 关闭浏览器:
quit()
- 发起请求:
-
selenium处理iframe
- 如果定位的标签存在于iframe标签之中,则必须使用
switch_to.frame(id)
- 动作链(拖动):
from selenium.webdriver import ActionChains
- 实例化一个动作链对象:
action = ActionChains(bro)
- 点击且长按操作:
click_and_hold(div)
- 移动操作:
move_by_offset(x, y)
x-水平方向 y-竖直方向 - 让动作链立即执行:
perform()
- 释放动作链对象:
action.release()
- 实例化一个动作链对象:
- 如果定位的标签存在于iframe标签之中,则必须使用
-
代码部分
①展示代码
演示文档中要用到chromedriver.exe,下载教程:
Selenium Chrome驱动安装(windows系统)
备注:
1.咱在运行的时候Chromedriver需要使用绝对路径,相对路径会报错;
2.使用的Chromedriver版本要与电脑上浏览器版本一致;
3.可能是因为版本更新了,教程中的代码无法正常运行(只能进行到打开浏览器和百度页面),因为找不到“设置”选项。
样例代码:
from selenium import webdriver
from time import sleep
# 后面是你的浏览器驱动位置,记得前面加r'','r'是防止转义字符的
driver = webdriver.Chrome(r'D:\pycharm\PyCharm Community Edition 2020.1.2\CrawlerCourse\07DynamicLoad_dataProcessing\chromedriver.exe')
# 用get打开百度页面
driver.get("http://www.baidu.com")
# 查找页面的“设置”选项,并进行点击
driver.find_elements_by_link_text('设置')[0].click()
sleep(2)
# 打开设置后找到“搜索设置”选项,设置为每页显示50条
driver.find_elements_by_link_text('搜索设置')[0].click()
sleep(2)
# 选中每页显示50条
m = driver.find_element_by_id('nr')
sleep(2)
m.find_element_by_xpath('//*[@id="nr"]/option[3]').click()
m.find_element_by_xpath('.//option[3]').click()
sleep(2)
# 点击保存设置
driver.find_element_by_class_name("prefpanelgo").click()
sleep(2)
# 处理弹出的警告页面
# 确定accept()
# 取消dismiss()
driver.switch_to_alert().accept()
sleep(2)
# 找到百度的输入框,并输入“鲁智深”
driver.find_element_by_id('kw').send_keys('鲁智深')
sleep(2)
# 点击搜索按钮
driver.find_element_by_id('su').click()
sleep(2)
# 在打开的页面中找到“鲁智深 - 百度图片”,并打开这个页面
driver.find_elements_by_link_text('鲁智深 - 百度图片')[0].click()
# 关闭浏览器
driver.quit()
②selenium模块使用样例
selenium模块代码使用样例:
from selenium import webdriver
from lxml import html
etree = html.etree
from time import sleep
# 实例化一个浏览器对象(传入浏览器的驱动程序)
bro = webdriver.Chrome(executable_path=r'D:\pycharm\PyCharm Community Edition 2020.1.2\CrawlerCourse\07DynamicLoad_dataProcessing\chromedriver.exe')
# 让浏览器发起一个指定url对应请求
bro.get('http://scxk.nmpa.gov.cn:81/xk/')
# 获取浏览器当前页面的页面源码数据
page_text = bro.page_source
# 解析企业名称
tree = etree.HTML(page_text)
li_list = tree.xpath('//ul[@id="gzlist"]/li')
# print(li_list)
for li in li_list:
name = li.xpath('./dl/@title')[0]
print(name)
sleep(5)
bro.quit()
③selenium其它动态操作
代码如下:
from selenium import webdriver
from time import sleep
bro = webdriver.Chrome(executable_path=r'D:\pycharm\PyCharm Community Edition 2020.1.2\CrawlerCourse\07DynamicLoad_dataProcessing\chromedriver.exe')
bro.get('https://www.taobao.com/')
# 实现标签定位
search_input = bro.find_element_by_id('q')
# 标签交互
search_input.send_keys('Iphone')
# 执行一组js程序
# ghrome中可以手动在console界面执行以下括号中的代码
bro.execute_script('window.scrollTo(0, document.body.scrollHeight)')
sleep(2)
# class="btn-search tb-bg"
# 空格表示分隔,使用时选择一个,不可两个同时使用
# 点击搜索按钮
btn = bro.find_element_by_css_selector('.btn-search')
btn.click()
bro.get('https://www.baidu.com')
sleep(2)
# 回退
bro.back()
sleep(2)
# 前进
bro.forward()
sleep(5)
bro.quit()c.
④动作链和iframe操作
代码如下:
from selenium import webdriver
from time import sleep
# 导入动作链对应的类
from selenium.webdriver import ActionChains
bro = webdriver.Chrome(executable_path=r'D:\pycharm\PyCharm Community Edition 2020.1.2\CrawlerCourse\07DynamicLoad_dataProcessing\chromedriver.exe')
bro.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')
# 如果定位的标签是存在于iframe标签之中的,必须进行如下操作再进行标签定位
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()立即执行动作链操作
# move_by_offset(x, y): x-水平方向 y-竖直方向
action.move_by_offset(17, 0).perform()
sleep(0.3)
# 释放动作链
action.release()
print(div)
bro.quit()
⑤模拟QQ空间登录
代码如下:
from selenium import webdriver
from time import sleep
bro = webdriver.Chrome(executable_path=r'D:\pycharm\PyCharm Community Edition 2020.1.2\CrawlerCourse\07DynamicLoad_dataProcessing\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()
uesrName_tag = bro.find_element_by_id('u')
password_tag = bro.find_element_by_id('p')
uesrName_tag.send_keys('qq账号')
sleep(1)
password_tag.send_keys('******')
sleep(1)
btn = bro.find_element_by_id('login_button')
btn.click()
sleep(5)
bro.quit()
⑥谷歌无头浏览器&反检测
- chrome_options方法已经被弃用了(如果继续用代码也可以运行,但是会有警告让你用options作为参数),被options替代了,但是无头化和规避检测同时使用时options参数又不能重复,所以现在这里的代码应该怎么写还是个谜。
代码如下:
from selenium import webdriver
from 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=r'D:\pycharm\PyCharm Community Edition 2020.1.2\CrawlerCourse\07DynamicLoad_dataProcessing\chromedriver.exe',
chrome_options=chrome_options, options=option)
# 无可视化界面(无头浏览器) phantomJs
# webdriver.phantomjs
# 不建议用phantomJs,因为它已经停止更新与维护
bro.get('https://www.baidu.com/')
print(bro.page_source)
sleep(2)
bro.quit()
2.12306模拟登录
- 12306模拟登录
- 超级鹰:http://www.chaojiying.com/
- 使用流程:
- 注册:普通用户
- 登录:普通用户
- 题分查询:充值(1元1000题分)
- 创建软件(id)
- 下载示例代码
- 类型9004:坐标多选,返回1~4个坐标,如:x1,y1|x2,y2|x3,y3
- 使用流程:
- 12306模拟登录编码流程:
- 使用selenium打开登录页面
- 对当前selenium打开的这张页面进行截图
- 对当前图片局部区域(验证码图片)进行裁剪
- 好处:将验证码图片和模拟登陆进行一一对应
- 使用超级鹰识别验证码图片(坐标)
- 超级鹰:http://www.chaojiying.com/
代码如下(点击超级鹰识别出来的的图片时位置总是对不上[之前裁剪的时候参数已经调整好了没问题],而且识别很多次也不对):
from selenium import webdriver
import time
import requests
from hashlib import md5
from PIL import Image
from selenium.webdriver import ActionChains
class Chaojiying_Client(object):
def __init__(self, username, password, soft_id):
self.username = username
password = password.encode('utf8')
self.password = md5(password).hexdigest()
self.soft_id = soft_id
self.base_params = {
'user': self.username,
'pass2': self.password,
'softid': self.soft_id,
}
self.headers = {
'Connection': 'Keep-Alive',
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
}
def PostPic(self, im, codetype):
"""
im: 图片字节
codetype: 题目类型 参考 http://www.chaojiying.com/price.html
"""
params = {
'codetype': codetype,
}
params.update(self.base_params)
files = {'userfile': ('ccc.jpg', im)}
r = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files, headers=self.headers)
return r.json()
def ReportError(self, im_id):
"""
im_id:报错题目的图片ID
"""
params = {
'id': im_id,
}
params.update(self.base_params)
r = requests.post('http://upload.chaojiying.net/Upload/ReportError.php', data=params, headers=self.headers)
return r.json()
if __name__ == '__main__':
# 使用selenium打开登录页面
bro = webdriver.Chrome(executable_path=r'D:\pycharm\PyCharm Community Edition 2020.1.2\CrawlerCourse\07DynamicLoad_dataProcessing\chromedriver.exe')
bro.get('https://kyfw.12306.cn/otn/resources/login.html')
time.sleep(1)
btn = bro.find_element_by_xpath('/html/body/div[2]/div[2]/ul/li[2]/a')
btn.click()
time.sleep(3)
# save_screenshot 将当前页面进行截图
bro.save_screenshot('./aa.png')
# 确定验证码图片对应的左上角和右下角的坐标(裁剪的区域就确定)
code_img_ele = bro.find_element_by_xpath('//*[@id="J-loginImg"]')
location = code_img_ele.location # 验证码图片左上角的坐标 x y
print('location:', location)
size = code_img_ele.size # 验证码标签对应的长和宽
print('size:', size)
# 左上角和右下角坐标
# 截图时有默认缩放,裁剪时坐标乘1.25,点击对应图片时坐标乘0.8
rangle = (int(location['x'])*1.25, int(location['y'])*1.25,
int(location['x'] + size['width'])*1.25, int(location['y'] + size['height'])*1.25)
# 至此验证码图片区域就确定下来了
i = Image.open('./aa.png')
code_img_name = './code.png'
# crop根据指定区域进行图片裁剪
frame = i.crop(rangle)
frame.save(code_img_name)
# 将验证码图片提交给超级鹰进行识别
chaojiying = Chaojiying_Client('zhangyi132', 'zhangjinpeng101', '920271')
im = open('code.png', 'rb').read()
print(chaojiying.PostPic(im, 9004)['pic_str'])
result = chaojiying.PostPic(im, 9004)['pic_str']
all_list = [] # 要存储即将被点击的点的坐标 [[x1, y1], [x2, y2]]
if '|' in result:
list_1 = result.split('|')
count_1 = len(list_1)
for i in range(count_1):
xy_list = []
x = int(list_1[i].split(',')[0]) / 1.25
y = int(list_1[i].split(',')[1]) / 1.25
xy_list.append(x)
xy_list.append(y)
all_list.append(xy_list)
else:
xy_list = []
x = int(result[i].split(',')[0]) / 1.25
y = int(result[i].split(',')[1]) / 1.25
xy_list.append(x)
xy_list.append(y)
all_list.append(xy_list)
print(all_list)
# 遍历列表,使用动作链对每一个列表元素对应的x,y指定的位置进行点击操作
for l in all_list:
x = l[0]
y = l[0]
ActionChains(bro).move_to_element_with_offset(code_img_ele, x, y).click().perform()
time.sleep(0.5)
bro.find_element_by_id('username').send_keys('******')
time.sleep(2)
bro.find_element_by_id('password').send_keys('******')
time.sleep(2)
bro.find_element_by_id('loginSub').click()
time.sleep(5)
bro.quit()
第八章
scrapy框架
1.基础了解
- 什么是框架?
- 就是一个集成了很多功能并且具有很强通用性的一个项目模板。
- 如何学习框架?
- 专门学习框架封装的各种功能的详细用法。
- 什么是scrapy?
- 爬虫中封装好的一个明星框架。
- 功能:
- 高性能的持久化存储
- 异步数据的下载
- 高性能的数据解析
- 分布式
- scrapy框架的基本使用
- 环境的安装:
- mac or Linux:
pip install scrapy
- windows:
pip install wheel
- 下载twisted:下载地址http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
- 安装twisted:Twisted-21.7.0-py3-none-any.whl
pip install pywin32
pip install scarpy
[PS:pywin32的主要作用:- 捕获窗口
- 模拟鼠标键盘动作
- 自动获取某路径下文件列表
- PIL截屏功能]
- 测试:在终端里录入scrapy指令,没有报错即表示安装成功
- mac or Linux:
- 基本使用:
- 1.创建一个工程:
scrapy startproject xxxPro
(在pycharm中下方的terminal中输入) - 2.
cd xxxPro
- 3.在spiders子目录中创建一个爬虫文件
scrapy genspider spiderName www.xxx.com
- 需要暂时修改的信息:
settings.py中修改并添加如下代码
1.ROBOTSTXT_OBEY的值由True改为False(robots协议,详见第一章)
2.添加指定代码,使相应数据中不输出正常的日志刷屏
- 1.创建一个工程:
- 环境的安装:
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
# 显示指定类型的日志信息
LOG_LEVEL = 'ERROR'
spiders子目录中创建的爬虫文件分析:
import scrapy
class FirstSpider(scrapy.Spider):
# 爬虫文件的名称:爬虫源文件的唯一标识
name = 'first'
# 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
# allowed_domains = ['www.baidu.com'] # 通常不用(例如爬取图片时图片的url就不会被访问到)
# 起始的url列表:该列表中存放的url会被scrapy自动进行的请求发送
start_urls = ['http://www.baidu.com/', 'https://www.sogou.com']
# 用作于数据解析:response参数表示的是请求成功后对应的响应对象
def parse(self, response):
print(response)
# pass
2.scrapy数据解析
-
scrapy数据解析:
-
需求:使用scrapy爬取糗事百科的段子
代码如下:
import scrapy
class QiubaiSpider(scrapy.Spider):
name = 'qiubai'
# allowed_domains = ['www.xxx.com']
start_urls = ['http://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之后,则表示将列表中每一个selector对象中data对应的字符串提取了出来
content = div.xpath('./a[1]/div/span//text()').extract()
content = ''.join(content)
print(author, content)
break
3.scrapy持久化存储
Ⅰ知识点
- scrapy持久化存储
- 基于终端指令:
- 要求:只可以将parse方法的返回值存储到本地的文本文件中
- 注意:持久化存储对应的文本文件的类型只可以为:‘json’, ‘jsonlines’, ‘jl’, ‘csv’, ‘xml’, ‘marshal’, ‘pickle’(没有’txt’)
- 指令:
scrapy crawl xxx -o filePath
- 好处:简洁高效便捷
- 缺点:局限性比较强(数据只可以存储到指定后缀的文本文件中)
- 基于管道:
- 编码流程:
- 1.数据解析
- 2.在item类中定义相关的属性
- 3.将解析的数据存储到item类型的对象
- 4.将item类型的对象提交给管道进行持久化存储的操作
- 5.在管道类的process_item中要将其接收到的item对象中存储的数据进行持久化存储操作
- 6.在配置文件中开启管道
- 好处:
- 通用性强
- 编码流程:
- 面试题:将爬取的数据一份存储到本地一份存储到数据库,如何实现?
- 基于终端指令:
Ⅱ代码部分
①基于终端指令
代码如下:
import scrapy
class QiubaiSpider(scrapy.Spider):
name = 'qiubai'
# allowed_domains = ['www.xxx.com']
start_urls = ['http://www.qiushibaike.com/text/']
def parse(self, response):
# 解析:作者的名称&段子的内容
div_list = response.xpath('//div[@class="col1 old-style-col1"]/div')
all_data = [] # 存储所有解析到的数据
# 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之后,则表示将列表中每一个selector对象中data对应的字符串提取了出来
content = div.xpath('./a[1]/div/span//text()').extract()
content = ''.join(content)
dic = {
'author': author,
'content': content
}
all_data.append(dic)
# print(author, content)
# break
return all_data
②基于管道
代码如下:
import scrapy
# from qiubaiPro.items import QiubaiproItem
from ..items import QiubaiproItem
class QiubaiSpider(scrapy.Spider):
name = 'qiubai'
# allowed_domains = ['www.xxx.com']
start_urls = ['http://www.qiushibaike.com/text/']
# -----基于终端-----
# def parse(self, response):
# # 解析:作者的名称&段子的内容
# div_list = response.xpath('//div[@class="col1 old-style-col1"]/div')
# all_data = [] # 存储所有解析到的数据
# # 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之后,则表示将列表中每一个selector对象中data对应的字符串提取了出来
# content = div.xpath('./a[1]/div/span//text()').extract()
# content = ''.join(content)
#
# dic = {
# 'author': author,
# 'content': content
# }
#
# all_data.append(dic)
# # print(author, content)
# # break
# return all_data
# -----基于管道-----
def parse(self, response):
# 解析:作者的名称&段子的内容
div_list = response.xpath('//div[@class="col1 old-style-col1"]/div')
all_data = [] # 存储所有解析到的数据
# print(div_list)
for div in div_list:
# xpath返回的是列表,但列表元素一定是selector类型的对象
# extract可以将selector对象中data参数存储的字符串提取出来
author = div.xpath('./div[1]/a[2]/h2/text() | ./div[1]/span/h2/text()')[0].extract()
# 列表调用了extract之后,则表示将列表中每一个selector对象中data对应的字符串提取了出来
content = div.xpath('./a[1]/div/span//text()').extract()
content = ''.join(content)
item = QiubaiproItem()
item['author'] = author
item['content'] = content
# 将ite提交给了管道
yield item
Ⅲ面试题(代码有报错,暂未解决)
- 面试题:将爬取到的数据一份存储到本地一份存储到数据库,如何实现?
- 管道文件中的一个管道类对应的是将数据存储到一种平台
- 爬虫文件提交的item只会给管道文件中第一个被执行的管道类接受
- process_item中的return item表示将item传递给下一个即将被执行的管道类
代码如下:
# pipelines.py
# 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 interface
from itemadapter import ItemAdapter
import pymysql
class QiubaiproPipeline:
fp = None
# 重写父类的一个方法:该方法只在开始爬虫的时候被调用一次
def open_spider(self, spider):
print('开始爬虫...')
self.fp = open('./qiubai.txt', 'w', encoding='utf-8')
# 专门用来处理item类型对象
# 该方法可以接收爬虫文件提交过来的item对象
# 该方法每接收到一个item就会被调用一次
def process_item(self, item, spider):
author = item['author']
content = item['content']
self.fp.write(author + ':' + content + '\n')
# 很重要的一行代码
return item # 传递给下一个即将被执行的管道类
def close_spider(self, spider):
print('结束爬虫')
self.fp.close()
# 管道文件中一个管道类对应将一组数据存储到一个平台或者载体中
class MysqlPipeline(object):
conn = None
cursor = None
def open_spider(self, spider):
self.conn = pymysql.Connect(host="localhost", user="root", passwd="123456", database="pydata", port=3306,
charset='utf-8')
def process_item(self, item, spider):
self.cursor = self.conn.cursor()
sql = """
create table qiubai(
num int primary key auto_increment,
author varchar(100) not null,
content varchar(9999) not null,
)
"""
self.cursor.execute(sql)
try:
self.cursor.execute('insert into qiubai values("%s", "%s")' % (item["author"], item["content"]))
self.conn.commit()
except Exception as e:
print(e)
self.conn.rollback()
return item
def close_spider(self, spider):
self.cursor.close()
self.conn.close()
# 爬虫文件提交的item类型的对象最终会提交给哪一个管道类?
# 先执行管道类优先级高的
4.基于spider的全站数据爬取
- 基于spider的全站数据爬取
- 就是将网站中某板块下的全部页码对应的页面数据进行爬取和解析
- 需求:爬取校花网中的照片的名称
- 实现方式:
- 1.将所有页面的url添加到start_urls列表(不推荐)
- 2.自行手动请求发送(推荐)
- 手动请求发送:
yield scrapy.Request(url, callback)
:callback专门用于数据解析
- 手动请求发送:
样例代码如下:
- 创建工程:
scrapy startproject xiaohuaPro
- 创建文件:
scrapy genspider xiaohua www.xxx.com
- 修改设置:
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
LOG_LEVEL = 'ERROR'
- 编写代码:
import scrapy
class XiaohuaSpider(scrapy.Spider):
name = 'xiaohua'
# allowed_domains = ['www.xxx.com']
start_urls = ['http://www.521609.com/meinvxiaohua/']
# 生成一个通用的url模板(不可变)
url = 'http://www.521609.com/meinvxiaohua/list12%d.html'
page_num = 2
def parse(self, response):
li_list = response.xpath('/html/body/div[4]/div[2]/div[2]/ul/li')
for li in li_list:
img_name = li.xpath('./a[2]/b/text() | ./a[2]/text()').extract_first()
print(img_name)
if self.page_num <= 11:
new_url = format(self.url % self.page_num)
self.page_num += 1
# 手动请求发送:callback回调函数是专门用于数据解析
yield scrapy.Request(url=new_url, callback=self.parse)
5.五大核心组件
调度器(过滤器会进行检查,避免重复请求;队列)
下载器使用到twisted,基于异步
管道用于持久化存储
引擎进行数据流处理,触发事物
spider
-
引擎(Scrapy)
用来处理整个系统的数据流处理, 触发事务(框架核心)
-
调度器(Scheduler)
用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
-
下载器(Downloader)
用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
-
爬虫(Spiders)
爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
-
项目管道(Pipeline)
负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
6.请求传参
- 请求传参
- 使用场景:如果要爬取的数据不在同一张页面中。(深度爬取)
- 需求:爬取boss的岗位名称,岗位描述(原网站似乎改为动态加载数据,代码不可用,仅供参考)
代码如下:
- settings.py
常规操作:
# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
LOG_LEVEL = 'ERROR'
开启配置:
ITEM_PIPELINES = {
'bossPro.pipelines.BossproPipeline': 300,
}
- pipelines.py
class BossproPipeline:
def process_item(self, item, spider):
print(item)
return item
- items.py
class BossproItem(scrapy.Item):
# define the fields for your item here like:
job_name = scrapy.Field()
job_desc = scrapy.Field()
# pass
- boss.py
import scrapy
from bossPro.items import BossproItem
class BossSpider(scrapy.Spider):
name = 'boss'
# allowed_domains = ['www.xxx.com']
start_urls = ['https://www.zhipin.com/c101010100/?query=python&ka=sel-city-101010100']
url = 'https://www.zhipin.com/c101010100/?query=python&page=%d'
page_num = 2
# 回调函数接受item
def parse_detail(self, response):
item = response.meta['item']
job_desc = response.xpath('/html/body/div[1]/div[2]/div[3]/div/div[2]/div[2]/div[1]/div//text()').extract()
job_desc = ''.join(job_desc)
# print(job_desc)
item['job_desc'] = job_desc
yield item
# 解析首页的岗位名称
def parse(self, response):
li_list = response.xpath('//*[@id="main"]/div/div[3]/ul/li')
# print(li_list)
for li in li_list:
item = BossproItem()
job_name = 'https://www.zhipin.com' + li.xpath('.//div[@class="info-primary"]/div[1]/div/div[1]/a/text()').extract_first()
item['job_name'] = job_name
# print(job_name)
# print('12')
detail_url = li.xpath('.//div[@class="info-primary"]/div[1]/div/div[1]/a/@href').extract_first()
# 对详情页发情求获取详情页的页面源码数据
# 手动请求的发送
# 请求传参:meta={},可以将meta字典传递给请求对应的回调函数
yield scrapy.Request(detail_url, callback=self.parse_detail, meta={'item': item})
# 分页操作
if self.page_num <= 3:
new_url = format(self.url % self.page_num)
self.page_num += 1
yield scrapy.Request(new_url, callback=self.parse)
7.图片数据爬取之ImagesPipeline
- 图片数据爬取之ImagesPipeline
- 基于scrapy爬取字符串类型的数据和爬取图片类型数据的区别?
- 字符串:只需要基于xpath进行解析且提交管道进行持久化存储
- 图片:xpath及洗出图片src的属性值。单独的对图片地址发起请求获取图片二进制类型的数据。
- ImagesPipeline:
- 只需要将img的src的属性值进行解析,提交到管道,管道就会对图片的src进行请求发送获取图片的二进制类型的数据,且还会帮我们进行持久化存储
- 需求:爬取站长素材中的高清图片
- 使用流程:
- 1.数据解析(图片的地址)
- 2.将存储图片地址的item提交到指定的管道类
- 3.在管道文件中自定制一个基于imagesPipeLine的一个管道类
get_media_requests
file_path
item_completed
- 4.在配置文件中:
- 指定图片的存储目录:
IMAGES_STORE = './imgs'
- 指定开启的管道:自定制的管道类
- 指定图片的存储目录:
- 基于scrapy爬取字符串类型的数据和爬取图片类型数据的区别?
代码如下:
- img.py
1.数据解析(图片的地址)
import scrapy
from ..items import ImgproItem
class ImgSpider(scrapy.Spider):
name = 'img'
# allowed_domains = ['www.xxx.com']
start_urls = ['https://sc.chinaz.com/tupian/']
def parse(self, response):
div_list = response.xpath('//div[@id="container"]/div')
for div in div_list:
# 注意:使用伪属性
src = 'https:' + div.xpath('./div/a/img/@src2').extract_first()
print(src)
item = ImgproItem()
item['src'] = src
yield item
- items.py
2.将存储图片地址的item提交到指定的管道类
import scrapy
class ImgproItem(scrapy.Item):
# define the fields for your item here like:
src = scrapy.Field()
# pass
- pipelines.py
重写类(类的方法)
3.在管道文件中自定制一个基于imagesPipeLine的一个管道类
# class ImgproPipeline:
# def process_item(self, item, spider):
# return item
from scrapy.pipelines.images import ImagesPipeline
import scrapy
class ImgsPipeline(ImagesPipeline):
# 可以根据图片地址进行图片数据的请求
def get_media_requests(self, item, info):
yield scrapy.Request(item['src'])
# 指定图片存储的路径
def file_path(self, request, response=None, info=None, *, item=None):
imgName = request.url.split('/')[-1]
return imgName
def item_completed(self, results, item, info):
return item # 返回给下一个即将被执行的管道类
- settings.py
常规操作
4.在配置文件中:
指定图片的存储目录:IMAGES_STORE = './imgs'
指定开启的管道:自定制的管道类
# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
LOG_LEVEL = 'ERROR'
开启管道、更改类名
ITEM_PIPELINES = {
# 开启管道、更改类名
'imgPro.pipelines.ImgsPipeline': 300,
}
指定图片存储的目录
IMAGES_STORE = './imgs'
8.中间件
①知识点
- 中间件
- 下载中间件
- 位置:引擎和下载器之间
- 作用:批量拦截到整个工程的所有请求和响应
- 拦截请求:
- UA伪装:process_request
- 代理IP:process_exception:return request
- 拦截响应:
- 篡改响应数据、响应对象
- 需求:爬取网易新闻中的新闻数据(标题和内容)
- 1.通过网易新闻的首页解析出五大板块对应的详情页url(没有动态加载)
- 2.每一个板块对应的新闻标题都是动态加载出来的(动态加载)
- 3.通过解析出每一条新闻详情页的url获取详情页的页面源码,解析出新闻内容
- 下载中间件
②代码
Ⅰ拦截请求
拦截请求代码部分:
- middle.py
import scrapy
class MiddleSpider(scrapy.Spider):
# 爬取百度
name = 'middle'
# allowed_domains = ['www.xxx.com']
start_urls = ['http://www.baidu.com/s?wd=ip']
def parse(self, response):
page_text = response.text
with open('./ip.html', 'w', encoding='utf-8') as fp:
fp.write(page_text)
- settings.py
# 协议改为FALSE
ROBOTSTXT_OBEY = False
# 开启DOWNLOADER_MIDDLEWARES
DOWNLOADER_MIDDLEWARES = {
'middlePro.middlewares.MiddleproDownloaderMiddleware': 543,
}
- middlewares.py
import random
class MiddleproDownloaderMiddleware:
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the downloader middleware does not modify the
# passed objects.
# @classmethod
# def from_crawler(cls, crawler):
# # This method is used by Scrapy to create your spiders.
# s = cls()
# crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
# return s
user_agent_list = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 "
"(KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 "
"(KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 "
"(KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 "
"(KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 "
"(KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 "
"(KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 "
"(KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 "
"(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 "
"(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]
PROXY_http = [
'153.180.102.104:80',
'195.208.131.189:56055',
]
PROXY_https = [
'120.83.49.90:9000',
'95.189.112.214:35508',
]
# 拦截请求
def process_request(self, request, spider):
# UA伪装
request.headers['User-Agent'] = random.choice(self.user_agent_list)
# 为了验证代理操作是否生效
request.meta['proxy'] = 'http://116.62.198.43'
return None
# 拦截所有的异常
def process_response(self, request, response, spider):
# Called with the response returned from the downloader.
# Must either;
# - return a Response object
# - return a Request object
# - or raise IgnoreRequest
return response
# 拦截发生异常的请求
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 # 将修正之后的请求对象进行重新的请求发送
# def spider_opened(self, spider):
# spider.logger.info('Spider opened: %s' % spider.name)
Ⅱ拦截响应
爬取网易新闻数据代码如下:
- wangyi.py
import scrapy
from selenium import webdriver
from ..items import WangyiproItem
class WangyiSpider(scrapy.Spider):
name = 'wangyi'
# allowed_domains = ['www.xxx.com']
start_urls = ['https://news.163.com/']
modules_urls = [] # 存储五个板块对应详情页url
# 解析五大板块对应详情页url
# 实例化一个浏览器对象
def __init__(self):
self.bro = webdriver.Chrome(executable_path=r'D:\pycharm\PyCharm Community Edition 2020.1.2\CrawlerCourse\07DynamicLoad_dataProcessing\chromedriver.exe')
def parse(self, response):
li_list = response.xpath('/html/body/div[1]/div[1]/div[2]/div[2]/div[2]/div[2]/div/ul/li')
alist = [3, 4, 6, 7, 8]
for index in alist:
module_url = li_list[index].xpath('./a/@href').extract_first()
self.modules_urls.append(module_url)
# 依次对每一个板块对应的页面进行请求
for url in self.modules_urls: # 对每一个板块的url进行请求发送
yield scrapy.Request(url, callback=self.parse_module)
# 每一个板块对应的新闻标题相关的内容都是动态加载
def parse_module(self, response): # 解析每一个板块页面中对应新闻的标题和新闻详情页的url
# response.xpath()
div_list = response.xpath('/html/body/div/div[3]/div[4]/div[1]/div[1]/div/ul/li/div/div')
for div in div_list:
title = div.xpath('./div/div[1]/h3/a/text()').extract_first()
news_detail_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()
item = WangyiproItem()
item['title'] = title
# 对新闻详情页的url发起请求
yield scrapy.Request(url=news_detail_url, callback=self.parse_detail, meta={'item': item})
def parse_detail(self, response): # 解析新闻内容
content = response.xpath('/html/body/div[3]/div[1]/div[3]/div[2]//text()').extract()
content = ''.join(content)
item = response.meta['item']
item['content'] = content
yield item
def close(self, spider):
self.bro.quit()
- middlewares.py
# Define here the models for your spider middleware
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/spider-middleware.html
from scrapy import signals
# useful for handling different item types with a single interface
from itemadapter import is_item, ItemAdapter
from scrapy.http import HtmlResponse
from time import sleep
class WangyiproDownloaderMiddleware:
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the downloader middleware does not modify the
# passed objects.
def process_request(self, request, spider):
# Called for each request that goes through the downloader
# middleware.
# Must either:
# - return None: continue processing this request
# - or return a Response object
# - or return a Request object
# - or raise IgnoreRequest: process_exception() methods of
# installed downloader middleware will be called
return None
# 该方法拦截五大板块对应的响应对象,进行篡改
def process_response(self, request, response, spider): # spider表示爬虫对象
bro = spider.bro # 获取了在爬虫类中定义的浏览器对象
sleep(2)
page_text = bro.page_source # 包含了动态加载的新闻数据
# 挑选出指定的响应对象进行篡改
# 通过url指定request
# 通过request指定response
if request.url in spider.modules_urls:
bro.get(request.url) # 五个板块对应的url进行请求
# response # 五大板块对应的响应对象
# 针对定位到的这些response进行篡改
# 实例化一个新的响应对象(符合需求的:包含动态加载出的新闻数据),替代旧的响应对象
# 如何获取动态加载出的新闻数据?
# 基于selenium便捷的获取动态加载数据
new_response = HtmlResponse(url=request.url, body=page_text, encoding='utf-8', request=request)
return new_response
else:
# response # 其他请求对应的响应对象
return response
def process_exception(self, request, exception, spider):
# Called when a download handler or a process_request()
# (from other downloader middleware) raises an exception.
# Must either:
# - return None: continue processing this exception
# - return a Response object: stops process_exception() chain
# - return a Request object: stops process_exception() chain
pass
- pipelines.py
class WangyiproPipeline:
def process_item(self, item, spider):
print(item)
return item
- items.py
import scrapy
class WangyiproItem(scrapy.Item):
# define the fields for your item here like:
title = scrapy.Field()
content = scrapy.Field()
pass
- settings.py
1.常规操作
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
ROBOTSTXT_OBEY = False
LOG_LEVEL = 'ERROR'
2.开启中间件
DOWNLOADER_MIDDLEWARES = {
'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543,
}
3.开启管道
ITEM_PIPELINES = {
'wangyiPro.pipelines.WangyiproPipeline': 300,
}
9.CrawlSpider
- CrawlSpider:类,Spider的一个子类
- 全站数据爬取的方式:
- 基于Spider:手动请求发送
- 基于CrawlSpider:
- CrawlSpider的使用:
- 创建一个工程
- cd XXX
- 创建爬虫文件(CrawlSpider):
scrapy genspider -t crawl xxx www.xxx.com
- 链接提取器:
- 作用:根据指定的规则(allow)进行指定链接的提取
- 规则解析器:
- 作用:将链接提取器提取到的链接进行指定规则(callback)的解析
- 需求:爬取sun网站中的编号、新闻标题、新闻内容、标号
- 分析:爬取的数据不在同一张页面中。
- 1.可以使用链接提取器提取所有的页码链接
- 2.让链接提取器提取所有的新闻详情页的链接
- 全站数据爬取的方式:
案例代码(目标网站已改版,代码暂未调整):
- sun.py
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from ..items import SunproItem, DetailItem
# 需求:爬取sun网站中的编号、新闻标题、新闻内容、标号
class SunSpider(CrawlSpider):
name = 'sun'
# allowed_domains = ['www.xxx.com']
start_urls = ['http://wz.sun0769.com/index.php/question/questionType?type=4&page=']
# 这个网址进不去了,改版之后是:https://wz.sun0769.com/political/index,代码暂时不做调整
# 链接提取器:根据指定规则(allow="正则")进行指定链接的提取
link = LinkExtractor(allow=r'type=4&page=\d+')
link_detail = LinkExtractor(allow=r'question/\d+/\d+\.shtml')
rules = (
# 规则解析器:将链接提取器提取到的链接进行指定规则(callback)的解析操作
Rule(link, callback='parse_item', follow=True),
# follow = True : 可以将链接提取器 继续作用到 链接提取器提取到的链接 所对应的页面中
Rule(link_detail, callback='parse_detail', follow=True) # 此处 follow=True 可以不写
)
# 解析新闻编号和新闻标题
# 如下两个方法不可以实现请求传参!
# 无法将两个解析方法解析的数据存储到同一个item中,可以存储到两个item中
def parse_item(self, response):
# 注意:xpath表达式中不可以出现tbody标签
tr_list = response.xpath('//*[@id="morelist"]/div/table[2]//tr/td/table//tr')
for tr in tr_list:
new_num = tr.xpath('./td[1]/text()').extract_first()
new_title = tr.xpath('./td[2]/a[2]/@title').extract_first()
item = SunproItem()
item['title'] = new_title
item['new_num'] = new_num
yield item
# 解析新闻内容和新闻
def parse_detail(self, response):
new_id = response.xpath('/html/body/div[9]/table[1]//tr/td[2]/span[2]/text()').extract_first()
new_content = response.xpath('/html/body/div[9]/table[2]//tr[1]//text()').extract()
new_content = ''.join(new_content)
# print(new_id, new_content)
item = DetailItem()
item['content'] = new_content
item['new_id'] = new_id
yield item
- items.py
import scrapy
class SunproItem(scrapy.Item):
# define the fields for your item here like:
title = scrapy.Field()
new_num = scrapy.Field()
class DetailItem(scrapy.Item):
new_id = scrapy.Field()
content = scrapy.Field()
- pipelines.py
class SunproPipeline:
def process_item(self, item, spider):
# 如何判断item的类型
# 将数据写入数据库时,如何保证数据的一致性
if item.__class__.__name__ == 'DetailItem':
print(item['new_id'], item['content'])
else:
print(item['new_num'], item['title'])
return item
- settings.py
常规操作 + 开启管道
10.分布式爬虫
- 分布式爬虫
-
概念:我们需要搭建一个分布式的机群,让其对一组资源进行分布联合爬取
-
作用:提升爬取数据的效率
-
如何实现分布式?
- 安装一个scrapy-redis的组件
- 原生的scrapy是不可以实现分布式爬虫,必须要让scrapy-redis组件一起实现分布式爬虫
- 为什么原生的scrapy不可以实现分布式?
- 调度器不可以被分布式机群共享
- 管道也不可以被分布式机群共享
- scrapy-redis组件作用:
- 可以给原生的scrapy框架提供可以被共享的管道和调度器
- 实现流程:
- 创建一个工程
- 创建一个基于CrawlSpider的爬虫文件
- 修改当前的爬虫文件:
- 导包:
from scrapy-redis.spiders import RedisCrawlSpider
- 将
start_urls
和allowed_domains
进行注释 - 添加一个新属性:
redis_key = 'sun'
可以被共享的调度器队列名称 - 编写数据解析相关的操作
- 将当前爬虫类的父类修改成RedisCrawlSpider
- 导包:
- 修改配置文件settings
- 指定使用可以被共享的管道:
ITEM_PIPELINES = { 'scrapy_redis.pipelines.RedisPipeline': 400, }
- 指定调度器:
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
SCHEDULER_PERSIST = True
- 指定redis服务器:
REDIS_HOST = 'redis服务器的ip地址'
REDIS_PORT = 6379
- 指定使用可以被共享的管道:
- redis相关配置操作:
- 配置redis的配置文件:
- Linux或者mac:redis.conf
- windows:redis.windows.conf
- 打开配置文件修改:
- 将bind 127.0.0.1删除或注释
- 关闭保护模式:protected-mode yes改为no
- 结合着配置文件开启redis服务
- redis-sever 配置文件
- 启动客户端:
- redis-cli
- 配置redis的配置文件:
- 执行工程:
scrapy runspider xxx.py
- 向调度器的队列中放入一个起始的url:
- 调度器的队列在redis客户端中
lpush xxx www.xxx.com
- 调度器的队列在redis客户端中
- 爬取到的数据存储在了redis的proName:items这个数据结构中
-
案例代码(目标网站已改版,部分代码暂未调整)
- fbs.py
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from scrapy-redis.spiders import RedisCrawlSpider
from ..items import FbsproItem
class FbsSpider(RedisCrawlSpider):
name = 'fbs'
# allowed_domains = ['www.xxx.com']
# start_urls = ['http://www.xxx.com/']
redis_key = 'sun'
rules = (
Rule(LinkExtractor(allow=r'type=4&page=\d+'), callback='parse_item', follow=True),
)
def parse_item(self, response):
tr_list = response.xpath('//*[@id="morelist"]/div/table[2]//tr/td/table//tr')
for tr in tr_list:
new_num = tr.xpath('./td[1]/text()').extract_first()
new_title = tr.xpath('./td[2]/a[2]/@title').extract_first()
item = FbsproItem()
item['title'] = new_title
item['new_num'] = new_num
yield item
- items.py
import scrapy
class FbsproItem(scrapy.Item):
# define the fields for your item here like:
title = scrapy.Field()
new_num = scrapy.Field()
# pass
- settings.py
常规操作(3步) + 指定管道 + 指定调度器 + 指定服务器
# 指定管道
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 400,
}
# 指定调度器
# 使用scrapy-redis组件的去重队列(增加了一个去重容器类的配置,作用是用redis的set集合来存储请求的指纹数据,从而实现请求去重的持久化存储)
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 使用scrapy-redis组件自己的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 是否允许暂停(配置调度器是否要持久化,也就是当爬虫结束了,要不要清空redis中请求队列和去重指纹的set)
SCHEDULER_PERSIST = True
# 指定redis服务
REDIS_HOST = '127.0.0.1' # redis远程服务器的ip(修改)
REDIS_PORT = 6379
第九章
增量式爬虫
1.知识点
- 增量式爬虫
- 概念:监测网站数据更新的情况,只会爬取网站最新更新出来的数据。
- 分析:
- 指定一个起始url
- 基于CrawlSpider获取其他页码链接
- 基于Rule将其他页码链接进行请求
- 从每一个页码对应的页面源码中解析出每一部电影详情页的url
- 核心: 检测电影详情页的url之前有没有请求过
- 将爬取过的电影详情页url存储
- 存储到redis的set数据结构中
- 将爬取过的电影详情页url存储
- 对详情页的url发起请求,然后解析出电影的名称和简介
- 进行持久化存储
2.代码
代码如下:
- movie.py
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from redis import Redis
from ..items import MovieproItem
class MovieSpider(CrawlSpider):
name = 'movie'
# allowed_domains = ['www.xxx.com']
start_urls = ['https://www.4567tv.tv/frim/index1.html']
rules = (
Rule(LinkExtractor(allow=r'/frim/index1-\d+\.html'), callback='parse_item', follow=True),
)
# 创建redis链接对象
conn = Redis(host='127.0.0.1', port=6379)
# 用于解析每一个页码对应页面中的电影详情页的url
def parse_item(self, response):
li_list = response.xpath('/html/body/div[1]/div/div/div/div[2]/ul/li')
for li in li_list:
# 获取详情页的url
detail_url = 'https://www.4567tv.tv' + li.xpath('./div/a/@href').extract_first()
# 将详情页的url存入redis的set中
ex = self.conn.sadd('urls', detail_url)
if ex == 1:
print('该url未被爬取,可进行数据爬取')
yield scrapy.Request(url=detail_url, callback=self.parse_detail)
else:
print('数据还未更新,暂无进行数据爬取')
# 解析详情页中的电影名称和类型,进行持久化存储
def parse_detail(self, response):
item = MovieproItem
item['name'] = response.xpath('/html/body/div[1]/div/div/div/div[2]/h1/text()')
item['desc'] = response.xpath('/html/body/div[1]/div/div/div/div[2]/p[5]/spam[2]//text()')
item['desc'] = ''.join(item['desc'])
yield item
- pipelines.py
class MovieproPipeline:
conn = None
def open_spider(self, spider):
self.conn = spider.com
def process_item(self, item, spider):
dic = {
'name': item['name'],
'desc': item['desc']
}
print(dic)
self.conn.lpush('movieData', dic)
return item
- items.py
import scrapy
class MovieproItem(scrapy.Item):
# define the fields for your item here like:
name = scrapy.Field()
desc = scrapy.Field()
# pass
- settings.py
常规操作
补充内容:
协程 & asyncio & 异步
异步编程(课程简介)
- 为什么要讲?
- 越来越多的人关心async异步相关问题,并且该部分知识点不易学习(异步非阻塞、asyncio)
- 异步相关话题和框架越来越多,例如:tornado、fastapi、django 3.x asgi、 aiohttp都在异步–>提升性能
- 课程大纲
- 第一部分:协程
- 第二部分:asyncio模块进行异步编程
- 第三部分:实战案例
1.协程
- 协程不是计算机提供,程序员人为创造
- 协程(Coroutine),也可以被称为微线程,是一种用户态内的上下文切换技术。简而言之,其实就是通过一个线程实现代码块相互切换执行。例如:
def func1():
print(1)
...
print(2)
def func2():
print(3)
...
print(4)
func1()
func2()
- 实现协程的方法:
- greenlet,早期模块
- yied关键字
- asyncio装饰器(py3.4)
- async、await关键字(py3.5)【推荐】(主流)
1.1greenlet实现协程
pip install greenlet
- 示例代码如下:
1.2 yield关键字
- 示例代码如下:
1.3asyncio
-
在python3.4及之后的版本。
-
注意:遇到IO阻塞自动切换
1.4async & await 关键字
- 在python3.5及之后的版本。
- 将上面1.3中的代码稍加修改:
- 函数前添加
async
修饰器 yield from
改为await
- 函数前添加
2.协程的意义
- 在一个线程中遇到IO等待时间,线程不会干等,会利用空闲时间做其他事。
- 案例:下载3张图片
- 普通方式(同步):
pip install requests
- 协程方式(异步):
- 普通方式(同步):
3.异步编程
3.1 事件循环
- 理解成死循环,检测并执行某些代码
import asyncio
# 生成或获取一个事件循环
loop = asyncio.get_event_loop()
# 将任务放到任务列表
loop.run_until_complete(任务)
3.2 快速上手
- 协程函数:定义函数时
async def 函数名
- 协程对象:执行
协程函数()
得到的协程对象
async def func():
pass
result = func()
- 注意:执行协程函数创建协程对象,函数内部代码不会执行
- 如果想要运行协程函数内部代码,必须要将协程对象交给事件来循环处理
import asyncio
async def func():
print("test-1")
result = func()
# python3.7之前
loop = asyncio.get_event_loop()
loop.run_until_complete(result)
# python3.7之后
asyncio.run(result)
3.3 await
- await + 可等待的对象(协程对象、Future、Task对象–>IO等待)
示例1:
import asyncio
async def func():
print("come on~")
response = await asyncio.sleep(2) # 模拟网络请求
print("over!", response)
asyncio.run( func() )
示例2:
import asyncio
async def others():
print("start")
await asyncio.sleep(2)
print("over!")
return '返回值'
async def func():
print('执行协程函数内部代码')
# 遇到IO操作挂起当前协程(任务),等待IO操作完成之后再继续向下执行
# 当前协程挂起时,事件循环可以去执行其他协程(任务)
response = await others()
print('IO请求结束,结果为:', response)
asyncio.run( func() )
示例3:
import asyncio
async def others():
print("start")
await asyncio.sleep(2)
print("over!")
return '返回值'
async def func():
print('执行协程函数内部代码')
# 遇到IO操作挂起当前协程(任务),等待IO操作完成之后再继续向下执行
# 当前协程挂起时,事件循环可以去执行其他协程(任务)
response1 = await others()
print('IO请求结束,结果为:', response1)
response2 = await others()
print('IO请求结束,结果为:', response2)
asyncio.run( func() )
- await就是等待对象的值得到结果之后再继续进行
3.4 Task对象
- 在事件循环中添加多个任务
-
- task用于并发调度协程,通过
asyncio.create_task(协程对象)
的方式创建Task对象,这样可以让协程加入事件循环中等待被调度执行,除了使用asyncio.create_task(协程对象)
函数(3.7之后才能使用)外,可以使用loop.create_task()
或者ensure_future()
函数。不建议手动实例化Task对象。
- task用于并发调度协程,通过
示例1:
示例2:
示例3:
3.5 asyncio.Future对象
- Task继承Future,task对象内部await结果的处理基于future
示例1:
示例2:
3.6 concurrent.futures.Future对象
-
该future对象和上一个future对象无关
-
使用线程池、进程池实现异步操作时用到的对象
-
以后写代码可能会存在交叉时间。例如:crm项目80%都是基于异步协程 + MySQL(不支持)【线程、进程做异步编程】
-
future的转换:
fut = loop.run_in_executor(None, func1)
-
案例:asyncio + 不支持异步的模块
3.7异步迭代器
- 迭代器:
在其内部实现yield
方法和next
方法的对象。 - 可迭代对象:在类内部实现一个
iter
方法,并返回一个迭代器。 - 异步迭代器:
实现了__aiter__()
和__anext__()
方法的对象,必须返回一个awaitable
对象。async_for
支持处理异步迭代器的
__anext__()
方法返回的可等待对象,直到引发一个stopAsyncIteration
异常,这个改动由PEP 492
引入。 - 异步可迭代对象:
可在async_for
语句中被使用的对象,必须通过它的__aiter__()
方法返回一个asynchronous_iterator(异步迭代器)
。这个改动由PEP 492
引入。
3.8异步上下文管理器
-
上下文管理器:
with open时 :enter,exit。 -
异步上下文管理器:
通过定义__aenter__()
和__aexit__()
方法来对async_with
语句中的环境进行控制。
4.uvloop
- uvloop:是asyncio的事件循环的替代方案。事件循环效率>默认asyncio的事件循环。
pip install uvloop
注意:一个asgi–>uvicorn 内部使用的就是uvloop
5.实战案例
5.1 异步redis
- 再通过Python代码操作redis时,链接/操作/断开都是网络IO。
pip install aioredis
示例1:
示例2:
5.2 异步MySQL
pip install aiomysql
示例1:
示例2:
5.3 FastAPI框架
pip install fastapi
pip install uvicorn
(asgi内部基于uvloop)
示例(luffy.py):
5.4 爬虫
pip install aiohttp
总结
- 最大的意义:通过一个线程利用其IO等待时间去做一些其他事情。
总结
爬虫这东西,技术学到手,有收获就行。