0
点赞
收藏
分享

微信扫一扫

爬取极客时间超级VIP课程内容,不包含越权处理

七千22 2022-04-21 阅读 93
python爬虫

引言

不知道你有没有遇到这种情况,购买了极客时间上的 VIP 或者 超级VIP,想静下来认认真真的学习一门课程,眼看一个月过去了,还是有没学习多少,但是VIP的过期时间一直在慢慢逼近。

你有可能会想,要不直接在极客时间APP上下载专栏文章的内容吧,可是找来找去,仅在专栏文章能够获取到音视频的缓存,而不能有效的进行下载文章内容。

为了能够保存文章的内容,就去各种脚本网站获取相关脚本,可是就是找不到真正满意的。

我就是在这种情况下,产生了获取文章内容的想法。

注意,为了双方的利益,本脚本不涉及对越权进行处理,仅提供获取文章的信息。即:您得先购买VIP或者超级VIP;拒绝白嫖

注意,为了双方的利益,本脚本不涉及对越权进行处理,仅提供获取文章的信息。即:您得先购买VIP或者超级VIP;拒绝白嫖

注意,为了双方的利益,本脚本不涉及对越权进行处理,仅提供获取文章的信息。即:您得先购买VIP或者超级VIP;拒绝白嫖

脚本实现

组织树

├── conf
│   ├── __init__.py
│   └── config.py					# 配置文件信息,全局配置文件
├── interial							
│   ├── __init__.py
│   ├── logger.py					# 日志配置,使用loguru
│   ├── mysql.py					# Mysql 配置,使用orm peewee轻量型模块
│   └── redis.py					# Redis 配置,暂未使用
├── logs									# 日志文件
│   └── crawler_2022_4_20.log
├── main.py
├── media									# 媒体存储文件
├── model									# 模型文件夹
│   ├── __init__.py
│   └── jike.py						# 定义orm 模型
├── requirements.txt			# 依赖库
├── spider
│   ├── __init__.py
│   ├── jike.py						# 爬虫主体
└── venv

模型层

from interial.mysql import BaseContainer, BaseModel
from peewee import *
from datetime import datetime


class JikeModel(BaseModel):
    article_id = CharField()
    chapter_id = CharField()
    audio_url = CharField()
    article_title = CharField()
    author_name = CharField()
    column_id = IntegerField()
    article_summary = CharField()
    article_content = TextField()
    create_at = DateTimeField(default=datetime.now)

    class Meta:
        table_name = "JikeTime"


if not JikeModel.table_exists():
    JikeModel.create_table()

class JikeContainer(BaseContainer):
    def __init__(self):
        self.article_id = ""
        self.chapter_id = "0"
        self.audio_url = ""
        self.article_title = ""
        self.author_name = ""
        self.column_id = 0
        self.article_summary = ""
        self.article_content = ""

定义这一层的目的就是为了对传入的数据进行模型实例的创建,在BaseContainer类存在两个方法:from_dictto_dao

  • from_dict:类方法,传入参数为dict对象,会根据定义的属性进行匹配,且初始化创建Container对象

  • to_dao:实例方法,传入参数为Model的名称,会根据Model类的属性进行匹配创建Model对象

BaseModel类存在两个方法:addadd_list;和一个元类属性指向绑定的数据库。

  • add:实例方法,提交至数据库
  • add_list:类方法,使用事物进行添加一组数据

爬虫主体

获取课程列表

ua = fake_useragent.UserAgent()	# `import fake_useragent` after `pip install fake-useragent`

def get_products() -> list:
    url = "https://time.geekbang.org/serv/v1/column/label_skus"
    data = {
        "label_id": 0,
        "type": 0
    }
    headers = {"User-Agent": ua.random, "Referer": "https://time.geekbang.org"}
    rsp = requests.post(url=url, headers=headers, json=data)
    res = jsonpath(rsp.json(), "$...list[?(@.in_pvip==1)]..column_sku")	# `from jsonpath import jsonpath` after `pip install jsonpath`
    return res # return list(product_id)

在这一部分里面是通过访问API接口获得一个json对象,通过伪造爬虫请求头的数据,避免网站的反爬取策略,这就是添加fake-useragentReferer的原因。

对于requests返回的Response对象进行解析有两种较简洁的方法:

  • 对于html对象使用lxml的 etree xpath 或者 BeautifulSoup进行解析
  • 对于json对象使用jsonpath或者使用dict进行手动的访问

该函数的返回是超级VIP课程的id列表

获取课程文章列表

def get_article_ids(product_id) -> list:
  	data = {
             "cid": product_id,
             "size": 500,
             "prev": 0,
             "order": "earliest",
             "sample": False
         }
    headers = {"User-Agent": ua.random, "Referer": "https://time.geekbang.org"}
    rsp = requests.post(url="https://time.geekbang.org/serv/v1/column/articles", headers=headers, json=data)
    res = jsonpath(rsp.json(), "$...id")
    return res

由内容看出,此处的代码和上一部分极其相似,仅URLdatajsonpath三处地方不一致,传入的参数是由课程的专属id,返回的内容是课程的文章列表

获取文章内容

def get_article(article_id: int) -> dict:
    data = {
        "id": str(article_id),
        "include_neighbors": True,
        "is_freelyread": True
    }
    url = "https://time.geekbang.org/serv/v1/article"
    headers = {"User-Agent": ua.random, "Referer": "https://time.geekbang.org",
               "Cookie": "GCID=****; GCESS=****; "}
    rsp = requests.post(url=url, headers=headers, json=data)
    res = {}
    try:
        if rsp.status_code != 200:
            res = {}
            print(rsp.text)
        res = rsp.json()
    except Exception as e:
        print(e)
    finally:
        time.sleep(random.random())
        return res

在这一部分中,传入的参数是文章的id,在请求过程中,对请求头添加了cookie进行鉴权操作,极客时间的鉴权验证两个字段:GCIDGCESS

  • GCID 为uuid字符串
  • GCESS 为加密字符串

此处不对这两个字段的内容进行解析,具体内容请到浏览器的开发者模式进行获取相应数据,或者进行抓包操作。

请求之后将会获得文章的内容,对返回的内容进行一个简单的验证,不论执行正确还是失败,都会进行数据的返回和休眠处理,休眠处理是为了防止请求过快,对目标服务器造成大的压力。在运动过程中,不仅需要满足自己的快感,还需要考虑对方的感受进行适应动作。

存入数据库

@Secretary
def pipeline(info: dict, article_id: str):
    if not info:
        return
    data = {**info.get('data'), "article_id": article_id}
    container = JikeContainer.from_dict(data)
    res, error = container.to_dao(JikeModel).add()
    return res, error

装饰器是对日志记录器进行的一个封装,这个记录器只会进行一个黑盒操作,记录函数执行前执行后的状况(即:输入> 函数执行>输出)。对于请求返回的数据,是将数据的内容和文章的ID进行一个字典构造,并将所有信息发送给JiKeContainer进行from_dict处理,再to_dao处理成JiKeModel的模型实例,之后再进行实例的提交。

最后输出数据结果。

执行程序

if __name__ == "__main__":  
	  for i, cid in enumerate(get_products()):
        visited = [i.article_id for i in JikeModel.select(JikeModel.article_id).distinct()]
        if str(cid) in visited:
            continue
        for j in get_article_ids(cid):
            pipeline(get_article(j), str(cid))
执行过程

在这里插入图片描述

执行结果
SELECT count(DISTINCT article_id) FROM JikeTime; # output result >> 203 即超级VIP可查阅的课程为203
SELECT COUNT(article_id) FROM JikeTimel; # output result >> 9201 即抓取到文章量为9201条,不排除有数据漏查抓的情况,毕竟缺少了异常处理

总结

脚本实现用时1小时03分,大部分时间是花在了抓包的过程中,实际编码用时25min,得益于之前对抓取模式的处理(避免重复造轮子而弄了复用性还行的模型层,使得开发的重点压缩到了请求-应答部分);其次,该脚本为初期的demo,串行执行,执行速度较慢,至于并发的程序就不发出来了,为了目标服务器着想,如果有需要的话,请自行修改。

最后,如果想获得此次脚本的代码,欢迎点击下载。(审核中)

举报

相关推荐

0 条评论