
让数据收集像吃饼干一样简单,还能保证电脑不卡顿
当我们谈论爬虫开发时,很多人会立刻想到Python。但实际上,Go语言正在成为爬虫开发的隐形冠军。
这就像从单线程的蜗牛变成了多线程的猎豹,爬取速度简直不可同日而语。
Go语言内置的net/http包让我们只需几行代码就能发送HTTP请求,而其独特的goroutine和channel机制使得并发爬取变得异常简单。
想象一下,你一个人同时操作多台电脑收集资料,而你的对手只能一台一台地操作——这就是使用Go语言开发爬虫的优势!
更重要的是,Go语言编译出的可执行文件是静态链接的,可以直接运行在任何主流操作系统上,无需环境依赖,部署异常简便。
无论你是Linux、Windows还是macOS用户,都可以轻松运行相同的爬虫程序,这种跨平台一致性大大减少了部署时的麻烦。
任何伟大的工程都需要从打好地基开始,搭建Go爬虫开发环境其实非常简单。
首先,你需要安装Go语言环境。可以从Go官方网站下载并安装最新版本的Go编程语言。安装完成后,设置GOPATH环境变量,并创建一个新的工作目录用于存放你的Go项目。
在Linux系统上,如果你使用的是Debian系列,可以通过以下命令安装:
sudo apt update
sudo apt install golang-go
安装完成后,可以通过以下命令检查Go版本:
go version
接下来,为爬虫项目创建一个专门的工作目录:
mkdir my-crawler
cd my-crawler
go mod init my-crawler
这些命令会创建一个新的项目目录并初始化Go模块,为后续开发做好准备。
Go语言强大的标准库是它的另一大优势。除了标准库,你可能需要安装一些第三方库,例如goquery用于解析HTML内容。只需在终端中运行以下命令:
go get -u github.com/PuerkitoBio/goquery
这个著名的goquery库能让HTML解析变得像jQuery一样简单直观。
让我们先从一个简单的爬虫程序开始,它会把整个网页内容抓取下来。这就好比我们第一次学钓鱼——先不管能不能钓到特定的鱼,确保能把钓竿甩出去再说。
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
func main() {
resp, err := http.Get("http://example.com")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(body))
}
这个简单的程序使用了Go语言内置的net/http包。http.Get函数会返回一个http.Response对象,通过读取resp.Body可以获取网页内容。
但这里有个小问题——如果网站要求URL必须包含http://或https://前缀,我们的代码可能会出错。优化方法很简单,添加一个URL检查函数即可:
func checkUrl(s string) string {
if strings.HasPrefix(s, "http") {
return s
}
return fmt.Sprint("http://", s)
}
这个函数可以确保我们请求的URL始终具有正确的前缀,避免因URL格式错误导致的请求失败。
获取网页内容只是第一步,真正有价值的是从HTML中提取我们需要的信息。这时候,goquery库就派上用场了。
如果说原始HTML是一团杂乱无章的毛线,那么goquery就是帮我们理清这团毛线的神奇钩针。
package main
import (
"fmt"
"log"
"net/http"
"github.com/PuerkitoBio/goquery"
)
func main() {
resp, err := http.Get("http://example.com")
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
fmt.Println("Error:", err)
return
}
doc.Find("h1").Each(func(i int, s *goquery.Selection) {
fmt.Println(s.Text())
})
}
以上代码示例展示了如何使用goquery库解析HTML文档,并提取所有h1标签的文本内容。
如果你需要提取更复杂的数据,比如网页中的所有链接,goquery也能让这项工作变得简单:
func main() {
url := "https://example.com"
resp, err := http.Get(url)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
log.Fatal(err)
}
doc.Find("a").Each(func(i int, s *goquery.Selection) {
href, exists := s.Attr("href")
if exists {
fmt.Println(href)
}
})
}
这段代码会找出页面中的所有链接并打印出来,演示了如何使用goquery选择和提取HTML元素的属性。
现在让我们进入Go语言爬虫最精彩的部分——并发处理。通过goroutine和channel,我们可以同时处理多个请求,效率呈指数级增长。
package main
import (
"fmt"
"net/http"
"sync"
)
func fetch(url string, wg *sync.WaitGroup) {
defer wg.Done()
resp, err := http.Get(url)
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
fmt.Println("Fetched:", url)
}
func main() {
var wg sync.WaitGroup
urls := []string{
"http://example.com",
"http://example.org",
"http://example.net",
}
for _, url := range urls {
wg.Add(1)
go fetch(url, &wg)
}
wg.Wait()
}
在这个例子中,fetch函数用于发送HTTP请求,并在完成后通过调用wg.Done()通知WaitGroup。在主函数中,我们创建一个WaitGroup,并为每个URL启动一个goroutine来并发处理请求。
如果要更精细地控制并发并收集处理结果,可以使用channel:
package main
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strings"
"time"
)
func main() {
start := time.Now()
ch := make(chan string)
for _, url := range os.Args[1:] {
url = checkUrl(url)
go fetch(url, ch)
}
for range os.Args[1:] {
fmt.Println(<-ch)
}
fmt.Printf("总耗时: %.2fs
", time.Since(start).Seconds())
}
func fetch(url string, ch chan<- string) {
start := time.Now()
resp, err := http.Get(url)
if err != nil {
ch <- fmt.Sprintf("获取 %s 出错: %v", url, err)
return
}
nbytes, err := io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close()
if err != nil {
ch <- fmt.Sprintf("读取 %s 出错: %v", url, err)
return
}
secs := time.Since(start).Seconds()
ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url)
}
func checkUrl(s string) string {
if strings.HasPrefix(s, "http") {
return s
}
return fmt.Sprint("http://", s)
}
这段代码的精妙之处在于,它使用goroutine同时发起多个请求,再通过channel收集结果。就像同时派出多个侦察兵,然后等待他们一个个回来报告那样高效!
爬取到的数据如果不保存,就像沙滩上的画作,潮水一来就消失了。常见的做法是将数据存储到文件或数据库中。
好的存储方案就像是给收集来的数据一个安全的家,随时可以找到并使用。
CSV是一种通用且简单的格式,适合存储表格型数据。以下示例展示了如何将数据保存到CSV文件:
package main
import (
"encoding/csv"
"os"
)
func saveToCSV(data [][]string, filename string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
for _, record := range data {
if err := writer.Write(record); err != nil {
return err
}
}
return nil
}
func main() {
data := [][]string{
{"Name", "Age"},
{"Alice", "30"},
{"Bob", "25"},
}
if err := saveToCSV(data, "output.csv"); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Data saved to output.csv")
}
}
这个示例展示了如何使用Go语言的标准库encoding/csv将数据保存到CSV文件中。
如果只需要简单保存网页内容,纯文本格式是最直接的选择:
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
)
func main() {
resp, err := http.Get("http://example.com")
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error:", err)
return
}
err = os.WriteFile("webpage.html", body, 0644)
if err != nil {
fmt.Println("Error writing file:", err)
return
}
fmt.Println("Web page saved to webpage.html")
}
这种方法适合保存完整的HTML内容,便于后续处理或分析。
对于结构化数据,JSON格式是理想的选择:
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"github.com/PuerkitoBio/goquery"
)
type PageData struct {
URL string `json:"url"`
Title string `json:"title"`
Links []string `json:"links"`
}
func main() {
var pageData PageData
pageData.URL = "http://example.com"
resp, err := http.Get(pageData.URL)
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
fmt.Println("Error:", err)
return
}
pageData.Title = doc.Find("title").Text()
doc.Find("a").Each(func(i int, s *goquery.Selection) {
href, exists := s.Attr("href")
if exists {
pageData.Links = append(pageData.Links, href)
}
})
jsonData, err := json.MarshalIndent(pageData, "", " ")
if err != nil {
fmt.Println("Error encoding JSON:", err)
return
}
err = os.WriteFile("page_data.json", jsonData, 0644)
if err != nil {
fmt.Println("Error writing file:", err)
return
}
fmt.Println("Data saved to page_data.json")
}
这个示例演示了如何提取网页的标题和所有链接,并将这些结构化数据保存为JSON格式。
现在,让我们把前面学到的所有知识整合起来,创建一个完整的、可以并发爬取多个新闻网站标题并保存结果的爬虫程序。
这就像是从学做一道菜升级到了准备一整桌宴席,虽然挑战更大,但成就感也更强。
package main
import (
"encoding/csv"
"fmt"
"log"
"net/http"
"os"
"sync"
"github.com/PuerkitoBio/goquery"
)
type NewsItem struct {
Source string
Title string
URL string
}
func fetchNews(url string, source string, ch chan<- NewsItem, wg *sync.WaitGroup) {
defer wg.Done()
resp, err := http.Get(url)
if err != nil {
log.Printf("Error fetching %s: %v", url, err)
return
}
defer resp.Body.Close()
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
log.Printf("Error parsing document from %s: %v", url, err)
return
}
doc.Find("h1, h2, h3").Each(func(i int, s *goquery.Selection) {
title := s.Text()
if len(title) > 10 { // 过滤过短的标题
ch <- NewsItem{
Source: source,
Title: title,
URL: url,
}
}
})
}
func main() {
sources := map[string]string{
"Example News": "http://example.com",
"Example Blog": "http://example.org",
}
var wg sync.WaitGroup
newsCh := make(chan NewsItem, 100)
// 启动爬取goroutines
for source, url := range sources {
wg.Add(1)
go fetchNews(url, source, newsCh, &wg)
}
// 关闭channel的goroutine
go func() {
wg.Wait()
close(newsCh)
}()
// 收集结果
var newsItems []NewsItem
for item := range newsCh {
newsItems = append(newsItems, item)
}
// 保存到CSV文件
file, err := os.Create("news.csv")
if err != nil {
log.Fatal("Error creating CSV file:", err)
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
// 写入CSV头部
writer.Write([]string{"Source", "Title", "URL"})
// 写入数据
for _, item := range newsItems {
err := writer.Write([]string{item.Source, item.Title, item.URL})
if err != nil {
log.Printf("Error writing record to CSV: %v", err)
}
}
fmt.Printf("Saved %d news items to news.csv
", len(newsItems))
}
这个完整的爬虫示例演示了如何:
使用goroutine并发爬取多个网站使用goquery解析HTML并提取标题使用channel收集爬取结果将最终结果保存到CSV文件在编写爬虫时,请确保遵守目标网站的robots.txt文件规定,并且不要发送过多的请求以免对服务器造成负担。
在爬取任何网站前,务必检查其robots.txt文件,尊重网站的爬虫规则。有些网站可能明确禁止爬虫访问某些敏感区域。
避免在短时间内发送大量请求,可以通过添加适当的延迟来减轻对目标网站的压力:
import "time"
// 在请求之间添加延迟
func politeFetch(url string) {
// 爬取代码...
time.Sleep(1 * time.Second) // 延迟1秒
}
健全的错误处理机制是稳定爬虫的必备特性:
func safeFetch(url string) (*http.Response, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
}
return resp, nil
}
虽然Go的并发能力强大,但过高的并发数可能导致本地资源耗尽或被目标网站封禁。可以通过带缓冲的channel来实现并发控制:
func controlledCrawl(urls []string, maxConcurrency int) {
sem := make(chan struct{}, maxConcurrency)
var wg sync.WaitGroup
for _, url := range urls {
sem <- struct{}{} // 获取信号量
wg.Add(1)
go func(u string) {
defer wg.Done()
defer func() { <-sem }() // 释放信号量
fetch(u)
}(url)
}
wg.Wait()
}
Go语言为爬虫开发带来了独特的优势,其简洁的语法、强大的并发模型和出色的性能,使得构建高效稳定的爬虫系统变得轻而易举。
通过本文的介绍,相信你已经掌握了使用Go语言开发爬虫的基础知识,特别是如何将抓取的数据存储到不同类型的文件中。
现在,是时候动手尝试创建你自己的Go语言爬虫了!从简单的示例开始,逐步增加复杂度,你会发现Go语言能让数据抓取变得既高效又有趣。
记住,一个好的爬虫不仅要高效抓取数据,还要尊重目标网站的资源,合理控制请求频率,遵守网络礼仪。