学了网络编程,那咱得用,于是就去查了下go爬虫方面的应用,一般来说是使用 go-colly 和 goquery 框架,我这篇用的是 goquery 。
本来我想打算用豆瓣电影来试试的,但是我用 res, err = http.Get(url);
发送了请求,请求地址是没错的,但是没有响应数据,是空的。不知道是不是因为它这个网页是动态的?或者是做了什么反爬机制?
我也不是很懂这方面的知识,之前也就顶多用Java jsoup 去解析过一些小说网站下载小说。而这对我个不懂爬虫的人来说,我也就会解析个html,要是拿不到html,那就没办法了(⊙o⊙)
所以这篇文章的前提是,给那个网站发送请求,返回回来的响应数据能拿到html。
这里演示的是去某小说网站下载小说。
要用这个框架,还是老规矩,我们得先去下载:
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,以上就是本篇文章的全部内容了,我们下篇文章见!