基于node实现CSDN博客导出为markdown

本文最后更新于:1 年前

目录

前言

准备工作

实现过程

实现效果

写在最后


前言

这段时间准备搭建自己的博客挂到服务器上,于是想着把博客平台的文章导出,然而CSDN没有博客导出功能,在网上搜的方式是用博客搬家导入博客园然后导出为xml文件,由于xml文件也需要解析,而且操作方式并不简单,所以写了一个服务将CSDN的博客导出为md格式文件

准备工作

  • node环境

依赖:

  • axios(http请求)
  • cheerio(html解析)
  • html-to-md(HTML转换成md)
  • single-line-log(单行显示log,用于进度条加载)

实现过程

问题一:通过community/home-api/v1/get-business-list接口可以获取到个人博客的列表,请求采用分页懒加载,并且分页大小是20,请求详情时会做请求并发拦截,同一个ip短时间只能请求一次,所以在拿数据时需要做个延时,或者一篇一篇请求(我这里加了个进度条,一篇一篇请求)

问题二:生成markdown文件名时注意标题,Windows系统文件名不支持“\/:*?\“<>|”等特殊字符

此外暂时没遇到其他问题

下面进入到实现过程

  • 运行前使用node环境传递参数
  • 在package.json脚本中配置启动命令:nodeserver-type:csdn-id:time_____
    其中type表示博客导出平台方便后续拓展,id是用户名
    *
  • 通过process.argv获取node环境下上述参数,并使用外观模式针对不同博客平台进行分离
  • 初始化axios拦截器,并分页请求文章列表接口获取文章基本数据
  • 拿到数据后爬取文章详情页面的博客内容
  • 将博客信息转换成markdown格式文件并输出至文件夹

代码如下,其中引入的MessageCenter 是发布订阅消息中心

const axios = require("axios");
const cheerio = require("cheerio");
const html2md = require("html-to-md");
const singleLineLog = require("single-line-log").stdout;
const path = require("path");
const fs = require("fs");
const { MessageCenter } = require("./lib/MessageCenter");
// 配置默认值
const defaultVal = {
  type: "csdn",
  id: "time_____",
};
// 各类博客的配置项
let blogConfig = {
  csdn: {
    // 博客分页:page:第几页,size:分页大小,businessType:排序方式,blog表示博客
    pageConfig: {
      page: 1,
      size: 20,
      businessType: "blog",
    },
    totalPage: 1, //总页数
    blogList: [],
    // 博客列表
    blogListUrl:
      "https://blog.csdn.net/community/home-api/v1/get-business-list",
    // 获取博客列表
    getBlogList() {
      return axios.get(this.blogListUrl, {
        params: {
          username: global.id,
          ...this.pageConfig,
        },
      });
    },
    getBlogItem(blog) {
      return axios.get(blog);
    },
    // 爬取数据的标签,有兴趣自己可以加
    getBlogInfo: {
      getContent: ($) => $("#content_views").html(),
      getTagsCategory: ($) => {
        const target = $(".tag-link");
        let tagsCategory = {
          tags: [],
          category: [],
        };
        for (let i = 0; i < target.length; i++) {
          const tag = target[i].children[0]["data"];
          // 通过属性data-report-click判断分类和标签
          (Object.keys(target[i].attribs).includes("data-report-click") &&
            tagsCategory["tags"].push(tag)) ||
            tagsCategory["category"].push(tag);
        }
        return tagsCategory;
      },
    },
  },
};
// 全局变量
let global = {};
// 异步函数
const asyncFunction = {
  // 分页获取博客列表
  getBlogList: async () => {
    const { data } = await blogConfig[global.type].getBlogList();
    blogConfig[global.type].totalPage = getTotalPage(
      data.total,
      blogConfig[global.type].pageConfig.size
    );
    blogConfig[global.type].blogList = concatList(
      data.list,
      blogConfig[global.type].blogList
    );
    data.list.forEach((_) => console.log(_.title));
    if (isInTotalPage()) {
      console.log(
        `获取列表成功,共${blogConfig[global.type].blogList.length}篇文章`
      );
      return MessageCenter.emit(
        "getBlogInfo",
        blogConfig[global.type].blogList
      );
    }
    await asyncFunction["getBlogList"]();
  },
  //批量获取博客详情
  getBlogInfo: async (blogList, count = 0, total) => {
    !total && (total = blogList.length);
    const blogItem = blogList[count];
    if (count++ >= total) {
      console.log("获取文章内容成功");
      return MessageCenter.emit("loadBlog", blogList);
    }
    // 进度条
    progressBar("获取文章内容中", count / total);
    blogItem.htmlContent = await blogConfig[global.type].getBlogItem(
      blogItem.url
    );
    asyncFunction["getBlogInfo"](blogList, count, total);
  },
  // 生成博客文件
  loadBlog: async (blogList) => {
    const getTagsCategory = blogConfig[global.type].getBlogInfo.getTagsCategory;
    const content = blogConfig[global.type].getBlogInfo.getContent;
    await Promise.all(
      blogList.map((_) => {
        const $ = cheerio.load(_.htmlContent);
        const { tags, category } = getTagsCategory($);
        return createMdFile(_.title, content($), _.postTime, tags, category);
      })
    );
    MessageCenter.emit("loadFinish");
  },
  loadFinish() {
    console.log("导出成功");
  },
};
// 初始化script参数
(function (argv) {
  global.type = getValue(filterArgs(argv, "type")[0], ":") || defaultVal.type;
  global.id = getValue(filterArgs(argv, "id")[0], ":") || defaultVal.id;
  initAxios();
  init();
  MessageCenter.emit("getBlogList");
})(process.argv);
function init() {
  MessageCenter.on("getBlogList", asyncFunction["getBlogList"]);
  MessageCenter.on("getBlogInfo", asyncFunction["getBlogInfo"]);
  MessageCenter.on("loadBlog", asyncFunction["loadBlog"]);
  MessageCenter.on("loadFinish", asyncFunction["loadFinish"]);
}
// 生成进度条
function progressBar(label, percentage, totalBar = 50) {
  const empty = "░";
  const step = "█";
  const target = (percentage * totalBar).toFixed();
  let bar = [];
  for (let i = 0; i < totalBar; i++) {
    (target >= i && (bar[i] = step)) || (bar[i] = empty);
  }
  singleLineLog(
    `${label || ""}  ${bar.join("")}${(100 * percentage).toFixed(2)}%`
  );
}
// 获取页数
function getTotalPage(total, size) {
  return Math.round(total / size);
}
// 是否是最后一页
function isInTotalPage() {
  return (
    blogConfig[global.type].pageConfig.page++ >=
    blogConfig[global.type].totalPage
  );
}
// npm script参数判断
function filterArgs(args, key) {
  return args.filter((_) => _.includes(key));
}
// 拆分字符串
function getValue(str, keyWord) {
  return typeof str === "string" && str.split(keyWord)[1];
}
// 替换特殊字符
function replaceKey(str) {
  const exp = /[`\/:*?\"<>|\s]/g;
  // /[`~!@#$^&*()=|{}':;',\\\[\]\.<>\/?~!@#¥……&*()——|{}【】';:""'。,、?\s]/g;
  return str.replace(exp, " ");
}
// 连接列表数组
function concatList(list, targetList) {
  return [...targetList, ...list];
}

// 生成博客md,title文章标题, content文章内容, date文章时间, tags文章标签, category文章分类
function createMdFile(title, content, date, tags, category) {
  return writeFile(
    `${replaceKey(title)}.md`,
    `${createMdTemplete(title, date, tags, category)}${html2md(content)}`,
    "./blog/"
  );
}
// md文件模板配置
function createMdTemplete(title, date, tags, category) {
  return `---\ntitle:  ${title} \ndate:  ${date} \ntags:  [${tags}] \ncategory:  [${category}] \n---\n`;
}
// 写入文件
function writeFile(filename, data, dir) {
  return new Promise((resolve, reject) => {
    fs.writeFile(
      path.join(__dirname, dir + filename),
      data,
      (err) => (err && reject(err)) || resolve(err)
    );
  });
}
//响应拦截器
function initAxios() {
  axios.interceptors.response.use(
    function ({ data, status }) {
      if (data.code === 200 || status === 200) {
        return data;
      }
      return Promise.reject(data);
    },
    function (error) {
      // 对响应错误做点什么
      console.log(error);
      return Promise.reject(error);
    }
  );
}

实现效果

这里我只用了5篇博客做了个示范

至此,使用node导出csdn博客的内容就实现完成

最后使用导出的md文件导入到自己的博客服务器吧

写在最后

源码:myCode: 一些小案例 - Gitee.com

脚本+Jenkins+hexo完整版:blog_website: 基于 node 编写的CSDN博客导出的爬虫脚本+hexo部署

感谢你看到这里,如果文章对你有帮助,请三连支持一下,你的支持是作者创作的动力