关于爬虫(Spider/Crawler),相信你多多少少接触过。比如你老板要你从某网站获取一批企业的黄页信息,这时候爬虫就派上用场了。而本文,我们的任务是编写一个网页抓取程序,提取网页中的 url. 我们的程序可以继续访问抓取到的 url,深度抓取更多的 url.
1. 链接抓取器
关于这个程序,我就不多废口舌了,你也不必过多的去研究它,掌握用法即可,我们的目的是学习并发。这个程序位于 gopl/goroutine/link 目录下。
package link
import (
"io/ioutil"
"net/http"
"regexp"
"strings"
)
// 从指定的种子地址提取 url
func ExtractLinks(seed string) ([]string, error) {
var urls []string
pattern := regexp.MustCompile(`<a\b[^>]+\bhref="([^"]*)"[^>]*>[\s\S]*?</a>`)
resp, err := http.Get(seed)
if err != nil {
return nil, err
}
defer resp.Body.Close()
buf, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
res := pattern.FindAllStringSubmatch(string(buf), -1)
for _, e := range res {
if len(e) != 2 {
continue
}
if strings.HasPrefix(e[1], "http") {
urls = append(urls, e[1])
}
}
return urls, nil
下面是一个简单的测试示例:
// +build ignore
package main
import (
"fmt"
"gopl/goroutine/link"
"log"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage:\n\tgo run main.go <url>")
os.Exit(1)
}
urls, err := link.ExtractLinks(os.Args[1])
if err != nil {
log.Print(err)
return
}
for _, url := range urls {
fmt.Printf("%v\n", url)
}
}
接下来运行
go run main.go 'http://www.baidu.com/s?wd=goroutine'
,你的屏幕会打印类似这样的结果:

图1 链接抓取器(只展示部分结果)
2. 深度链接抓取器
看到图 1 的结果了吧,上面是我们抓取的 http://www.baidu.com/s?wd=goroutine 这个网页的链接。但是作为一只“优秀”的爬虫,这点结果根本不能满足,我们希望继续访问抓取到的链接,然后获取更多的链接。思路非常简单:
- 每个 goroutine 从一个种子 url 提取所有的链接,保存到任务列表
- 主协程从任务列表提取已经抓取的 url,并启动新的协程继续抓取
下面是这个深度链接抓取器的代码,在路径 gopl/goroutine/concurrence 下面:
// crawler01.go
package main
import (
"fmt"
"gopl/goroutine/link"
"log"
"os"
)
// url 抓取,返回抓取到的所有 url
func crawl(url string) []string {
fmt.Println(url)
urls, err := link.ExtractLinks(url)
if err != nil {
log.Print(fmt.Sprintf("\x1b[31m%v\x1b[0m", err))
}
return urls
}
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage:\n\tgo run crawl.go <url>")
os.Exit(1)
}
// 任务队列
workList := make(chan []string)
// 记录该 url 是否已经访问过,防止无限循环
seen := make(map[string]bool)
go func() {
workList <- os.Args[1:]
}()
for list := range workList {
for _, url := range list {
if seen[url] {
continue
}
seen[url] = true
go func(url string) {
workList <- crawl(url)
}(url)
}
}
}
接下来,运行一下看看,我们把结果重定向到 links.go 文件中,错误会输出到屏幕上。
$ go run crawler01.go 'http://www.baidu.com/s?wd=goroutine'
要不了一会儿,你的屏幕应该就会出现:
图2 输出错误
这个程序几乎永远不会停止,使用 CTRL C 终止它吧。另外我们的 links.txt 文件也抓取了不少 url 了。
2.1 为什么出现 too many open files
这是文件描述符被耗尽了。一般你的系统都有同时打开的最大文件描述符个数的限制。
图3 文件描述符限制
在我的系统中,这个限制是 7168。而每访问一个 url 都要建立一个 tcp 链接,意味着消耗掉一个描述符,因此这个程序理论上最大可以同时打开 7168 个 url。
但是我们的程序太 TM 并行了,开启的 goroutine 远远超出了这个限制。如何解决?什么?修改系统最大描述符限制的上限?别开开玩笑了,goroutine 是可以轻松开到上万的,这样会把你的系统搞死。
3. 总结
- 练习链接抓取器
- 练习并发的深度链接抓取器
- 为什么程序会出现 too many open files 的错误