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

多态冰川
• 阅读 3223

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

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

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

1、单任务版爬虫架构

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

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

下面是具体流程说明:

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

项目目录

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

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,在此先谢谢大家了。

觉得文章不错的话就点个赞吧~~谢谢

点赞
收藏
评论区
推荐文章
Irene181 Irene181
4年前
3000字 “婴儿级” 爬虫图文教学 | 手把手教你用Python爬取 “实习网”!
1\.为"你"而写这篇文章,是专门为那些"刚学习"Python爬虫的朋友,而专门准备的文章。希望你看过这篇文章后,能够清晰的知道整个"爬虫流程"。从而能够"独立自主"的去完成,某个简单网站的数据爬取。好了,咱们就开始整个“爬虫教学”之旅吧!2\.页面分析①你要爬取的网站是什么?首先,我们应该清楚你要爬去的网站是什么?由于这里我们想要
python知道 python知道
4年前
《Python3网络爬虫开发实战》
提取码:1028内容简介······本书介绍了如何利用Python3开发网络爬虫,书中首先介绍了环境配置和基础知识,然后讨论了urllib、requests、正则表达式、BeautifulSoup、XPath、pyquery、数据存储、Ajax数据爬取等内容,接着通过多个案例介绍了不同场景下如何实现数据爬取,后介绍了pyspider框架、S
python爬虫增加多线程获取数据
Python爬虫应用领域广泛,并且在数据爬取领域处于霸主位置,并且拥有很多性能好的框架,像Scrapy、Request、BeautifuSoap、urlib等框架可以实现爬行自如的功能,只要有能爬取的数据,Python爬虫均可实现。数据信息采集离不开Pyt
分享如何使用java写个小爬虫
爬虫行业的兴起是大数据时代下必须的产物,大家学习阿爬虫肯定是为了爬取有价值的数据信息。关于爬虫的基础知识我们这里不进行阐述,今天我们就只是进行一个简单的爬虫实践。那首先我们就需要确定下我们的目标网站,这里我们就以一些房产信息的网站为例统计一些信息。关于爬虫中的一系列反爬问题我们也不在这里做深入的了解,都是学习爬虫的必备知识,最简单的就是在访问过程中我们肯定会
python使用aiohttp通过设置代理爬取基金数据
说到python爬虫,我们就会想到它那强大的库,很多新手小白在选择框架的时候都会想到使用Scrapy,但是仅仅停留在会使用的阶段。在实际爬虫过程中遇到反爬机制是再常见不过的,今天为了增加对爬虫机制的理解,我们就通过手动实现多线程的爬虫过程,同时引入IP代理
Stella981 Stella981
4年前
Python爬虫教程
本篇是介绍在Anaconda环境下,创建Scrapy爬虫框架项目的步骤,且介绍比较详细Python爬虫教程31创建Scrapy爬虫框架项目首先说一下,本篇是在Anaconda环境下,所以如果没有安装Anaconda请先到官网下载安装Anaconda
Stella981 Stella981
4年前
Python爬取暴走漫画动态图
最近再之乎上看到比较好的Python爬虫教程,看过之后对爬虫有了大概的了解,随后自己写了个爬取暴走漫画(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fbaozoumanhua.com%2Fcatalogs%2Fgif)动图的爬虫练练手,另外附上Python爬虫教程(https://w
Stella981 Stella981
4年前
Python实现王者荣耀小助手(一)
简单来说网络爬虫,是指抓取万维网信息的程序或者脚本,Python在网络爬虫有很大优势,今天我们用Python实现获取王者荣耀相关数据,做一个小助手:前期准备,环境搭建:Python2.7sys模块提供了许多函数和变量来处理Python运行时环境的不同部分;urllib模块提供了一系列用于操作URL的功能,爬虫所需要的功能,基本上在urll
Stella981 Stella981
4年前
Scrapy框架之分布式操作
一、分布式爬虫介绍  分布式爬虫概念:多台机器上执行同一个爬虫程序,实现网站数据的分布爬取。1、原生的Scrapy无法实现分布式爬虫的原因?调度器无法在多台机器间共享:因为多台机器上部署的scrapy会各自拥有各自的调度器,这样就使得多台机器无法分配start\_urls列表中的url。管
Python爬虫过程中DNS解析错误解决策略
在Python爬虫开发中,经常会遇到DNS解析错误,这是一个常见且也令人头疼的问题。DNS解析错误可能会导致爬虫失败,但幸运的是,我们可以采取一些策略来处理这些错误,确保爬虫能够正常运行。本文将介绍什么是DNS解析错误,可能的原因,以及在爬取过程中遇到DN
多态冰川
多态冰川
Lv1
西山白雪三城戍,南浦清江万里桥。
文章
3
粉丝
0
获赞
0