用js脚本下载某书的所有文章
前言
在某书上的写了好几年的文章,发现某书越来越烂了,全是广告,各种擦边标题党文章和小说等,已经不适合技术人员了。
想把某书上的文章全部下载下来整理一下,某书上是有一个下载所有文章功能的,用了以后发现下载功能现在有问题,无法下载个人账号里所有文章,不知道是不是下载功能根据日期什么判断了,还是bug了,试了好几次都这样,官方渠道只能放弃了。
手动一篇一篇粘贴的成本太高了,不仅有发布的文章,还有各种没有发布的笔记在里面,各种文章笔记加起来好几百篇呢,既然是工程师,就用工程师思维解决实际问题,能用脚本下载个人账号的下的所有文章吗?
思路梳理
由于是下载个人账号下的所有文章,包含发布的和未发布的,来看下个人账号的文章管理后台
根据操作以及分析浏览器控制台 网络
请求得知,文章管理后台逻辑是这样的,默认查询所有文集(文章分类列表), 默认选中第一个文集,查询第一个文集下的所有文章,默认展示第一篇文章的内容,点击文集,获取当前文集下的所有文章,默认展示文集中的第一篇文章,点击文章获取当前文章数据,来分析一下相关的接口请求
获取所有文集
https://www.jianshu.com/author/notebooks
这个 Get
请求是获取所有 文集
,用户信息是放在 cookie
里
来看下返回结果
[
{
"id": 51802858,
"name": "思考,工具,痛点",
"seq": -4
},
{
"id": 51783763,
"name": "安全",
"seq": -3
},
{
"id": 51634011,
"name": "数据结构",
"seq": -2
},
...
]
接口返回内容很简单,一个 json
数据,分别是:id、文集名称、排序字段。
获取文集中的所有文章
https://www.jianshu.com/author/notebooks/51802858/notes
这个 Get
请求是根据 文集id
获取所有文章,51802858
为 "思考,工具,痛点"
文集的id, 返回数据如下
[
{
"id": 103888430, // 文章id
"slug": "984db49de2c0",
"shared": false,
"notebook_id": 51802858, // 文集id
"seq_in_nb": -4,
"note_type": 2,
"autosave_control": 0,
"title": "2022-07-18", // 文章名称
"content_updated_at": 1658111410,
"last_compiled_at": 0,
"paid": false,
"in_book": false,
"is_top": false,
"reprintable": true,
"schedule_publish_at": null
},
{
"id": 98082442,
"slug": "6595bc249952",
"shared": false,
"notebook_id": 51802858,
"seq_in_nb": -3,
"note_type": 2,
"autosave_control": 3,
"title": "架构图",
"content_updated_at": 1644215292,
"last_compiled_at": 0,
"paid": false,
"in_book": false,
"is_top": false,
"reprintable": true,
"schedule_publish_at": null
},
...
]
接口返回的 json
数据里包含 文章id
和 文集名称
,这是接下来需要的字段,其他字段暂时忽略。
获取文章内容
https://www.jianshu.com/author/notes/98082442/content
这个 Get
请求是根据 文章id
获取文章 Markdown
格式内容, 98082442
为 《架构图》
文章的id, 接口返回为 Markdown
格式的字符串
{"content":"![微服务架构图 (3).jpg](https://upload-images.jianshu.io/upload_images/6264414-fa0a7893516725ff.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n"}
现在,我们了解清楚了文集,文章,以及文档内容的获取方式,接下来开始脚本实现。
代码实现
由于我是前端攻城狮,优先考虑使用 js 来实现下载,还有一个考虑因素是,接口请求里面的用户信息是通过读取 cookie
来实现的,js 脚本在浏览器的控制台执行发起请求时,会自动读取 cookie
,很方便。
如果要下载个人账号下所有文章的话,根据梳理出来的思路编写代码就行
获取所有文集id
fetch("https://www.jianshu.com/author/notebooks")
.then((res) => res.json())
.then((data) => {
// 输出所有文集
console.log(data);
})
使用fetch
函数进行请求,得到返回结果,上面的代码直接在浏览器控制台执行即可,控制台输出效果如下
根据文集数据获取所有文章
上一步得到了所有文集,使用 forEach
循环所有文集,再根据 文集id
获取对应文集下的所有文章,依然使用 fetch
进行请求
...
let wenjiArr = [];
wenjiArr = data; // 文集json数据
let articleLength = 0;
wenjiArr.forEach((item, index) => {
// 根据文集获取文章
fetch(`https://www.jianshu.com/author/notebooks/${item.id}/notes`)
.then((res2) => res2.json())
.then((data2) => {
console.log("输出文集下的所有文章:", data2);
});
});
根据文章id获取文章内容,并下载 Markdown 文件
有了文章 id, 根据 id 获取内容,得到的内容是一个对象,对象中的 content
属性是文章的 Markdown
字符串,使用 Blob
对象和 a
标签,通过 click()
事件实现下载。
在这里的代码中使用 articleLength
变量记录了一下文章数量,使用循环中的文集名称和文章名称拼成 Markdown
文件名 item.name - 《item2.title》.md
...
console.log(item.name + " 文集中的文章数量: " + data2.length);
articleLength = articleLength + data2.length;
console.log("articleLength: ", articleLength);
data2.forEach(async (item2, i) => {
// 根据文章id获取Markdown内容
fetch(`https://www.jianshu.com/author/notes/${item2.id}/content`)
.then((res3) => res3.json())
.then((data3) => {
console.log(data3);
const blob = new Blob([data.content], {
type: "text/markdown",
});
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = item.name + " - 《" + item2.title + `》.md`;
link.click();
});
});
代码基本完成,运行
在浏览器控制台中运行写好的代码,浏览器下方的下载提示嗖嗖的显示,由于没有做任何处理,当前脚本执行过程中报错了,文章下载了几十个以后就停止了,提示 429
HTTP 请求码 429 表示客户端发送了太多的请求,服务器无法处理。这种错误通常表示服务器被攻击或过载。
文章内容太多了,意料之中的情况,需要改进代码
思路改进分析
根据问题分析,脚本里的代码是循环套循环发请求的,这部分改造一下试试效果。
把每个循环里面发送 fetch
请求的外面是加个 setTimeout
, 第一个循环里面的 setTimeout
延迟参数设置为 1000 * index
, index
为当前循环的索引,第一个请求0秒后执行,后面每一次都加1秒后执行,由于文集的数量不多,大约20个,这一步这样实现是没问题的。
重点是第二个循环,根据文集获取所有文章,每个文集里多的文章超过50篇,少的可能2,3篇,这里面的 setTimeout
延迟参数这样设置 2000 * (i + index)
, i
为第二个循环的索引,这样保证在后面的请求中避免了某个时间段发送大量请求,导致丢包的问题。
再次执行代码,对比控制台输出的文章数量和下载目录中的文章(项目)数量,如果一致,说明文章都下载好了
改造后的完整代码地址
思考
整体来看,文章下载脚本的逻辑并不复杂,接口参数也简单明确,两个 forEach
循环,三个 fetch
请求,把获取到的文章内容实用 a
标签下载下来就行了。关于大量请求发送导致 429
或者请求丢失的问题,脚本中使用了一种方案,当时还想到了另外两种方案:
请求同步执行
通过同步的方式先得到所有文集下的所有文章,再根据文章列表数组同步发请求下载,请求一个个发,文章一篇篇下载
Promise.all
使用 Promise.all()
分批发送请求,避免一次请求发送太多
也可能还有其他的解决方案,欢迎大家评论区讨论交流,一起学习共同进步
^-^