从语雀到本地:打造一个文档导出工具

项目背景

在日常的文档管理中,我们经常会使用语雀等在线文档平台来协作编写和管理文档。但有时候我们需要将这些文档导出到本地,进行备份或者进一步处理。虽然语雀提供了 API 接口,但手动调用和处理还是比较繁琐的。

于是我开发了这个简单而实用的工具,可以自动从语雀 API 获取文档树形结构,批量下载文档内容和图片,并保存为本地的 Markdown 文件。

技术栈

这个项目使用了以下技术:

  • Node.js: JavaScript 运行环境
  • ES6+ 模块化 : 使用 import/export 语法
  • Axios: 强大的 HTTP 客户端,用于请求语雀 API
  • 文件系统操作 : Node.js 原生的 fs 模块
  • 路径处理 : Node.js 原生的 path 模块

核心功能

1. 获取文档树形结构

首先通过语雀的 TOC API 获取知识库的完整文档树:

javascript 复制代码
const TreeData = axios.get(
  "https://www.yuque.com/api/v2/repos/namespace/toc",
  // www可替换公司内语雀域名
  // namespace 替换为语雀知识库的命名空间
  {
    headers: {
      "Content-Type": "application/json",
      "X-Auth-Token": "your-token-here",
    },
  },
);

这个 API 会返回知识库中所有文档和目录的层级结构,包括:

  • 文档类型(DOC/TITLE)
  • 文档标题
  • 文档 slug(用于访问具体文档)
  • 层级关系(父子、兄弟节点)

2. 图片下载和本地化

文档中的图片通常存储在远程服务器上,为了让 Markdown 文件可以离线使用,需要把图片下载到本地:

javascript 复制代码
async function downloadImage(imageUrl, imageDirPath, fileName) {
  try {
    const response = await axios.get(imageUrl, {
      responseType: "arraybuffer",
      timeout: 10000,
    });
    const imagePath = path.join(imageDirPath, fileName);
    fs.writeFileSync(imagePath, response.data);
    console.log(`🖼️  图片已下载: ${fileName}`);
    return imagePath;
  } catch (err) {
    console.error(`❌ 图片下载失败: ${imageUrl} - ${err.message}`);
    return null;
  }
}

关键点:

  • 使用 responseType: "arraybuffer" 获取二进制图片数据
  • 设置 10 秒超时,避免因网络问题导致程序卡死
  • 错误处理:下载失败时记录日志但不中断整个流程

3. Markdown 图片路径替换

下载图片后,需要替换 Markdown 中的图片链接:

javascript 复制代码
async function processImages(content, imageDirPath) {
  // 确保 image 目录存在
  if (!fs.existsSync(imageDirPath)) {
    fs.mkdirSync(imageDirPath, { recursive: true });
  }

  let updatedContent = content;
  let imageIndex = 0;

  // 匹配 markdown 图片格式: ![alt](url)
  const markdownImageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
  const matches = [...content.matchAll(markdownImageRegex)];

  for (const match of matches) {
    const imageUrl = match[2];
    const alt = match[1];

    // 跳过本地路径
    if (imageUrl.startsWith("file://") || imageUrl.startsWith("/")) {
      continue;
    }

    // 生成本地图片名称
    const urlParts = imageUrl.split("?")[0]; // 去掉查询参数
    const ext = urlParts.split(".").pop() || "png";
    const fileName = `image_${imageIndex++}.${ext}`;

    // 下载图片
    const imagePath = await downloadImage(imageUrl, imageDirPath, fileName);

    if (imagePath) {
      // 替换为相对路径
      const absolutePath = `/mkdir/image/${fileName}`;
      updatedContent = updatedContent.replace(
        `![${alt}](${imageUrl})`,
        `![${alt}](${absolutePath})`,
      );
    }
  }

  return updatedContent;
}

这个函数的亮点:

  • 使用正则表达式 matchAll 找出所有图片链接
  • 跳过已经是本地路径的图片
  • 自动提取图片扩展名
  • 按顺序命名图片文件(image_0.png, image_1.jpg...)
  • 替换原始 URL 为本地路径

4. 批量处理文档

主流程中遍历所有文档项,批量下载和保存:

javascript 复制代码
TreeData.then(async (res) => {
  const data = res.data.data;

  // 用于保存所有项的分层信息
  const structureInfo = [];

  // 遍历所有项
  for (const item of data) {
    // 保存分层信息到数组
    structureInfo.push({
      uuid: item.uuid,
      type: item.type,
      title: item.title,
      slug: item.slug,
      id: item.id,
      level: item.level,
      depth: item.depth,
      parent_uuid: item.parent_uuid,
      child_uuid: item.child_uuid,
      sibling_uuid: item.sibling_uuid,
    });

    // 如果是文档类型,获取内容并保存
    if (item.type === "DOC") {
      try {
        const docRes = await axios.get(
          `https://easedata.yuque.com/api/v2/repos/wt15ay/wwuyoq/docs/${item.slug}?raw=1`,
          {
            headers: {
              "X-Auth-Token": "your-token-here",
            },
          },
        );

        let content = docRes.data.data?.body || "";

        // 处理图片下载和替换
        content = await processImages(content, imageDir);

        const filePath = path.join(mdDir, `${item.slug}.md`);
        fs.writeFileSync(filePath, content, "utf-8");
        console.log(`📄 创建文件: ${filePath}`);
      } catch (err) {
        console.error(`❌ 获取文档失败 ${item.title}:`, err.message);
      }
    } else if (item.type === "TITLE") {
      console.log(`📁 目录项: ${item.title}`);
    }
  }

  // 保存分层结构到 JSON 文件
  const structureFilePath = path.join(mdDir, "structure.json");
  fs.writeFileSync(
    structureFilePath,
    JSON.stringify(structureInfo, null, 2),
    "utf-8",
  );
  console.log(`\n📋 分层结构已保存: ${structureFilePath}`);
  console.log("✅ 所有文档已生成完毕!");
}).catch((err) => {
  console.error("❌ 获取树形结构失败:", err.message);
});

处理逻辑:

  1. 收集所有文档的结构信息
  2. 针对 DOC 类型的文档,获取完整内容
  3. 使用 ?raw=1 参数获取 Markdown 原始内容
  4. 处理图片下载和路径替换
  5. 保存为 .md 文件
  6. 最后导出完整的结构信息为 JSON 文件

项目结构

复制代码
umi-APi/
├── index.js          # 主程序入口
├── package.json      # 项目配置文件
├── pnpm-lock.yaml    # 依赖锁定文件
├── node_modules/     # 依赖包
└── mkdir/            # 导出的文档目录
    ├── image/        # 下载的图片
    ├── *.md          # 导出的 Markdown 文件
    └── structure.json # 文档结构信息

使用方法

1. 安装依赖

bash 复制代码
pnpm install

2. 配置 Token

在代码中替换你的语雀 API Token:

javascript 复制代码
headers: {
  "X-Auth-Token": "your-yuque-token-here",
}

3. 运行程序

bash 复制代码
node index.js

运行后,程序会:

  • 创建 mkdirmkdir/image 目录
  • 下载所有文档到 mkdir 目录
  • 下载所有图片到 mkdir/image 目录
  • 生成 structure.json 文件记录文档结构

技术亮点

1. ES6 模块化

项目使用了 ES6 的 import/export 语法,在 package.json 中设置了 "type": "module",让 Node.js 支持原生 ES 模块。

2. 异步处理

大量使用 async/await 来处理异步操作,代码清晰易读,避免了回调地狱。

3. 正则表达式处理

使用正则表达式和 matchAll 方法,精准匹配和替换 Markdown 中的图片链接。

4. 错误处理

在关键的网络请求处理中都加入了 try-catch,确保单个文档或图片失败不会影响整体流程。

5. 文件系统操作

灵活使用 fs 模块的同步和异步方法,合理处理目录创建、文件读写等操作。

可能的改进方向

  1. 配置文件化: 将 Token、知识库 ID 等配置项提取到配置文件中
  2. 命令行参数: 支持通过命令行参数指定知识库 ID 和输出目录
  3. 并发控制: 对于大量文档和图片的下载,可以添加并发控制,提高效率
  4. 增量更新: 支持检测已下载的文档,只更新有变化的部分
  5. 进度显示: 添加进度条,让用户了解下载进度
  6. 目录结构还原: 根据文档的层级关系创建对应的文件夹结构
  7. 重试机制: 对于失败的请求添加自动重试

总结

这个工具虽然代码量不多,但实现了一个完整的文档导出功能。通过学习这个项目,你可以掌握:

  • 如何使用 Node.js 进行文件系统操作
  • 如何调用第三方 API
  • 如何处理异步流程
  • 如何进行字符串正则匹配和替换
  • 如何下载和处理二进制文件

希望这个小工具能对你的文档管理工作有所帮助!如果你有任何改进建议,欢迎交流讨论。

参考资料


本文档由作者原创,转载请注明出处。

相关推荐
新缸中之脑13 小时前
NodeLLM:Node.js的AI基础设施
人工智能·node.js
csdn_aspnet13 小时前
JavaScript常用算法深度解析:从浏览器到Node.js的实战
javascript·node.js
michael_ouyang13 小时前
IM 会话同步企业级方案选型
前端·websocket·electron·node.js
绝世这天下14 小时前
【使用 NVM 安装 Node.js 22 并配置国内镜像加速】
node.js
EndingCoder15 小时前
Node.js 与 TypeScript:服务器端开发
前端·javascript·typescript·node.js
web小白成长日记1 天前
Node.js 编程实战:部署 Node.js 应用 —— Docker 容器化部署
docker·容器·node.js
*小雪2 天前
nvm的安装与管理和npm audit的报错解决
前端·npm·node.js
xinhuanjieyi2 天前
将 Node.js 安装到 /ext 目录的办法
node.js
weixin_427771612 天前
npm 绕过2FA验证
前端·npm·node.js