该笔记是我在学习b站up主路飞学城IT的爬虫视频时做的,详细内容请去b站找原视频,文章仅供参考,如有不对请指正,另外文章内可能有些网站已失效,请自行寻找适合的网站
爬虫是从互联网上爬取各类资源,包括图片,文字,视频等格式,其原理就是用代码模拟浏览器下载各种资源。爬虫不一定要使用python语言,也可以使用java、c等,其原因还是因为python比较简洁,并且有丰富的第三方库,使爬虫技术更为简便。
什么是robots.txt?robots.txt就是一个文件包含了这个网页哪些可以爬哪些不可爬,查看方法就是在该url后面添加"/robots.txt",例http://www.bilibili.com/robots.txt。
第一个小爬虫就是爬取整个百度的网页,比较简单
from urllib.request import urlopen
url = "http://www.baidu.com"
resp = urlopen(url)
with open("myBaidu.html", mode="w", encoding="utf-8") as f: # 这里需要注意Windows用户需要添一个“encoding='utf-8'”,因为百度网页编码格式是utf-8,而open()函数默认是gbk,否则出现的网页将会乱码
f.write(resp.read().decode("utf-8"))
print('success!')
要熟练使用浏览器数据抓包工具,F12-Network
协议:就是两个计算机之间为了能够流畅的进行沟通而设置的一个君子协议,常见的协议有TCP/IP,SOAP协议,HTTP协议,SMTP协议等等······
HTTP协议,Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(www:World Wide Web)服务器传输超文本到本地浏览器的传送协议,直白点就是浏览器和服务器之间的数据交互遵守的就是HTTP协议。
HTTP协议把一条消息分为三大块内容,无论是请求还是响应都是三块内容
请求:
请求行 -> 请求方式 请求url地址 协议
请求头 -> 放一些服务器要使用的附加信息
请求体 -> 一般放一些请求参数
响应:
状态行 -> 协议 状态码
响应头 -> 放一些客户端要使用的一些附加信息
响应体 -> 服务器返回的真正客户端要用的内容(HTML,json)等
请求头中最常见的一些重要内容(爬虫需要):
响应头中一些重要的内容:
首先安装requests模块 pip install requests
import requests
url = 'https://www.sogou.com/web?query=周杰伦'
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36" # 这里的消息头可以去浏览器审查元素Network里找到,具体为Network第一个文件里的Request Headers——user-agent,可以理解为模拟浏览器标识
}
resp = requests.get(url, headers=headers)
print(resp)
print(resp.text)
在百度翻译上找到获取翻译结果的url:https://fanyi.baidu.com/sug
在这里用的是POST方法,上传需要翻译的单词,返回翻译结果,post上传参数为data
import requests
url = "https://fanyi.baidu.com/sug"
text = input("请输入你要翻译的英文单词")
data = {
"kw": text
}
# 发送post请求,发送的数据必须放在字典中,通过data参数进行传递
resp = requests.post(url, data=data)
print(resp.json()) # 将服务器返回的内容直接处理成json() -> dict
爬虫不好使第一个尝试User-Agent,python爬虫默认的user-agent:python-requests/2.25.1,不是浏览器标识
在这里使用的是GET方法,获取豆瓣电影排行,get上传参数为param
import requests
url = "https://movie.douban.com/j/chart/top_list"
# 重新封装参数
param = {
"type": "24",
"interval_id": "100:90",
"action": "",
"start": 0,
"limit": 20
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 "
"Safari/537.36 "
}
resp = requests.get(url, params=param, headers=headers)
print(resp.json())
resp.close()
在程序的最后需要关闭resp(连接端口),不关闭的话可能会因为多次访问最后进不去,所以需要在最后添加一句resp.close(),包括打开文件,最后也要关闭
在上一章中,我们基本上掌握了抓取整个网页的基本技能。但是呢,大多数情况下,我们并不是需要整个网页的内容,只需要其中的一小部分。那么这就涉及到了数据提取的问题。
本课程中,提供三种解析方式:
这三种方式可以混合进行使用,完全以结果做导向,只要能拿到你想要的数据,用什么方案并不重要,当你掌握这些之后再考虑性能问题。
Regular Expression,正则表达式,一种使用表达式的方式对字符串进行匹配的语法规则。
我们抓取到的网页源代码本质上就是一个超长的字符串,想从里面提取内容,用正则再适合不过。
正则的优点:速度快,效率高,准确性高
正则的缺点:新手上手难度比较高
正则的语法:使用元字符进行排列组合用来匹配字符串,在线测试正则表达式http://tool.oschina.net/regex/
元字符:具有固定含义的特殊符号
常用元字符:
. 匹配除换行以外的任意字符
\w 匹配字母或数字或下划线
\s 匹配任意的空白符
\d 匹配数字
\n 匹配一个换行符
\t 匹配一个制表符
^ 匹配字符串的开始
$ 匹配字符串的结尾
\W 匹配非字母或数字或下划线
\S 匹配非空白符
\D 匹配非数字
a|b 匹配字符a或字符b
() 匹配括号内的表达式,也表示一个组
[...] 匹配字符组中的字符
[^...] 匹配除了字符组中字符的所有字符
量词:控制前面的元字符出现的次数
* 重复零次或更多次
+ 重复一次或更多次
? 重复零次或一次
{
n} 重复n次
{
n,} 重复n次或更多次
{
n,m} 重复n到m次
贪婪匹配和惰性匹配
.* 贪婪匹配
.*? 惰性匹配
爬虫中最多使用的就是惰性匹配,因此对此需要重视
惰性匹配就是尽可能少的去匹配内容,举例
str:玩儿吃鸡游戏,晚上一起上游戏,干嘛呢?打游戏啊
reg:玩儿.*?游戏
# 这里的原理是:首先匹配“玩儿”两个字,然后再找“.*”次“游戏”,“.*”是尽可能多的进行匹配,因此此时匹配到的会是“玩儿吃鸡游戏,晚上一起上游戏,干嘛呢?打游戏”,然后“?”限制搜索次数,限制到最小次数,最终结果就为“玩儿吃鸡游戏”
此时结果为:玩儿吃鸡游戏
str:<div class="jay">周杰伦</div><div class="jj">林俊杰</div>
reg: <div class=".*?">.*?</div>
结果:<div class="jay">周杰伦</div>
<div class="jj">林俊杰</div>
学习正则后,该如何在程序中使用呢?
import re
# findall:匹配字符串中所有符合正则的内容
lst = re.findall(r"\d+", "我的电话号是10086,我的女朋友电话号是10010")
print(lst)
# finditer:匹配字符串中所有的内容[返回的迭代器],从迭代器中拿到内容需要.group()
it = re.finditer(r"\d+", "我的电话号是10086,我的女朋友电话号是10010")
for i in it:
print(i.group())
# search是找到一个结果就返回,返回的结果是match对象,拿数据需要.group()
s = re.search(r"\d+", "我的电话号是10086,我的女朋友电话号是10010")
print(s.group())
# match是从头开始匹配,因此第一个是中文匹配不到
s = re.match(r"\d+", "我的电话号是10086,我的女朋友电话号是10010")
print(s.group())
当正则表达式很长的时候,我们也可以使用预加载正则表达式
# 预加载正则表达式
obj = re.compile(r"\d+")
ret = obj.finditer("我的电话号是10086,我的女朋友电话号是10010")
for it in ret:
print(it.group())
obj.findall("sadadsa223dawswefq123fasdigjoihuiohuiogsdf")
print(ret)
那么如何单独提取出字符串中的内容呢?
import re
s = """
张富帅
张富贵
吕富帅
小狗头
小煞笔
"""
# (?P<分组名字>正则)可以单独从正则匹配的内容中进一步提取内容
obj = re.compile(r"(?P.*?) ", re.S) # re.S 让.能匹配换行符
res = obj.finditer(s)
for it in res:
print(it.group("name"))
print(it.group("id"))
import requests
import re
import csv
# 提取页面
url = "http://movie.douban.com/top250"
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 "
"Safari/537.36 "
}
resp = requests.get(url, headers=headers)
page_content = resp.text
# 解析数据 拿到电影名,导演,年份,评分,评分人数信息
obj = re.compile(r'.*?.*?(?P.*?) .*?'
r'.*?导演: (?P.*?) .*?
(?P.*?) .*?'
r'
.*?'r'(?P.*?) ', re.S)
# 开始匹配
result = obj.finditer(page_content)
f = open("top250.csv", mode="w", encoding='utf-8')
csvwriter = csv.writer(f)
for it in result:
# print(it.group("title"))
# print(it.group("director").strip())
# print(it.group("year").strip())
# print('评分'+it.group("average"))
# print(it.group('people'))
dic = it.groupdict()
dic['director'] = dic['director'].strip()
dic['year'] = dic['year'].strip()
csvwriter.writerow(dic.values())
print('success')
2.5 屠戮电影天堂电影信息
- 定位到2021必看热片
- 从2021必看热片中提取电影子页面的链接地址
- 请求子页面中的链接地址,拿到我们想要的下载磁链接
import requests
import re
# 定位阶段
domain = "https://dytt89.com/"
resp = requests.get(domain, verify=False) # verify=False 去掉安全验证
resp.encoding = 'gb2312' # 指定字符集
# print(resp.text)
# 提取阶段 拿到里面的
obj1 = re.compile(r"2021必看热片.*?(?P.*?)
"
, re.S)
obj2 = re.compile(r", re.S)
obj3 = re.compile(r'◎片 名 (?P.*?)
.*?r'?P.*?)"', re.S)
result1 = obj1.finditer(resp.text)
child_href_list = []
for it in result1:
ul = it.group('ul')
# 提取子页面链接
result2 = obj2.finditer(ul)
for itt in result2:
# 拼接子页面的url地址:域名+子页面地址
child_href = domain + itt.group('href').strip('/')
child_href_list.append(child_href) # 把子页面链接存储起来
# 提取子页面内容
for href in child_href_list:
child_resp = requests.get(href, verify=False)
child_resp.encoding = 'gb2312'
result3 = obj3.search(child_resp.text)
print(result3.group('movie'))
print(result3.group('download'))
2.5 Bs解析前戏-Html语法规则
bs4解析比较简单,但是需要一定的html知识,然后再去使用bs4去提取,逻辑和编写难度就会非常简单清晰,有前端基础的可略过
HTML(Hyper Text Markup Language)超文本标记语言,是我们编写网页的最基本也是最核心的一种语言。其语法规则就是用不同的标签对网页上的内容进行标记,从而使网页显示出不同的展示效果。
<h1>
Hello World!
h1>
上述代码的含义是在页面显示“Hello World!”一句,但是这句话被
和
标记了。白话就是括起来了,被H1标签括起来了。这个时候,浏览器在展示的时候就会让“Hello World!”这句话加粗加大,变为标题,所以HTML的语法就是用类似这样的标签对页面内容进行标记。不同的标签表现出来的效果也是不一样的。
h1:一级标题
h2:二级标题
p:段落
font:字体(已被废弃,但还能用)
body:主体
标签还有很多,这里就不一一列举。接下来是属性
<h1 align='center'>
Hello World!
h1>
<li id='1'>ali>
<li id='2'>bli>
<li id='3'>cli>
其中"align"就是标签属性,"center"就是属性值,后续的bs4解析就是可以根据id的属性值进行检索。
2.6 Bs4解析入门-搞搞菜价
首先pip install bs4安装模块
- 拿到页面源代码
- 使用bs4进行解析 拿到数据
视频中的网站源代码已改变,因此这里选用的url是:http://www.bjtzh.gov.cn/bjtz/home/jrcj/index.shtml,最后结果类似
import requests
from bs4 import BeautifulSoup
import csv
url = "http://www.bjtzh.gov.cn/bjtz/home/jrcj/index.shtml"
resp = requests.get(url)
resp.encoding = 'utf-8'
f = open("vegetable_price.csv", mode="w", encoding='utf-8')
csvwriter = csv.writer(f)
# 解析数据
# 1.把页面源代码交给BeautifulSoup进行处理,生成bs对象
page = BeautifulSoup(resp.text, "html.parser") # 指定html解析器
# 2.从bs对象中查找数据
# find(标签,属性=值) 只找第一个
# findall(标签,属性=值) 找到所有的
table = page.find("table", attrs={
"style": "margin: 0px auto; width: 588px; height: 847px; border-collapse: collapse;",
"width": "588",
"cellspacing": "0",
"cellpadding": "0",
"border": "1",
"align": "center"
})
# 拿到所有数据行
trs = table.find_all("tr")[7:]
for tr in trs: # 每一行数据
tds = tr.find_all('td') # 拿到每行数据中的td
name = tds[0].text # .text表示拿到被标签标记的内容
kind = tds[1].text
high = tds[2].text
low = tds[3].text
csvwriter.writerow([name, kind, high, low])
f.close()
print('success!')
2.7 Bs4解析案例-抓取优美图库图片
- 拿到主页面的源代码 提取子页面的链接地址 href
- 通过href拿到子页面的内容,从子页面找到图片的下载地址 img->src
- 下载图片
import requests
from bs4 import BeautifulSoup
import time
url_index = "https://umei.cc"
url = "https://umei.cc/bizhitupian/weimeibizhi/"
resp = requests.get(url)
resp.encoding = "utf-8"
# 把源代码交给BeautifulSoup
main_page = BeautifulSoup(resp.text, "html.parser")
a_list = main_page.find("div", class_="TypeList").find_all("a")
# print(a_list)
for a in a_list:
href = url_index + a.get('href') # 直接通过get就可以直接拿到属性值
# 拿到子页面源代码
child_resp = requests.get(href)
child_resp.encoding = "utf-8"
# 从子页面拿到图片下载链接
child_page = BeautifulSoup(child_resp.text, "html.parser")
p = child_page.find("p", align="center")
img = p.find("img")
src = img.get("src")
# 下载图片
img_resp = requests.get(src)
# img_resp.content # 这里拿到的是字节
img_name = src.split("/")[-1] # 切割 拿到url中的最后一个/以后的内容
with open("Wallpaper/"+img_name, mode='wb') as f:
f.write(img_resp.content) # 图片内容写入文件
print("success!", img_name)
time.sleep(1) # 防止访问过多服务器压力过大
print("all over")
2.8 XPath入门
xpath是在XML文档中搜索内容的一门语言
html是xml的一个子集
安装lxml模块 pip install lxml
from lxml import etree
xml = """
1
野花遍地香
1.23
周大强
周芷若
周杰伦
蔡依林
rerererererer
rerererererer2
rerererererer3
胖胖陈
胖胖不陈
"""
tree = etree.XML(xml)
# result = tree.xpath("/book") # /表示层级关系,第一个/是根节点
# result = tree.xpath("/book/name/text()") # text()表示拿文本
# result = tree.xpath("/book/author//nick/text()") # 后代 拿出nick里的文本以及三个rerere
# result = tree.xpath("/book/author/*/nick/text()") # *任意节点,通配符 只拿出re1,re2
result = tree.xpath("/book//nick/text()") # 拿出所有nick的文本
print(result)
在html文件中,[]可以表示索引,索引为第几个,例如///
- [1]//text()表示第一条
- 中标签的文字内容;
-
[]里面也可以表示为标签的属性筛选,例如///
- /[@href=‘dapao’]/text(),表示href为“dapao”的标签的文字内容;
-
///
- //@href可以单取a标签href的属性值。
-
小技巧:可以从网页中按F12,页面源代码中可以快速复制xpath
2.9 Xpath实战 抓取猪八戒网信息
- 拿到页面源代码
- 提取和解析数据
在这里我搜索的是“小程序开发”,遇到许多视频中没有出现的问题,好在通过百度也算是解决了,如果有更好的解决方法麻烦大佬留言
import requests
from lxml import etree
# 我这搜索的是小程序开发,爬取过程中有许多不方便的,尽量尝试搜索英文
url = "https://beijing.zbj.com/search/f/?kw=%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%BC%80%E5%8F%91"
resp = requests.get(url)
# print(resp.text)
# 解析
html = etree.HTML(resp.text)
# 拿到第一个服务商的div
divs = html.xpath("/html/body/div[6]/div/div/div[3]/div[4]/div[1]/div")
for div in divs: # 每一个服务商的信息
price_w = div.xpath('./div/div/a[1]/div[2]/div[1]/span[1]/text()')
if not price_w: # 我在爬取价格时遇到空字符,因此设个if语句跳过该价格
break
price = price_w[0]
title = "小程序".join(div.xpath('./div/div/a[1]/div[2]/div[2]/p/text()'))
company = div.xpath('./div/div/a[2]/div[1]/p/text()') # 爬取结果含有换行符
company = list(filter(None, [x.strip() for x in company]))[0] # 去除换行符后再将list中的空字符去除
location = div.xpath('./div/div/a[2]/div[1]/div/span/text()')[0]
print(title, price, company, location)
第三章 Requests进阶
3.1 Requests进阶概述
我们在之前的爬虫中其实已经使用过headers了。header为HTTP协议中的请求头,一般存放一些和请求内容无关的数据,有时也会存放一些安全验证信息。比如常见的User-Agent,token,cookie等。
通过requests发送的请求,我们可以把请求头信息放在headers中,也可以单独进行存放,最终由requests自动帮我们拼接成完整的http请求头。
本章内容:
- 模拟浏览器登录->处理cookie
- 防盗链处理->抓取梨视频数据
- 代理->放hi被封IP
综合训练:抓取网易云评论信息
3.2 处理cookie 登录小说网
登录->得到cookie
带着cookie去请求到书架url -> 书架上的内容
必须得把上面的两个操作连起来 我们可以使用session进行请求->session可以认为一连串的请求。在这个过程中cookie不会丢失
import requests
# 会话
session = requests.session()
data = {
"loginName": "13757696746",
"password": "123qweasdzxc"
}
# 1.登录
url = "https://passport.17k.com/ck/user/login"
resp = session.post(url, data=data)
# 拿书架的数据
resp_b = session.get("https://user.17k.com/ck/author/shelf?page=1&appKey=2406394919")
print(resp_b.json())
# 另一种方法
resp = requests.get("https://user.17k.com/ck/author/shelf?page=1&appKey=2406394919", headers={
"Cookie": "浏览器中复制的cookie"
})
print(resp.json())
3.3 防盗链 抓取梨视频
爬取过程中视频url并不会出现在页面源代码里,推测视频链接是由js生成,通过拦截发现一段与视频链接非常相似的链接,于是需要将其拼接
- 拿到contID
- 拿到videoStatus返回的json -> srcURL
- srcURL里面的内容进行修整
- 下载视频
什么是防盗链:溯源,防盗链相当于在页面请求过程中有个层级关系,它要求你必须是从第一个页面转到第二个页面,否则你直接访问第二个页面是不行的,防盗链就是这个页面的上一级页面
import requests
url = "https://www.pearvideo.com/video_1738675"
contID = url.split("_")[1]
videoStatusUrl = f"https://www.pearvideo.com/videoStatus.jsp?contId={
contID}&mrd=0.5611111607819312"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 "
"Safari/537.36 ",
# 防盗链:
"Referer": url
}
resp = requests.get(videoStatusUrl, headers=headers)
dic = resp.json()
srcUrl = dic["videoInfo"]["videos"]["srcUrl"]
systemTime = dic["systemTime"]
srcUrl = srcUrl.replace(systemTime, f"cont-{
contID}")
with open("videos/a.mp4", mode='wb') as f:
f.write(requests.get(srcUrl).content)
print("success!")
3.4 代理
原理:通过第三方的一个机器去发送请求
import requests
# 36.112.139.146
proxies = {
"http": "http://36.112.139.146:3128"
}
resp = requests.get("http://www.baidu.com", proxies=proxies)
resp.encoding = "utf-8"
print(resp.text)
3.5 综合训练 抓取网易云音乐评论信息
- 找到未加密的参数
- 想办法把参数进行加密(必须参考网易的洛基),params => encText,encSecKey => encSecKey
- 请求到网易,拿到评论信息
爬取过程中遇到极其复杂的信息加密,Network项目中拦截到神评后,可以发现该请求的data是加密了的,在Initiator里可以看到它生成神评都是经过哪些js,点击第一个也就是最后运行的js文件查看代码,对该行代码标记后往前推找到对应url,可以看到右边Scope栏中Local底下有加密的data信息,那么我们可以倒推代码找到它是在哪一行里加密的,所以在右边Call Stack栏里往后倒推,一个一个查看Local属性里的data是否有加密,最后排查到u0x.be1x这一步中data还未加密,可以推测这段js就是对data的加密。注意:js文件中的变量名每次刷新都会变化
u9l.be9V = function(Y9P, e9f) {
var i9b = {
}
, e9f = NEJ.X({
}, e9f)
, mo3x = Y9P.indexOf("?");
if (window.GEnc && /(^|\.com)\/api/.test(Y9P) && !(e9f.headers && e9f.headers[eu0x.Bl8d] == eu0x.Io0x) && !e9f.noEnc) {
if (mo3x != -1) {
i9b = j9a.gX1x(Y9P.substring(mo3x + 1));
Y9P = Y9P.substring(0, mo3x)
}
if (e9f.query) {
i9b = NEJ.X(i9b, j9a.fP1x(e9f.query) ? j9a.gX1x(e9f.query) : e9f.query)
}
if (e9f.data) {
i9b = NEJ.X(i9b, j9a.fP1x(e9f.data) ? j9a.gX1x(e9f.data) : e9f.data)
}
i9b["csrf_token"] = u9l.gP1x("__csrf");
Y9P = Y9P.replace("api", "weapi");
e9f.method = "post";
delete e9f.query;
var bUG7z = window.asrsea(JSON.stringify(i9b), bsB3x(["流泪", "强"]), bsB3x(WU8M.md), bsB3x(["爱心", "女孩", "惊恐", "大笑"]));
e9f.data = j9a.cs0x({
params: bUG7z.encText,
encSecKey: bUG7z.encSecKey
})
}
var cdnHost = "y.music.163.com";
var apiHost = "interface.music.163.com";
if (location.host === cdnHost) {
Y9P = Y9P.replace(cdnHost, apiHost);
if (Y9P.match(/^\/(we)?api/)) {
Y9P = "//" + apiHost + Y9P
}
e9f.cookie = true
}
cwR2x(Y9P, e9f)
}
过程比较复杂,最好跟着视频学习.
在该方法里一步一步推导,可以发现
var bUG7z = window.asrsea(JSON.stringify(i9b), bsB3x(["流泪", "强"]), bsB3x(WU8M.md), bsB3x(["爱心", "女孩", "惊恐", "大笑"]));
这里后面开始的加密,仔细研究可以看出来是替换了内容params => encText,encSecKey => encSecKey,那么就去找window.asrsea()这个方法,搜索后发现它的值全靠这一句window.asrsea = d,网上看可以看到d方法的定义过程
function d(d, e, f, g) {
var h = {
}
, i = a(16);
return h.encText = b(d, g),
h.encText = b(h.encText, i),
h.encSecKey = c(i, e, f),
h
}
d()的四个元素中,d代表数据,e在控制台中过几遍可以发现是固定值010001,f是一串很长的外星文,g也是固定值“0CoJUm6Qyw8W8jud”
然后就根据属性值,分析d()究竟要干什么,接下来内容的分析就不再做详细的介绍,a()返回16位随机字符串
我这爬取了用户的昵称以及评论,具体步骤需要去b站看视频
from Crypto.Cipher import AES
from base64 import b64encode
import requests
import json
url = "https://music.163.com/weapi/comment/resource/comments/get?csrf_token="
# 请求方式POST
data = {
"csrf_token": "",
"cursor": "-1",
"offset": "0",
"orderType": "1",
"pageNo": "1",
"pageSize": "20",
"rid": "R_SO_4_65538",
"threadId": "R_SO_4_65538"
}
# 服务于d
e = "010001"
f = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e "
g = "0CoJUm6Qyw8W8jud"
i = "7HCsoSguhIA6SpNw" # 手动固定 函数中是随机的
encSecKey = "21fb180e564113d59d37865081a91daf1f775fb67ef063dc046bda9966613ea4a384b597e11ce05c442df9dfa8538347c58aa87d9be92636fbda399b28f04bbf31e91751e25f359a05538b8d5c51999a03e1348e21cbe90fbfa54d013399c0ab240e41c73750ef463542fe5c14637db16abeffa8a2ab74027e085aa570c01395 "
# 转化成16的倍数,为下方的加密算法服务
def to_16(data):
pad = 16 - len(data) % 16
data += chr(pad) * pad
return data
def get_encSecKey(): # 由于i是固定的,因此encSecKey也是固定的,c()函数获得的结果也是固定的
return encSecKey
def get_params(data): # 默认这里接受到的为字符串
first = enc_params(data, g)
second = enc_params(first, i)
return second # 返回的就是params
def enc_params(data, key): # 加密过程
# 导入AES加密模块需要导入新包
iv = "0102030405060708"
data = to_16(data)
aes = AES.new(key=key.encode("utf-8"), IV=iv.encode('utf-8'), mode=AES.MODE_CBC) # 创建加密器
bs = aes.encrypt(data.encode('utf-8')) # 加密,加密内容的长度必须是16的倍数
return str(b64encode(bs), 'utf-8') # 转化成字符串返回
# 处理加密过程
"""
function a(a) { # 返回随机的16位字符串
var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
for (d = 0; a > d; d += 1) # 循环16次
e = Math.random() * b.length, # 随机数
e = Math.floor(e), # 取整
c += b.charAt(e); # 去字符串中的x位置
return c
}
function b(a, b) { # a是要加密的内容,
var c = CryptoJS.enc.Utf8.parse(b) # b是密钥
, d = CryptoJS.enc.Utf8.parse("0102030405060708")
, e = CryptoJS.enc.Utf8.parse(a) # e是数据
, f = CryptoJS.AES.encrypt(e, c, { # c 加密的密钥
iv: d, # 偏移量
mode: CryptoJS.mode.CBC # 模式:cbc
});
return f.toString()
}
function c(a, b, c) {
var d, e;
return setMaxDigits(131),
d = new RSAKeyPair(b,"",c),
e = encryptedString(d, a)
}
function d(d, e, f, g) {
var h = {} # 这里为空
, i = a(16); # i就是16位随机值,把i设为固定值
return h.encText = b(d, g), # g密钥
h.encText = b(h.encText, i), # 返回的就是params i也是密钥
h.encSecKey = c(i, e, f), # 返回的就是encSecKey,e和f是定死的,如果此时把i固定得到的key是固定的
h
}
function e(a, b, d, e) {
var f = {};
return f.encText = c(a + e, b, d),
f
}
两次加密:
数据+g => b => 第一次加密+i => b => params
"""
# 发送请求,得到评论结果
resp = requests.post(url, data={
"params": get_params(json.dumps(data)),
"encSecKey": get_encSecKey()
})
dic = resp.json()
hotComments = dic['data']["hotComments"]
for i in hotComments:
username = i["user"]["nickname"]
content = i["content"]
print(username, ":", content)
第四章 异步
4.1 第四章概述
到目前为止,我们可以解决爬虫的基本抓取流程了,但是抓取效率还不够高。如何提高抓取效率呢?我们可以选择多线程,多进程,协程等操作完成异步爬虫。
什么是异步?假设我们有一万条数据需要爬取,一个一个爬的话就会需要很长的时间,那异步就是多条线路同时进行,可以一次性爬取多条数据。
本章内容:
- 快速学会多线程
- 快速学会多进程
- 线程池和进程池
- 扒光新发地
- 协程
- 多任务异步协程实现
- aiohttp模块详解
- 扒光一本小说
- 综合训练-抓取一部电影
4.2 多线程
- 进程是资源单位,每一个进程至少要有一个线程
- 线程是执行单位
第一套写法
from threading import Thread
def func():
for i in range(1000):
print("func ", i)
if __name__ == '__main__':
t = Thread(target=func) # 创建线程并给线程安排任务,相当于创建一个员工,括号内为他要做的工作
t.start() # 多线程状态为可以开始工作状态,具体的执行时间由CPU决定
for i in range(1000):
print("main", i)
第二套写法
from threading import Thread
class MyThread(Thread):
def run(self):
for i in range(1000):
print("子线程", i)
if __name__ == '__main__':
t = MyThread()
# t.run() # 方法调用了,依然是单线程
t.start() # 开启线程
for i in range(1000):
print("主线程", i)
4.3 多进程
多进程的写法与多线程基本相同
from multiprocessing import Process
def fuc():
for i in range(1000):
print("子进程", i)
if __name__ == '__main__':
p = Process(target=fuc)
p.start()
for i in range(1000):
print("主线程", 1)
那如果要区分两个进程应该怎么写?
from threading import Thread
def fuc(name): # 打印括号内的名字
for i in range(1000):
print(name, i)
if __name__ == '__main__':
t1 = Thread(target=fuc, args=(" 周杰伦",)) # 传递参数必须是元组
t1.start()
t2 = Thread(target=fuc, args=("王力宏",))
t2.start()
4.4 线程池与进程池入门
线程池:一次性开辟一些线程,我们用户直接给线程池子提交任务。线程任务的调度交给线程池来完成
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# ThreadPoolExecutor, ProcessPoolExecutor一个对应线程一个对应进程,选择使用
def fn(name):
for i in range(1000):
print(name, i)
if __name__ == '__main__':
# 创建线程池
with ThreadPoolExecutor(50) as t: # 创建50个线程
for i in range(100):
t.submit(fn, name=f"线程{
i}")
# 等待线程池中的任务全部执行完毕,才继续执行(守护)
print("Done")
4.5 线程池案例-抓取新发地菜价
- 如何提取单个页面的数据
- 上线程池,多个页面同时抓取
因为页面更新,数据不会保存在页面源代码,更新后是用json生成数据,因此与视频代码不同
import requests
import csv
from concurrent.futures import ThreadPoolExecutor
f = open("data.csv", mode="w", encoding="utf-8", newline="")
csvwriter = csv.writer(f)
def download_one_page(page):
# 拿到页面源代码
url = "http://www.xinfadi.com.cn/getPriceData.html"
data = {
"limit": "20",
"current": f"{
page}", # 对应第几页
}
resp = requests.post(url, data=data)
for txt in resp.json()["list"]:
# 提取自己需要的内容
dic = [txt["prodName"], txt["prodCat"], txt["lowPrice"], txt["highPrice"], txt["place"], txt["pubDate"]]
# 将数据存放至文件中
csvwriter.writerow(dic)
print(f"第{
page}页下载完成")
if __name__ == '__main__':
# for i in range(1, 17712): # 效率极其低下
# download_one_page(i)
# 创建线程池
with ThreadPoolExecutor(50) as t:
for i in range(200):
# 把下载任务提交给线程池
t.submit(download_one_page, i)
print("全部下载完毕")
4.6 协程
4.6.1 协程概念
当代码中time.sleep()的时候,当前线程是处于阻塞状态,CPU是部位我工作的
同样的,input()程序也是处于阻塞状态
requests.get(url) 在网络请求返回数据之前,程序也是处于阻塞状态
一般情况下,当程序处于IO操作的时候,线程都会处于阻塞状态
协程:当程序遇见IO操作的时候,可以选择性的切换到其他任务上。在微观上是一个任务一个任务的进行切换,切换条件一般就是IO操作;在宏观上,我们能看到的其实是多个任务一起在执行。
4.6.2 多任务异步交互
import asyncio
import time
# async def func():
# print("你好,我叫赛利亚")
#
#
# if __name__ == '__main__':
# g = func() # 此时的函数是异步协程函数,此时函数执行得到的是一个协程对象
# asyncio.run(g) # 协程程序运行需要asyncio模块的支持
# async def func1():
# print("你好,我是func1")
# # time.sleep(3) # 当程序出现同步操作的时候,异步就中断了
# await asyncio.sleep(3) # 异步操作的代码,表明在这段等待时间切换到下一个任务
# print("你好,我是func1")
#
#
# async def func2():
# print("你好,我是func2")
# # time.sleep(4)
# await asyncio.sleep(4)
# print("你好,我是func2")
#
#
# async def func3():
# print("你好,我是func3")
# # time.sleep(2)
# await asyncio.sleep(2)
# print("你好,我是func3")
#
# if __name__ == '__main__':
# f1 = func1()
# f2 = func2()
# f3 = func3()
# tasks = [
# f1, f2, f3
# ]
# t1 = time.time()
# # 一次性启动多个任务(协程)
# asyncio.run(asyncio.wait(tasks))
# t2 = time.time()
# print(t2-t1)
# 上面的这种并不是推荐写法,推荐写法为下方这种,因为这种写法可以套在爬虫上
# async def func1():
# print("你好,我是func1")
# await asyncio.sleep(3)
# print("你好,我是func1")
#
#
# async def func2():
# print("你好,我是func2")
# await asyncio.sleep(4)
# print("你好,我是func2")
#
#
# async def func3():
# print("你好,我是func3")
# await asyncio.sleep(2)
# print("你好,我是func3")
#
#
# async def main():
# # 第一种写法
# # f1 = func1()
# # await f1 # 一般await挂起操作放在协程对象前面
# # 第二种写法(推荐)
# tasks = [
# asyncio.create_task(func1()), # py3.8以后加上asyncio.create_task()
# asyncio.create_task(func2()),
# asyncio.create_task(func3())
# ]
# await asyncio.wait(tasks)
#
#
# if __name__ == '__main__':
# asyncio.run(main())
# 在爬虫领域的应用
async def download():
print("准备开始下载")
await asyncio.sleep(2) # 模拟网络请求
print("下载完成")
async def main():
urls = [
"http://www.baidu.com",
"http://www.bilibili.com",
"http://www.163.com"
]
tasks = []
for url in urls:
d = download(url)
tasks.append(d)
await asyncio.wait(tasks)
if __name__ == '__main__':
asyncio.run(main())
4.6.3 关于异步协程-过时警告
在python3.8的版本后,task打包需要添加asyncio.create_task(),括号内为任务,3.11版本后将会彻底删除,到时候会直接报错。
4.7 异步http请求aiohttp模块
首先要安装模块pip install aiohttp
requests.get()同步的代码–>异步操作aiohttp
import aiohttp
import asyncio
urls = {
"https://img-pre.ivsky.com/img/tupian/pre/202101/31/weiershi_kejiquan.jpg",
"https://img-pre.ivsky.com/img/tupian/pre/202101/31/weiershi_kejiquan-001.jpg",
"https://img-pre.ivsky.com/img/tupian/pre/202101/31/weiershi_kejiquan-003.jpg"
}
async def aiodownload(url):
name = url.rsplit("/", 1)[1]
# s = aiohttp.ClientSession() <==> requests.session()
# s.get(),post() = requests.get(),post()
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
# 请求回来了 写入文件
with open("Wallpaper/"+name, mode="wb") as f:
f.write(await resp.content.read()) # 读取内容是异步的 需要await挂起
print(name, "done!")
async def main():
tasks = []
for url in urls:
tasks.append(asyncio.create_task(aiodownload(url)))
await asyncio.wait(tasks)
if __name__ == '__main__':
# 这里使用asyncio.run(main())会报RuntimeError: Event loop is closed,改为下方这种就不会报错了
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
4.8 异步爬虫实战-扒光一部小说
- 同步操作:访问 getCatalog 拿到所有章节cid和名称
- 异步操作:访问 getChapterContent 下载所有的文章内容
# http://dushu.baidu.com/api/pc/getCatalog?data={'book_id':'4306063500'} # 获取章节的内容
# 获得小说内容
# http://dushu.baidu.com/api/pc/getChapterContent?data={'book_id':'4306063500','cid':'4306063500|11348571','need_bookinfo':1}
import requests
import asyncio
import aiohttp
import aiofiles
import json
async def aiodownload(cid, b_id, title):
data = {
"book_id": b_id,
"cid": f"{
b_id}|{
cid}",
"need_bookinfo": 1
}
data = json.dumps(data)
url = f"http://dushu.baidu.com/api/pc/getChapterContent?data={
data}"
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
dic = await resp.json()
async with aiofiles.open("西游记/" + title+".txt", mode="w", encoding="utf-8") as f:
await f.write(dic["data"]["novel"]["content"])
print(title, "success")
async def getCatalog(url):
resp = requests.get(url)
dic = resp.json()
tasks = []
for item in dic["data"]["novel"]["items"]: # item就是对应每个章节的名称和id
title = item["title"]
cid = item["cid"]
# 准备异步任务
tasks.append(asyncio.create_task(aiodownload(cid, b_id, title)))
await asyncio.wait(tasks)
print("All Done")
if __name__ == '__main__':
b_id = "4306063500"
url = 'http://dushu.baidu.com/api/pc/getCatalog?data={"book_id":"' + b_id + '"}'
asyncio.run(getCatalog(url))
4.9 爬取视频
4.9.1 综合训练-视频网站的工作原理
我们在编写网站的时候,对于视频文件会有一个视频标签,但是如果一个视频网站这样放视频那么每次播放的时候都相当于把视频完整下载,那这个会非常耗时。
那一般的视频网站是怎么做的?
用户上传 -> 转码(把视频做处理,2k,1080,标清) -> 切片处理(把单个文件进行拆分成多个文件,用户在拖动进度条的时候只需要加载对应文件)
既然要把视频切成非常多个小碎片,那就需要一个文件来记录:1.视频播放顺序,2.视频存放的路径。该文件一般为M3U文件,M3U文件中的内容经过utf-8的编码后,就是M3U8文件,今天我们看到的各大视频网站平台使用的几乎都是M3U8文件。
M3U8文件解读:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:13 每个视频功片最大时长
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METH0D=AES-128,URI="key.key" 切片文件的加密方式以及加密的密钥地址,如果有加密,需要先解密才能播放
#EXTINF:12.600000, 持续时间
cFN803436000.ts 这里面不带'#'开头的就是每个ts文作的地址
#EXTINF:10.000000,
cFN8o3436001.ts
#EXTINF:10.000000,
cFN8o3436002.ts
#EXTINF:10.000000,
cFN8o3436003.ts
#EXTINF:10.000000,
cFN8o3436004.ts
#EXTINF:10.000000,
cFN8o3436005.ts
#EXTINF:6.880000
cFN803436006.ts
那么想要抓取一个视频的流程:
- 找到M3U8(各种手段)
- 通过M3U8下载到ts文件
- 可以通过各种手段(不仅是编程手段)把ts文件合并为一个mp4文件
4.9.2 抓取云播TV-简单版
网站失效,使用云播tv
url:https://www.yunbtv.com/
import requests
import re
url = "https://video.buycar5.cn/20200813/uNqvsBhl/2000kb/hls/index.m3u8"
key_uri= "https://ts1.yuyuangewh.com:9999/20200813/uNqvsBhl/2000kb/hls/key.key"
# 1.首先打印出m3u8文件的内容 发现内容有加密
m3u8_text = requests.get(url, verify=False)
# 2.将m3u8文件下载并改名为index.m3u8
# with open("download_video/"+"index.m3u8", mode="wb") as f:
# f.write(m3u8_text.content)
# m3u8_text.close()
# print("m3u8 success")
# 3.下载key.key文件并改名为key.m3u8
# key_text = requests.get(key_uri)
# with open("download_video/"+"key.m3u8", mode="wb") as f:
# f.write(key_text.content)
# key_text.close()
# print("key success")
# 4.解析m3u8文件
n = 1
with open("download_video/index.m3u8", mode='r', encoding='utf-8') as f:
for line in f:
line = line.strip() # 先去掉空格,换行符
if line.startswith("#"): # 如果以#开头跳过该行
continue
# 下载视频片段
resp2 = requests.get(line, verify=False)
f = open("download_video/"+f"{
n}.ts", mode="wb")
f.write(resp2.content)
f.close()
resp2.close()
n += 1
print(f"第{
n-1}个完成")
print("All Done")
这是根据我在网上搜到的一些资料做的,与视频不同,并且还未优化
第五章 selenium
5.1 selenium引入概念
selenium是一个自动化测试工具,它可以打开浏览器,然后像人一样去操作浏览器,程序员可以从selenium中直接提取网页上的各种信息
环境搭建:
- pip install selenium
- 下载浏览器驱动http://npm.taobao.org/mirrors/chromedriver
- 下载对应浏览器版本的文件解压缩,把浏览器驱动chromedriver放在python解释器所在的文件夹
- 让selenium启动chrome
from selenium.webdriver import Chrome
# 1.创建浏览器对象
web = Chrome()
# 2.打开一个网址
web.get("http://www.baidu.com")
5.2 selenium各种操作-抓拉钩
本节中使用selenium来抓取抓钩招聘网的岗位信息
from selenium.webdriver import Chrome
from selenium.webdriver.common.keys import Keys
import time
web = Chrome()
web.get("http://lagou.com")
# 找到某个元素 点击
el = web.find_element_by_xpath('//*[@id="changeCityBox"]/p[1]/a')
el.click() # 点击事件
time.sleep(1) # 让浏览器缓一会
# 找到输入框 输入python => 输入回车/点击搜索按钮
web.find_element_by_xpath('//*[@id="search_input"]').send_keys("python", Keys.ENTER)
time.sleep(1)
# 查找存放数据的位置 进行数据提取
# 找到页面中存放数据的所有li
li_list = web.find_elements_by_xpath('//*[@id="s_position_list"]/ul/li')
for li in li_list:
job_name = li.find_element_by_tag_name("h3").text
job_price = li.find_element_by_xpath("./div/div/div[2]/div/span").text
job_company = li.find_element_by_xpath("./div/div[2]/div/a").text
print(job_name, job_company, job_price)
5.3 各种操作-窗口间的切换
from selenium.webdriver import Chrome
from selenium.webdriver.common.keys import Keys
import time
web = Chrome()
web.get("http://lagou.com")
el = web.find_element_by_xpath('//*[@id="changeCityBox"]/p[1]/a')
el.click()
time.sleep(1)
web.find_element_by_xpath('//*[@id="search_input"]').send_keys("python", Keys.ENTER)
time.sleep(1)
web.find_element_by_xpath('//*[@id="s_position_list"]/ul/li[1]/div[1]/div[1]/div[1]/a/h3').click()
# 在selenium眼中 新窗口是默认切换不过来的
web.switch_to.window(web.window_handles[-1])
# 在新窗口中提取内容
job_detail = web.find_element_by_xpath('//*[@id="job_detail"]/dd[2]/div').text
print(job_detail)
# 关掉子窗口
web.close()
# 变更selenium的窗口视角 回到原本的窗口
web.switch_to.window(web.window_handles[0])
print(web.find_element_by_xpath('//*[@id="s_position_list"]/ul/li[1]/div[1]/div[1]/div[1]/a/h3').text)
5.4 selenium操作-无头浏览器
爬取某个页面信息时希望浏览器在后台默默运行
from selenium.webdriver import Chrome
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.select import Select
import time
# 无头浏览器 准备好参数配置
opt = Options()
opt.add_argument("--headless")
opt.add_argument("--disable-gpu")
web = Chrome(options=opt) # 把参数配置设置到浏览器中
web.get("http://www.endata.com.cn/BoxOffice/BO/Year/index.html")
# 定位到下拉列表
sel_el = web.find_element_by_xpath('//*[@id="OptionDate"]')
# 对元素进行包装,包装成下拉菜单
sel = Select(sel_el)
# 让浏览器进行调整选项
for i in range(len(sel.options)): # i就是每一个下拉框选项的索引位置
sel.select_by_index(i) # 按照索引切换
time.sleep(2)
table = web.find_element_by_xpath('//*[@id="TableList"]/table')
print(table.text) # 打印所有文本信息
print("=============================================")
print("All Done")
web.close()
# 如何拿到页面代码Elements(经过数据加载以及js执行之后的结果的html内容)
# print(web.page_source)
5.5 selenium各种操作-超级鹰处理验证码
- 图像识别
- 选择互联网上成熟的验证码破解工具
超级鹰就是网上的一种识别验证码的工具,需要自行注册以及购买使用积分,在官网的开发文档中可以找到对应语言的文档,只需运行该文档就可以实现功能
5.6 selenium -超级鹰干超级鹰
这一节的内容就是使用超级鹰自动登录超级鹰网站,主要考验的就是对超级鹰方法的使用
from selenium.webdriver import Chrome
from chaojiying import Chaojiying_Client
import time
web = Chrome()
web.get("http://www.chaojiying.com/user/login/")
# 处理验证码
img = web.find_element_by_xpath("/html/body/div[3]/div/div[3]/div[1]/form/div/img").screenshot_as_png
chaojiying = Chaojiying_Client('超级鹰用户名', '超级鹰密码', 'ID')
verity_code = chaojiying.PostPic(img, 1902)['pic_str']
# 向页面中填入用户名,密码,验证码
web.find_element_by_xpath('/html/body/div[3]/div/div[3]/div[1]/form/p[1]/input').send_keys("超级鹰用户名")
web.find_element_by_xpath('/html/body/div[3]/div/div[3]/div[1]/form/p[2]/input').send_keys("超级鹰密码")
web.find_element_by_xpath('/html/body/div[3]/div/div[3]/div[1]/form/p[3]/input').send_keys(verity_code)
time.sleep(3)
# 点击登录
web.find_element_by_xpath('/html/body/div[3]/div/div[3]/div[1]/form/p[4]/input').click()
5.7 selenium-搞定12306的登陆问题
12306登陆页面已取消图片验证,因此与视频有所不同
12306可以检测你的浏览器是否是自动测试软件控制,因此如果没有特殊方法无法通过滑块验证,检测原理就是浏览器控制台中输入window.navigator.webdriver,可以发现我们测试中的Chrome浏览器返回的结果为True,而一般浏览器是False,所以12306就是根据这个返回的结果判断你是不是在自动测试。
不被检测方法:
-
Chrome版本号小于88:在你启动浏览器的时候(此时没有加载任何网页内容),向页面嵌入js代码,去掉webdriver,也就是在web.get()代码前嵌入
-
web.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": """
navigator.webdriver = undefined
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
}]
"""
})xxxxxxxxxx web.executeweb.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": """ navigator.webdriver = undefined Object.defineProperty(navigator, 'webdriver', { get: () => undefined }] """})
-
Chrome版本号大于88:需要导入一个包,增加options属性
-
from selenium.webdriver.chrome.options import Options
option = Options()
option.add_argument('--disable-blink-features=AutomationControlled')
web = Chrome(options=option)
以下是我的代码
from selenium.webdriver import Chrome
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.chrome.options import Options
import time
option = Options()
option.add_argument('--disable-blink-features=AutomationControlled')
web = Chrome(options=option)
web.get("https://kyfw.12306.cn/otn/resources/login.html")
time.sleep(2) # 等待响应
# 切换到账号登陆
web.find_element_by_xpath('//*[@id="toolbar_Div"]/div[2]/div[2]/ul/li[2]/a').click()
time.sleep(1)
# 填写账号密码
web.find_element_by_xpath('//*[@id="J-userName"]').send_keys("123456789")
web.find_element_by_xpath('//*[@id="J-password"]').send_keys("123456789")
# 点击登录
web.find_element_by_xpath('//*[@id="J-login"]').click()
time.sleep(2)
# 滑块拖拽验证 使用动作链
span_element = web.find_element_by_xpath('//*[@id="nc_1_n1z"]')
ActionChains(web).drag_and_drop_by_offset(span_element, 320, 0).perform()
你可能感兴趣的:(笔记,python,爬虫,python,爬虫)