起源

最近刚好入门了Python爬虫,在此之前系统完整的学了Python的基础入门知识,学习爬虫顺便也可以巩固之前学习的Python基础知识。

学习Python爬虫,先去看了B站的介绍视频,很短,只有4个小时,我也没有做笔记,就直接2个小时看完,学会了Python的request请求,网页的知识之前在HTML和CSS学习过了。所以感觉还是比较简单的,跟着视频做了爬取豆瓣TOP250电影名的小项目。其他的就一点都没学过。

接着我就遇到了一个问题,我想要批量下载B站UP主分享在蓝奏云上的文件,资源很丰富,文件资料很多,一个个下载就很慢很费劲,我就想着能不能用Python爬虫完成这个小项目
项目文件地址

过程

我先把需要的5个链接和对应的密码,复制保存到本地的文本里。如何通过Python进行整理和格式化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
MIUI解bl锁和刷机相关工具:  
https://wwvd.lanzoul.com/b030ok9yb
密码:9cj9
Root相关文件:
https://wwvd.lanzoul.com/b030q0gef
密码:hktq
面具模块(持续更新ing):
https://wwvd.lanzoul.com/b030q3egd
密码:hf8n
lsp模块(持续更新ing):
https://wwvd.lanzoul.com/b0312jcmf
密码:9d18
各种优化软件(工具软件):
https://wwr.lanzoui.com/b030oka5i
密码:fwjr

提取其中的标题作为文件夹名,链接和密码,分别存在两个列表中,索引一一对应,方便后续的操作。并把这一步操作分装成一个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 整理链接地址  
def get_url_data():
fp = open('蓝奏云文件链接地址.txt', 'r', encoding='utf-8')
lines = fp.readlines()
fp.close()
url = []
password = []
dir_name = []
for line in lines:
if line[0] == 'h':
url.append(line.strip()) # 去除结尾换行符
elif line.startswith('密码'):
password.append(line.strip()[3:])
else:
dir_name.append(line.strip()[:-1])
return url, password, dir_name

接着就是变量这两个列表,分别访问对应的分享文件链接,这里需要注意的是,这里的分享链接,里面还有很多的文件。

先用request请求链接,这里遇到了我第一个问题,就是我们应怎么把密码输入进去。查看源码,发现是一个输入框input接收密码,但是我是应该直接把密码写在这个文本框中还是直接get或者post请求。

源码看着看着,我找到了答案。这里网页用了ajax的post请求,把一堆参数(我也不知道是什么)里面包含pwd,也就是我们输入的密码,一起提交上去。所以我的方向就是,用request的post方法进行请求。准备data和headers的过程中,我发现这个data里面的其他参数也变动的,每次请求都不一样,那我该怎么写这个参数。于是我又去看JavaScript的代码,发现里面竟然有这些参数,这些参数全部都是通过js代码中的ajax请求上去,参数的值也都写在上面,那么我的思路就是先请求当前这个网页,从网页中拿到js中的参数。

但是在操作的过程中,我发现Python中的request根本请求不到js代码,只有js的链接地址,跟网页的元素查看器显示的不一样。上网搜索发现,request请求的是网页渲染之前的代码,当然不可能有js代码,这就是我遇到的第二个问题。

这个问题我想的有点久,最后我想到的是,可以不可以通过获取script标签上面的div标签,在通过div的下一个兄弟标签来获取script标签,因为div标签是在网页渲染前就已经存在了,而且这样的方法在JavaScript的代码中经常会用到。

于是我试着去操作一下,发现是真的可行的,==不过也不知道其中的原理是什么。== 拿到了script标签中的内容,接下来就是提取我们需要的信息,这里就是考察我对字符串的各种操作的熟悉程度,于是我的写法是这样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_key(url: str):  
response = requests.get(url=url, headers=headers)
soup = BeautifulSoup(response.text, 'html.parser')
div = soup.find('div', attrs={'class': 'd', 'id': 'info'}) # 获取到div标签
script = div.next_sibling.next_sibling
# 获取下一个兄弟节点 即script
# print(script)
# var ibf1fz = '1693922274';
# var _hddhs = 'c5010165681fd1c5fd8be8a4bcf20906'; # 获取ibf1fz和_hddhs
ibf1fz = script.string.split(';')[5].split('=')[1][2:-1]
hddhs = script.string.split(';')[6].split('=')[1][2:-1]
# fid: 4805955
# uid: 2427876 # 获取fid和uid
fid = script.string.split(',')[3].split(':')[1]
uid = script.string.split(',')[4].split(':')[1][1:-1]
return ibf1fz, hddhs, fid, uid

利用拿到的这些参数,我就可以用post请求了,这里请求的源地址是source_url = 'https://wwr.lanzoui.com/filemoreajax.php'
这样我就拿到了Ajax返回来的一整个json数据,我转成字典,发现这里面的数据全都是这个分享链接下的所有文件的信息,包括文件名、文件id(可以拼接成下载链接)等等信息。

于是我又利用字典的知识,把整个json数据中的文件名和文件id分别存在各自的列表中,同样分装成函数,并返回这两个列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def get_download_id(t: str, k: str, f: str, u: str, pwd: str):  
# 请求表单数据
data = {
'lx': '2',
'fid': f,
'uid': u,
'pg': '1',
'rep': '0',
't': t, # 当前时间戳
'k': k, # 感觉应该是32位md5加密后的密码 可以从网页源码中找到
'up': '1',
'ls': '1',
'pwd': pwd
}
response = requests.post(url=source_url, data=data, headers=headers)
# print(response.text)
# 返回数据为json格式,我们要提取里面id的值
# print(response.json())
download_id = []
file_name_all = []
download_id_lists = response.json()['text']
for data_dict in download_id_lists:
download_id.append(data_dict['id'])
file_name_all.append(data_dict['name_all'])
return download_id, file_name_all

接下来,就是根据拼接成的文件下载链接,进行下载。这里遇到了第三个问题。我们用浏览器访问下载链接,发现了这个页面又三个下载按钮,分别是联通下载、电信下载、普通下载,这三个标签套在同一个a标签中😄,然后这三个按钮又是属于另外一个HTML页面,通过链接显示在这个页面中,于是我直接请求那三个按钮的页面,进行操作。

有趣的是,这里的下载按钮同样是用Ajax请求的,我同样按照上面的方法,先找到div标签,再用下一个兄弟的下一个兄弟获取到了script中的内容,这个内容里面有我所需要的data参数的值,同样字符串操作获得了值,并封装成函数返回这些参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def get_ajaxdata(download_url: str):  
response = requests.get(url=download_url, headers=headers)
soup = BeautifulSoup(response.text, 'html.parser')
download_btn_url = lanzou_url + soup.find('iframe', attrs={'class': 'ifr2'})['src']
# print(download_btn_url)
# TODO 用request请求返回浏览器渲染之前的网页源码,没有包含文件下载地址
# TODO 用selenium模拟浏览器请求,但是速度太慢了,而且还要安装chromedriver.exe
# TODO 可以直接请求 ajaxm.php 文件,里面返回了文件下载地址
response = requests.get(url=download_btn_url, headers=headers, timeout=10000)
response.encoding = 'utf-8' # 防止中文乱码
soup = BeautifulSoup(response.text, 'html.parser')
# print(soup)
script = soup.find('div', attrs={'class': 'load'}).nextSibling.nextSibling.nextSibling.nextSibling
# print(script)
# wsk_sign = 'c20230818';
# aihidcms = '19Yt';
# iucccjdsd = '';
# ws_sign = 'c20230818';
# sasign = 'sA2UHOV5vUGFSW1FuBjZTb1Y9V2EHbQMzBDZUZgZrVGZVYQBxASgDagdgC2sBbFxpAWsHMlM6VmADNQE0Sv2_c';
# ajaxdata = '?ctdf'; # 获取各个元素的值
wsk_sign = script.string.split(';')[0].split('=')[1][2:-1]
aihidcms = script.string.split(';')[1].split('=')[1][2:-1]
iucccjdsd = script.string.split(';')[2].split('=')[1][2:-1]
ws_sign = script.string.split(';')[3].split('=')[1][2:-1]
sasign = script.string.split(';')[4].split('=')[1][2:-1]
ajaxdata = script.string.split(';')[5].split('=')[1][2:-1]
# print(wsk_sign, aihidcms, iucccjdsd, ws_sign, sasign, ajaxdata)
return aihidcms, iucccjdsd, sasign, ajaxdata

用上面得到的参数进行post请求,就可以得到Ajax返回的一个json数据,进行数据分析,这里我们就得到了文件下载的直链了,只要点击这个链接,浏览器就可以开始自动下载。

接下来的事情就简单了,只要get请求这些链接,把返回值写入文件中。这里我用标题创建了文件夹,把每个文件命名为之前返回的文件名,这样一切就完美了。但是在这里我遇到了最困难的问题,也是卡的时间最久的一次。

就是按照我的想法这样操作,根本行不通,下载下来的文件都是5kb,而且打不开,根本就不是我想要的文件。于是我开始分析,先把返回值打印出来,发现是一个html格式的文本,难怪不对。我当时就在想难道我这个链接不是文件的直链?但是浏览器一打开就可以下载,也不对啊。于是我认为是大文件没有分批下载,于是我把写入文件代码改成

1
2
3
4
with open(dir_name + '/' + file_name_all[0], 'wb') as fp:  
for chunk in response.iter_content(chunk_size=102400):
if chunk:
fp.write(chunk)

但是还是不行,之后我又尝试了很多方法,都不行,上网搜索了很多Python请求文件的方法,也都不行。最后我几乎都认定我得到的这个链接不是文件的直链,我用Firefox浏览器打开,跳出来一个页面,有一个下载按钮,于是我又开始像前面的操作一样分析代码,但是我又被卡住了,明明用正确的data却请求不到数据。

最后我上网搜索到了别人写的蓝奏云下载的爬虫,我发现链接确实是直链,因为别人也是用这个链接下载的,唯一的不同就是,下载时的请求不同,还有写了一堆请求头。问题就这样解决了
请求头

最后根据网上的方法,我也成功把文件下载下来。

反思

我这个程序,写的看起来就像屎山,以后再回来说不定就读不懂了。所以养成良好的编码习惯还是很有必要的。可以多参考别人写的好的代码格式,也可以看Python的编码规范。

这里面的代码,还有很多的原理问题需要解决和思考

  1. 最后问题的解决,原理到底是什么?
  2. Python请求链接下载文件的方法有多少种?
  3. 获取script标签的操作肯定不是标准的写法,但是这样能成功的原理是什么?Python爬虫遇到这种情况,规范正确的解决方法是什么?
  4. ……

这样问题应该可以在我系统学习了Python爬虫之后得到解决。