前言
在某书上的写了好几年的文章,发现某书越来越烂了,全是广告,各种擦边标题党文章和小说等,已经不适合技术人员了。
想把某书上的文章全部下载下来整理一下,某书上是有一个下载所有文章功能的,用了以后发现下载功能现在有问题,无法下载个人账号里所有文章,不知道是不是下载功能根据日期什么判断了,还是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()
分批发送请求,避免一次请求发送太多
也可能还有其他的解决方案,欢迎大家评论区讨论交流,一起学习共同进步
^-^