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。但是实际上爬虫的核心还是这些。