背景
不想继续在粪坑网站写博客了,于是自己用Vuepress搭了一个静态博客,配合github pages和服务器使用效果还挺不错。但之前写的博客基本全在CSDN,想导出来,人工一篇一篇复制就太累了,于是用go写了个爬虫自动把自己csdn上的所有文章的markdown源码导出并迁移到新站点上。这也应该是我CSDN上的最后一篇博客
思路
- 个人主页https://blog.csdn.net/Apale_8可以获取博客列表
 - 编辑页可以查看markdown源码
 - 导出为统一的格式(标题、写作时间、分类、标签和内容等信息)
 - 转换为VuePress的格式
 
博客列表
可以观察到博客列表是动态加载的,因此一定有接口

f12查看网络,先清空之前的所有请求,然后向下滚动,可以看到一个这样的接口


参数很简单,page和size,所以直接以page=1,size取一个>=自己博客数量的数字即可获取到所有博客的链接
package main
import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)
const listApi = `https://blog.csdn.net/community/home-api/v1/get-business-list?page=%d&size=%d&businessType=lately&noMore=false&username=%s`
func getArticleList(username string) []Meta {
	resp, err := http.Get(fmt.Sprintf(listApi, 1, blogNum, username))
	if err := err; err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	var respData ListRespData
	b, err := io.ReadAll(resp.Body)
	if err := err; err != nil {
		panic(err)
	}
	if err := json.Unmarshal(b, &respData); err != nil {
		panic(err)
	}
	fmt.Printf("读取博客列表成功, 总共: %d 篇\n", len(respData.Data.List))
	return respData.Data.List
}
type ListRespData struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
	Data    Data   `json:"data"`
}
type Meta struct {
	Type         string `json:"type"`
	FormatTime   string `json:"formatTime"`
	Title        string `json:"title"`
	Description  string `json:"description"`
	HasOriginal  bool   `json:"hasOriginal"`
	DiggCount    int    `json:"diggCount"`
	CommentCount int    `json:"commentCount"`
	PostTime     int64  `json:"postTime"`
	CreateTime   int64  `json:"createTime"`
	URL          string `json:"url"`
	ArticleType  int    `json:"articleType"`
	ViewCount    int    `json:"viewCount"`
	Rtype        string `json:"rtype"`
}
type Data struct {
	List  []Meta      `json:"list"`
	Total interface{} `json:"total"`
}
(代码里resp的几个结构体直接拷贝网页请求用工具生成即可)
博客内容
博客内容要从编辑页面获取,因此需要登录
先f12从请求里面复制一下cookie备用

然后在编辑页面按f12找找接口,是可以找到getArticle这个接口的
https://bizapi.csdn.net/blog-console-api/v3/editor/getArticle?id=119971640&model_type=
但遗憾的是这个接口做了签名认证,读懂前端js的签名逻辑对我还是有点困难,遂放弃
柳暗花明
github搜了一圈,发现有人用另一个不需要签名的接口爬过csdn markdown,应该是一个旧版的接口,但还能用
https://blog-console-api.csdn.net/v1/editor/getArticle?id=%s
于是成功地爬到了博客内容
需要注意的是,富文本编辑器和markdown编辑器的内容,在不同的字段,需要判断下
const (
	articleApi = `https://blog-console-api.csdn.net/v1/editor/getArticle?id=%s`
)
func getArticle(meta Meta) Article {
	articleData := getArticleContent(meta)
	content := articleData.Markdowncontent
	contentType := "markdown"
	if content == "" { // 如果没有markdown内容, 就是用富文本编辑器编写的
		content = articleData.Content
		contentType = "HTML"
	}
	return Article{
		Content:     content,
		Title:       articleData.Title,
		CreateTime:  meta.CreateTime,
		FormatTime:  meta.FormatTime,
		Categories:  articleData.Categories,
		Tags:        strings.Split(articleData.Tags, ","),
		ContentType: contentType,
	}
}
func getArticleContent(meta Meta) ArticleData {
	client := &http.Client{}
	req, err := http.NewRequest("GET", fmt.Sprintf(articleApi, meta.URL[strings.LastIndex(meta.URL, "/")+1:]), nil)
	req.Header.Set("cookie", cookie)
	resp, err := client.Do(req)
	if err := err; err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	var respData ArticleRespData
	if err := json.NewDecoder(resp.Body).Decode(&respData); err != nil {
		panic(err)
	}
	return respData.Data
}
type ArticleRespData struct {
	Code int         `json:"code"`
	Msg  string      `json:"msg"`
	Data ArticleData `json:"data"`
}
type ArticleData struct {
	ArticleID        string        `json:"article_id"`
	Title            string        `json:"title"`
	Description      string        `json:"description"`
	Content          string        `json:"content"`
	Markdowncontent  string        `json:"markdowncontent"`
	Tags             string        `json:"tags"`
	Categories       string        `json:"categories"`
	Type             string        `json:"type"`
	Status           int           `json:"status"`
	ReadType         string        `json:"read_type"`
	Reason           string        `json:"reason"`
	ResourceURL      string        `json:"resource_url"`
	OriginalLink     string        `json:"original_link"`
	AuthorizedStatus bool          `json:"authorized_status"`
	CheckOriginal    bool          `json:"check_original"`
	EditorType       int           `json:"editor_type"`
	Plan             []interface{} `json:"plan"`
	VoteID           int           `json:"vote_id"`
	ScheduledTime    int           `json:"scheduled_time"`
	Level            string        `json:"level"`
	CoverType        string        `json:"cover_type"`
	CoverImages      []interface{} `json:"cover_images"`
}
type Article struct {
	Title       string
	CreateTime  int64
	FormatTime  string
	Categories  string
	Tags        []string
	ContentType string
	Content     string // 有markdown则存markdown,没有则存html
}
转换格式
VuePress里面格式是这样的

简单转一下即可
输出的文件名用了随机串,因为中文名称在编译的时候有点问题
func toMarkdownForVuePress(a Article, path string) {
	if path[:len(path)-1] != "/" {
		path += "/"
	}
	content := fmt.Sprintf(vuePressFormat, a.Title, strings.ReplaceAll(a.FormatTime, ".", "-"), genTags(a.Tags...), genTags(a.Categories), a.Content)
	os.WriteFile(path+uuid.New()+".md", []byte(content), 0o644) // 写入文件, uuid作为文件名,因为中文名可能有问题
}










