New一个golang爬虫

刚好七八双月结束,工作整理完毕,下个双月OKR还没开始。做久了IOS开发也来扩展下领域,抽空几天学了下Golang,实现一个爬虫。

一、知识要点

1、爬虫

1.1 工作方式

传统爬虫从一个或若干初始网页的URL开始,获得初始网页上的URL,在抓取网页的过程中,不断从当前页面上抽取新的URL放入队列,直到满足系统的一定停止条件。聚焦爬虫的工作流程较为复杂,需要根据一定的网页分析算法过滤与主题无关的链接,保留有用的链接并将其放入等待抓取的URL队列。然后,它将根据一定的搜索策略从队列中选择下一步要抓取的网页URL,并重复上述过程,直到达到系统的某一条件时停止。另外,所有被爬虫抓取的网页将会被系统存贮,进行一定的分析、过滤,并建立索引,以便之后的查询和检索;对于聚焦爬虫来说,这一过程所得到的分析结果还可能对以后的抓取过程给出反馈和指导。

1.2 分类

  • 全网爬虫,爬行对象从一些种子 URL 扩充到整个 Web,主要为门户站点搜索引擎和大型 Web 服务提供商采集数据。
  • 聚焦网络爬虫,是指选择性地爬行那些与预先定义好的主题相关页面的网络爬虫。
  • 增量式网络爬虫,是指对已下载网页采取增量式更新和只爬行新产生的或者已经发生变化网页的爬虫,它能够在一定程度上保证所爬行的页面是尽可能新的页面。
  • Deep Web 爬虫,表层网页是指传统搜索引擎可以索引的页面,以超链接可以到达的静态网页为主构成的Web页面。Deep Web 是那些大部分内容不能通过静态链接获取的、隐藏在搜索表单后的,只有用户提交一些关键词才能获得的 Web 页面。

1.3爬虫算法

  • 深度优先策略

    其基本方法是按照深度由低到高的顺序,依次访问下一级网页链接,直到不能再深入为止。 爬虫在完成一个爬行分支后返回到上一链接节点进一步搜索其它链接。 当所有链接遍历完后,爬行任务结束。 这种策略比较适合垂直搜索或站内搜索, 但爬行页面内容层次较深的站点时会造成资源的巨大浪费。

  • 广度优先策略

    此策略按照网页内容目录层次深浅来爬行页面,处于较浅目录层次的页面首先被爬行。 当同一层次中的页面爬行完毕后,爬虫再深入下一层继续爬行。 这种策略能够有效控制页面的爬行深度,避免遇到一个无穷深层分支时无法结束爬行的问题,实现方便,无需存储大量中间节点,不足之处在于需较长时间才能爬行到目录层次较深的页面

2、golang

2.1 语法学习

  • 为了让学习更加快速,想要优先上手,而不是沉浸在大量语法里面,找了一遍基础的语法文档,直接全读整体语法,先有个基础但是全面的认识。
    语法教程链接

  • 然后直接手一本web教程书籍,此本书籍是开源的,在github上有1.78万star,5年前就开始书写,一直被追捧和使用。书籍链接

2.2 环境安装

1.1 在MacOSX上安装

  • 下载地址
  • 源码包:go1.4.linux-amd64.tar.gz。
  • 将下载的源码包解压至 /usr/local目录。
tar -C /usr/local -xzf go1.4.linux-amd64.tar.gz

  • 将 /usr/local/go/bin 目录添加至PATH环境变量:
export PATH=$PATH:/usr/local/go/bin

  • 注意:MAC 系统下你可以使用 .pkg 结尾的安装包直接双击来完成安装,安装目录在 /usr/local/go/ 下。

1.2 其他方式

参考链接

二、代码实现

先确立一个小目标,就是我们要爬取的网页的数据源是什么。一直觉得国内的大学排名争议比较有趣,TOP2的两所,但是TOP5的有8所,TOP10的有20所,哈哈,所以来爬个大学排行榜玩玩吧。

1、网页抓取

1.1 定义一个学校


type SchoolObj struct {
    rankTypeName string
    RankIndex int
    SchoolName string
    EnrollOrder string
    StarLevel string
    LocationName string
    SchoolType  string
    UrlAddress string
    SchoolTags []string
}

1.2 单页面html解析

  • 引入go语言的http函数包和上面定义的学校结构题

  • 发起一个网页请求返回,go语言会返回网页的<html>以下全部的html格式字符串

  • 如何从这些字符串中遍历查找和解析出我们需要的学校排名字段?

因为有过前端开发的经验,我自然而然想到,使用CSS选择器会比直接使用遍历算法来得高效,有CSS的选择规则,我可以批量规律的获取和处理HTML的DOM结构数据。端开发中的jQuery提供了方便的操作 DOM 的 API。使用 Go 语言做服务器端开发,有时候需要解析 HTML 文件,比如抓取网站内容、写一个爬虫等。这时候如果有一个类似 jQuery 的库可以使用,操作 DOM 会很方便,而且,上手也会很快。果然,还真有这样的工具,此处推荐一个GitHub的开源框架 — Goquery 。

A、使用介绍:

goquery定义了一个Document结构,直接对应网页Javascript的Document节点,通过一个NewDocument方法,传入参数地址为网页的url地址,直接生产一个虚拟的go语言上的dom。

type Document struct {
    *Selection
    Url      *url.URL
    rootNode *html.Node
}

func NewDocument(url string) (*Document, error) {
    // Load the URL
    res, e := http.Get(url)
    if e != nil {
        return nil, e
    }
    return NewDocumentFromResponse(res)
}


Document有定义find方法,方法的使用和JQuery里面一直,传入目标字符串的css选择器即可。通过对Document执行find查找方法,获得全部学校目标的字符串数组。

doc.Find(".bangTable table tr")

这里的选择器怎么来的呢,我们在chrome里面打开url地址,找到我们想要收集的数据排名,右键打开审查元素,可以看到HTML的选择器名称。这里需要有一点CSS基础,因为有的选择器不是直接唯一的,需要自己去判断,怎样的选择器组合才能准确的拿到想要的目标字符串。

《New一个golang爬虫》

Document有定义each方法,用于遍历数组,也就是各个大学所对应的dom节点。在each方法中继续使用查找方法,并最后获得想要的字符串。

每一个dom对应一个SchoolStruct,新建并赋值,放入数组中返回。

B、代码如下:

import (
    "github.com/PuerkitoBio/goquery"
    "SchoolReptile/struct"
    "net/http"
)

func GaokaoquanRank(urlAddress string) []SchoolStruct.SchoolObj {

    var array [] SchoolStruct.SchoolObj

    doc, err := goquery.NewDocument(urlAddress)
    if err != nil {
        log.Fatal(err)
    }

    // Find the review items
    doc.Find(".bangTable table tr").Each(func(i int, s *goquery.Selection) {
        // For each item found, get the band and title
        var obj SchoolStruct.SchoolObj
        obj.RankIndex = s.Find(".t1 span").Text()
        obj.SchoolName = s.Find(".t2 a").Text()
        obj.UrlAddress ,_ = s.Find(".t2 a").Attr("href")
        obj.LocationName = s.Find(".t3").Text()
        obj.SchoolType = s.Find(".t4").Text()
        obj.StarLevel = s.Find(".t5").Text()
        obj.EnrollOrder = "本科第一批"
        array = append(array, obj)

    })

    return array
}

2、接口请求

我们再爬去数据的时候,一般都能直接抓取网页数据,但是有的数据在第一页炳辉展示出来,需要有点击操作,比如加载更多。此处的大学排行有200位,第一页请求只有20位,这时候就会发现,接口请求的方便。
有的网页在接口上做了cookie校验,摸清别人的请求规则,才能正确模拟出请求获得返回数据。

我们此处拿乐学高考作文例子,获取各个类型的大学排行榜。通过charles代理,我们获得请求的各类参数。

  • 拼接请求url
url := LexueHost+"/college/ranking?page="+pageStr+"&rank_type="+rankObj.RankType+"&page_size=15"

  • 发送HTTP请求,获取返回

网络请求返回的是一个字符串结构的数据,我们需要把它映射成map结构好获取key对应的value值。

这里推荐一个go语言在json解析上的一个开源库Simplejson,将返回的数据进行JSON结构化,然后通过get方法可以直接获得对应的参数值。

defer resp.Body.Close()

data, err := ioutil.ReadAll(resp.Body)

jsonBody,err := simplejson.NewJson(data)

schoolJsonArray,err := jsonBody.Get("schools").Array()
    
  • 多页请求使用递归的方式,不断改变get请求的pageStr参数,pageindex ++ ,当判断请求返回的json为空的时候,则说明接口请求已经到到了最后一页,跳出递归
var nextArray [] SchoolStruct.SchoolObj
nextArray = LexueRankEachList(rankObj,pageIndex)

B、代码如下:

import (
    "SchoolReptile/struct"
    "net/http"
    "io/ioutil"
    "fmt"
    "bytes"
    "encoding/json"
    "strings"
    "github.com/bitly/go-simplejson"
    "strconv"
)

func LexueRankEachList(rankObj SchoolStruct.RankTypeObj,pageIndex int ) []SchoolStruct.SchoolObj {

    pageStr := strconv.Itoa(pageIndex)

    url := LexueHost+"/college/ranking?page="+pageStr+"&rank_type="+rankObj.RankType+"&page_size=15"

    resp, err := http.Get(url)
    if err != nil {
        // handle error
    }

    defer resp.Body.Close()

    data, err := ioutil.ReadAll(resp.Body)

    jsonBody,err := simplejson.NewJson(data)

    schoolJsonArray,err := jsonBody.Get("schools").Array()

    var array [] SchoolStruct.SchoolObj

    if len(schoolJsonArray) <= 0 {
        println("请求到头了")
        return array
    }

    for i,_ := range schoolJsonArray {
        schoolJson := jsonBody.Get("schools").GetIndex(i)
        var obj SchoolStruct.SchoolObj
        obj.RankIndex = strconv.Itoa(schoolJson.Get("school_rank").MustInt())
        obj.SchoolName = schoolJson.Get("school_name").MustString()
        obj.SchoolTags = schoolJson.Get("school_tags").MustStringArray()
        array = append(array, obj)
        println(obj.RankIndex,obj.SchoolName,obj.SchoolTags)
    }

    pageIndex++
    var nextArray [] SchoolStruct.SchoolObj
    nextArray = LexueRankEachList(rankObj,pageIndex)
    if len(nextArray) > 0 {
        for _,obj := range nextArray {
            array = append(array,obj)
        }
    }

    return array

}

3、保存到Excel

前两部获得了网络数据,并解析生成了对应的SchoolStruct数组,这个时候我们只需要创建excel边。遍历数组,把数组里面的数据字段都存入表格即可,git开源库xlsx能够让我们轻松的创建、查找、赋值Excel表。

代码如下:

func SaveSchoolRank(schoolArray [] SchoolStruct.SchoolObj,excelName string,sheetName string)  {

    var file *xlsx.File
    var sheet *xlsx.Sheet
    var row *xlsx.Row
    var cell *xlsx.Cell
    var err error

    file,err = xlsx.OpenFile(excelName + ".xlsx")

    if err != nil {
        file = xlsx.NewFile()
        sheet,err = file.AddSheet(sheetName)
    } else {
       sheet = file.Sheet[sheetName]
    }

    if err == nil {

        for i := 0; i < len(schoolArray); i++ {
            obj := schoolArray[i]

            row = sheet.AddRow()
            cell = row.AddCell()
            cell.Value = obj.RankIndex

            cell = row.AddCell()
            cell.Value = obj.SchoolName

            cell = row.AddCell()
            cell.Value = obj.StarLevel

            cell = row.AddCell()
            cell.Value = obj.LocationName

            cell = row.AddCell()
            cell.Value = obj.EnrollOrder

            cell = row.AddCell()
            cell.Value = obj.SchoolType

            cell = row.AddCell()
            cell.Value = obj.UrlAddress


            var tagStr string
            for _,value := range obj.SchoolTags {
                tagStr += "+" + value
            }
            cell = row.AddCell()
            cell.Value = tagStr


            if err != nil {
                fmt.Printf(err.Error())
            }
        }

    }

    err = file.Save(excelName + ".xlsx")
    if err != nil {
        fmt.Printf(err.Error())
    }
}

    原文作者:aloris
    原文地址: https://www.jianshu.com/p/99456156e1ce
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞