前言
使用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,
})
}
总结:
单机版爬虫已完成
结语
后面章节将介绍并发版爬虫。