在上一篇中,咱们根据命令行的 URL 参数输入,抓取对应的网页内容并保存到本地磁盘,今天来记录一下如何利用并发,来抓取多个站点的网页内容。正则表达式
首先,咱们在上一次代码的基础上稍做改造,使它可以获取多个站点的内容。下面代码中,咱们首先定义好三个 URL,而后逐个发送网络请求,获取数据并保存,最后统计消耗的总时间:bash
// fetch.go package main import ( "os" "fmt" "time" "regexp" "net/http" "io/ioutil" ) // 建立正则常量 var RE = regexp.MustCompile("\\w+\\.\\w+$") func main() { urls := []string { "http://www.qq.com", "http://www.163.com", "http://www.sina.com", } // 开始时间 start := time.Now() for _, url := range urls { start := time.Now() // 发送网络请求 res, err := http.Get(url) if err != nil { fmt.Fprintf(os.Stderr, "fetch: %v\n", err) os.Exit(1) } // 读取资源数据 body, err := ioutil.ReadAll(res.Body) // 关闭资源 res.Body.Close() if err != nil { fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err) os.Exit(1) } fileName := getFileName(url) // 写入文件 ioutil.WriteFile(fileName, body, 0644) // 消耗的时间 elapsed := time.Since(start).Seconds() fmt.Printf("%.2fs %s\n", elapsed, fileName) } // 消耗的时间 elapsed := time.Since(start).Seconds() fmt.Printf("%.2fs elapsed\n", elapsed) } // 获取文件名 func getFileName(url string) string { // 从URL中匹配域名后面部分 return RE.FindString(url) + ".txt" }
在上面代码中,咱们使用正则表达式来从 URL 中匹配域名后面部分,做为最终的文件名。关于正则表达式,后续会作总结。网络
下面来看看程序运行后的控制台信息:并发
$ ./fetch 0.12s qq.com.txt 0.20s 163.com.txt 0.27s sina.com.txt 0.59s elapsed
从打印信息中能够看出,最后消耗的总时间等于三次执行的总和。这种方式效率低下,而且不能充分利用计算机资源,下面咱们就对程序进行改造,使其可以并发地执行三个抓取操做:性能
// fetch.go package main import ( "os" "fmt" "time" "regexp" "net/http" "io/ioutil" ) // 建立正则 var RE = regexp.MustCompile("\\w+\\.\\w+$") func main() { urls := []string { "http://www.qq.com", "http://www.163.com", "http://www.sina.com", } // 建立channel ch := make(chan string) // 开始时间 start := time.Now() for _, url := range urls { // 开启一个goroutine go fetch(url, ch) } for range urls { // 打印channel中的信息 fmt.Println(<-ch) } // 总消耗的时间 elapsed := time.Since(start).Seconds() fmt.Printf("%.2fs elapsed\n", elapsed) } // 根据URL获取资源内容 func fetch(url string, ch chan<- string) { start := time.Now() // 发送网络请求 res, err := http.Get(url) if err != nil { // 输出异常信息 ch <- fmt.Sprint(err) os.Exit(1) } // 读取资源数据 body, err := ioutil.ReadAll(res.Body) // 关闭资源 res.Body.Close() if err != nil { // 输出异常信息 ch <- fmt.Sprintf("while reading %s: %v", url, err) os.Exit(1) } // 写入文件 ioutil.WriteFile(getFileName(url), body, 0644) // 消耗的时间 elapsed := time.Since(start).Seconds() // 输出单个URL消耗的时间 ch <- fmt.Sprintf("%.2fs %s", elapsed, url) } // 获取文件名 func getFileName(url string) string { // 从URL中匹配域名部分 return RE.FindString(url) + ".txt" }
上面代码中,咱们先建立一个 channel,而后对每一个抓取操做开启一个 goroutine,待抓取程序完成后,经过 channel 发送消息告知主线程,主线程再作相应的处理操做。关于这部分的原理细节,后续再作总结。fetch
咱们运行上面的程序,执行结果以下:url
$ ./fetch 0.10s http://www.qq.com 0.19s http://www.163.com 0.29s http://www.sina.com 0.29s elapsed
从结果中能够看出,最后消耗的总时间与耗时最长的那个操做等同,可见并发在性能方面带来的提高是很是可观的。命令行