引言
不知道你有没有遇到这种情况,购买了极客时间
上的 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_dict
和to_dao
。
-
from_dict
:类方法,传入参数为dict对象,会根据定义的属性进行匹配,且初始化创建Container
对象 -
to_dao
:实例方法,传入参数为Model的名称,会根据Model类的属性进行匹配创建Model对象
在BaseModel
类存在两个方法:add
和add_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-useragent
和Referer
的原因。
对于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
由内容看出,此处的代码和上一部分极其相似,仅URL
、data
和jsonpath
三处地方不一致,传入的参数是由课程的专属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
进行鉴权操作,极客时间的鉴权验证两个字段:GCID
和GCESS
。
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,串行执行,执行速度较慢,至于并发的程序就不发出来了,为了目标服务器着想,如果有需要的话,请自行修改。
最后,如果想获得此次脚本的代码,欢迎点击下载。(审核中)