上一篇博客《Golang实现简单爬虫框架(1)——项目介绍与环境准备》中咱们介绍了go语言的开发环境搭建,以及爬虫项目介绍。html
本次爬虫爬取的是珍爱网的用户信息数据,爬取步骤为:golang
注意:在本此爬虫项目中,只会实现一个简单的爬虫架构,包括单机版实现、简单并发版以及使用队列进行任务调度的并发版实现,以及数据存储和展现功能。不涉及模拟登陆、动态IP等技术,若是你是GO语言新手想找练习项目或者对爬虫感兴趣的读者,请放心食用。正则表达式
首先咱们实现一个单任务版的爬虫,且不考虑数据存储与展现模块,首先把基本功能实现。下面是单任务版爬虫的总体框架segmentfault
下面是具体流程说明:数据结构
项目目录架构
在正式开始讲解前先看一下项目中的数据结构。并发
// /engine/types.go package engine // 请求结构 type Request struct { Url string // 请求地址 ParseFunc func([]byte) ParseResult // 解析函数 } // 解析结果结构 type ParseResult struct { Requests []Request // 解析出的请求 Items []interface{} // 解析出的内容 }
Request
表示一个爬取请求,包括请求的URL
地址和使用的解析函数,其解析函数返回值是一个ParseResult
类型,其中ParseResult
类型包括解析出的请求和解析出的内容。解析内容Items
是一个interface{}
类型,即这部分具体数据结构由用户本身来定义。app
注意:对于Request
中的解析函数,对于每个URL使用城市列表解析器仍是用户列表解析器,是由咱们的具体业务来决定的,对于Engine
模块没必要知道解析函数具体是什么,只负责Request
中的解析函数来解析传入的URL对应的网页数据框架
须要爬取的数据的定义函数
// /model/profile.go package model // 用户的我的信息 type Profile struct { Name string Gender string Age int Height int Weight int Income string Marriage string Address string }
Fetcher模块任务是获取目标URL的网页数据,先放上代码。
// /fetcher/fetcher.go package fetcher import ( "bufio" "fmt" "io/ioutil" "log" "net/http" "golang.org/x/net/html/charset" "golang.org/x/text/encoding" "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" ) // 网页内容抓取函数 func Fetch(url string) ([]byte, error) { client := &http.Client{} req, err := http.NewRequest("GET", url, nil) if err != nil { log.Fatalln(err) } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36") resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() // 出错处理 if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("wrong state code: %d", resp.StatusCode) } // 把网页转为utf-8编码 bodyReader := bufio.NewReader(resp.Body) e := determineEncoding(bodyReader) utf8Reader := transform.NewReader(bodyReader, e.NewDecoder()) return ioutil.ReadAll(utf8Reader) } func determineEncoding(r *bufio.Reader) encoding.Encoding { bytes, err := r.Peek(1024) if err != nil { log.Printf("Fetcher error %v\n", err) return unicode.UTF8 } e, _, _ := charset.DetermineEncoding(bytes, "") return e }
由于许多网页的编码是GBK,咱们须要把数据转化为utf-8编码,这里须要下载一个包来完成转换,打开终端输入gopm get -g -v golang.org/x/text
能够把GBK编码转化为utf-8编码。在上面代码
bodyReader := bufio.NewReader(resp.Body) e := determineEncoding(bodyReader) utf8Reader := transform.NewReader(bodyReader, e.NewDecoder())
能够写为utf8Reader := transform.NewReader(resp.Body, simplifiedchinese.GBK.NewDecoder())
也是能够的。可是这样问题是通用性太差,咱们怎么知道网页是否是GBK编码呢?此时还能够引入另一个库,能够帮助咱们判断网页的编码。打开终端输入gopm get -g -v golang.org/x/net/html
。而后把判断网页编码模块提取为一个函数,如上代码所示。
// /zhenai/parser/citylist.go package parser import ( "crawler/engine" "regexp" ) const cityListRe = `<a href="(http://www.zhenai.com/zhenghun/[0-9a-z]+)"[^>]*>([^<]+)</a>` // 解析城市列表 func ParseCityList(bytes []byte) engine.ParseResult { re := regexp.MustCompile(cityListRe) // submatch 是 [][][]byte 类型数据 // 第一个[]表示匹配到多少条数据,第二个[]表示匹配的数据中要提取的任容 submatch := re.FindAllSubmatch(bytes, -1) result := engine.ParseResult{} //limit := 10 for _, item := range submatch { result.Items = append(result.Items, "City:"+string(item[2])) result.Requests = append(result.Requests, engine.Request{ Url: string(item[1]), // 每个城市对应的URL ParseFunc: ParseCity, // 使用城市解析器 }) //limit-- //if limit == 0 { // break //} } return result }
在上述代码中,获取页面中全部的城市与URL,而后把每一个城市的URL
做为下一个Request
的URL
,对应的解析器是ParseCity
城市解析器。
在对ParseCityList
进行测试的时候,若是ParseFunc: ParseCity,
,这样就会调用ParseCity
函数,可是咱们只想测试城市列表解析功能,不想调用ParseCity
函数,此时能够定义一个函数NilParseFun
,返回一个空的ParseResult
,写成ParseFunc: NilParseFun,
便可。
func NilParseFun([]byte) ParseResult { return ParseResult{} }
由于http://www.zhenai.com/zhenghun
页面城市比较多,为了方便测试能够对解析的城市数量作一个限制,就是代码中的注释部分。
注意:在解析模块,具体解析哪些信息,以及正则表达式如何书写,不是本次重点。重点是理解各个解析模块之间的联系与函数调用,同下
// /zhenai/parse/city.go package parser import ( "crawler/engine" "regexp" ) var cityRe = regexp.MustCompile(`<a href="(http://album.zhenai.com/u/[0-9]+)"[^>]*>([^<]+)</a>`) // 用户性别正则,由于在用户详情页没有性别信息,因此在用户性别在用户列表页面获取 var sexRe = regexp.MustCompile(`<td width="180"><span class="grayL">性别:</span>([^<]+)</td>`) // 城市页面用户解析器 func ParseCity(bytes []byte) engine.ParseResult { submatch := cityRe.FindAllSubmatch(bytes, -1) gendermatch := sexRe.FindAllSubmatch(bytes, -1) result := engine.ParseResult{} for k, item := range submatch { name := string(item[2]) gender := string(gendermatch[k][1]) result.Items = append(result.Items, "User:"+name) result.Requests = append(result.Requests, engine.Request{ Url: string(item[1]), ParseFunc: func(bytes []byte) engine.ParseResult { return ParseProfile(bytes, name, gender) }, }) } return result }
package parser import ( "crawler/engine" "crawler/model" "regexp" "strconv" ) var ageRe = regexp.MustCompile(`<div class="m-btn purple" [^>]*>([\d]+)岁</div>`) var heightRe = regexp.MustCompile(`<div class="m-btn purple" [^>]*>([\d]+)cm</div>`) var weightRe = regexp.MustCompile(`<div class="m-btn purple" [^>]*>([\d]+)kg</div>`) var incomeRe = regexp.MustCompile(`<div class="m-btn purple" [^>]*>月收入:([^<]+)</div>`) var marriageRe = regexp.MustCompile(`<div class="m-btn purple" [^>]*>([^<]+)</div>`) var addressRe = regexp.MustCompile(`<div class="m-btn purple" [^>]*>工做地:([^<]+)</div>`) func ParseProfile(bytes []byte, name string, gender string) engine.ParseResult { profile := model.Profile{} profile.Name = name profile.Gender = gender if age, err := strconv.Atoi(extractString(bytes, ageRe)); err == nil { profile.Age = age } if height, err := strconv.Atoi(extractString(bytes, heightRe)); err == nil { profile.Height = height } if weight, err := strconv.Atoi(extractString(bytes, weightRe)); err == nil { profile.Weight = weight } profile.Income = extractString(bytes, incomeRe) profile.Marriage = extractString(bytes, marriageRe) profile.Address = extractString(bytes, addressRe) // 解析完用户信息后,没有请求任务 result := engine.ParseResult{ Items: []interface{}{profile}, } return result } func extractString(contents []byte, re *regexp.Regexp) string { submatch := re.FindSubmatch(contents) if len(submatch) >= 2 { return string(submatch[1]) } else { return "" } }
Engine模块是整个系统的核心,获取网页数据、对数据进行解析以及维护任务队列。
// /engine/engine.go package engine import ( "crawler/fetcher" "log" ) // 任务执行函数 func Run(seeds ...Request) { // 创建任务队列 var requests []Request // 把传入的任务添加到任务队列 for _, r := range seeds { requests = append(requests, r) } // 只要任务队列不为空就一直爬取 for len(requests) > 0 { request := requests[0] requests = requests[1:] // 抓取网页内容 log.Printf("Fetching %s\n", request.Url) content, err := fetcher.Fetch(request.Url) if err != nil { log.Printf("Fetch error, Url: %s %v\n", request.Url, err) continue } // 根据任务请求中的解析函数解析网页数据 parseResult := request.ParseFunc(content) // 把解析出的请求添加到请求队列 requests = append(requests, parseResult.Requests...) // 打印解析出的数据 for _, item := range parseResult.Items { log.Printf("Got item %v\n", item) } } }
Engine
模块主要是一个Run
函数,接收一个或多个任务请求,首先把任务请求添加到任务队列,而后判断任务队列若是不为空就一直从队列中取任务,把任务请求的URL传给Fetcher
模块获得网页数据,而后根据任务请求中的解析函数解析网页数据。而后把解析出的请求加入任务队列,把解析出的数据打印出来。
package main import ( "crawler/engine" "crawler/zhenai/parser" ) func main() { engine.Run(engine.Request{ // 配置请求信息便可 Url: "http://www.zhenai.com/zhenghun", ParseFunc: parser.ParseCityList, }) }
在main
函数中直接调用Run
方法,传入初始请求。
本次博客中咱们用Go语言实现了一个简单的单机版爬虫项目。仅仅聚焦与爬虫核心架构,没有太多复杂的知识,关键是理解Engine
模块以及各个解析模块之间的调用关系。
缺点是单机版爬取速度太慢了,并且没有使用到go语言强大的并发特性,因此咱们下一章会在本次项目的基础上,重构项目为并发版的爬虫。
若是想获取Google工程师深度讲解go语言视频资源的,能够在评论区留言。
项目的源代码已经托管到Github上,对于各个版本都有记录,欢迎你们查看,记得给个star,在此先谢谢你们了。
以为文章不错的话就点个赞吧~~谢谢