最近,腾讯视频上映了一部名为《传闻中的陈芊芊》的网剧,又甜又虐又搞笑,就连我这个不怎么看剧的人,看了几分钟左右的剪辑就突然开始了我的追剧之路。剧情大概是男主…
呀!跑题了!差点就又开始了…/偷笑/偷笑
想着最近也没有什么事做,所以就写一个实例吧,用来爬取这部电视剧,顺便还可以看看剧,嘻嘻嘻…
私信小编01即可获取大量Python学习资料
事先说明: 我是以会员的身份看到了最新的一集,并非在此传播盗版视频,只是为了分享我的思路及方法。 所以我将所有的链接域名都进行了修改。
直达模块:
- 导入:分析及代码实现:源码及运行过程示例:源码:运行过程示例:
导入:
咳咳!言归正传,当我们在写爬虫实例时,就肯定会对每个网页的链接进行分析,然后才能模拟请求。就 目前而言 ,根据我的 不完全统计 显示。有以下三种情况:
在浏览器地址栏 显示 域名加搜索的关键字(和其他参数):
https://www.*****.tv/tv/wNiNmMnRjZ.html?q=传闻中的陈芊芊在浏览器地址栏 显示 域名和加密后的搜索关键字(和其他参数):
https://www.*****.tv/tv/wNiNmMnRjZ.html?q=%E4%BC%A0%E9%97%BB%E4%B8%AD%E7%9A%84%E9%99%88%E8%8A%8A%E8%8A%8A在浏览器地址栏 显示 的是经过网站处理过的链接。
其实这种处理并不是特别麻烦,是要你知道是怎么处理的(嘿嘿嘿,好像说了句废话),这第三种正是我今天要说的内容之一,且听我慢慢道来。
(其实这第一种和第二种也可以说成为是一种情况,因为 URL加密 是浏览器自动加密的。在模拟请求时不需要做任何处理。)
分析及代码实现:
为了以后下载想看的电影或者电视剧,所以我直接从搜索影片入手:当我以 传闻中 为关键词搜索时:情况如下:
浏览器地址栏中的地址一看就知道,这并不仅仅是 URL加密 。可是这又是怎么处理的呢?一开始我也是摸不着头脑,看了源码中的几个 JS 脚本,一直都没有找到关键信息。后来在我无意中发现:这里的地址和 加密后的URL 有关。如下:(忽略后缀)
伪原链接: https://www.****.tv/tv/wNiNmMnRjZ.html?q=传闻中
URL加密: https://www.****.tv/tv/wNiNmMnRjZ.html?q=%E4%BC%A0%E9%97%BB%E4%B8%AD
此处链接: https://www.****.tv/tv/wNiNmMnRjZ.html?q=_E4_BC_A0_E9_97_BB_E4_B8_AD
你看出来了吗?没错,困扰我这么几个小时的疑惑总算是解决了。
此处的链接是将加密后的URL链接中的 “ % ” 更换为 “ _ ” ,最后再加上 “ .html ” 的后缀。
解决了这个问题,我们就来开始写请求:先是没头没脑的导库,接着开始拼接成正确的网址:
from urllib.parse import quote
def user_ui():
print('***********************************************************')
keyword = '传闻中' # input('请输入搜索的视频关键字:')
url = 'https://www.****.tv/s/go.php?q=' + quote(keyword, 'utf-8') # 进行url加密
URL = (url.split('go.php?q=')[0] + url.split('go.php?q=')[1]).replace('%', '_') + '.html'
# 根据网页情况,更改加密后的url
print(URL)
user_ui
'''
输出:
https://www.****.tv/s/_E4_BC_A0_E9_97_BB_E4_B8_AD.html
'''
注解: 导入from urllib.parse import quote 库,以便于我们 手动加密 原URL。
由于整个运行过程中需要多次发送请求,所以,为减少代码量,直接写一个请求函数:
import requests
referer = 'https://www.****.tv'
header = {
'referer': referer,
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36 Edg/83.0.478.37'
}
proxies = ['HTTP://60.13.42.120:9999', 'HTTP://163.204.244.207:9999', 'HTTP://113.121.39.121:9999',
'HTTP://125.117.134.99:9000', 'HTTP://123.169.114.81:9999', 'HTTP://58.253.159.230:9999',
'HTTP://182.46.99.247:9999', 'HTTP://171.15.48.235:9999', 'HTTP://120.83.105.226:9999',
'HTTP://113.124.95.210:9999', 'HTTP://171.13.103.241:9999', 'HTTP://123.163.96.42:9999',
'HTTP://27.42.168.46:49345', 'HTTP://122.234.27.22:9000', 'HTTP://125.108.73.47:9000',
'HTTP://125.108.97.209:9000', 'HTTP://113.124.85.24:9999', 'HTTP://27.220.51.228:9000',
'HTTP://113.124.84.205:9999', 'HTTP://110.243.31.147:9999']
def requests_url(url): # 请求网址
try:
proxy = {'http': random.choice(proxies)}
print(proxy)
r = requests.get(url, proxies=proxy, headers=header)
r.raise_for_status()
r.encoding = 'UTF-8'
return r
except:
print("*********请求失败!***********")
经过以上处理后,我们已经获得了搜索的正确链接和请求函数。接下来我们开始根据源码来提取出我们所需要的各种视频信息,保存在 videos_info 中:
开始提取数据:
html = requests_url(URL).text # 请求网页
# 搜索时间以及数量
search_time = parsel.Selector(html).xpath('//div[@class="breadcrumbs"]/text()').extract()[0]
# 视频信息标签
videos_info = parsel.Selector(html).xpath('//dd').extract()
print(search_time)
print(videos_info)
本次以 传闻中 作为关键词搜索时只搜索到了 1 条记录。但是如果以后搜索的记录很多的话,就需要翻页了,所以后期我在这里做了一点小改进:
html = requests_url(URL).text
# 搜索时间以及数量
search_time = parsel.Selector(html).xpath('//div[@class="breadcrumbs"]/text()').extract()[0]
# 视频信息标签
videos_info = parsel.Selector(html).xpath('//dd').extract()
try: # 若搜索结果小于10项,则无页码信息
print(search_time, end='')
# 页码
page_num = parsel.Selector(html).xpath('//div[@class="pages"]/a/text()').extract()[-2]
print(',共{}页。每页10项。'.format(page_num))
except:
print(end='
')
注解: 为什么要用 try…except 呢?
那是因为当搜索的结果每一页显示 10 条记录。而当结果小于 10 条时,源码中并没有出现显示页码的标签。
现在,我们已经获取了搜索页面的所有视频的信息标签(此时只有一条),接下来就开始提取关键信息:
- 由 <em>…</em> 标签包裹起来的文字在网页中显示为红色字体,即我们搜索的关键字。但是我们并不关心谁是关键字,所以用 replace 去掉此标签。 之后用 BeautifulSoup库 进行解析提取,并将提取的视频名与拼接后的链接保存到 video 中。
注解: videos_info 中储存的是搜索的所有记录,而 video_info 中是某一条记录。
def split_info(videos_info):
num = 1
for video_info in videos_info:
# 去掉关键字高亮标签
info = str(video_info).replace('<em>', '').replace('</em>', '') # 去掉源码中红色字体的标注
soup = BeautifulSoup(info, 'html.parser')
info = soup.find_all('p')
# 将通过关键字搜索到的视频名与对应链接保存下来
# 对应链接为拼接后的完整链接,如:https://www.*****.tv/tv/wNiNmMnRjZ.html
video[info[0].strong.a.string + info[0].span.string] = 'https://www.pianku.tv' + info[0].strong.a.attrs['href']
name2_info = info[1].string # 又名
area_info = info[2].string # 地区与类型
actor_info = info[3].string # 演员
introduction = info[4].string # 简介
print('***********************************************************')
print('{:0>2d}: '.format(num), end='')
num += 1
print(' ' + info[0].strong.a.string + info[0].span.string) # 名称
print(' ' + name2_info)
print(' ' + area_info)
print(' ' + actor_info)
print(' ' + introduction)
运行结果:
接下来: 要处理的是:根据前面的序号来选择下载的视频:
怎么样根据序号来选择呢?
- 我们之前已经将视频的名字与拼接后的链接保存到了 video字典 中。那么我们就可以通过视频的名字来访问该视频的链接。
- 那么问题又来了,怎样通过序号来确定视频的名字呢?这首先啊,必须先通过 .keys() 来取出所有的名字,接着强转为 list 类型。这样的话不就可以通过下标来访问了咩。
choice = input('
请输入序号选择:')
NAME = list(video.keys())[int(choice) - 1] # 通过下标确定进一步搜索的类容
URL = video[NAME]
通过 input 输入的数据为字符串类型,需强转为 int 类型。
当我们选择好要下载视频时,就可以通过名字来获取视频的主页链接,之后就可以请求具体的视频链接了:
如下图:我们通过控制台的预览可以发现,请求当前的网址获得的并不是我们所需要的!
- 但是啊,我们发现,按照原链接爬取的网页源码之中,没有任何与播放集数有关的数据,难道是 动态加载?
既然当前网页中有资源下载与在线播放的链接,根据 可见既可爬的原理,我们得出:一定有来处! 只不过还需我们去寻找。
终于,在所有的请求里面,我们很容易的就找到了数据来源:
于是我们就拿到了下载的请求链接:
https://www.++++.tv/ajax/downurl/wNiNmMnRjZ_tv/
为了更方便的来理解并构造新的请求链接:我去查看了一下其他类型的链接,又发现了以下的几种情况:
剧集:
https://www.++++.tv/tv/wNiNmMnRjZ.html (原链接)
https://www.++++.tv/ajax/downurl/wNiNmMnRjZtv/ (播放链接)
电影:
https://www.++++.tv/mv/wNjNWYyATY.html (原链接)
https://www.++++.tv/ajax/downurl/wNjNWYyATYmv/ (播放链接)
动漫:
https://www.++++.tv/ac/wNiFjMnNmM.html (原链接)
https://www.++++.tv/ajax/downurl/wNiFjMnNmM_ac/ (播放链接)
def get_video(url):
# 正确的链接拼接
URL = 'https://www.****.tv/ajax/downurl/' + url.split('/')[-1].split('.')[0] + '_'+url.split('/')[3]+'/' # 根据重定向拼接真实的链接
提示: 下载链接最后的 “ / ” ,是必须要加上的,没有的话,依然会加载不出来。
乍一看这个链接,链接中包含 ajax 的字样,我立马就想到了 json 数据的处理。嘿嘿!机智的我,???
可理想是美好的,现实总是不要太残酷!
还没有一分钟我就把处理代码写好了,却一直在报错。(又是写bug的一天呢!)
结果白白浪费了那几分钟,又回到刚刚分析的地方,查看了网页的 响应 部分,结果:
这那里是什么 json 数据哦,明明还是一段不完整的 html 代码。
我写了个小例子来格式化这段代码:
import requests
from bs4 import BeautifulSoup
url='https://www.****.tv/ajax/downurl/wNiNmMnRjZ_tv/'
header = {
'referer': 'https://www.****.tv',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36 Edg/83.0.478.37'
}
r = requests.get(url, headers=header)
soup=BeautifulSoup(r.text,'html.parser')
print(soup.prettify())
结果如下:
然后再来重新写这段代码:
def get_video(url):
# 根据重定向拼接真实的链接
URL = 'https://www.****.tv/ajax/downurl/' + url.split('/')[-1].split('.')[0] + '_'+url.split('/')[3]+'/'
# 请求视频链接
response = requests_url(URL)
# 找到第一集的播放链接
soup = BeautifulSoup(response.text, 'html.parser').find('ul', attrs={'class': "player ckp"}).find('li').a.attrs['href']
# 重新拼接每一集主页链接
URL1 = 'https://www.****.tv' + soup
知道了如何请求播放链接,就以第一集来作为例子进行分析:
在第一集的播放页面中,捕获到了两个 index. m3u8 的文件,我们知道这是视频的一种格式。由多个 ts 文件 组成。
我们再次查看 预览 ,可以看到:其实第一个 index. m3u8 的内容指向了第二个 index. m3u8 文件。而视频的内容(ts 文件)就储存在这里面。
不信?那就看看吧:
- 经过验证,的的确确是第二个没错了,所以,接下来的问题是:这个链接是怎么出来的?
打开 第一集 的网页源码可以看到:这第一个 index. m3u8 文件的链接就在源代码中:
我们可以看到,每一集的 伪链接(第一个 index. m3u8 文件) 都在这里了。
注解: 这里必须从第一集开始分析爬取,为什么? 那是因为当我们选择的不是第一集的话,播放页的源码中将会只含有从本集开始到最新一集的链接,之前的都没有。这就是我一直强调第一集的原因。
至此,我们也看到了所有的链接都包含在 <script>…</script> 的标签中。可是,标签的内容全是字符串,既不是 json 格式,也没有网页标签。该如何提取呢?
答案是: 正则表达式!
通过 正则表达式,我们可以提取任意满足表达式的字符串,在这里,我用了很简单的表达式来方便大家看懂:
def get_video(url):
# 正确的链接拼接
URL = 'https://www.****.tv/ajax/downurl/' + url.split('/')[-1].split('.')[0] + '_'+url.split('/')[3]+'/' # 根据重定向拼接真实的链接
# 请求视频链接
response = requests_url(URL)
# 找到每一集的链接
soup = BeautifulSoup(response.text, 'html.parser').find('ul', attrs={'class': "player ckp"}).find('li').a.attrs['href']
# 重新拼接每一集主页链接
URL1 = 'https://www.****.tv' + soup
response1 = requests_url(URL1).text
soup1 = BeautifulSoup(response1, 'html.parser').find_all('script')[12] # 获取script标签
# 通过正则表达式找出所有下载链接
p = re.compile(r"https://.*?/index.m3u8")
information = p.findall(str(soup1))
num = 1
for info in information:
download['第{}集'.format(num)] = str(info).replace('index.m3u8','1000k/hls/index.m3u8')
num += 1
现在我们已经获取到了每一集的第一个 index. m3u8 文件链接,怎么才能跳转到第二个 index. m3u8 文件链接呢?
我们继续观察两个链接的区别在哪里:
第一个:
https:// .com/20200518/7302_c75efddb/index.m3u8
第二个:
https://.com/20200518/7302_c75efddb/ 1000k/hls/ index.m3u8
看出来了吗?对了,就是增添了路径而已,这样的话,继续添加代码:
def get_video(url):
'''
与上面的代码一样,所以就省略了
'''
response1 = requests_url(URL1).text
# 获取script标签
soup1 = BeautifulSoup(response1, 'html.parser').find_all('script')[12]
# 通过正则表达式找出所有下载链接
p = re.compile(r"https://.*?/index.m3u8")
information = p.findall(str(soup1))
num = 1
for info in information:
# download = {} 是全局变量
download['第{}集'.format(num)] = str(info).replace('index.m3u8','1000k/hls/index.m3u8')
num += 1
到目前为止,我们已经获得了所有的 m3u8 格式的真实链接。接下来就是下载的问题了,简单点就是通过 ffmpy3 下载,这里我就不过多的讲了。因为在我之前的文章中有写到使用方法:Python 爬虫用最普通的方法爬取ts文件并合成为mp4格式。
源码及运行过程示例:
源码:
import re
import os
import time
import ffmpy3
import random
import parsel
import requests
from bs4 import BeautifulSoup
from urllib.parse import quote
from multiprocessing.pool import ThreadPool
referer = 'https://www.****.tv'
header = {
'referer': referer,
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36 Edg/83.0.478.37'
}
proxies = ['HTTP://60.13.42.120:9999', 'HTTP://163.204.244.207:9999', 'HTTP://113.121.39.121:9999',
'HTTP://125.117.134.99:9000', 'HTTP://123.169.114.81:9999', 'HTTP://58.253.159.230:9999',
'HTTP://182.46.99.247:9999', 'HTTP://171.15.48.235:9999', 'HTTP://120.83.105.226:9999',
'HTTP://113.124.95.210:9999', 'HTTP://171.13.103.241:9999', 'HTTP://123.163.96.42:9999',
'HTTP://27.42.168.46:49345', 'HTTP://122.234.27.22:9000', 'HTTP://125.108.73.47:9000',
'HTTP://125.108.97.209:9000', 'HTTP://113.124.85.24:9999', 'HTTP://27.220.51.228:9000',
'HTTP://113.124.84.205:9999', 'HTTP://110.243.31.147:9999']
path = './Spider/'
video = {} # 保存搜索记录
download = {} # 保存下载链接
def requests_url(url): # 请求网址
try:
proxy = {'http': random.choice(proxies)}
print(proxy)
r = requests.get(url, proxies=proxy, headers=header)
r.raise_for_status()
r.encoding = 'UTF-8'
return r
except:
print("*********请求失败!***********")
def split_info(videos_info):
num = 1
for video_info in videos_info:
info = str(video_info).replace('<em>', '').replace('</em>', '') # 去掉源码中红色字体的标注
soup = BeautifulSoup(info, 'html.parser')
info = soup.find_all('p')
# 将通过关键字搜索到的视频名与对应链接保存下来 (对应链接为拼接后的完整链接,如:https://www.****.tv/tv/wNiNmMnRjZ.html)
video[info[0].strong.a.string + info[0].span.string] = 'https://www.pianku.tv' + info[0].strong.a.attrs['href']
name2_info = info[1].string # 又名
area_info = info[2].string # 地区与类型
actor_info = info[3].string # 演员
introduction = info[4].string # 简介
print('***********************************************************')
print('{:0>2d}: '.format(num), end='')
num += 1
print(' ' + info[0].strong.a.string + info[0].span.string) # 名称
print(' ' + name2_info)
print(' ' + area_info)
print(' ' + actor_info)
print(' ' + introduction)
def get_video(url):
global referer
referer = url
# 根据重定向拼接真实的链接
URL = 'https://www.****.tv/ajax/downurl/' + url.split('/')[-1].split('.')[0] + '_' + url.split('/')[3] + '/'
# 请求视频链接
response = requests_url(URL)
soup =
BeautifulSoup(response.text, 'html.parser').find('ul', attrs={'class': "player ckp"}).find('li').a.attrs[
'href'] # 找到每一集的链接
URL1 = 'https://www.****.tv' + soup # 重新拼接每一集主页链接
response1 = requests_url(URL1).text
soup1 = BeautifulSoup(response1, 'html.parser').find_all('script')[12] # 获取script标签
# 通过正则表达式找出所有下载链接
p = re.compile(r"https://.*?/index.m3u8")
information = p.findall(str(soup1))
num = 1
for info in information:
download['第{}集'.format(num)] = str(info).replace('index.m3u8', '1000k/hls/index.m3u8')
num += 1
def video_download(name): # 通过ffmpy3下载
try:
if os.path.exists(path + NAME + '/'):
pass
else:
os.makedirs(path + NAME + '/')
ffmpy3.FFmpeg(inputs={download[name]: None}, outputs={path + NAME + '/' + name + '.mp4': None}).run()
print('************' + name + '下载成功!' + '************')
except:
print('============' + name + '下载失败!' + '============')
def user_ui():
global NAME, page_num
print('***********************************************************')
keyword = '传闻中' # input('请输入搜索的视频关键字:')
url = 'https://www.****.tv/s/go.php?q=' + quote(keyword, 'utf-8') # 进行url加密
URL = (url.split('go.php?q=')[0] + url.split('go.php?q=')[1]).replace('%', '_') + '.html' # 根据网页,更改加密后的url
html = requests_url(URL).text
search_time = parsel.Selector(html).xpath('//div[@class="breadcrumbs"]/text()').extract()[0] # 搜索时间以及数量
videos_info = parsel.Selector(html).xpath('//dd').extract() # 视频信息标签
try: # 若搜索结果小于10项,则无页码信息
print(search_time, end='')
page_num = parsel.Selector(html).xpath('//div[@class="pages"]/a/text()').extract()[-2]
print(',共{}页。每页10项。'.format(page_num))
except:
print(end='
')
split_info(videos_info) # 拆分显示视频信息
choice = input('
请输入序号选择:')
NAME = list(video.keys())[int(choice) - 1] # 通过下标确定进一步搜索的类容
URL = video[NAME]
get_video(URL) # 获取下载链接
user_ui()
time1 = time.time()
pool = ThreadPool(10) # 开启线程池
results = pool.map(video_download, download.keys())
pool.close()
pool.join()
time2 = time.time()
print('*********耗时:{}*********'.format(time2 - time1))
运行过程示例:
最后的最后:来点小福利: