美文网首页golang爬虫
Golang实现简单爬虫框架(2)——单任务版爬虫

Golang实现简单爬虫框架(2)——单任务版爬虫

作者: 盐的甜 | 来源:发表于2019-05-21 22:31 被阅读0次

    Golang实现简单爬虫框架(2)——单任务版爬虫

    上一篇博客Golang实现简单爬虫框架(1)——项目介绍与环境准备
    )中我们介绍了go语言的开发环境搭建,以及爬虫项目介绍。

    本次爬虫爬取的是珍爱网的用户信息数据,爬取步骤为:

    注意:在本此爬虫项目中,只会实现一个简单的爬虫架构,包括单机版实现、简单并发版以及使用队列进行任务调度的并发版实现,以及数据存储和展示功能。不涉及模拟登录、动态IP等技术,如果你是GO语言新手想找练习项目或者对爬虫感兴趣的读者,请放心食用。

    1、单任务版爬虫架构

    首先我们实现一个单任务版的爬虫,且不考虑数据存储与展示模块,首先把基本功能实现。下面是单任务版爬虫的整体框架

    批注 2019-05-20 114847.png

    下面是具体流程说明:

    • 1、首先需要配置种子请求,就是seed,存储项目爬虫的初始入口
    • 2、把初始入口信息发送给爬虫引擎,引擎把其作为任务信息放入任务队列,只要任务队列不空就一直从任务队列中取任务
    • 3、取出任务后,engine把要请求的任务交给Fetcher模块,Fetcher模块负责通过URL抓取网页数据,然后把数据返回给Engine
    • 4、Engine收到网页数后,把数据交给解析(Parser)模块,Parser解析出需要的数据后返回给Engine,Engine收到解析出的信息在控制台打印出来

    项目目录

    批注 2019-05-20 115906.png

    2、数据结构定义

    在正式开始讲解前先看一下项目中的数据结构。

    // /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{}类型,即这部分具体数据结构由用户自己来定义。

    注意:对于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
    }
    

    3、Fetcher的实现

    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。然后把判断网页编码模块提取为一个函数,如上代码所示。

    4、Parser模块实现

    (1)解析城市列表与URL:
    // /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作为下一个RequestURL,对应的解析器是ParseCity城市解析器。

    在对ParseCityList进行测试的时候,如果ParseFunc: ParseCity,,这样就会调用ParseCity函数,但是我们只想测试城市列表解析功能,不想调用ParseCity函数,此时可以定义一个函数NilParseFun,返回一个空的ParseResult,写成ParseFunc: NilParseFun,即可。

    func NilParseFun([]byte) ParseResult {
        return ParseResult{}
    }
    

    因为http://www.zhenai.com/zhenghun页面城市比较多,为了方便测试可以对解析的城市数量做一个限制,就是代码中的注释部分。

    注意:在解析模块,具体解析哪些信息,以及正则表达式如何书写,不是本次重点。重点是理解各个解析模块之间的联系与函数调用,同下

    (2)解析用户列表与URL
    // /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
    }
    
    
    (3)解析用户数据
    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 ""
        }
    }
    

    5、Engine实现

    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模块得到网页数据,然后根据任务请求中的解析函数解析网页数据。然后把解析出的请求加入任务队列,把解析出的数据打印出来。

    6、main函数

    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方法,传入初始请求。

    7、总结

    本次博客中我们用Go语言实现了一个简单的单机版爬虫项目。仅仅聚焦与爬虫核心架构,没有太多复杂的知识,关键是理解Engine模块以及各个解析模块之间的调用关系。

    缺点是单机版爬取速度太慢了,而且没有使用到go语言强大的并发特特性,所以我们下一章会在本次项目的基础上,重构项目为并发版的爬虫。

    如果想获取Google工程师深度讲解go语言视频资源的,可以在评论区留言。

    项目的源代码已经托管到Github上,对于各个版本都有记录,欢迎大家查看,记得给个star,在此先谢谢大家了。

    相关文章

      网友评论

        本文标题:Golang实现简单爬虫框架(2)——单任务版爬虫

        本文链接:https://www.haomeiwen.com/subject/pkzszqtx.html