Python爬虫实现全自动爬取拉钩教育视频

ps:改良之后的多线程版本在最后

背景

大饼加了不少技术交流群,之前在群里看到拉钩教育平台在做活动,花了1块钱买了套课程。比较尴尬的是大饼一般都会在上下班的路中学习下(路上时间比较久)而这个视频无法缓存,只能通过在线的方式进行学习,这(what`s the fuck…)
对于没有多少流量的我来说没法忍…于是乎想着通过爬虫的方式将视频爬取下来…

主要技术知识运用

1、request模块发送请求
2、json模块对返回值进行处理
3、AES解密

主要流程

1、登录后选择已购买的课程(这是我已购买的其中一套课程,以此为例)
在这里插入图片描述
2、为了一次性拿到所有课程名以及课程链接,这里可以通过f12抓包的方式获取
在这里插入图片描述
通过对响应的分析,可以确定就是这个请求…此时可以通过headers查看请求的地址以及响应的一些参数。
在这里插入图片描述
request headers中有很详细的信息,一般cooike肯定是需要带上的(代码中的cooike是错误的,这里只截取了一部分),大饼这里偷了下懒,把一些相对重要的信息都配置到headers中了

    def __init__(self):
        self.url='https://gate.lagou.com/v1/neirong/kaiwu/getCourseLessons?courseId=17'
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36',
            'Cookie':'16ded4ec8fd4fc%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_referrer%22%3A%22%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24os%22%3A%22Windows%22%2C%22%24browser%22%3A%22Chrome%22%2C%22%24browser_version%22%3A%2280.0.3987.132%22%7D%2C%22first_id%22%3A%22172322a320715c-0a184f9286834f-4313f6a-2073600-172322a320890a%22%7D',
            'Referer':'https://kaiwu.lagou.com/course/courseInfo.htm?courseId=17',
            'Origin':'https://kaiwu.lagou.com',
            'Sec-fetch-dest':'empty',
            'Sec-fetch-mode':'cors',
            'Sec-fetch-site':'same-site',
            'x-l-req-header':'{deviceType:1}'}

3、配置好了一些基本信息后就可以开始发送请求了
此次请求是为了拿到所有课程的m3u8以及课程名
ps:很多视频网站会通过m3u8的方式将视频分割成多个ts文件,我们要做的就是分析m3u8文件得到所有ts文件的url 同时如果加密了需要得到key
在这里插入图片描述
请求发送后返回来的是一个json格式的数据,从上图可以看到,m3u8的url其实就在这个json的数据中,而我们就是需要想办法得到这个m3u8的url

具体代码如下:
通过parse_one方法得到了所有课程的m3u8以及课程名,通过字典的形式进行存储,然后把得到的字典传递给下一个方法

    def parse_one(self):
        '''

        :return: url_list:所有课程的m3u8 url
                 name_list:所有课程的名字
        '''
        url_list=[]
        name_list=[]
        html=requests.get(url=self.url,headers=self.headers).text
        dit_message=json.loads(html)
        message_list=dit_message['content']['courseSectionList']

        for message in message_list:
            # print(message)
            for i in message['courseLessons']:
                if i['videoMediaDTO'] ==None:
                    pass
                else:
                    url_list.append(i['videoMediaDTO']['fileUrl'])
                    name_list.append(i['theme'])
        #将两个列表合并成一个字典,每个m3u8对应一个name
        m3u8_dict=dict(zip(url_list,name_list))
        # print(m3u8_dic)
        self.get_key(m3u8_dict) #返回所有课程的m3u8以及课程名

4、拿到了m3u8的url后,就可以通过同样的方式发送请求,可以从此请求中获取ts 以及key相关的信息
请求发送后得到下面这些信息(只截取了一部分)
我们要做的是从下面这些信息中拿到key(key的url已经找到了,通过requests发送请求即可得到真正的key)以及所有ts的url(这里ts的url只给出了部分需要自己拼接,用发送请求的url进行拼接)具体代码如下:
在这里插入图片描述

 def get_key(self,m3u8_dict):
        global key
        ts_list=[]
        for k in m3u8_dict:
            url_list=k.split('.')[0:-2]
            str_url='.'.join(url_list)
            true_url=str_url.split('/')[0:-1]
            t_url='/'.join(true_url)    #拼接ts的url前面部分

            html = requests.get(url=k, headers=self.headers).text  #请求返回包含ts以及key数据
            time.sleep(1)

            message=html.split('\n') #获取key以及ts的url
            name=m3u8_dict[k]
            for i in message:
                if 'URI' in i:
                    key_url=i.split(',')[1].split('"')[1]
                    key = requests.get(url=key_url, headers=self.headers).content  
                # print(i)
                if 'v.f240.ts?'in i:
                    ts_url=t_url+'/'+i
                    # print(ts_url)

            # print(key)

                    self.write(key,ts_url,name)

代码解读:(对响应回来的数据进行各种切割拼接的操作以得到真正有用的内容)
1、遍历上一个请求穿过来的字典,k是m3u8的url,通过这个url可以拼接出ts真正的url
2、获取key
3、将key,ts的url以及课程的名字传递给下一个方法

5、发送请求写入数据
这个步骤中在写入的过程需要进行解密

    def write(self,key,ts_url,name):
        # print(key)
        cryptor = AES.new(key, AES.MODE_CBC, iv=key)
        if os.path.exists('{}.mp4'.format(name)):
            pass
        else:
            with open('{}.mp4'.format(name),'ab')as f:

                html=requests.get(url=ts_url,headers=self.headers).content
                time.sleep(1)
                f.write(cryptor.decrypt(html))
                print('{}爬取完毕'.format(name)+'对应的key是{}'.format(key))

这里我进行了一些判断(之前有一次爬取时到一半远程主机断开了连接,怀疑是被检测到了)如果课程视频文件已经在本地了,那么下次爬取将会从这个课程之后进行爬取

解密的部分就不再赘述了,基本上都是这么个格式…

总结

课程视频虽然能够爬取下来了,但是目前这个代码还是有很多可以优化的地方,比如
1、可以使用多线程的方式提高爬取的效率(大饼不是很着急,放在后台慢慢爬也能接受)
2、key的部分发送了很多重复的请求,每个m3u8中的key是一样的所以每个m3u8只要获取一次key就可以了

有空的时候会再对其进行适当的优化

写在最后

这个视频爬虫基本上就是这样了,属于非常基础的爬虫,适合跟大饼类似的爬虫基础玩家阅读,同时非常欢迎大佬莅临,要是能传授几招就更好了,哈哈哈!!!(想的略多…)

多线程版本

爬取视频

import threading
from queue import Queue
import re
import requests
import json
from Crypto.Cipher import AES
import time
import os
import pdfkit


class LaGou_spider():
    def __init__(self,url):
        self.url = url
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36',
            'Cookie': 'cookie信息',
            'Referer': 'https://kaiwu.lagou.com/course/courseInfo.htm?courseId=17',
            'Origin': 'https://kaiwu.lagou.com',
            'Sec-fetch-dest': 'empty',
            'Sec-fetch-mode': 'cors',
            'Sec-fetch-site': 'same-site',
            'x-l-req-header': '{deviceType:1}'}
        # self.url='https://gate.lagou.com/v1/neirong/kaiwu/getCourseLessons?courseId=46'
        self.queue = Queue()  # 初始化一个队列
        self.error_queue = Queue()
    def get_id(self):
        ture_url_list=[]
        html = requests.get(url=self.url, headers=self.headers).text
        dit_message = json.loads(html)
        message_list = dit_message['content']['courseSectionList']
        for message in message_list:
            id1=message["courseLessons"]
            for t_id in id1:
                ture_url="https://gate.lagou.com/v1/neirong/kaiwu/getCourseLessonDetail?lessonId={}".format(t_id["id"])
                ture_url_list.append(ture_url)
        return ture_url_list
    def parse_one(self,ture_url_list):
        """

        :return:获得所有的课程url和课程名 返回一个队列(请求一次)
        """
        for ture_url in ture_url_list:
            # print(ture_url)
            html = requests.get(url=ture_url, headers=self.headers).text
            # print(html)
            dit_message = json.loads(html)
            message_list = dit_message['content']
            # print(message_list["videoMedia"])
            if message_list["videoMedia"] == None:
                continue
            else:
                name=message_list["theme"]
                m3u8=message_list["videoMedia"]["fileUrl"]
                # print(m3u8)
                m3u8_dict = {m3u8:name}  # key为视频的url,val为视频的name
                if os.path.exists("{}.mp4".format(name)):
                    print("{}已经存在".format(name))
                    pass
                else:
                    # print(m3u8_dict)
                    self.queue.put(m3u8_dict)  # 将每个本地不存在的视频url(m3u8)和name加入到队列中
        # for message in message_list:
        #     # print(message)
        #     for i in message['courseLessons']:
        #         if i['videoMediaDTO'] == None:
        #             pass
        #         else:
        #             key = i['videoMediaDTO']['fileUrl']
        #             val = i['theme']
        #             m3u8_dict = {key: val}  # key为视频的url,val为视频的name
        #             # print(m3u8_dict)
        #
        return self.queue
    #
    def get_key(self, **kwargs):
        # global key
        m3u8_dict = kwargs
        # print(m3u8_dict)
        for k in m3u8_dict:  # 获取某个视频的url
            name = ''
            # print(k)
            true_url = k.split('/')[0:-1]
            t_url = '/'.join(true_url)  # 拼接ts的url前面部分
            html = requests.get(url=k, headers=self.headers).text  # 请求返回包含ts以及key数据
            # print(html)
            message = html.split('\n')  # 获取key以及ts的url
            key_parse = re.compile('URI="(.*?)"')
            key_list = key_parse.findall(html)
            # print("密匙链接"+key_list)
            # print(key_list[0])
            key = requests.get(url=key_list[0],
                               headers=self.headers).content  # 一个m3u8文件中的所有ts对应的key是同一个 发一次请求获得m3u8文件的key
            # print(key)
            name1 = m3u8_dict[k]  # 视频的名字
            # print("视频名:"+name1)
            if "|" or '?' or '/' in name1:
                name = name1.replace("|" , "-")
                for i in message:
                    if '.ts' in i:
                        ts_url = t_url + '/' + i
                        # print("ts_url"+ts_url)
                        self.write(key, ts_url, name, m3u8_dict)
            else:
                name = name1
                for i in message:
                    # print(i)
                    if '.ts' in i:
                        ts_url = t_url + '/' + i
                        # print(ts_url)
                        self.write(key, ts_url, name, m3u8_dict)

    def write(self, key, ts_url, name01, m3u8_dict):
        dir='D:\\video'
        if not os.path.exists(dir):
            os.makedirs(dir)
        cryptor = AES.new(key, AES.MODE_CBC, iv=key)
        with open('{}\\{}.mp4'.format(dir,name01), 'ab')as f:
            try:
                html = requests.get(url=ts_url, headers=self.headers).content
                f.write(cryptor.decrypt(html))
                print('{},{}写入成功'.format(ts_url, name01))
            except Exception as e:
                print('{}爬取出错'.format(name01))
                while True:
                    if f.close():  # 检查这个出问题的文件是否关闭  闭关则删除然后重新爬取,没关闭则等待10s,直到该文件被删除并重新爬取为止
                        os.remove('{}.mp4'.format(name01))
                        print('{}删除成功'.format(name01))
                        thread = self.thread_method(self.get_key, m3u8_dict)
                        print("开启线程{},{}重新爬取".format(thread.getName(), name01))
                        thread.start()
                        thread.join()
                        break
                    else:
                        time.sleep(10)

    def thread_method(self, method, value):  # 创建线程方法
        thread = threading.Thread(target=method, kwargs=value)
        return thread

    def main(self):
        global m3u8
        thread_list = []
        ture_url_list=self.get_id()
        m3u8_dict = self.parse_one(ture_url_list)
        while not m3u8_dict.empty():
            for i in range(5):  # 创建线程并启动
                if not m3u8_dict.empty():
                    m3u8 = m3u8_dict.get()
                    # print(type(m3u8))
                    thread = self.thread_method(self.get_key, m3u8)
                    thread.start()
                    print(thread.getName() + '启动成功,{}'.format(m3u8))
                    time.sleep(1)
                    thread_list.append(thread)
                else:
                    break
            for k in thread_list:
                k.join()  # 回收线程


if __name__ == "__main__":
    run = LaGou_spider()
    # run.get_id()
    time1 = time.time()
    run.main()
    time2 = time.time()
    print(time2 - time1)


爬取文章


import threading
from queue import Queue
import requests
import json
import time
import pdfkit




class LaGou_Article_Spider():
    def __init__(self,url):
        self.url = url
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36',
            'Cookie': 'cookie信息',
            'Referer': 'https://kaiwu.lagou.com/course/courseInfo.htm?courseId=17',
            'Origin': 'https://kaiwu.lagou.com',
            'Sec-fetch-dest': 'empty',
            'Sec-fetch-mode': 'cors',
            'Sec-fetch-site': 'same-site',
            'x-l-req-header': '{deviceType:1}'}
        self.textUrl='https://gate.lagou.com/v1/neirong/kaiwu/getCourseLessonDetail?lessonId='  #发现课程文章html的请求url前面都是一样的最后的id不同而已
        self.queue = Queue()  # 初始化一个队列
        self.error_queue = Queue()

    def parse_one(self):
        """

        :return:获取文章html的url
        """
        # id_list=[]
        html = requests.get(url=self.url, headers=self.headers).text
        dit_message = json.loads(html)
        message_list = dit_message['content']['courseSectionList']
        # print(message_list)
        for message in message_list:
            for i in message['courseLessons']:
                true_url=self.textUrl+str(i['id'])
                self.queue.put(true_url)#文章的请求url


        return self.queue

    def get_html(self,true_url):
        """

        :return:返回一个Str 类型的html
        """
        global article_name
        html=requests.get(url=true_url,timeout=10,headers=self.headers).text
        dit_message = json.loads(html)
        str_html=str(dit_message['content']['textContent'])
        article_name1=dit_message['content']['theme']
        if "|" or '?' or '/' in article_name1:
            article_name=article_name1.replace("|"and'?'and'/', "-")
        else:
            article_name=article_name1
        self.htmltopdf(str_html,article_name)

    def htmltopdf(self,str_html,article_name):
        path_wk = r'D:\wkhtmltox-0.12.6-1.mxe-cross-win64\wkhtmltox\bin\wkhtmltopdf.exe'
        config = pdfkit.configuration(wkhtmltopdf=path_wk)
        options = {
            'page-size': 'Letter',
            'encoding': 'UTF-8',
            'custom-header': [('Accept-Encoding', 'gzip')]
        }
        pdfkit.from_string(str_html,"D:\\video\\{}.pdf".format(article_name),configuration=config,options=options)



    def thread_method(self, method, value):  # 创建线程方法
        thread = threading.Thread(target=method, args=value)
        return thread

    def main(self):

        thread_list = []
        true_url= self.parse_one()
        while not  true_url.empty():
            for i in range(10):  # 创建线程并启动
                if not true_url.empty():
                    m3u8 = true_url.get()
                    print(m3u8)
                    thread = self.thread_method(self.get_html, (m3u8,))
                    thread.start()
                    print(thread.getName() + '启动成功,{}'.format(m3u8))
                    thread_list.append(thread)
                else:
                    break
            while len(thread_list)!=0:
                for k in thread_list:
                    k.join()  # 回收线程
                    print('{}线程回收完毕'.format(k))
                    thread_list.remove(k)


if __name__ =="__main__":
    run = LaGou_spider('faklf')
    run.main()


整合视频和文章

from LaGou_spider import LaGou_Article_Spider
from LaGou_spider import LaGou_spider
print("请输入课程编号:")
number=int(input())
url='https://gate.lagou.com/v1/neirong/kaiwu/getCourseLessons?courseId={}'.format(number)
video=LaGou_spider.LaGou_spider(url)
video.main()
article=LaGou_Article_Spider.LaGou_Article_Spider(url)
article.main()

使用方法:
1、启动整合视频和文章
2、输入课程ID(课程ID在进入课程页面时可以从url处得到)
3、自动下载视频和文章(文章会保存成PDF)
(不一定所有的课程都能爬取成功…,且爬且珍惜,该教程主要用于学习交流)

代码非最优…凑合能用,比起之前的版本速度提升了很多,可根据自己电脑的性能以及网速多起几个线程…大饼起了10个线程,视屏应该是4.27G,用了10分钟左右,还能接受!

改良点:
爬取的过程中进行了判断,有时爬取速度过快远程主机会断开连接,当远程主机断开连接后,程序会报错,这时捕获这个异常,然后删除这个爬取了一半的文件,再起一个线程重新爬取这个视频,这样以来只要出错,后续依然会爬取这个文件,直到全部爬取成功为止…实现真正的自动爬取所有视频
(大饼可能人品比较好在测试的过程中并未出现上述情况,所以该功能还有待验证)
最后慢就是快,莫贪…
cooike部分需要自己添加

  • 10
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值