Go语言爬虫系列教程 实战项目JS逆向实现CSDN文章导出教程

爬虫实战:JS逆向实现CSDN文章导出教程

在这篇教程中,我将带领大家实现一个实用的爬虫项目:导出你在CSDN上发布的所有文章。通过分析CSDN的API请求签名机制,我们将绕过平台限制,获取自己的所有文章内容,并以Markdown格式保存到本地。## 1.基础知识:什么是JS逆向?

1.1 JS逆向的概念

JavaScript逆向工程是指分析网站的前端JavaScript代码,理解其加密、签名等机制,并用其他编程语言重新实现的过程。

1.2 为什么需要逆向?

现代网站通常会对API请求进行保护:

  • 签名验证:防止恶意请求
  • 频率限制:防止过度访问
  • 身份验证:确保请求来源合法## 2. CSDN API分析

2.1 准备工作

在开始分析之前,让我们做好准备工作:

环境准备:

  1. 浏览器:推荐使用Chrome或Firefox,开发者工具功能强大
  2. CSDN账户:需要登录才能访问文章管理页面
  3. 基础知识:了解HTTP请求、JSON格式、浏览器开发者工具的基本使用获取登录Cookie:

重要提示:Cookie是你的登录凭证,相当于临时身份证,不要泄露给他人!

  1. 打开CSDN网站并登录
  2. 按F12打开开发者工具
  3. 切换到"网络"标签页(Network)
  4. 刷新页面,找到任意请求
  5. 在请求头中找到Cookie字段,复制其值

2.2 获取API和观察API请求

为了避免文章格式错乱,我是通过内容管理页面来获取文稿的原始数据。通过浏览器开发者工具,发现文章列表的请求地址为https://bizapi.csdn.net/blog/phoenix/console/v1/article/list?page=2&pageSize=20 ,请求参数主要有两个page=2pageSize=20

在这里插入图片描述

接着我们看下请求头,发现请求包含以下特殊的请求头:

  • X-Ca-Key:API密钥
  • X-Ca-Nonce:随机字符串
  • X-Ca-Signature:请求签名
  • X-Ca-Signature-Headers:用于签名的请求头字段
    在这里插入图片描述

2.3 使用Postman模拟请求

我们先使用Postman模拟请求,看下那些值是必须的,经过测试,发现这四个值是不能缺少的
在这里插入图片描述

2.4 定位加密算法

经过多次刷新请求 ,发现 X-Ca-Key 为固定值203803574 , x-ca-signature-headers 也是固定值x-ca-key,x-ca-nonce ,这是一个初步分析的过程。这些请求参数一般都是通过JS生成,所以我们现在需要去分析下Js , 使用浏览器开发者工具,搜索关键词
在这里插入图片描述

因为进过测试已经知道X-Ca-Key的值是固定的203803574 ,我们搜索这个值,(当然也可以搜索 X-Ca-KeyX-Ca-NonceX-Ca-SignatureX-Ca-Signature-Headers 这些请求参数的名字,这个需要根据网站灵活变动)。 经过搜索我们发现了好几个结果,这个需要根据代码来判断,加密方法到底在哪个js文件中。
在这里插入图片描述

点击搜索结果,我们发现 X-Ca-NonceLe函数生成,X-Ca-Signature 是通过Ee函数生成的
在这里插入图片描述
断点是调试的利器,可以让代码执行暂停,观察变量值。

  1. 找到可能的加密函数后,在行号左侧点击设置断点
  2. 重新发起请求,代码会在断点处暂停
  3. 在控制台中输入变量名,查看其值
  4. 使用单步执行功能,跟踪代码执行过程

我们可以通过添加断点,来看下这些变量的具体值
在这里插入图片描述

注意:断点是一个很有用的功能,一定要善于使用我们可以在这个JS文件中去寻找Le函数和Ee函数,当然如果你没有耐心,因为我们已经添加了断点,可以使用浏览器的控制台去输入函数名查看函数的具体实现和具体位置。

在这里插入图片描述

直接点击输出结果,会自动跳转到代码的位置,
在这里插入图片描述

通过分析网站的JavaScript代码,我们发现了签名生成的核心代码:

const Pe = e => {let t = {};for (let a in e) {let c = a.toLowerCase();c.startsWith("x-ca-") && ("x-ca-signature" !== c && "x-ca-signature-headers" !== c && "x-ca-key" !== c && "x-ca-nonce" !== c || (t[c] = e[a]))}return t
}// ... 其他代码 ...const Ee = ({method, url, appSecret, accept, date, contentType, params, headers}) => {let stringToSign = "";// 构建待签名字符串stringToSign += method + "\n";           // HTTP方法stringToSign += accept + "\n";           // Accept头stringToSign += "\n";                    // 空行stringToSign += contentType + "\n";      // Content-TypestringToSign += date + "\n";            // 日期// 添加特定头部// ... 处理headers ...// 添加URL和参数// ... 处理URL和查询参数 ...// 使用HMAC-SHA256计算签名const signature = CryptoJS.HmacSHA256(stringToSign, appSecret);return signature.toString(CryptoJS.enc.Base64);
}

2.5 分析加密函数

通过分析提供的JavaScript代码,可以看2个关键头部字段的生成方式: X-Ca-Nonce是一个随机生成的UUID,主要依赖于Ue函数,该函数使用随机数替换UUID模板中的’x’和’y’字符。Le函数会检查是否已有nonce,如果没有则生成一个新的。X-Ca-Signature是请求内容的HMAC-SHA256签名,生成过程如下:

1.构建待签名的字符串,包含:

method: s,  //请求方法(method)
url: r,   //API URLaccept: t,  //Accept头params: n,  //请求参数date: a,   //日期contentType: o, headers: e.headers,  //请求头appSecret: l  //加密Secret(固定值)

2.提取特定的请求头
3.对URL参数进行排序
4.使用HMAC-SHA256算法对构建的字符串进行加密
5.将结果转为Base64编码## 2. Go语言实现签名算法

根据分析的签名生成逻辑,我们用Go语言实现相同的功能:

2.1 UUID生成

首先实现一个生成UUID格式随机字符串的函数,用于X-Ca-Nonce:

// 生成UUID格式的随机字符串
func generateUUID() string {rand.Seed(time.Now().UnixNano())uuid := "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"result := ""for _, char := range uuid {if char == 'x' || char == 'y' {randomInt := rand.Intn(16)var value intif char == 'x' {value = randomInt} else {// char == 'y'// y的值必须是8、9、A、B中的一个value = (randomInt & 0x3) | 0x8}result += fmt.Sprintf("%x", value)} else {result += string(char)}}return result
}// 生成X-Ca-Nonce
func generateNonce() string {return generateUUID()
}

2.2 签名生成

然后实现签名生成算法:

// 生成CSDN API签名
func generateSignature(method, requestURL, appSecret, accept string, headers map[string]string) string {// 1. 解析URL,分离路径和查询参数parsedURL, _ := url.Parse(requestURL)path := parsedURL.Path   // 获取路径部分query := parsedURL.Query()     // 获取查询参数// 2. 构建待签名的字符串(严格按照顺序)stringToSign := strings.ToUpper(method) + "\n" // HTTP方法,必须大写stringToSign += accept + "\n"    // Accept头stringToSign += "\n" // 空行// 签名时Content-Type需要设为空stringToSign += "\n"stringToSign += "\n" // 日期为空// 添加特定请求头stringToSign += "x-ca-key:" + headers["x-ca-key"] + "\n"stringToSign += "x-ca-nonce:" + headers["x-ca-nonce"] + "\n"// 添加路径stringToSign += path// 如果有查询参数,添加排序后的查询参数if len(query) > 0 {queryKeys := make([]string, 0, len(query))for k := range query {queryKeys = append(queryKeys, k)}sort.Strings(queryKeys)queryParts := []string{}for _, key := range queryKeys {values := query[key]for _, value := range values {queryParts = append(queryParts, key+"="+value)}}queryString := strings.Join(queryParts, "&")fmt.Println(queryString)stringToSign += "?" + queryString}// 使用HMAC-SHA256计算签名h := hmac.New(sha256.New, []byte(appSecret))h.Write([]byte(stringToSign))signature := base64.StdEncoding.EncodeToString(h.Sum(nil))return signature
}

2.3 生成完整请求头

接下来实现生成完整请求头的函数:

func generateRequestHeaders(method, requestURL, appKey, appSecret, accept string) map[string]string {// 创建基本头部headers := map[string]string{"Accept":   accept,"x-ca-key": appKey,}// 生成noncenonce := generateNonce()headers["x-ca-nonce"] = nonce// 生成签名signature := generateSignature(method, requestURL, appSecret, accept, headers)headers["x-ca-signature"] = signature// 添加签名头列表headers["x-ca-signature-headers"] = "x-ca-key,x-ca-nonce"return headers
}
```## 3.项目具体实现实现步骤:1. 通过文章列表API`https://bizapi.csdn.net/blog/phoenix/console/v1/article/list` 获取到文章id
2. 通过文章ID, 访问文章详情API`https://bizapi.csdn.net/blog-console-api/v3/editor/getArticle?id=[id] `
3. 通过API获取到文章的 title、markdowncontent、content  ,把文章保存到本地
### 3.1 常量定义```go
package mainconst (// CSDN API相关常量APP_KEY    = "203803574"                                    // 固定的API密钥APP_SECRET = "9znpamsyl2c7cdrr9sas0le9vbc3r6ba"            // 用于签名的密钥ACCEPT     = "application/json, text/plain, */*"           // Accept头OUTPUT_DIR = "Output/"                                      // 输出目录// 请替换为你自己的Cookie!// 获取方法:登录CSDN后,在开发者工具的Network标签中找到任意请求的Cookie头COOKIE = `你的CSDN_Cookie_这里`)

3.2 实现HTTP请求函数

现在,我们需要创建一个函数来发送带有正确签名的HTTP请求:

func CSDNGetHttp(requestURL string) string {method := "GET"headers := generateRequestHeaders(method, requestURL, APP_KEY, APP_SECRET, ACCEPT)client := &http.Client{}req, err := http.NewRequest(method, requestURL, nil)if err != nil {fmt.Println(err)return ""}// 添加所有必要的请求头req.Header.Add("accept", "application/json, text/plain, */*")req.Header.Add("accept-encoding", "gzip, deflate, br, zstd")req.Header.Add("accept-language", "zh-CN,zh;q=0.9")req.Header.Add("cookie", Cookie) // 使用预设的Cookiereq.Header.Add("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")req.Header.Add("x-ca-key", "203803574")req.Header.Add("x-ca-nonce", headers["x-ca-nonce"])req.Header.Add("x-ca-signature", headers["x-ca-signature"])req.Header.Add("x-ca-signature-headers", "x-ca-key,x-ca-nonce")res, err := client.Do(req)if err != nil {fmt.Println(err)return ""}defer res.Body.Close()body, err := io.ReadAll(res.Body)if err != nil {fmt.Println(err)return ""}return string(body)
}

3.3 定义数据结构

为了方便解析API返回的JSON数据,我们定义了两个结构体:

// 文章列表结构
type ArticleList struct {Code int `json:"code"`Data struct {List []struct {ArticleId string `json:"articleId"` // 文章IDTitle     string `json:"title"`     // 文章标题} `json:"list"`Page  int `json:"page"`  // 页码Size  int `json:"size"`  // 每页大小Total int `json:"total"` // 总记录数} `json:"data"`
}// 文章详情结构
type Article struct {Code    int    `json:"code"`TraceId string `json:"traceId"`Data    struct {ArticleId       string `json:"article_id"`Title           string `json:"title"`           // 文章标题Content         string `json:"content"`         // HTML内容Markdowncontent string `json:"markdowncontent"` // Markdown内容} `json:"data"`Msg string `json:"msg"`
}

3.4 实现文章导出功能

最后,我们实现完整的文章导出功能:

func main() {ArticleIds := make([]string, 0)// 获取第一页文章列表content := CSDNGetHttp("https://bizapi.csdn.net/blog/phoenix/console/v1/article/list?page=1&pageSize=20")var firstArticleList ArticleListerr := json.Unmarshal([]byte(content), &firstArticleList)if err != nil {fmt.Println(err)}// 提取第一页文章IDfor _, article := range firstArticleList.Data.List {ArticleIds = append(ArticleIds, article.ArticleId)}total := firstArticleList.Data.TotalpageMax := 0// 计算总页数if total > 20 {if total%20 == 0 {pageMax = total / 20} else {pageMax = total/20 + 1}// 获取剩余页的文章for page := 2; page <= pageMax; page++ {requestURL := fmt.Sprintf("https://bizapi.csdn.net/blog/phoenix/console/v1/article/list?page=%d&pageSize=20", page)content1 := CSDNGetHttp(requestURL)var articleList ArticleListerr := json.Unmarshal([]byte(content1), &articleList)if err != nil {fmt.Println(err)}for _, article := range articleList.Data.List {fmt.Println(article.Title)ArticleIds = append(ArticleIds, article.ArticleId)}}}fmt.Println("总文章数:", len(ArticleIds))// 创建输出目录os.MkdirAll(Output, os.ModePerm)// 获取每篇文章详情并保存for _, articleId := range ArticleIds {rawUrl := fmt.Sprintf("https://bizapi.csdn.net/blog-console-api/v3/editor/getArticle?id=%s", articleId)content := CSDNGetHttp(rawUrl)var article Articleerr := json.Unmarshal([]byte(content), &article)if err != nil {fmt.Println(err)}title := article.Data.TitlemarkdownContent := article.Data.Markdowncontenterr = savetoMdfile(Output+title, markdownContent)if err != nil {fmt.Println(err)}// 避免请求频率过高time.Sleep(4 * time.Second)}
}// 将文章保存为Markdown文件
func savetoMdfile(title, markdownContent string) error {// 构建文件名filename := fmt.Sprintf("%s.md", title)// 创建或打开文件file, err := os.Create(filename)if err != nil {return fmt.Errorf("创建文件失败: %w", err)}defer file.Close()// 写入Markdown内容_, err = file.WriteString(markdownContent)if err != nil {return fmt.Errorf("写入文件失败: %w", err)}fmt.Printf("文件 %s 保存成功\n", filename)return nil
}

3.5 运行程序

将以上代码整合成一个完整的Go程序,设置好以下常量:

const (APP_KEY    = "203803574"APP_SECRET = "9znpamsyl2c7cdrr9sas0le9vbc3r6ba"ACCEPT     = "application/json, text/plain, */*"Output     = "Output/"Cookie     = `你的CSDN Cookie` // 替换为你的CSDN登录Cookie
)

然后编译并运行程序:

go build -o csdn_exporter
./csdn_exporter

程序将会:

  1. 获取你CSDN博客的所有文章
  2. 下载每篇文章的Markdown内容
  3. 将文章保存到Output目录下的Markdown文件中

4. 注意事项

  1. Cookie获取:请使用浏览器开发者工具获取自己的CSDN Cookie
  2. 请求频率:为避免触发CSDN的反爬机制,程序每获取一篇文章后会等待4秒
  3. 文件命名:文件名使用文章标题,可能需要处理标题中的特殊字符
  4. 合法使用:本教程仅用于导出自己的CSDN文章,请勿用于非法目的

免责声明:本教程仅用于学习和研究目的,请遵守CSDN的用户协议和相关法律法规,仅导出自己的文章内容。作者不对任何滥用行为负责。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/80101.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

交叉熵损失函数,KL散度, Focal loss

交叉熵损失函数&#xff08;Cross-Entropy Loss&#xff09; 交叉熵损失函数&#xff0c;涉及两个概念&#xff0c;一个是损失函数&#xff0c;一个是交叉熵。 首先&#xff0c;对于损失函数。在机器学习中&#xff0c;损失函数就是用来衡量我们模型的预测结果与真实结果之间…

149.WEB渗透测试-MySQL基础(四)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易锦网校会员专享课 上一个内容&#xff1a;148.WEB渗透测试-MySQL基础&#xff08;三&#xff09; 非关系型数据库&#xff1a; &a…

c/c++中程序内存区域的划分

c/c程序内存分配的几个区域&#xff1a; 1.栈区&#xff1a;在执行函数时&#xff0c;函数内局部变量的存储单元都可以在栈上创建&#xff0c;函数执行结束时这些存储单元自动被释放&#xff0c;栈内存分配运算内置于处理器的指令集中&#xff0c;效率很高但是分配的内存容量有…

构建稳定的金字塔模式生态:从自然法则到系统工程

在自然界中&#xff0c;金字塔结构广泛存在于生态系统之中&#xff0c;表现为营养级能量金字塔、生物量金字塔和数量金字塔等形式。这种结构不仅形象地描述了生态能量流转的规律&#xff0c;也体现出生态系统中“稳定性”与“层级性”的天然法则。在现代软件架构、企业组织、平…

Vue 3.0双向数据绑定实现原理

Vue3 的数据双向绑定是通过响应式系统来实现的。相比于 Vue2&#xff0c;Vue3 在响应式系统上做了很多改进&#xff0c;主要使用了 Proxy 对象来替代原来的 Object.defineProperty。本文将介绍 Vue3 数据双向绑定的主要特点和实现方式。 1. 响应式系统 1.1. Proxy对象 Vue3 …

TIP-2021《SRGAT: Single Image Super-Resolution With Graph Attention Network》

推荐深蓝学院的《深度神经网络加速&#xff1a;cuDNN 与 TensorRT》&#xff0c;课程面向就业&#xff0c;细致讲解CUDA运算的理论支撑与实践&#xff0c;学完可以系统化掌握CUDA基础编程知识以及TensorRT实战&#xff0c;并且能够利用GPU开发高性能、高并发的软件系统&#xf…

大语言模型与多模态模型比较

一、核心差异&#xff1a;输入数据类型与模态融合 输入数据类型 LLM&#xff1a;仅处理文本数据&#xff0c;例如文本分类、机器翻译、问答等任务&#xff0c;通过大规模语料库学习语言规律。 LMM&#xff1a;支持文本、图像、音频、视频等多种模态输入&#xff0c;例如根据图…

Apache HttpClient 5 用法-Java调用http服务

Apache HttpClient 5 核心用法详解 Apache HttpClient 5 是 Apache 基金会推出的新一代 HTTP 客户端库&#xff0c;相比 4.x 版本在性能、模块化和易用性上有显著提升。以下是其核心用法及最佳实践&#xff1a; 一、添加依赖 Maven 项目&#xff1a; <dependency><…

基于 Spark 的流量统计

一、引言 在互联网行业&#xff0c;流量统计是分析网站或应用用户行为、评估业务表现、优化资源分配以及制定营销策略的关键环节。借助 Apache Spark 强大的分布式数据处理能力&#xff0c;我们可以高效地对大规模的流量数据进行统计分析&#xff0c;获取有价值的洞察。本文将…

Python模块化编程进阶指南:从基础到工程化实践

一、模块化编程核心原理与最佳实践 1.1 模块化设计原则 根据企业级项目实践&#xff0c;模块化开发应遵循以下核心原则&#xff1a; ​​单一职责原则​​&#xff1a;每个模块只承担一个功能域的任务&#xff08;如用户认证模块独立于日志模块&#xff09;​​接口隔离原则…

锐捷交换机STP环路日志信息解读

因公司网络组建使用锐捷全系列交换机&#xff0c;近期设备巡检时发现部分日志提示信息&#xff0c; 接入交换机NBS3100-24GT4SFP-V2&#xff0c;设备频繁打出STP Blocking的日志信息。 误以为是环路导致&#xff0c;故进行实验测试&#xff0c;来验证环路情况下会如何报日志。…

使用Python调用DeepSeek的示例

使用Python调用DeepSeek API的示例代码,包括API密钥的获取、基本请求的发送以及响应处理。请确保你已经注册了DeepSeek账号并获取了API密钥。 文章目录 前言一、获取API密钥二、python示例代码三、代码说明四、注意事项五、扩展功能总结前言 提示:这里可以添加本文要记录的大…

mysql的not exists走索引吗

在MySQL中&#xff0c;​NOT EXISTS子句是否使用索引取决于子查询中关联字段是否建立了合适的索引。以下是关键点总结&#xff1a; ​索引的作用​&#xff1a; 当子查询的关联字段&#xff08;例如B.a_id&#xff09;存在索引&#xff08;如普通B-tree索引&#xff09;时&…

Python线性回归:从理论到实践的完整指南

Python线性回归&#xff1a;从理论到实践的完整指南 线性回归是数据科学和机器学习中最基础且最重要的算法之一。本文将深入探讨如何使用Python实现线性回归&#xff0c;从理论基础到实际应用&#xff0c;帮助读者全面理解这一重要的统计学和机器学习方法。 什么是线性回归&a…

鸿蒙OSUniApp 实现的二维码扫描与生成组件#三方框架 #Uniapp

UniApp 实现的二维码扫描与生成组件 前言 最近在做一个电商小程序时&#xff0c;遇到了需要扫描和生成二维码的需求。在移动应用开发中&#xff0c;二维码功能已经成为标配&#xff0c;特别是在电商、社交和支付等场景下。UniApp作为一个跨平台开发框架&#xff0c;为我们提供…

Westlake-Omni 情感端音频生成式输出模型

简述 github地址在 GitHub - xinchen-ai/Westlake-OmniContribute to xinchen-ai/Westlake-Omni development by creating an account on GitHub.https://github.com/xinchen-ai/Westlake-Omni Westlake-Omni 是由西湖心辰&#xff08;xinchen-ai&#xff09;开发的一个开源…

uv python 卸载

又是查了半天 官网wiki没有 网上一堆傻子胡说 uv提示也不对 AI还在这尼玛胡编乱造 开始 我原来装了这几个环境 uv python list 现在python3.7.7不需要了&#xff0c;卸载&#xff0c;直接 uv python uninstall 3.7.7 去找你自己要卸载的版本号&#xff0c;不需要整个包名复制…

使用哈希表封装myunordered_set和myunordered_map

文章目录 使用哈希表封装myunordered_set和myunordered_map实现出复用哈希表框架&#xff0c;并支持insert支持迭代器的实现constKey不能被修改unordered_map支持[ ]结语 我们今天又见面啦&#xff0c;给生活加点impetus&#xff01;&#xff01;开启今天的编程之路&#xff01…

后端框架(2):Java的反射机制

什么是java反射机制&#xff1f; 回顾之前java程序如何使用类 1.分析&#xff0c;确定类名&#xff0c;属性名&#xff0c;方法......创建类 2.创建类的对象 3.使用 一切都是已知的。 在程序开发中&#xff0c;在哪儿需要使用哪个类的对象&#xff0c;就在那儿创建这个类对象…

ch10 课堂参考代码

ch10 最小生成树 生成树&#xff1a;对于 n 个结点 m 条边的无向图 G&#xff0c;由全部 n 个结点和其中 n - 1 条边构成的无向连通子图称为 G 的一棵生成树。 如果图 G 原本就不连通&#xff0c;则不存在生成树&#xff0c;只存在生成森林。 最小生成树&#xff08;Minimum…