一、Urllib
1.爬虫概述
爬虫:网络爬虫,是一种按照一定规则,自动抓取互联网信息的程序或者脚本,其本质是模拟浏览器打开网页,获取网页中我们想要的数据。常用的百度、谷歌的搜索引擎也是一个爬虫,把互联网中的数据搜集组合起来便于用户检索。
注:爬虫并不是Python独有的,可以做爬虫的语言有很多例如:PHP, JAVA, C#, C++, Python,选择Python做爬虫是因为Python相对来说比较简单,而且功能比较齐全。
2.Urllib简介
Urllib 库,它是 Python 内置的 HTTP 请求库,也就是说我们不需要额外安装即可使用,它包含四个模块:
1.第一个模块 request,它是最基本的 HTTP 请求模块,我们可以用它来模拟发送请求,就像在浏览器里输入网址然后敲击回车一样,只需要给库方法传入 URL 还有额外的参数,就可以模拟实现这个过程了。
2.第二个 error 模块,即异常处理模块,如果出现请求错误,我们可以捕获这些异常,然后进行重试或其他操作,保证程序不会意外终止。
3.第三个 parse 模块是一个工具模块,提供了许多 URL 处理方法,比如拆分、解析、合并等的方法。
4.第四个模块是 robotparser,主要是用来识别网站的 robots.txt 文件,然后判断哪些网站可以爬,哪些网站不可以爬的,其实用的比较少。
简单来说:
1.urllib.request 负责请求;
2.urllib.error 异常处理模块;
3.urllib.parse url 负责解析;
4.urllib.robotparser 负责robots.txt文件的解析;
3.Urllib.request的基本使用
import Urllib.request
url = 'http://www.baidu.com' # 定义url
response = urllib.request.urlopen(url) # 模拟浏览器向服务器发送请求
# print(type(response)) # 打印response的类型
# content = response.read() # 返回的是二进制字节码码
# 利用decode('编码格式')将二进制字节码转换为字符串
content = response.read().decode('utf-8') # 接收内容
print(content)
注:URL是对互联网上得到的资源的位置和访问方法的一种简洁表示,是互联网上标准资源的地址。 简单地说URL就是web地址,俗称“网址”。
打印response 的类型为 <class ‘http.client.HTTPResponse’>
1.http.client:HTTP 协议客户端;
2.HTTPResponse :
HTTPResponse实例表示客户端发出请求之后,服务端的 HTTP 响应,包含 http code、响应头、响应内容的。HTTPResponse继承自io.BufferedIOBase,拥有 IO 字节流的相关操作方法。HTTPResponse对象支持with上下文管理器,在with语句退出时会自动调用其close()方法。调用HTTPConnection.close()方法关闭套接字连接时也会自动调用HTTPResponse.close()。
4.response的类型和方法
这里的response是指 urllib.request.urlopen(url) 的返回值,实际上,reponse这个词可以换成任何一个,不过对于爬虫来说,一般用这个词代表request的返回值。
import urllib.request
url = 'http://www.baidu.com'
# 模拟浏览器向服务器发送请求
response = urllib.request.urlopen(url)
# response的类型
# 运行下个语句可知,response的类型是HTTPResponse
# print(type(response))
# 按照一个字节一个字节的方式去读
# content = response.read()
# print(content)
# 返回多少个字节
# content = response.read(5) 读取5个字节
# print(content)
# 读取一行
# content = response.readline()
# print(content)
# 按行读取所有内容
# content = response.readlines()
# print(content)
# 返回状态码,如果是200,那么就证明代码逻辑没有问题
# print(response.getcode())
# 返回url地址
# print(response.geturl())
#获取状态信息
print(response.getheaders())
5.urlretrieve()下载
urllib.request.urlretrieve(url, filename);
url代表的是下载的路径, filename代表文件的名字。
参数可以是赋值的形式,也可以直接写值。
eg:赋值的形式:
urllib.request.urlretrieve(url=url, filename=filename)
下面给出具体的代码示例,以下载网页源码为例:
import urllib.request
url_page = 'http://www.baidu.com'
urllib.request.urlretrieve(url_page, 'baidu.html')
注:根据下载内容的不同,文件名称的后缀也要做出相应的变化。
eg:网页源码-> .html
图片 -> .jpg
视频 -> .mp4
关于参数赋值和直接写值情况的选择:
若函数参数的顺序不乱,且两参数中间无其它参数,则可采用直接写值的写法。
若参数顺序打乱或顺序不乱两参数中间预留有其它参数(但没有用到)的情况需要赋值。
6.请求对象的定制
url的组成:
eg: https://cn.bing.com/search?q=周杰伦
协议: http/https(更加安全)
主机(指域名): www.baidu.com
端口号: http: 80 https: 443 mysql:3306 oracle:1521 redis:6379 mongodb:27017
路径:search
参数:q=周杰伦
锚点: 锚记/锚点是网页制作中超级链接的一种,又叫命名锚记。它就像定位器一样是一种页面内的超级链接,可以迅速跳到某个节点。
在爬取某些网页时,很有可能会遇到反扒,所以我们需要伪装自己,尽量将自己伪装成正常的浏览器访问服务器。这时候就需要用到UA:User Agent中文名为用户代理,简称UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU类型、浏览器及版本、浏览器内核、浏览器渲染引擎、浏览器语言、浏览器插件等。
而UA是需要放到请求头中的
eg:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}
headers是字典类型的,但urllib.request.urlopen()不能接受字典类型的参数,所以需要用到请求对象的定制即:urllib.request.Request(url=url, headers=headers)
返回的对象一般命名为request,完整为: request = urllib.request.Request(url=url, headers=headers)
request 的类型为 <class ‘urllib.request.Request’>
爬取百度的完整代码示例:
import urllib.request
#定义url
url = 'https://www.baidu.com'
#请求头
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}
#请求对象的定制,因为默认参数url和headers中还有data,所以采用赋值的方式
request = urllib.request.Request(url=url, headers=headers)
#模拟浏览器发送请求
response = urllib.request.urlopen(request)
#解析内容
content = response.read().decode('utf-8')
#输出获取的内容
print(content)
7.urllib_get请求
GET请求一般用于我们向服务器获取数据 。
7.1quote()方法
当我们想要爬取搜索结果页面时:
我们通过搜索结果的页面复制的url是 https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=%E5%91%A8%E6%9D%B0%E4%BC%A6
可以看到 wd=周杰伦 已经被自动转换为Unicode码的形式,可以通过更换wd后的值获取任意搜索结果的页面,例如:wd=毛不易 就可以得到毛不易搜索页面的结果,不过这就碰到了一个问题,那就是网页不会识别中文,所以需要将中文毛不易转换为Unicode码值,这时就需要用到quote()方法了。
以获取毛不易搜索结果页面源码为例:
import urllib.request
# 导入此包以使用中文转换为Unicode码的函数
import urllib.parse
#原始url
base_url = 'https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd='
#中文转化为Unicode
name = urllib.parse.quote('毛不易')
url = base_url + name
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}
request = urllib.request.Request(url=url, headers=headers)
response = urllib.request.urlopen(request)
content = response.read().decode('utf-8')
print(content)
7.2urlencode()方法
与quote()方法类似,不同之处是urlencode()可以将多个中文转化为Unicode
以周杰伦搜索页面为例
import urllib.request
import urllib.parse
base_url = 'https://www.baidu.com/s?'
data = {
'wd': '周杰伦',
'sex': '男',
'location': '中国台湾省'
}
#多个中文转Unicode
data = urllib.parse.urlencode(data)
url = base_url + data
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}
#请求对象的定制
request = urllib.request.Request(url=url, headers=headers)
#模拟浏览器向服务器发送请求
response = urllib.request.urlopen(request)
content = response.read().decodea('utf-8')
print(content)
8.urllib_post请求
之前说过Request请求对象的里有data参数,它就是用在POST里的,我们要传送的数据就是这个参数data,data是一个字典,里面要匹配键值对。
以获取百度翻译为例
import urllib.request
import urllib.parse
# 这个网址是在sug headers中的Request URL(内容是单词的翻译)
# 如果复制翻译网址栏的url那么爬取的就不是单词翻译,具体表现获取的数据是否为json类型
url = 'https://fanyi.baidu.com/sug'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}
data = {
'kw': 'spider' # 爬取百度翻译中spider的单词解释
}
# post的请求的参数 必须要进行编码
data = urllib.parse.urlencode(data).encode('utf-8')
# post的请求的参数是不会拼接在url后面的,而是需要放在请求定制的参数中
request = urllib.request.Request(url=url, data=data, headers=headers)
resposne = urllib.request.urlopen(request)
# 返回的json数据,需要解析
content = response.read().decode('utf-8')
# print(type(content)) <class 'str'> json字符串
import json
obj = json.loads(content) #json字符串变成python字典
# print(type(obj)) <class 'dict'> 字典类型
print(obj)
在使用百度翻译时若想要翻译英文,就需要切换到英文输入法,否则在network中找不到name为 sug 的选项。
为什么进行encode(utf-8)的编码?
因为请求对象定制中,urllib.request.Request(url=url, data=data, headers=headers)中参数data的数据类型要求是bytes类型,所以需要惊醒encode()编码(对应的解码时 decode())。
如何看出是json字符串?
因为返回的数据格式是{key:value, key:[{…}]},即json数据的格式。
json字符串与json对象
(1)JSON字符串
JSON字符串与普通的字符串没有任何特殊的地方,但是之所以称为JSON字符串是因为,这个字符串符合json数据的格式
(2)JSON对象
JSON对象主要是在JavaScript的说法。在面向对象编程中,类的实例化叫做对象,对象拥有不同的属性,键值对就是对象的属性和值。
** python json模块的四种方法**
loads()
:将json数据转化成dict数据dumps()
:将dict数据转化成json数据load()
:读取json文件数据,转成dict数据dump()
:将dict数据转化成json数据后写入json文件
9.get请求和post请求的区别
9.1两种请求的简单对比
在客户机和服务器之间进行请求-响应时,两种最常被用到的方法是:GET 和 POST。
GET - 从指定的资源请求数据;
POST - 向指定的资源提交要被处理的数据;
选项 | GET | POST |
---|---|---|
后退按钮/刷新 | 无害 | 数据会被重新提交(浏览器应该告知用户数据会被重新提交)。 |
书签 | 可以被收藏为书签 | 不可以被收藏为书签 |
缓存 | 能被缓存 | 不能被缓存 |
编码类型 | application/x-www-form-urlencoded | application/x-www-form-urlencoded或multipart/form-data。为二进制数据使用多重编码。 |
历史 | 参数保留在浏览器历史中 | 参数不会保留在浏览器历史中 |
对数据长度的限制 | 只允许ASCII字符 | 没有限制,也允许二进制数据 |
安全性 | 与POST相比,GET的安全性较差,因为发送的数据是URL的一部分 | POST比GET更安全,因为参数不会被保存在浏览器历史或web服务器日志中。 |
可见性 | 数据在URL中对所有人都是可见的 | 数据不会显示在URL中 |
9.2抓取方式的异同
(1)Get方式
GET方法是最常见也是最简单的,HTTP默认的请求方法就是GET。
一般用于我们向服务器获取数据,可以直接将URL输入,不需要其它的转换,即所有需要请求的信息都包含在URL中。
* 没有请求体
* 数据必须在1K之内!
* GET请求数据会暴露在浏览器的地址栏中
常用的操作:
① 在浏览器的地址栏中直接给出URL,那么就一定是GET请求;
② 点击页面上的超链接也一定是GET请求;
③ 提交表单时,表单默认使用GET请求,但可以设置为POST;
get请求就是在url后面以拼接方式传参,但是如果参数是中文时需要转码处理,否则会报错。
(2)Post方式
post用于将数据发送到服务器来创建/更新资源。
通过post发送到服务器的数据存储在 HTTP 请求的请求主体中:
POST /test/demo_form.php HTTP/1.1
Host: w3school.com.cn
name1=value1&name2=value2
post要获取的内容只靠网址是不能获取到的,需要提交一些额外的信息。
这种信息在不同的网页中发挥不同功能,例如在查询天气的网页,可能就是要输入城市信息;在登录某些网页时,又是账号和密码的载体。
post请求:
① 数据不会出现在地址栏中
② 数据的大小没有上限
③ 有请求体
④ 请求体中如果存在中文,会使用URL编码!
一般HTTP请求提交数据,需要编码成URL编码格式,然后做为URL的一部分,或者作为参数传到Request对象中。
特殊点:
Request请求对象里有data参数,而post请求通过Request对象中的data属性来传参,用来存放请求体数据。
data是一个字典,里面要有匹配键值对。
10.urllib_Ajax
AJAX 是 Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)的缩写 。
AJAX 是与服务器交换数据并更新部分网页的艺术,在不重新加载整个页面的情况下,对网页的某部分进行更新。 传统的网页(不使用AJAX)如果需要更新内容,必需重载整个网页。
有些网页内容使用AJAX加载,只要记得,AJAX一般返回的是JSON,直接对AJAX地址进行post或get,就返回JSON数据了。
简而言之,AJAX和平时爬取的方式没有太大的不同,只是下载多页数据时,url可能只变化了一部分,只要改变这一部分就能达到更换另一个网页的作用。
eg:以豆瓣电影排行榜的前三页url为例
https://movie.douban.com/j/chart/top_list?type=5&#interval_id=100%3A90&action=&start=0&limit=20
https://movie.douban.com/j/chart/top_list?type=5&interval_id=100%3A90&action=&start=20&limit=20
https://movie.douban.com/j/chart/top_list?type=5&#interval_id=100%3A90&action=&start=40&limit=20
三个网页的url的格式相同,唯一的不同之处就是start=’ '这个属性,所以可以利用这个特性进行多个网页的下载。
10.1Ajax的get请求
和之前的爬虫下载数据相似,不同之处是这里是实现多页爬取。
以下载豆瓣排行榜的前十页数据为例;
import urllib.request
import urllib.parse
# 将各部分封装成方法
# 请求对象的定制
def create_request(page):
base_url = 'https://movie.douban.com/j/chart/top_list? type=5&interval_id=100%3A90&action=&'
# 接在base_url后的属性
data = {
start = (page-1) * 20, # start和页数的关系
limit = 20 # 一页有二十条数据
}
# 转换为Unicode编码
data = urllib.parse.urlencode(data) # get请求不需要encode('utf-8')编码
url = base_url + data
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}
# 请求对象的定制
request = urllib.request.Reqeust(url=url, headers=headers)
return request
# 获取返回内容
def get_content(request):
response = urllib.request.urlopen(reqeust)
content = response.read().decode('utf-8')
return content
# 将数据保存到文件中
def down_load(page, content):
with open('douban' + str(page) + '.json', 'w', encoding='utf-8') as fp:
fp.write(content)
# 调用函数,下载数据
# 程序的入口
if __name__ == 'main':
start_page = int(input('请输入起始页码:'))
end_page = int(input('请输入结束页码:'))
# range(a, b) 左闭右开
for page in range(start_page, end_page+1):
# 通过for循环对每一页进行请求对象的定制
request = create_request(page)
# 获取响应的数据
content = get_content(request)
# 下载数据到文件中
down_load(page, content)
if name == ‘main’:的意义:
一个python文件通常有两种使用方法,第一是作为脚本直接执行,第二是 import 到其他的 python 脚本中被调用(模块重用)执行。因此 if name == ‘main’: 的作用就是控制这两种情况执行代码的过程,在 if name == ‘main’: 下的代码只有在第一种情况下(即文件作为脚本直接执行)才会被执行,而 import 到其他脚本中是不会被执行的。
10.2Ajax的post请求
以肯德基官网为例
import urllib.request
import urllib.parse
def create_reqeust(page):
base_url = 'http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=cname'
data = {
'cname': '北京',
'pid': '',
'pageIndex': page,
'pageSize': '10',
}
data = urllib.parse.urlencode(data).encode('utf-8')
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}
request = urllib.request.Request(url=base_url, data=data, headers=headers)
return request
def get_content(reqeust):
response = urllib.request.urlopen(reqeust)
content = response.read().decode('utf-8')
return content
def down_load(page, content):
with open('kfc_' + str(page) + '.json', 'w', encoding='utf-8') as fp:
fp.write(content)
if __name__ == 'main':
start_page = int(input("请输入起始页码:"))
end_page = int(input("请输入结束页码:"))
for page in range(start_page, end_page + 1):
# 请求对象定制
request = create_request(page)
# 获取网页源码
content = get_content(request)
# 下载
down_load(page, content)
Ajax get请求和post请求的不同之处也就是urllib_get请求和urllib_post请求的不同之处。
11.异常
当代码出现异常时,捕捉这个异常,避免程序遇到异常而崩溃。
利用错误的url进行爬虫访问,从而捕捉异常
import urllib.request
import urllib.error
url = 'https://www.nibuhao.com'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}
try:
request = urllib.request.Request(url=url, headers=headers)
response = urllib.request.urlopen(request)
content = response.read().decode('utf-8')
print(content)
except urllib.error.HTTPError:
print("系统正在升级......")
except urllib.error.URLError:
print("系统正在升级......")
当 try:代码块出现错误时,将会捕捉异常,然后输出“系统正在升级”。
12.Cookie登录
12.1关于Cookie
(1)Cookie是什么?
- Cookie 是浏览器访问服务器后,服务器传给浏览器的一段数据。
- 浏览器需要保存这段数据,不得轻易删除。
- 此后每次浏览器访问该服务器,都必须带上这段数据。
(2)Cookie的两个作用
- 识别用户登录
- 记录用户操作历史
12.2Cookie登录实例
下面以微博的Cookie登录为例:
import urllib.request
url = 'https://weibo.com/set/index'
# cookie 中携带着你的登录信息,如果有登录之后的cookie,那么我们就可以携带着cookie进入到任何页面
# referer 判断当前路径是不是由上一个路径进来的 一般情况下是做图片的防盗链
headers = {
'cookie': 'PC_TOKEN=b8f7fc90b0; XSRF-TOKEN=b3l2PGnCDcoXHxTywucpnojA; SUB=_2A25JFErhDeThGeFN4lQX9CfNyTqIHXVqYDsprDV8PUNbmtAGLXndkW9NQ6TeAaBTHJqirM75ZCUBwjMaiU_P_qC2; SUBP=0033WrSXqPxfM725Ws9jqgMF55529P9D9Wh-Eo.u-BUA.hSDZlWi_0fj5JpX5KzhUgL.FoM01KqcSh.peoq2dJLoIp7LxKML1KBLBKnLxKqL1hnLBoMNe0.cSoB4eKzc; ALF=1710321201; SSOLoginState=1678785201; WBPSESS=d4xMu9nMFhY85YGY8BTO1g9EOrtW0yuonZm_34TC4IY9qWQIcXbFzn9FvhQhhMUxJQIAQDk677YrrNdq3i9wvhg_h3h0ZFxWvy75Ve_2HkGsyQ1IgoePm_vMWW-DUrIUY1LzJ55up6dRcEzYs9rPDQ==',
'referer': 'https: //weibo.com/newlogin?tabtype=weibo&gid=102803&openLoginLayer=0&url=https%3A%2F%2Fweibo.com%2F',
'user-agent': ' Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36',
}
# 请求对象的定制
request = urllib.request.Request(url=url, headers=headers)
# 模拟浏览器向服务器发送数据
response = urllib.request.urlopen(request)
# 获取响应的数据
content = response.read().decode('utf-8')
with open('weibo.html', 'w', encoding='utf-8') as fp:
fp.write()
13.handler处理器
handler处理器的作用:
首先简单介绍一下handler处理器:handler处理器是继urlopen()方法之后又一种模拟浏览器向服务器发起请求的方法或技术。
它的意义在于使用handler处理器,能够携带代理ip,这为对抗反爬机制提供了一种策略(很多的网站会封掉短时间多次访问的ip地址)。
13.1 handler处理器的基本使用
例:使用handler处理器获取百度源码
import urllib.request
url = 'https://www.baidu.com'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}
request = urllib.request.Request(url=url, headers=headers)
response = urllib.request.urlopen(request)
content = response.read().decode('utf-8')
with open('baidu01.html', 'w', encoding='utf-8') as fp:
fp.write(content)
13.2代理
(1)为什么要使用代理ip
随着信息的越来越庞大,获取数据的途径也不断增多,各个渠道都不会让使用者轻易的采集到本网的信息,而通过ip访问的频率可以对该ip进行判断,是否属于脚本机器在用,从而进行拦截,导致使用者不能采集该网站的信息。
(2)代理ip的来源
目前免费的ip来源有很多,比如: 站大爷 快代理 芝麻代理 等这些每日提供一些免费的ip网站,也可以进行注册长期使用。 另外,我们也可以手动去采集一些ip,通过脚本自动判断该ip是否可用,加入到ip池中。
这里以使用代理爬取ip查询网为例:
import urllib.request
url = 'http://www.ip138.com/'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}
request = urllib.request.Request(url=url, headers=headers)
# 模拟浏览器访问服务器
# response = urllib.request.urlopen(request)
# 需要代理的ip
proxies = {
'http': '182.139.110.52:9000'
}
# handler build_opener open
# handler 的参数是需要代理的ip(以字典的形式存在)
handler = urllib.request.ProxyHandler(proxies=proxies)
opener = urllib.request.build_opener(handler)
response = opener.open(request)
# 获取响应信息
content = response.read().decode('utf-8')
# 保存
with open('dali.html', 'w', encoding='utf-8') as fp:
fp.write(content)
注:代理ip很有可能会失效,所以测试运行可能会不正确
13.3代理池
创建一个含有多个代理ip的字典列表,利用随机函数获取其中任意一个代理ip以达到换区不同ip爬取数据的目的
如:
import request
import random
# 代理池
proxies_pool = [
{'http': '118.24.219.151:134222'},
{'http': '118.24.219.151:134333'},
]
# 获取其中一个代理
proxies = random.choice(proxies_pool)
# 需要代理的ip
proxies = {
'http': '182.139.110.52:9000'
}
# handler build_opener open
# handler 的参数是需要代理的ip(以字典的形式存在)
handler = urllib.request.ProxyHandler(proxies=proxies)
opener = urllib.request.build_opener(handler)
response = opener.open(request)
# 获取响应信息
content = response.read().decode('utf-8')
# 保存
with open('dali.html', 'w', encoding='utf-8') as fp:
fp.write(content)
注:代理ip很有可能会失效,所以测试运行可能会不正确