Golang | 写一个最简单的爬虫
2020 07 27, Mon
有的同学经常说想写个爬虫,爬比如说一些咨询,爬一些社交媒体,爬种子站,或者爬小说什么的,然后就端出来了祖传的scrapy。
实际上没必要,爬虫就是个很简单的队列处理程序。
刚刚好最近和女朋友看剧,但是不想追着看,为了等更新以后让电脑通知我,我写了一个片段。
基本逻辑
首先我们要有个起点,然后我们要有个顺序,最后我们要可以按照一定的顺序把这些网页挨个访问一遍。
归纳一下,我们需要:
- 有个队列,安排爬虫的顺序
- 队列里面要放一个起始的url,用来存放爬虫的起点
- 我们要从队列里面按照一定规则,最简单的就是FIFO,取出URL
- 我们要访问这个网页,然后把接下来要爬的网页放进队列,把爬取的结果存下来
实现思路
起始URL
我们就用命令行参数。简单。
队列
最简单的队列用channel就行。
HTTP处理
最简单的用http.Client,直接Get就好了。
解析HTML
golang有一个官方出的包,可以解析HTML。
实现
整个程序就是一个无限循环。有点像我《之前博客中讲的事件循环》:
- 初始化
- 循环
- 获取事件
- 处理事件
- 处理之后存储结果,需要的话把新的任务放入队列
- 需要的时候做fps capping
package main
import (
"flag"
"fmt"
"log"
"net/http"
"regexp"
"time"
"context"
"golang.org/x/net/html"
)
var (
// interval 控制两次访问的间隔
interval time.Duration
// timeout 控制http超时时间
timeout time.Duration
)
func init() {
flag.DurationVar(&interval, "interval", 0, "interval between http get")
flag.DurationVar(&timeout, "timeout", time.Second*15, "timeout for http get")
}
func walk(root *html.Node, f func(*html.Node) bool) {
for n := root.FirstChild; n != nil; n = n.NextSibling {
if f(n) {
walk(n, f)
}
}
}
func doWork(url string) ([]string, error) {
// 访问URL,解析html
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
ctx := req.Context()
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
req = req.WithContext(ctx)
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode/100 != 2 {
return nil, fmt.Errorf("unexcepted status code: %v", res.StatusCode)
}
node, err := html.Parse(res.Body)
if err != nil {
return nil, err
}
// 遍历结点
// 把期待的结果存到某个地方
var results = make(map[string]string)
walk(node, func(node *html.Node) bool {
if node.Type == html.ElementNode {
for _, attr := range node.Attr {
if attr.Key == "href" {
if matched, err := regexp.MatchString("magnet:.*", attr.Val); err != nil {
log.Println("error when iterating through DOM: ", err)
return false
} else if matched {
var content string
for child := node.FirstChild; child != nil; child = child.NextSibling {
if child.Type == html.TextNode {
content = child.Data
break
}
}
results[content] = attr.Val
return false
}
}
}
}
return true
})
// 想的话发个邮件什么的
// 最后返回要添加回队列的URL
// 我这段程序节选自一个网页监测的片段,因为是要监测同一个页面,所以把原来的页面URL重新放回去就好
return []string{url}, nil
}
func putTasks(ch chan string, tasks ...string) {
for _, task := range tasks {
ch <- task
}
}
func main() {
// 初始化
flag.Parse()
urls := make(chan string)
// 初始化:这里把要做的任务放进channel里面
go putTasks(urls, flag.Args()...)
// 大循环:
for task := range urls {
log.Print("visiting ", task)
start := time.Now()
// 干活
results, err := doWork(task)
if err != nil {
log.Print("failed to process url: ", task, ": ", err)
continue
}
go putTasks(urls, results...)
// 两次间隔如果小于预期间隔,就睡会儿
duration := time.Now().Sub(start)
if duration < interval {
time.Sleep(interval - duration)
}
}
}
总结
我们用非常简单的东西,用了100多行代码,写了一个非常简单的爬虫。
我们这个爬虫很简陋,不大好存储结果,也没有持久化,无法在多个机器上同时部署,也没有界面可以看。但是这就是爬虫的核心,存储结果和持久化需要一个Datastore,分布式可以用消息队列来做,有了前面两个可视化也就非常好做了。
时至今日有很多爬虫框架,有python的scrapy,有go的colly。但是实际上爬虫的核心还是这些。