https://blog.csdn.net/liujiayu2/article/details/86083400

http://api-component.yxt.com/v1/authex/bce/valid?f=https://streamobs.yunxuetang.cn/knowledgefiles/13601042801/videos/202006/96be6f1183ca4434804ac154ce0a8b27_0302042302_720p_enc.m3u8&m=job-kftkqw414djsh1ni

  1. # -*- coding:utf-8 -*-
  2. import os
  3. import sys
  4. import requests
  5. import datetime
  6. from Crypto.Cipher import AES
  7. from binascii import b2a_hex, a2b_hex
  8. ## pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple pycryptodome
  9. def download(url):
  10. download_path = os.getcwd() + "\download"
  11. if not os.path.exists(download_path):
  12. os.mkdir(download_path)
  13. #新建日期文件夹
  14. download_path = os.path.join(download_path, datetime.datetime.now().strftime('%Y%m%d_%H%M%S'))
  15. #print download_path
  16. os.mkdir(download_path)
  17. all_content = requests.get(url).text # 获取第一层M3U8文件内容
  18. if "#EXTM3U" not in all_content:
  19. raise BaseException("非M3U8的链接")
  20. if "EXT-X-STREAM-INF" in all_content: # 第一层
  21. file_line = all_content.split("\n")
  22. for line in file_line:
  23. if '.m3u8' in line:
  24. url = url.rsplit("/", 1)[0] + "/" + line # 拼出第二层m3u8的URL
  25. all_content = requests.get(url).text
  26. file_line = all_content.split("\n")
  27. unknow = True
  28. key = ""
  29. for index, line in enumerate(file_line): # 第二层
  30. if "#EXT-X-KEY" in line: # 找解密Key
  31. method_pos = line.find("METHOD")
  32. comma_pos = line.find(",")
  33. method = line[method_pos:comma_pos].split('=')[1]
  34. print ("Decode Method: ", method)
  35. uri_pos = line.find("URI")
  36. quotation_mark_pos = line.rfind('"')
  37. key_path = line[uri_pos:quotation_mark_pos].split('"')[1]
  38. key_url = url.rsplit("/", 1)[0] + "/" + key_path # 拼出key解密密钥URL
  39. res = requests.get(key_url)
  40. key = res.content
  41. print ("key_url: " , key_url)
  42. print ("key: " , key)
  43. if "EXTINF" in line: # 找ts地址并下载
  44. unknow = False
  45. pd_url = url.rsplit("/", 1)[0] + "/" + file_line[index + 1] # 拼出ts片段的URL
  46. #print pd_url
  47. res = requests.get(pd_url)
  48. c_fule_name = file_line[index + 1].rsplit("/", 1)[-1]
  49. if len(key): # AES 解密
  50. cryptor = AES.new(key, AES.MODE_CBC, key)
  51. with open(os.path.join(download_path, c_fule_name + ".mp4"), 'ab') as f:
  52. f.write(cryptor.decrypt(res.content))
  53. else:
  54. with open(os.path.join(download_path, c_fule_name), 'ab') as f:
  55. f.write(res.content)
  56. f.flush()
  57. if unknow:
  58. raise BaseException("未找到对应的下载链接")
  59. else:
  60. print ("下载完成")
  61. merge_file(download_path)
  62. def merge_file(path):
  63. os.chdir(path)
  64. cmd = "copy /b * new.tmp"
  65. os.system(cmd)
  66. os.system('del /Q *.ts')
  67. os.system('del /Q *.mp4')
  68. os.rename("new.tmp", "new.mp4")
  69. if __name__ == '__main__':
  70. _url = "https://streamobs.yunxuetang.cn/knowledgefiles/13601042801/videos/202006/96be6f1183ca4434804ac154ce0a8b27_0302042302_720p_enc.m3u8"
  71. download(_url)

m3u8

  1. '''
  2. 站点地址
  3. https://www.sanjieke.cn/free.html
  4. 课程列表json
  5. https://class.sanjieke.cn/course/class_content_with_checkpoint?cid=3003359
  6. 加载第一个视频页面
  7. https://class.sanjieke.cn/course/section_content?cid=3003359§ion_id=1673672
  8. https://class.sanjieke.cn/course/class_content_with_checkpoint?cid=17301420
  9. 加载第一个视频
  10. https://service.sanjieke.cn/video/master/1673668.m3u8?class_id=3003359
  11. 从加载页中取出m3u8文件地址
  12. https://service.sanjieke.cn/video/media/1673668/608p.m3u8?class_id=3003359
  13. 获取key地址
  14. https://service.sanjieke.cn/video/key/1673668?class_id=3003359
  15. '''
  16. print('''
  17. 三节课m3u8下载器v2
  18. 1、需要先报名这个课程
  19. 2、输入课程的id就能下载
  20. 3、时间久了可能需要更新一下cookies
  21. ''')
  22. import os
  23. import re
  24. import requests
  25. from Crypto.Cipher import AES
  26. headrer = {
  27. 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36',
  28. 'Cookie':'acw_tc=276aedd715843843846276228e7237cfd3f2985ec8423b6b72e1f786d0f2f0; sajssdk_2015_cross_new_user=1; PHPSESSID=8c8i2ohp56ah69jr6i8jp07pa5; sjk_jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIyIiwianRpIjoiNDE4ZDViOWRjOTQ3YjFmYzNjMGM5MzhmODgxMzU0NmMzZDgwMGQ5NWVkM2JlMjlhNjQ4MmFkODAyMTZlNGU0ZTdiMDY1OWMwNjA3ZDJlMDkiLCJpYXQiOjE1ODQzODQ0MTcsIm5iZiI6MTU4NDM4NDQxNywiZXhwIjoxNTg3MDYyODE3LCJzdWIiOiIyMDc5NTY2NiIsInNjb3BlcyI6W119.bykO1OWL9_mqZKhI6qEpwwrbTEpEk25Xc-bwFXG47lktpRfKLBPw22WOUqYRfl0qi-WkkItBgcEe9TVU8jfFcg; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2220795666%22%2C%22%24device_id%22%3A%22170e4ab2004b55-0f846e6d4f2e96-4313f6a-3686400-170e4ab2005aa9%22%2C%22props%22%3A%7B%22%24latest_referrer%22%3A%22https%3A%2F%2Fwww.baidu.com%2Fs%22%2C%22%24latest_traffic_source_type%22%3A%22%E8%87%AA%E7%84%B6%E6%90%9C%E7%B4%A2%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E4%B8%89%E8%8A%82%E8%AF%BE%22%7D%2C%22first_id%22%3A%22170e4ab2004b55-0f846e6d4f2e96-4313f6a-3686400-170e4ab2005aa9%22%7D; Hm_lvt_85148c078fe4635816b80e5a36990313=1584384385,1584384418; Hm_lpvt_85148c078fe4635816b80e5a36990313=1584384436'
  29. }
  30. class_id =input('请输入课程编号:')
  31. # 获取这个课程所有课时的ID
  32. class_url = 'https://class.sanjieke.cn/course/class_content_with_checkpoint?cid='+class_id
  33. class_contect = requests.get(class_url,headers=headrer).text.encode('utf-8').decode('unicode-escape')
  34. course_title = re.compile('"course_title":"(.*?)"',re.S).findall(class_contect)
  35. if len(course_title)==0:
  36. print('课程不在有效期内或未报名')
  37. raise IndexError
  38. course_title = course_title[0]
  39. course_id_name_list = re.compile('"node_id":(\d*),.*?"title":"(.*?)"',re.S).findall(class_contect) # 课时ID,名称
  40. # 创建与课程同名文件夹
  41. course_title = re.sub(u"([^\u4e00-\u9fa5\u0030-\u0039\u0041-\u005a\u0061-\u007a])", "", course_title)
  42. if not os.path.exists(r'./' + r'./' + course_title): # 判断文件夹是否存在
  43. os.makedirs(r'./' + r'./' + course_title) # 如果不存在则创建文件夹
  44. # 获取m3u8文件的ID以及对应的名称
  45. i = 1
  46. for course_id_name in course_id_name_list:
  47. course_url = 'https://class.sanjieke.cn/course/section_content?cid='+class_id+'§ion_id='+course_id_name[0]
  48. course_contect = requests.get(course_url,headers=headrer)
  49. course_contect = course_contect.text.replace('\\/','/').encode('utf-8','ignore').decode('unicode-escape','ignore')
  50. course_m3u8_id = re.compile('"id":"(\d*)"', re.S).findall(course_contect)
  51. if len(course_m3u8_id) == 0:
  52. course_m3u8_id = re.compile('"id":(\d*)', re.S).findall(course_contect)
  53. # 读取每个m3u8文件的内容
  54. if not len(course_m3u8_id) == 0:
  55. # 获取m3u8文件下载地址
  56. course_m3u8_url = 'https://service.sanjieke.cn/video/master/'+course_m3u8_id[0]+'.m3u8?class_id='+class_id[0]
  57. course_m3u8_contect = requests.get(course_m3u8_url,headers=headrer).text.replace('\\/','/')
  58. course_m3u8_contect = course_m3u8_contect.encode('utf-8','ignore').decode('unicode-escape','ignore')
  59. course_m3u8_down_url = re.compile('http.*?\.m3u8',re.S).findall(course_m3u8_contect)[0]
  60. # 读取m3u8文件原始内容
  61. m3u8_res = requests.get(course_m3u8_down_url,headers=headrer).text
  62. m3u8_res = m3u8_res.encode('utf-8','ignore').decode('unicode-escape','ignore')
  63. URI = 'https://service.sanjieke.cn/video/key/'+course_m3u8_id[0]
  64. key = requests.get(URI,headers=headrer).content
  65. iv = re.compile('IV=0x(.*)').findall(m3u8_res)
  66. ts_url = re.compile('(https://vcdn.*)').findall(m3u8_res)
  67. file_url = r'./' + course_title + r'./ ' + str(i).rjust(2, '0') + ' ' + course_id_name[1] + '.mp4' #构造文件下载地址
  68. for k in range(len(iv)):
  69. # 获取ts文件的内容
  70. ts_contrect = requests.get(ts_url[k])
  71. # 解密ts文件
  72. cryptor = AES.new(key, AES.MODE_CBC, bytes.fromhex(iv[k]))
  73. # 下载ts文件
  74. with open(file_url, 'ab') as f:
  75. f.write(cryptor.decrypt(ts_contrect.content))
  76. print(' 正在下载片段'+str(k).rjust(2, '0') + ' ' + course_id_name[1] + '.MP4')
  77. print(str(i).rjust(2, '0') + ' ' + course_id_name[1] + '.MP4' + ' 下载成功!')
  78. i = i + 1
  79. print(course_title +'下载完成!')