大神论坛

找回密码
快速注册
查看: 134 | 回复: 0

[经验分享] 逆向分析武海笔院网站书籍结构及详细逆向PDF下载

主题

帖子

0

积分

初入江湖

UID
652
积分
0
精华
威望
0 点
违规
大神币
68 枚
注册时间
2023-10-14 10:44
发表于 2024-06-30 11:50
本帖最后由 恋爱选举巧克力 于 2024-06-30 11:50 编辑

缘起

由于一些不可言说的理由(不能说,就算一直盯着我也不能说),我凑齐五十下品灵石前往 武海笔院 购买书籍。

武海笔院 的书籍有两种阅读方式:在网页端看、下载电子书用它的软件看,都不能带出去,所以我想要将书籍下载下来,选择的方式自然也是:分析网页端,爬取它的图片并生成 PDF。所以此种方式下载的 PDF 是无法复制文字的

想要下载完整的书籍需要先用灵石购买它,也就是获得该书籍的阅读权限

我先去散修城的藏经阁寻找方案(指搜索文章、github),前人几乎都是基于 web端爬取图片、合并成 PDF。仔细分析了他们的实现方案、并结合现在灵兽分身的踪迹(指网站的工作方式),发现这些方案容易和该灵兽分身(浏览器)有牵连,会吸引其本体的注意从而被灭(如封 IP、封账号),就像这样:

最后,我于高山之上闭关 1 天另寻他法,且看后文分析。

留影之术

下面让我们先探讨现有的、下载图书的方式,最后说明我的解决方案 —— 留影之术。

# ======= 回忆长廊 =======
启动

# 当前进度:查看书籍的每一页是怎样的结构

查看网站 HTML 结构,发现每一页的内容由 6 张小图片构成,如下:

通过浏览器的抓包分析,找到这 6 张小图片的请求。

显而易见地:我们需要爬取这些小图片、合成每一页的图片,最后合并所有的页,得到一个 PDF 文件

现在让我们仔细探讨现有的爬取方式。

模拟请求

不推荐此方式。

经过测试,发现在鼠标滚轮滚动时会触发一个 save 请求(滚动一次就触发一次)。

这是在做什么呢?这是在记录我的阅读时长,或者说阅读习惯,具体说明如下:

模拟请求的方式有以下缺点:

  • 虽然可以模拟上述的 save 请求,但这需要大量测试是否可行,我的帐号只有一个,所以不能轻易尝试。
  • 考虑账号的安全性,模拟请求所耗费的时间、精力(分析请求、分析参数)以及可能承担的后果都使得我不推荐此方式。

自动化工具

不推荐此方式。

在部分解决方案中(注:此处原本想放一个链接,却发现其牵扯到灵兽的另一分身,不得不用我大圆满境界的打码术去除了)使用的是自动化工具 selenium,并且提到了“那 6 张小图片不能二次访问”,其中一个解决方案是用代码控制鼠标右键点击图片来保存。

自动化方案有以下缺点:

  • 自动化工具是可以被检测的,账号只有一个,我要选稳妥的方式
  • 保存图片的时候用 pyautogui 控制鼠标、键盘,这样就不能在电脑上做其它的事情了。

我的方案:代{过}{滤}理捕获,留影之术

既然小图片只能访问一次,我的想法是通过拦截响应来获取图片。这可以写成浏览器插件的形式,不过考虑到要捕获请求了,最好和浏览器分离开来,所以使用代{过}{滤}理的形式!

没错!完全和浏览器隔开,不沾染因果,此乃留影之术的本质

整个流程如下:

当然它的缺点很明显:非常占用资源,因为浏览器会一直发出图片请求、并解析、渲染图片啦

== 可是,(。・・)ノ 要怎么自动翻页呢??!

=> 经过前文的分析,我们要稳妥一点,所以不要自动翻页。

== 啊??那我该怎么办?w(゚Д゚)w

=> 手动翻页呗,难不成自己翻页太快也要封账号?!大家可是都会量子波动速读法的!—— 此法术只有在发动的时候才能记住文字,一旦停止施法,读过的内容就全部忘记了,这也许就是该法术的代价吧。

== 什么鸡肋术法,我就是想自动翻页、自动下载!(○´・д・)ノ

=> ……这样,你给我 100 下品灵石,我帮你翻页,这样对你来说也算是自动的。

== 啊!突然记起来传法殿中是有这么个量子什么阅读法的 ╥﹏╥... 可是我宗门贡献分不够兑换呀

=> 真拿你没办法,既然有缘,再多说道一二。现在你可以用任何方式、只要能让浏览器翻页就行 —— 前端就是这样的,只需要好好翻页就行了,可代{过}{滤}理端要考虑的事情就多了

== 嗯?这个句式好像有点眼熟 (´・ω・`)?

=> 咳咳……我认为比较稳妥的是:编写 JS 脚本实现滚动翻页(是让页面慢慢滚动从而翻页,而不是一页一页地、跳跃式翻页),不使用自动化工具来操作浏览器(不要和分身有任何牵连)。如此,自动翻页既安全、也可以做其他的事情嘛。

== 嗯嗯,然后呢?然后呢?

=> 放心,项目里有自动翻页的脚本啦。不过在我的测试中,自动翻页超过一定次数,会弹出错误信息(把我吓出一身冷汗,我甚至感觉到其本体的视线已经跨越时间与空间,将我钉在了此处,它似乎正看我的过去、我的来历,毕竟在他的眼中,小小练气修士是不敢招惹它的,如果它知道我没有强悍的背景,恐怕……额,好像可以无视警告继续翻页欸,咳咳,稳妥起见,可以暂停一会)。

重要声明

我并没有测试 “一个账号一天翻完了多本书会不会封账号”(这还要花钱买书测试呢!我也不推荐大家尝试),不过仔细想想,怎么说也是手动翻页的,不应该封号才对呀,除非它强烈抗议用户使用量子波动速读法

== 可是,它都明确说了禁止下载……

=> 咳!这个嘛,尽管放心,我也只是自己用啦,我也不推荐道友将这个拿出去乱搞哈。

最后,重要事情说三遍。

建议一天下载一本书,账号只有一个,要用最稳妥的方式!

建议一天下载一本书,账号只有一个,要用最稳妥的方式!

建议一天下载一本书,账号只有一个,要用最稳妥的方式!


环境配置

此部分是大家拿到源代码、在本地运行之前的配置。

从 https://github.com/Hosinoharu/WuHaiBiYuan_downloader 获取源代码。

配置 Python 环境

首先需要有 Python 3.11 及以上版本(因为我写代码时用的这个版本,我也没有用低版本的 Python 进行测试)。

# 首先进入到项目所在的目录 WuHaiBiYuan_downloader

# 这里使用的是内置的 venv 模块来创建虚拟环境
# 该虚拟环境保存在当前目录(项目所在目录)下的 .venv 目录中
python -m venv .venv

# 然后启动虚拟环境
.\.venv\Scripts\activate

# 修改该虚拟环境 pip 的下载源,大家也可以用自己常用的那个
pip config global.index-url https://mirrors.aliyun.com/pypi/simple/ --site

# 执行以下命令安装依赖包
pip install -r requirements.txt

# 测试核心的代{过}{滤}理软件是否安装正常
mitmdump.exe --version
# 执行上行命令之后会输出以下类似的信息
'''
PS > mitmdump.exe --version
Mitmproxy: 10.3.1
Python: 3.11.4
OpenSSL: OpenSSL 3.2.2 4 Jun 2024
Platform: Windows-10-10.0.19045-SP0
'''

# 然后启动代{过}{滤}理程序,开始配置证书
mitmdump.exe

配置浏览器的代{过}{滤}理

不同浏览器的代{过}{滤}理配置方式不同(有些浏览器无法单独配置代{过}{滤}理,只有配置到操作系统上),这个大家自己搜索吧。这里以 Firefox 为例,设置方式如下:

然后点击 OK 完整代{过}{滤}理设置。

配置代{过}{滤}理证书

访问 http://mitm.it 下载证书。我选择证书只用于浏览器本身。

上述的网址来自于官方文档,具体见 Getting Started (mitmproxy.org) 的说明

然后给浏览器安装下载的证书,具体步骤也可以点击上图中的 Show Instructions,下面是截图。

按下图进行选择,最后点击 OK 完成证书配置。

测试整个环境

至此环境配置结束,重新运行以下命令看看是否工作正常。

# -s 指定 py 脚本
# -q 表示 quiet,不输出冗余的信息
mitmdump.exe -s .\test\test_env.py -q

# 然后浏览器访问任意网站查看是否有输出结果

结束程序之后记得取消代{过}{滤}理

当结束 mitmdump 代{过}{滤}理之后,记得取消浏览器的代{过}{滤}理设置,不然浏览器无法访问任何网站啦。


使用方法

后文的截图可能和实际所示不同,因为现在(写下这段文字的时刻)修复了几个 bug,或者增加了另外一些功能,由于时间原因(才……才不是偷懒 (~ ̄▽ ̄)~),并没有重新截图与说明,但不影响使用。

测试翻页脚本

将项目中 web_script\web_scroll.js 复制出来,添加到油猴脚本(油猴脚本的安装、使用均省略)。

然后测试翻页脚本功能是否正常(注意,此时浏览器没有上代{过}{滤}理呢),此步骤是检测网站的 HTML 结构是否已经变化

默认情况下 3s 向下滚动一次(移动 300px),如果当前页面的 6 张小图片没有加载出来则不进行翻页

  • 如果网络慢,网页端都没有加载到图片就继续往下翻,那就不会再发出图片请求,会缺页。
  • 翻得太快网页端也不会发出图片请求,造成缺页。

不要觉得翻页慢,因为翻页的过程中完全可以用浏览器做其他的事情。“快速下载” 和 “账号” 哪一个重要大家自行判断

=> 强烈建议翻慢一点,真要是用了量子波动速度法……出了什么事我可不负责哈!

== 啊??还好我没有用宗门贡献分兑换这鸡肋术法!

=> 总之,现在只需要考虑怎么翻页翻得像人在看书就行

确认要下载的书籍

确认要下载哪本书籍,将其 id 添加到设置文件 proxy_server\settings.jsonc 中。

这是因为使用代{过}{滤}理的方案进行下载时,我们依然可以使用浏览器,为了避免下载其它无关紧要的书籍,需要提前指定要下载哪本书。

如果指定了多本书,自然可以开两个标签页同时下载,因为它们都会经过代{过}{滤}理端。

但是为了账号的安全性,不建议这样做。

下载书籍

在命令行中执行 mitmdump.exe -s  .\proxy_server\main.py -q 启动代{过}{滤}理

  1. 访问书本的阅读页,如 https://武海笔院网址/deep/read/pdf?bid=3199625,程序会自动识别书籍信息、目录信息
  2. 然后点击自动翻页按钮就行了。
  3. 之后可以干其他的事情,只需要让该浏览器标签页运行(但不要最小化)即可。

处理单页下载失败

如果下载过程中某一页没有下载也无妨,可以手动滚动页面再访问一次即可。

多次刷新网页、或者不小心关闭了标签页都无妨,代{过}{滤}理端不会重复下载已保存的图片。

处理代{过}{滤}理端重启

代{过}{滤}理端中途停掉也无妨,可以重启代{过}{滤}理、然后按照之前步骤翻页即可。

注意,当代{过}{滤}理端重新启动时,需要刷新阅读页,因为需要重新获取书籍信息、书签,这两个数据会用于判断书籍是否下载完毕、以及后续生成 PDF。同时重启代{过}{滤}理后,会提示有哪些页数还没有下载。

保存为 PDF 文件

首先是书籍所有图片、书签信息保存的目录。

等到下载完毕,就会自动合成 PDF 保存到 download_book 目录下。

如果某些原因导致:在下载完所有图片之后,并没有合并成 PDF ,那么可以执行 utils.py 来手动合并 PDF。

运行情况说明

运行过程中资源占用率较大,因为浏览器一直翻页、解析、渲染图片。这也是该方式的缺点了,但是账号安全(前提是不能翻页太快)。

最后,我下载的那本书共有 365 页,所有下载的图片共 43.4 MB,生成的 PDF 为 90.4 MB。如果大家想继续压缩大小,可以在设置文件中修改保存的图片质量。

同时下载多本书

为了账号的安全性,不建议多本下载,不过这里还是提一下。

首先要停止当前运行的代{过}{滤}理端,然后添加新的书籍 id

然后重新启动代{过}{滤}理,按照之前的步骤就可以下载了。

留影分身断因果

此部分记录整个分析过程、相关代码的逻辑解释,将来网站变动大家也能自己解决吧

  • 着重记录核心的思路,不包括如何获取书签、生成 PDF 等(已经有很多文章讲解过了)

也就是说现在可以忽略这部分内容

等到下载失败、程序运行异常(这可能是因为网站发生了变动),就可以看此部分的内容,了解大致的分析过程,再自己进行处理。

== 欸~下次变动再找你不就行了?

=> 不可,我一直在探索一个名为二次元的小世界,其内天然困阵居多,常常被困数十年,所以还是靠大家自己。而且经常更新也会无形间沾染该灵兽分身的因果。


根据之前的留影之术 —— 代{过}{滤}理方案,我们可以直接在代{过}{滤}理端保存那些小图片啦,但是问题来了:不知道这 6 张小图片的排列顺序,怎么拼接成完整的一页??

确定小图片的顺序

现在让我们进入 回忆长廊 整理思路。

# ======= 回忆长廊 =======
# 查看书籍的每一页是怎样的结构
每一页是由 6 个小图片组成的,并且在代{过}{滤}理端可以直接下载它们,但是代{过}{滤}理端不清楚这些小图片的顺序。
只要确定了顺序就可以合成书籍的一页啦。

# 当前进度:如何确定 6 个小图片的顺序

在代{过}{滤}理端只能看到请求的参数、响应等等,猜测小图片的顺序极大可能藏在这个小图片的请求链接中(不然服务器怎么知道请求的是哪个小图片呢)。

根据文章(此处内容已被大圆满层次的打码术进行删除,可通过必应搜索 武海笔院 jwt 来找到相关文章)确定参数 k 是 jwt 加密方式(此处不做解释,请自行必应搜索。它类似 base64,是一种固定的加密/编码方式),在 jwt在线解密/加密 - JSON中文网 可以进行解密,却没有发现特别明显的特征。

好啦!现在无法通过小图片的链接来判断它们的顺序,该怎么办??

哼哼,还好我精神力强大,仔细一扫,发现每次请求 6 张小图片时,会先发送 6 个请求并返回奇怪的数据,没错!就是这个!分析流程如下:

WARNING!请注意!我要起名字了,这都是为了后文便于讨论。

我将这 6 个请求称之为 req_before_split_page。表示的是:在小图片(也就是分割的图片)之前的请求。

这些 req_before_split_page(还记得这个名字不) 的返回值也是奇奇怪怪的。

好的,现在让我们进入 回忆长廊 整理思路。

# ======= 回忆长廊 =======
# 查看书籍的每一页是怎样的结构
每一页是由 6 个小图片组成的,并且在代{过}{滤}理端可以直接下载它们,但是代{过}{滤}理端不清楚这些小图片的顺序。
只要确定了顺序就可以合成书籍的一页啦。

# 当前进度:如何确定 6 个小图片的顺序
发现在获取 6 个小图片之前有 6 个请求,它们的请求参数中似乎包含了小图片的顺序信息。
而这些请求的返回值似乎还需要进一步处理才行…………

很好,现在需要确定上述 req_before_split_page 请求的响应值是如何处理的,这就需要通过 hook JSON.parse 来发现细节了。

(function () {

const deep = 5;

// 输出堆栈信息,至多向上输出 deep 层堆栈
// 因为有些网站会封装 JSON.parse 的调用
function get_caller_location() {
// stack[0] - Error 字符串
// stack[1] - 调用 new Error 的位置
// stack[2] - 调用本函数的位置
// stack[3] - 上一层的位置,后续应该从此处开始
const stack = (new Error).stack.split('\n');

if (stack.length >= 4) {
return "\t" + stack.slice(3, 3 + deep).join("\n\t");
}
return "\t" + stack.join("\n\t");
}

// 并未没有处理 .toString() 检测,先这样,够用了
const parse_proxy = new Proxy(JSON.parse, {
apply: function (target, thisArg, argumentsList) {
const result = Reflect.apply(target, thisArg, argumentsList);
console.log("===> Call JSON.parse\n", get_caller_location(), "\n", JSON.stringify(result));
return result;
}
});

Object.defineProperty(JSON, "parse", {
value: parse_proxy,
});
})();

好啦!现在让我们先刷新网页,等待页面加载完毕之后(排除掉干扰数据),注入上述代码,然后滑动鼠标滚轮,触发后续页面的加载!这样 hook JSON.parse 的结果都是有关于这些图片请求的。如下确实发现了线索!


注:若转载请注明大神论坛来源(本贴地址)与作者信息。

返回顶部