【Java转Go】Go爬虫篇一(goquery)

前言

学了网络编程,那咱得用,于是就去查了下go爬虫方面的应用,一般来说是使用 go-colly 和 goquery 框架,我这篇用的是 goquery 。

本来我想打算用豆瓣电影来试试的,但是我用 res, err = http.Get(url); 发送了请求,请求地址是没错的,但是没有响应数据,是空的。不知道是不是因为它这个网页是动态的?或者是做了什么反爬机制?

我也不是很懂这方面的知识,之前也就顶多用Java jsoup 去解析过一些小说网站下载小说。而这对我个不懂爬虫的人来说,我也就会解析个html,要是拿不到html,那就没办法了(⊙o⊙)

所以这篇文章的前提是,给那个网站发送请求,返回回来的响应数据能拿到html。

这里演示的是去某小说网站下载小说。

goquery

要用这个框架,还是老规矩,我们得先去下载:

go get github.com/PuerkitoBio/goquery

实现

思路

比如我要下载某本小说,先点进这本小说主页,然后我们需要获取它的文章目录列表,按F12,查看目录列表的元素,是什么class或者id。

后续我们拿到html时,就去body里面找到对应的目录列表的id或class。

比如目录列表是一个ul标签,ul下的li标签就是每一章的章节。然后这个ul有个id,我们可以通过这个id定位到这个ul,然后拿到它下面的所有li标签。

又或者是ul没有id,而是class,li标签也有class,这样就需要通过class来查找元素。

拿到目录列表后,遍历目录,一般会有a标签,a标签的href就是每章的地址。比如li标签里面放的a标签。

定位到a标签,拿到 href 属性的值,也就是章节地址,那我们就可以向这个地址发送请求了。

请求后解析响应数据,获取到文章正文内容。一般正文内容是放在p标签里,也有一些网站不用p标签,而是直接整章内容放在div里面,通过br标签换行和   缩进。

拿到文章内容后,就可以输出到文件了。

代码

前面学了管道和协程,学了就要用嘛,所以我这里也用了这两个。

定义一个管道,用来存放章节地址。协程的话,我只开了两个协程,一个是用来读取章节列表,并放入管道中;第二个是遍历管道,去发送请求,拿到章节内容。

因为我是把整本小说写入到一个txt文件中,不能开多个协程去发送请求,不然拿到的章节内容有可能是乱的,不是按顺序来的。如果你是将每个章节都单独写入到一个txt文件,那就无所谓顺序了。

package main

import (
	"bufio"
	"fmt"
	"github.com/PuerkitoBio/goquery"
	"net/http"
	"os"
	"sync"
)

var wg sync.WaitGroup
var chan1 = make(chan string, 10) // 存放章节地址

func main() {
    
    
	// 输出文件路径
	outPath := "C:\\Users\\Administrator\\Desktop\\1.txt"
	// 小说url地址
	var url = ""
	start(url, outPath)
}

func start(url string, outPath string) {
    
    
	var res *http.Response
	var err error
	// 输出到文件
	file, err := os.OpenFile(outPath, os.O_WRONLY|os.O_CREATE, 0666)
	if err != nil {
    
    
		fmt.Printf("文件打开失败:%v", err)
	}
	defer file.Close() // 按照defer的先进后出原则,这句代码会最后执行
	if res, err = http.Get(url); err != nil {
    
    
		fmt.Println(err)
		return
	}
	defer res.Body.Close() // 这句代码比 defer file.Close() 先执行
	doc, err := goquery.NewDocumentFromReader(res.Body)
	if err != nil {
    
    
		fmt.Println(err)
		return
	}
	writer := bufio.NewWriter(file) // NewWriter 带缓冲区的写入,写完之后要用flush刷新。
	wg.Add(2)                       // 下面开启了2个协程
	go getChapterList(doc)          // 开启一个协程,读取章节列表,并放入到管道中
	go getChapterContent(writer)    // 开启一个协程,读取 chan1管道中的href,去发送请求,拿到章节内容(这里不能开多个协程去请求,不然有可能不是按章节顺序去请求的)
	wg.Wait()                       // 等待协程执行完毕
	writer.Flush()
}

// 获取章节列表
func getChapterList(doc *goquery.Document) {
    
    
	// 获取章节列表
	doc.Find(".chapters").Each(func(i int, selection *goquery.Selection) {
    
    
		// 拿到 calss 为 .chapters 的li元素下的 a 标签
		attr := selection.First().Get(0).FirstChild.Attr
		href := attr[0].Val // 拿到a标签的href值
		chan1 <- href
	})
	close(chan1) // 将章节href全部写入完后,关闭管道
	wg.Done()    // 同时,主线程等待执行的协程数量-1
}

// 获取章节内容,并写入到文件中
func getChapterContent(writer *bufio.Writer) {
    
    
	var res *http.Response
	var err error
	for v := range chan1 {
    
    
		fmt.Println("正在下载:", v)
		if res, err = http.Get(v); err != nil {
    
    
			fmt.Println(err)
			return
		}
		doc, err := goquery.NewDocumentFromReader(res.Body)
		if err != nil {
    
    
			fmt.Println(err)
			return
		}
		var result string
		doc.Find("#content").Find("p").Each(func(i int, selection *goquery.Selection) {
    
    
			result += selection.Text() + "\r\n" // 每读一个p标签的内容,要加换行符,不然全部内容挤在一块了
		})
		writer.WriteString(result)
	}
	defer res.Body.Close() // 执行完这个函数后,关闭
	wg.Done()              // 同时,主线程等待执行的协程数量-1
}

怎么样,是不是感觉很简单?确实还挺简单的,要是能拿到html,那也就定位元素稍微要注意一点。其他的倒也没什么了。

ok,以上就是本篇文章的全部内容了,我们下篇文章见!

猜你喜欢

转载自blog.csdn.net/weixin_43165220/article/details/132515639