go语言爬虫单机

Single Crawler In Go

Posted by BY on August 1, 2018

前言

使用go语言做点东西,跟着网上大神一起学习,做做笔记

正文

学习一个语言快速的方法就是不断的用。

步骤1

首先获取网站的页面

package main

import (
	"net/http"
	"fmt"
	"io/ioutil"
)

func main() {
	resp, err := http.Get("http://www.zhenai.com/zhenhun")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusOK {
		fmt.Println("Error: status code", resp.StatusCode)
		return
	}

	all, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%s\n",all)
}

获取到网页上的内容时发现乱码,需要将原来的编码转化为UTF-8编码。

package main
import (
	"net/http"
	"fmt"
	"io/ioutil"
	"golang.org/x/text/encoding/simplifiedchinese"
	"golang.org/x/text/transform"
)

func main() {
	resp, err := http.Get("http://www.zhenai.com/zhenhun")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusOK {
		fmt.Println("Error: status code", resp.StatusCode)
		return
	}
	//这两行是为转码而写
	utf8Reader := transform.NewReader(resp.Body, simplifiedchinese.GBK.NewDecoder())
	all, err := ioutil.ReadAll(utf8Reader)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%s\n",all)
}

输入转码的两行代码后,发现idea找不到该包,需要从网上下载
golang.org/x包放到了https://github.com/golang/text 中,下载时需要先在本地安装目录下src/建立golang.org/x的目录后,再下载。 mkdir -p golang.org/x git clone https://github.com/golang/text.git 或go get github.com/golang/text后将包移到x目录 这样的写法有些问题,代码将编码写死成GBK到UTF-8。解决的办法有两个:一、在html中的head中检查字符集编码的类型,例如;二、使用库自动发现网页的编码 我们将采用第二种方法,同样的方法下载golang.org/x/net包

package main
import (
	"net/http"
	"fmt"
	"io/ioutil"
	"golang.org/x/text/transform"
	"io"
	"golang.org/x/text/encoding"
	"golang.org/x/net/html/charset"
	"bufio"
)

func main() {
	resp, err := http.Get("http://www.zhenai.com/zhenhun")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusOK {
		fmt.Println("Error: status code", resp.StatusCode)
		return
	}
	//在这里使用新增函数
	e := determineEncoding(resp.Body)
	utf8Reader := transform.NewReader(resp.Body, e.NewDecoder())
	all, err := ioutil.ReadAll(utf8Reader)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%s\n",all)
}
//新增检查io编码的函数
func determineEncoding(r io.ReadCloser) encoding.Encoding {
	bytes, err := bufio.NewReader(r).Peek(1024)
	if err != nil {
		panic(err)
	}
	e, _, _ := charset.DetermineEncoding(bytes, "")
	return e
}

步骤2

使用正则表达式获取URL等信息。(其实有其他方式获取到URL的方法,如CSS选择器、xpath等) 首先使用go语言自带的框架来测试一下,匹配邮箱

package main
import (
	"regexp"
	"fmt"
)
const name  = "My address is pamleft1994@gmail.com.cn"
func main() {
	re := regexp.MustCompile(`[a-zA-Z0-9]+@([a-zA-Z0-9]+\.)+[a-zA-Z0-9]+`)
	fmt.Println(re.FindString(name))
}

其中MustCompile函数中的正则表达式必须合法,若不合法则出异常。
FindString函数定义 func (re *Regexp) FindString(s string) string ,只匹配一个,若是匹配多个可以使用FindAllString函数,第二个参数n位-1表示匹配所有 其中函数有Find、FindAllSubmatch、FindAll、FindAllIndex、FindAllStringSubmatch、FindAllStringSubmatchIndex、FindAllSubmatchIndex等方法。其中函数名中带All是匹配多个,函数名带String的是入出参数为String,函数名带Submatch的是匹配字串。
接下来对获取到的网页数据进行正则表达式匹配

package main
import (
	"net/http"
	"fmt"
	"io/ioutil"
	"golang.org/x/text/transform"
	"io"
	"golang.org/x/text/encoding"
	"golang.org/x/net/html/charset"
	"bufio"
	"regexp"
)

func main() {
	resp, err := http.Get("http://city.zhenai.com/")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		fmt.Println("Error: status code", resp.StatusCode)
		return
	}
	e := determineEncoding(resp.Body)
	utf8Reader := transform.NewReader(resp.Body, e.NewDecoder())
	all, err := ioutil.ReadAll(utf8Reader)
	if err != nil {
		panic(err)
	}
	printCityList(all)
	fmt.Printf("%s\n",all)
}
func determineEncoding(r io.ReadCloser) encoding.Encoding {
	bytes, err := bufio.NewReader(r).Peek(1024)
	if err != nil {
		panic(err)
	}
	e, _, _ := charset.DetermineEncoding(bytes, "")
	return e
}

func printCityList(content []byte) {
	//<a href="http://city.zhenai.com/weinan"
	//								class="">渭南</a>
	re := regexp.MustCompile(`<a href="(http://city.zhenai.com/[a-zA-Z0-9]+)"[^>]*>([^<]*)</a>`)
	matches := re.FindAllSubmatch(content, -1)
	for _, match := range matches {
		fmt.Printf("City:%s URL:%s\n", match[2], match[1])
		fmt.Println()
	}
}

新增了printCityList函数,进行简单的正则表达式子串匹配,其中submatch方式获取到的第一条是整个表达式匹配到串,index为1、2…第1、2…个()匹配到的子串位序。 重点是如何写这个正则表达式 <a href="(http://city.zhenai.com/[a-zA-Z0-9]+)"[^>]*>([^<]*)</a>

步骤3

整体架构 可以想象成我们手动的点击网页。一、首先我们点击城市列表,将会出现很多城市。二、我们点开城市,将会看到很多用户的简要信息,并且这些用户信息是以分页的形式进行展示的。三、点开用户的简要信息后,将会看到这个用户的详细信息。我们可以从上述操作可以看出,城市列表页面、城市下用户简要信息页面和用户详细信息页面的展示方式有差别,我们可以将这样的结构抽象成为树形结构,城市列表拥有很多个城市节点,城市节点下有很多用户节点。同样按照手动点击页面的方式,首先需要城市列表的解析器,其次需要城市的解析器,最后还需要用户的解析器。每一层都对应一个解析器。将每一个抓取数据的请求,可以抽象为URL和其对应的解析器。每一个解析器的输入都是UTF-8编码的文本,输出是叶子请求(URL和子解析器,例如城市页面对应的解析器的输出为用户URL和用户解析器)。将单机版爬虫分为种子(初始请求)、引擎、获取器、任务队列。首先将种子加入任务队列,引擎从队列中获取一个请求,引擎根据请求中的Url使用获取器来获取页面,获取器返回文本,然后引擎调用请求中对应的解析器解析返回请求列表,引擎将返回的加入任务队列,直到引擎处理完所有的请求。 调度引擎

//引擎 ./engine/engine.go
func Run(seeds ...Request) {
	var requests [] Request
	for _, r := range seeds {
		requests = append(requests, r)
	}

	for len(requests) > 0 {
		r := requests[0]
		requests = requests[1:]
		log.Printf("Fetching %s", r.Url)
		body, err := fetcher.Fetch(r.Url)
		if err != nil {
			log.Printf("Fetcher:error fetching url %s %v", r.Url, err)
			continue
		}
		parseResult := r.ParserFunc(body)
		requests = append(requests, parseResult.Requests...)
		for _, item := range parseResult.Items {
			log.Printf("Got item %v", item)
		}
	}
}

类型

//类型 ./engine/type.go
type Request struct {
	Url string
	ParserFunc func([]byte) ParseResult
}

type ParseResult struct {
	Requests []Request
	Items []interface{}
}
//用户为解析树上的叶子,当用户解析器解析完后返回什么都不做的解析器
func NilParser([]byte) ParseResult{
	return ParseResult{}
}

页面获取器

//获取器 ./fetcher/fetch.go
//用户获取页面
func Fetch(url string) ([]byte, error) {
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		//fmt.Println("Error: status code", resp.StatusCode)
		return nil, fmt.Errorf("wrong status code:%d", resp.StatusCode)
	}
	bodyReader := bufio.NewReader(resp.Body)
	e := determineEncoding(bodyReader)
	utf8Reader := transform.NewReader(resp.Body, 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",err)
		return unicode.UTF8
	}
	e, _, _ := charset.DetermineEncoding(bytes, "")
	return e
}

解析器之城市列表解析器

//城市列表解析器 ./zhenai/parser/citylist.go 爬取的网站相关
func ParseCityList(content []byte) engine.ParseResult{
	//<a target="_blank" href="http://city.zhenai.com/dongguan">东莞</a>
	//<a href="http://city.zhenai.com/weinan"
	//								class="">渭南</a>
	re := regexp.MustCompile(`<a href="(http://city.zhenai.com/[a-zA-Z0-9]+)"[^>]*>([^<]*)</a>`)
	matches := re.FindAllSubmatch(content, -1)
	result := engine.ParseResult{}
	for _, match := range matches {
		result.Items = append(result.Items, string(match[2]))
		result.Requests = append(result.Requests,
			engine.Request{Url:string(match[1]),
							ParserFunc: engine.NilParser,
		})
	}
	return result
}

城市解析器

//城市解析器 ./zhenai/parser/city.go 爬取的网站相关
const cityRe = `<a href="(http://album.zhenai.com/u/[0-9]+)" target="_blank">([^<]*)</a>`
const curCity = `地区:</dt>
			
				<dd><a  href="http://city.zhenai.com/[a-zA-Z0-9]+" class="cur" >([^<]*)</a></dd>`
const nextPage = `<a class="next-page"
				href="(http://city.zhenai.com/[a-zA-Z0-9]+/[0-9]+)">下一页</a>`
//将下一页网址加入对列中
func ParseCity(contents []byte) engine.ParseResult {
	re := regexp.MustCompile(cityRe)
	matches := re.FindAllSubmatch(contents, -1)

	result := engine.ParseResult{}
	reNextPage := regexp.MustCompile(nextPage)
	mnp := reNextPage.FindAllSubmatch(contents,1)
	if nil != mnp {
		reCurCity := regexp.MustCompile(curCity)
		mcc := reCurCity.FindAllSubmatch(contents,1)
		if nil != mcc {
			result.Items = append(result.Items, "City "+string(mcc[0][1]))
			fmt.Println("run here")
			result.Requests = append(result.Requests,
				engine.Request{Url:string(mnp[0][1]),
					ParserFunc: ParseCity,
				})
		}

	}
	for _, m := range matches {
		name := string(m[2])
		result.Items = append(result.Items, "User "+string(m[2]))
		result.Requests = append(result.Requests, engine.Request{
			Url:string(m[1]),
			ParserFunc: func(c []byte) engine.ParseResult{
				return ParseProfile(c, name)
			},
		})
	}
	return result
}

用户简介解析器

//用户简介解析器 ./zhenai/parser/profile.go 爬取的网站相关
const ageReStr = `<span class="label">年龄:</span>([\d]+)岁</td>`
const heightReStr = `<span class="label">身高:</span>([\d]+)CM</td>`
const incomeReStr = `<td><span class="label">月收入:</span>([^<]+)</td>`
const weightReStr = `<span class="label">体重:</span><span field="">([\d]+)KG</span>`
const genderReStr = `<span class="label">性别:</span><span field="">([^<]+)</span>`
const xinzuoReStr = `<span class="label">星座:</span><span field="">([^<]+)</span>`
const marriageReStr = `<td><span class="label">婚况:</span>([^<]+)</td>`
const educationReStr = `<td><span class="label">学历:</span>([^<]+)</td>`
const occupationReStr = `<td><span class="label">职业:</span><span field="">([^<]+)</span></td>`
const hokouReStr = `<td><span class="label">籍贯:</span>([^<]+)</td>`
const houseReStr = `<span class="label">住房条件:</span><span field="">([^<]+)</span>`
const carReStr  = `<span class="label">是否购车:</span><span field="">([^<]+)</span>`

var ageRe = regexp.MustCompile(ageReStr)
var heightRe = regexp.MustCompile(heightReStr)
var incomeRe = regexp.MustCompile(incomeReStr)
var weightRe = regexp.MustCompile(weightReStr)
var genderRe = regexp.MustCompile(genderReStr)
var xinzuoRe = regexp.MustCompile(xinzuoReStr)
var marriageRe = regexp.MustCompile(marriageReStr)
var educationRe = regexp.MustCompile(educationReStr)
var occupationRe = regexp.MustCompile(occupationReStr)
var hokouRe = regexp.MustCompile(hokouReStr)
var houseRe = regexp.MustCompile(houseReStr)
var carRe = regexp.MustCompile(carReStr)
func ParseProfile(contents []byte, name string) engine.ParseResult {
	profile := model.Profile{}
	age, err := strconv.Atoi(extraString(contents, ageRe))
	if nil == err {
		profile.Age = age
	}
	height, err := strconv.Atoi(extraString(contents, heightRe))
	if nil == err {
		profile.Height = height
	}
	weight, err := strconv.Atoi(extraString(contents, weightRe))
	if nil == err {
		profile.Weight = weight
	}
	profile.Income = extraString(contents, incomeRe)
	profile.Gender = extraString(contents, genderRe)
	profile.Xinzuo = extraString(contents, xinzuoRe)
	profile.Marriage = extraString(contents, marriageRe)
	profile.Education = extraString(contents, educationRe)
	profile.Occupation = extraString(contents, occupationRe)
	profile.Hokou = extraString(contents, hokouRe)
	profile.House = extraString(contents, houseRe)
	profile.Car = extraString(contents, carRe)
	profile.Name = name
	result := engine.ParseResult{
		Items: []interface{}{profile},
	}
	return result
}
func extraString(contents []byte, re *regexp.Regexp) string {
	match := re.FindSubmatch(contents)
	if len(match) >= 2 {
		return string(match[1])
	} else {
		return ""
	}
}

main方法体

//main方法体 ./main.go
func main() {
	engine.Run(engine.Request{
		Url: url,
		ParserFunc:parser.ParseCityList,
	})
}

总结:

单机版爬虫已完成

结语

后面章节将介绍并发版爬虫。