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.pswp.cn/web/80101.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/80101.shtml
英文地址,請注明出處:http://en.pswp.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…