【爬虫】自动获取showdoc指定项目中的所有文档

🛫 导读

需求

showdoc是一个API文档、技术文档工具网站,经常能搜到一些很好的文档,但是由于没有权限,往往无法将这些文档下载和修改。(前几年该网站还经常出现崩溃问题,使得文档无法正常查看,让人头痛不已)。
今天我们就通过技术手段,将这些文件下载到本地,方便执行其他操作。

1️⃣ 格式分析

官方下载文件内容

showdoc本身支持下载功能,首先,我们自己生成一个项目,然后查看下官网提供的格式。
在这里插入图片描述

从图中,我们可以看出,文件分为三类

  • prefix_info.json
    • 包含了项目的所有内容(文档及目录结构、文档基本信息、文档内容)。
  • prefix_readme.md
    • 从该文件描述看,它只是保存了文件标题和下载文件名的对应关系。
  • prefix_***.md
    • 这些文件是每个文档中的内容。

工具apifox可以导入showdoc,就是用到了prefix_info.json文件。所以我们继续分析该文件结构

prefix_info.json文件格式

通过工具我们将prefix_info.json格式化,可以看到,除了一些基本信息外,核心数据都保存在pages字段(与内部的pages含义不一样)。
在这里插入图片描述

内部的pages就是一个数组,包含了文章列表,如下图所示
在这里插入图片描述

内部的catalogs字段,结构如下,其中catalogs就是个无线套娃的树状结构,我们需要做的就是递归处理该逻辑。
在这里插入图片描述

2️⃣ 封包分析

打开浏览器调试界面,我们可以看到,showdoc只有两类请求:
在这里插入图片描述

/api/page/info

其中/api/page/info内容相对简单,变化的只有page_id,而所有的page_id都在/api/item/info中保存着。
在这里插入图片描述
至于该接口的返回结果,我们只需要取出page_content字段即可。
在这里插入图片描述

/api/item/info

/api/item/info请求也相对简单,需要传递item_id即可。
在这里插入图片描述
其请求结果跟上文分析的prefix_info.json极为相似。
在这里插入图片描述

3️⃣ 编码

逻辑相对简单,函数相关含义都加了注释。打开目标页面的控制台,执行函数即可。


// 获取全部菜单
function getMenu() {
  return fetch("https://source.showdoc.com.cn/server/index.php?s=/api/item/info", {
    "headers": {
      "accept": "application/json, text/plain, */*",
      "accept-language": "zh,zh-CN;q=0.9,ja;q=0.8,ko;q=0.7,en;q=0.6,zh-TW;q=0.5",
      "content-type": "application/x-www-form-urlencoded",
      "sec-ch-ua": "\"Not_A Brand\";v=\"99\", \"Google Chrome\";v=\"109\", \"Chromium\";v=\"109\"",
      "sec-ch-ua-mobile": "?0",
      "sec-ch-ua-platform": "\"macOS\"",
      "sec-fetch-dest": "empty",
      "sec-fetch-mode": "cors",
      "sec-fetch-site": "same-site"
    },
    "referrer": "https://www.showdoc.com.cn/",
    "referrerPolicy": "strict-origin-when-cross-origin",
    "body": `item_id=${my_item_id}&keyword=&default_page_id=${my_default_page_id}&user_token=${my_user_token}&_item_pwd=null`,
    "method": "POST",
    "mode": "cors",
    "credentials": "omit"
  }).then(
    res => res.json()
  ).then(j => {
    console.log(j)
    // window.my_menu = j.menu
    return j.data.menu
  })
}

// 获取页面的markdown内容
function getPage(page_id) {
  return fetch("https://source.showdoc.com.cn/server/index.php?s=/api/page/info", {
    "headers": {
      "accept": "application/json, text/plain, */*",
      "accept-language": "zh,zh-CN;q=0.9,ja;q=0.8,ko;q=0.7,en;q=0.6,zh-TW;q=0.5",
      "content-type": "application/x-www-form-urlencoded",
      "sec-ch-ua": "\"Not_A Brand\";v=\"99\", \"Google Chrome\";v=\"109\", \"Chromium\";v=\"109\"",
      "sec-ch-ua-mobile": "?0",
      "sec-ch-ua-platform": "\"macOS\"",
      "sec-fetch-dest": "empty",
      "sec-fetch-mode": "cors",
      "sec-fetch-site": "same-site"
    },
    "referrer": "https://www.showdoc.com.cn/",
    "referrerPolicy": "strict-origin-when-cross-origin",
    "body": `page_id=${page_id}&user_token=${my_user_token}&_item_pwd=null`,
    "method": "POST",
    "mode": "cors",
    "credentials": "omit"
  }).then(
    res => res.json()
  ).then(j => {
    console.log(j)
    // window.my_menu = j.menu
    return j.data.page_content
  })
}

let sleep = function (time) {
  return new Promise((resolve) => {
    setTimeout(resolve, time)
  })
}

async function deal_catalogs(catalogs) {
  for (let index = 0; index < catalogs.length; index++) {
    const element = catalogs[index];

    // 处理目录
    await deal_catalogs(element.catalogs)
    // 处理页
    await deal_pages(element.pages)
  }
}

async function deal_pages(pages) {
  for (var i = pages.length - 1; i >= 0; i--) {
    var page = pages[i]
    console.log(page.page_id)
    var page_content = await getPage(page.page_id)
    page.page_content = page_content

    // sleep 1秒
    await sleep(1*1000)
  }
}


async function main() {
  // 设置token等
  // my_user_token = '请填写token,也是可以自动获取的,执行一个fetch请求即可;或者localstory中的userinfo获取'
  my_user_token = JSON.parse(localStorage.getItem('userinfo')).user_token

  // 根据url获取item_id、default_page_id
  var paths = location.pathname.split('/')
  my_item_id = paths[1]
  my_default_page_id = paths[2] || ''
  my_menu = {}
  // 获取所有menu
  getMenu()
  .then(async menu => {
    my_menu = menu
    // 处理目录
    await deal_catalogs(menu.catalogs)
    // 处理页
    await deal_pages(menu.pages)

    // 打印信息
    var d = {
      my_item_id: my_item_id,
      my_default_page_id: my_default_page_id,
      "item_type": "1",
      "item_name": "==",
      "item_description": "====",
      "password": "***",
      "pages": my_menu
    }
    console.log( JSON.stringify(d) )
  })
}

await main()

代码特点

  • 所有函数都是同步的,使得逻辑相对简单
  • 递归调用,每个函数执行明确的逻辑,保证正常退出循环
  • 使用浏览器生成的fetch函数,不用纠结请求的参数传递问题

问题

  • 部分文档生成的json无法直接给apifox使用,需要将其中的中文等特殊字符转换为unicode编码,使用了几个网上的工具,都无法完全解决导入问题。
    • https://khz.gitee.io/Ctool/tool.html#/tool/unicode 部分page导入失败
    • https://c.runoob.com/front-end/3602/ 导入后的文章会包含%编码

📖 参考资料

**ps:**文章中内容仅用于技术交流,请勿用于违规违法行为。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜猫逐梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值