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

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

前言

这段时间准备搭建自己的博客挂到服务器上,于是想着把博客平台的文章导出,然而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短时间只能请求一次,所以在拿数据时需要做个延时,或者一篇一篇请求(我这里加了个进度条,一篇一篇请求)


1.png


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


1.png


此外暂时没遇到其他问题


下面进入到实现过程


运行前使用node环境传递参数

在package.json脚本中配置启动命令:node server -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篇博客做了个示范

image.png

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


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


写在最后

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


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


相关文章
|
7天前
|
人工智能
【经验分享】如何快速转化笔记格式为标准的MarkDown格式并进行博客发布,提高生产力?
本文介绍如何将笔记转换为Markdown格式以快速发布博客。通过使用特定的Prompt和AI工具Claude 3 Sonnet,可以将Notepad++笔记转为适合CSDN博客的Markdown格式。转换要求包括:正确标记代码段、调整缩进和格式、使用Markdown标题、列表、链接和图片语法。Claude 3 Sonnet能有效处理格式转换,将转换后的Markdown内容复制到编辑器,即可便捷发布博客。
14 2
【经验分享】如何快速转化笔记格式为标准的MarkDown格式并进行博客发布,提高生产力?
|
19天前
|
Java Maven Kotlin
[AIGC] 请你写一遍博客介绍 “使用idea+kotinlin+springboot+maven 结合开发一个简单的接口“,输出markdown格式,用中文回答,请尽可能详细
[AIGC] 请你写一遍博客介绍 “使用idea+kotinlin+springboot+maven 结合开发一个简单的接口“,输出markdown格式,用中文回答,请尽可能详细
|
3月前
|
前端开发
【Node】一键生成博客标题图片
还在为写文章时找不到标题图片而困扰吗?举个例子,CSDN的博客文章如果你不给他图片的话,那么它会按照一些默认的标签图片作为你的文章封面,例如下面这样。
30 7
|
8月前
|
前端开发 安全
博客教程markdown--- (花里胡哨篇)
博客教程markdown--- (花里胡哨篇)
46 1
|
9月前
|
前端开发 NoSQL JavaScript
node-blog:用 node 搭建的个人开源博客
node-blog:用 node 搭建的个人开源博客
69 0
|
7月前
|
机器学习/深度学习 Cloud Native Go
猫头虎博客带您使用Markdown编辑器
猫头虎博客带您使用Markdown编辑器
57 1
|
7月前
|
机器学习/深度学习 uml
如何使用CSDN的Markdown编辑器?
如何使用CSDN的Markdown编辑器?
42 1
|
8月前
|
前端开发 程序员
博客教程markdown----(文本篇)
博客教程markdown----(文本篇)
64 0
|
9月前
|
开发者
博客写作神器:介绍与部署Markdown Nice
作为开发者和写作者,我们经常需要使用Markdown来编写文档、博客和技术文稿。然而,标准的Markdown格式可能显得有些普通,缺乏一些专业的排版效果。在这里,我们将向你介绍一个强大的工具——Markdown Nice(mdnice),它可以帮助你轻松地为你的Markdown文档添加漂亮的排版效果。
173 2
博客写作神器:介绍与部署Markdown Nice
|
9月前
|
编译器
如何使用Markdown写一篇博客
如何使用Markdown写一篇博客
66 0