GoLang爬虫,爬一下掘金

336 阅读7分钟

既然要做一个仿掘金的网页,那么必不可少的就是数据了,所以,今天我们来爬一爬掘金的文章。

分析网页

首先,打开掘金的首页,然后点开一篇文章,分析一下这篇文章的URL的组成部分 例如:

链接juejin.cn/post/712913…

分析可得这篇文章的URL由juejin.cn/post/加上后面的ID 7129139808768638984组成,所以我们下面的目的很简单,获取那些文章的ID。

回到首页,按下F12键,开始我们的分析之旅

image.png 一般而言这种下拉刷新的页面都会在页面到底的时候出现一个请求去获得新的数据,所以我们直接一拉到底,然后在筛选那一行选Fetch/XHR接着你一个个找过来可以看到一个 recommend打头的请求,点进去会发现这里面有我们想要的文章ID:

image.png (在分析这里的时候我发现掘金的文章刷新速度贼快,每次去请求这些文章的ID的时候,尽管条件相同但返回来的ID却不同,一开始还以为我搞错了)。

拿到了请求文章id的URL后,我门去Postman中试验一下,在实验的时候要注意请求是POST不是GET。而且很有意思的是如果直接用拿到的URL放到Postman中去请求文章ID的话,那么恭喜你,你会拿到一些奇怪的东西,并且不会出现每次得到的文章ID都不同的效果(原因是啥我也不懂,或许是来自东方的神秘力量吧)。所以在请求的时候我们要加上“请求负载”:

image.png

这样我们就能拿到想要的数据了。

image.png

代码

接下来就是去golang中敲代码了。

type MyJsonName struct { //构造一个接收数据的json结构体,有一个网站就是干这个的:http://json2struct.mervine.net/
	Count  int64  `json:"count"`
	Cursor string `json:"cursor"`
	Data   []struct {
		ItemInfo struct {
			ArticleID string `json:"article_id"`
		} `json:"item_info"`
		ItemType int64 `json:"item_type"`
	} `json:"data"`
}

type T struct { //构造要传输的json数据结构体(请求负载)
	IdType     int    `json:"id_type"`
	ClientType int    `json:"client_type"`
	SortType   int    `json:"sort_type"`
	Cursor     string `json:"cursor"`
	Limit      int    `json:"limit"`
}

func GetID() {//函数的目的是获取文章ID
	IDurl := "https://api.juejin.cn/recommend_api/v1/article/recommend_all_feed?aid=2608&uuid=7040152916813383179"
	res := T{
		IdType:     2,
		ClientType: 2608,
		SortType:   200,
		Cursor:     "0",
		Limit:      20,
	}
	restu, err := json.Marshal(res)
	if err != nil {
		fmt.Println(err)
	}
	req, err := http.Post(IDurl, "application/json",
		bytes.NewBuffer(restu)) //这里是构造post请求
	if err != nil {
		fmt.Println("错误:", err)
	}
	//*********************************
	bodyText, err := ioutil.ReadAll(req.Body)
	if err != nil {
		fmt.Println("错误:", err)
	}
	//*********************************

	var team_id MyJsonName
	_ = json.Unmarshal(bodyText, &team_id) //以自己写的结构体来接收包中的数据,解析bodyText的数据并存入team_id

	for _, result := range team_id.Data {
		s := result.ItemInfo.ArticleID
		if s != "" {
			url := fmt.Sprintf("https://juejin.cn/post/%s", s) //拼接url得到访问文章详细信息的链接
			Spider(url)//传入到另一个函数爬取文章详细信息
		}
	}
}


好,到此为止我们拿到了文章详细内容的链接,接下来就是爬取详情页的内容了。

上代码:

func Spider(url string) {
   //fmt.Println("开始下载")
   client := &http.Client{}
   req, err := http.NewRequest("GET", url, nil)//构造GET请求,获取信息
   if err != nil {
      log.Fatal(err)
   }
   req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36 Edg/104.0.1293.47")
   req.Header.Set("accept-language", "zh-CN,zh;q=0.9")
   req.Header.Set("sec-ch-ua", "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"100\", \"Microsoft Edge\";v=\"100\"")
   req.Header.Set("sec-ch-ua-mobile", "?0")
   req.Header.Set("sec-ch-ua-platform", "Windows")
   req.Header.Set("sec-fetch-dest", "image")
   req.Header.Set("sec-fetch-mode", "no-cors")
   req.Header.Set("sec-fetch-site", "cross-site")
   req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
   req.Header.Set("Upgrade-Insecure-Requests", "1")
    //上面那一大坨 req.Header打头的东西是请求头里面的东西,一般来说没什么影响
   resp, err := client.Do(req)
   if err != nil {
      fmt.Println("错误", err)
   }
   doc, _ := goquery.NewDocumentFromReader(resp.Body)
   doc.Find("body > script:nth-child(2)").Each(func(i int, s *goquery.Selection) { //在js中获取相应数据
       //这里我用了很笨的办法,因为直接去页面那里拿信息拿不到,所以只能在script中获得信息然后用正则亿点点的改(都是泪)
      sss := s.Text()
      rs := regexp.MustCompile(`article_info:+.*is_english`)//正则1
      rs1 := regexp.MustCompile(`title+.*,b`)//正则2   1、2都是为了获取标题
      title := rs.FindAllString(sss, -1)
      title = rs1.FindAllString(title[0], -1)
      title[0] = strings.Replace(title[0], "title:\"", "", -1)
      title[0] = strings.Replace(title[0], "\",b", "", -1)//对相应的字符进行替换(暂时我只想到这个方法)
      fmt.Println(title[0])
      re := regexp.MustCompile(`mark_content+:+.*display_count`) //正则3 获取内容
      //rs := regexp.MustCompile("[\u4e00-\u9fa5,?!、()。:“”]{1,}")
      match := re.FindAllString(sss, -1) //-1代表查找所有 //将所有符合要求的数据存进match中(数组)
      match[0] = strings.Replace(match[0], "\\n", "\n", -1)
      match[0] = strings.Replace(match[0], "\\u003C", "<", -1)
      match[0] = strings.Replace(match[0], "\\u003E", ">", -1)
      match[0] = strings.Replace(match[0], "\\\"", "\"", -1)
      match[0] = strings.Replace(match[0], "\\u002F", "/", -1)
      match[0] = strings.Replace(match[0], "mark_content:\"", "", -1)
      match[0] = strings.Replace(match[0], "\",display_count", "", -1)//对相应的字符进行替换(暂时我只想到这个方法)
       //看上面那一大坨字符替换就知道这个爬虫要改的地方还有很多,各位轻喷
      Info(title[0], match[0])//这个函数是负责把数据写到文件中,有需要的可以写到数据库里
   })
    
    func PathExists(path string) (bool, error) {
	_, err := os.Stat(path) //检查有无文件夹
	if err == nil {         //若有的话就返回
		return true, nil
	}
	if os.IsNotExist(err) { //无的话就创建
		// 创建文件夹
		err := os.MkdirAll(path, os.ModePerm)
		if err != nil {
			fmt.Println(err)
		} else {
			return true, nil
		}
	}
	return false, err
}

func Info(title string, content string) {
	Path := "g:\\juejin\\" //文件保存的路径
	PathExists(Path)//创建文件夹
	file, err := os.Create(Path + title + ".md")//将文件路径加标题加上.md生成markdown文件
	if err != nil {
		panic(err)
	}
	// 获得文件的writer对象
	writer := bufio.NewWriter(file)
	writer.WriteString(content) //将string数据写入缓存中
	writer.Flush()              //将缓存中的数据写入文件
	fmt.Println("over")
}

最后在Typora中打开是这样的:

image.png

完整代码:

package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"github.com/PuerkitoBio/goquery"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"regexp"
	"strings"
)

type MyJsonName struct { //构造一个接收数据的json结构体
	Count  int64  `json:"count"`
	Cursor string `json:"cursor"`
	Data   []struct {
		ItemInfo struct {
			ArticleID string `json:"article_id"`
		} `json:"item_info"`
		ItemType int64 `json:"item_type"`
	} `json:"data"`
}

type T struct { //构造要传输的json数据结构体(请求负载)
	IdType     int    `json:"id_type"`
	ClientType int    `json:"client_type"`
	SortType   int    `json:"sort_type"`
	Cursor     string `json:"cursor"`
	Limit      int    `json:"limit"`
}

func main() {
	//Spider()
	GetID()
}
func GetID() {
	IDurl := "https://api.juejin.cn/recommend_api/v1/article/recommend_all_feed?aid=2608&uuid=7040152916813383179"
	res := T{
		IdType:     2,
		ClientType: 2608,
		SortType:   200,
		Cursor:     "0",
		Limit:      20,
	}
	restu, err := json.Marshal(res)
	if err != nil {
		fmt.Println(err)
	}
	req, err := http.Post(IDurl, "application/json",
		bytes.NewBuffer(restu)) //这里是构造post请求
	if err != nil {
		fmt.Println("错误:", err)
	}
	//*********************************
	bodyText, err := ioutil.ReadAll(req.Body)
	if err != nil {
		fmt.Println("错误:", err)
	}
	//*********************************

	var team_id MyJsonName
	_ = json.Unmarshal(bodyText, &team_id) //以自己写的结构体来接收包中的数据,解析bodyText的数据并存入team_id
	//从中得到访问网页的重要ID,然后进行拼接后可得到网址

	for _, result := range team_id.Data {
		s := result.ItemInfo.ArticleID
		if s != "" {
			url := fmt.Sprintf("https://juejin.cn/post/%s", s) //拼接url
			Spider(url)
		}
	}
}
func PathExists(path string) (bool, error) {
	_, err := os.Stat(path) //检查有无文件夹
	if err == nil {         //若有的话就返回
		return true, nil
	}
	if os.IsNotExist(err) { //无的话就创建
		// 创建文件夹
		err := os.MkdirAll(path, os.ModePerm)
		if err != nil {
			fmt.Println(err)
		} else {
			return true, nil
		}
	}
	return false, err
}

func Info(title string, content string) {
	Path := "g:\\juejin\\" //文件保存的路径
	PathExists(Path)
	// 获得get请求响应的reader对象
	file, err := os.Create(Path + title + ".md")
	if err != nil {
		panic(err)
	}
	// 获得文件的writer对象
	writer := bufio.NewWriter(file)
	writer.WriteString(content) //将string数据写入缓存中
	writer.Flush()              //将缓存中的数据写入文件
	fmt.Println("over")
}
func Spider(url string) {
	//fmt.Println("开始下载")
	client := &http.Client{}
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36 Edg/104.0.1293.47")
	req.Header.Set("accept-language", "zh-CN,zh;q=0.9")
	req.Header.Set("sec-ch-ua", "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"100\", \"Microsoft Edge\";v=\"100\"")
	req.Header.Set("sec-ch-ua-mobile", "?0")
	req.Header.Set("sec-ch-ua-platform", "Windows")
	req.Header.Set("sec-fetch-dest", "image")
	req.Header.Set("sec-fetch-mode", "no-cors")
	req.Header.Set("sec-fetch-site", "cross-site")
	req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
	req.Header.Set("Upgrade-Insecure-Requests", "1")
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("错误", err)
	}
	doc, _ := goquery.NewDocumentFromReader(resp.Body)
	doc.Find("body > script:nth-child(2)").Each(func(i int, s *goquery.Selection) { //在js中获取相应数据
		sss := s.Text()
		rs := regexp.MustCompile(`article_info:+.*is_english`)
		rs1 := regexp.MustCompile(`title+.*,b`)
		title := rs.FindAllString(sss, -1)
		title = rs1.FindAllString(title[0], -1)
		title[0] = strings.Replace(title[0], "title:\"", "", -1)
		title[0] = strings.Replace(title[0], "\",b", "", -1)
		title[0] = strings.Replace(title[0], "\\", "", -1)

		fmt.Println(title[0])
		re := regexp.MustCompile(`mark_content+:+.*display_count`) //获取图片链接的正则
		//rs := regexp.MustCompile("[\u4e00-\u9fa5,?!、()。:“”]{1,}")
		match := re.FindAllString(sss, -1) //-1代表查找所有 //将所有符合要求的数据存进match中(数组)
		match[0] = strings.Replace(match[0], "\\n", "\n", -1)
		match[0] = strings.Replace(match[0], "\\u003C", "<", -1)
		match[0] = strings.Replace(match[0], "\\u003E", ">", -1)
		match[0] = strings.Replace(match[0], "\\\"", "\"", -1)
		match[0] = strings.Replace(match[0], "\\u002F", "/", -1)
		match[0] = strings.Replace(match[0], "mark_content:\"", "", -1)
		match[0] = strings.Replace(match[0], "\",display_count", "", -1)
		Info(title[0], match[0])
	})

}

这次的代码还有很多地方可以改进,希望各位轻喷。

好了,今天的代码就敲到这吧。