-
爬虫的分类
网络爬虫分为两类
1. 通用爬虫: 类似于baidu,google. 他们会把大量的数据挖下来,保存到自己的服务器上. 用户打开跳转的时候,其实先是跳转到他们自己的服务器.
2. 聚焦爬虫: 其实就是有目标的爬虫,比如我只需要内容信息. 那我就只爬取内容信息.
通常我们使用的爬虫都是聚焦爬虫
-
项目总体结构
爬虫的思想很简单.
1. 写一段程序,从网络上把数据抓下来
2. 保存到我们的数据库中
3. 写一个前端页面,展示数据
-
go语言的爬虫库/框架
以上是go语言中已经you封装好的爬虫库或者框架,但我们写爬虫的目的是为了学习. 所以.....不使用框架了
-
本课程的爬虫项目
1. 不用已有的爬虫库和框架
2. 数据库使用ElasticSearch
3. 页面展示使用标准库的http
这个练习的目的,就是使用go基础.之所以选择爬虫,是因为爬虫有一定的复杂性
-
爬虫的主题
哈哈,要是还没有女盆友,又不想花钱的童鞋,可以自己学习一下爬虫技术
-
如何发现用户
1. 通过http://www.zhenai.com/zhenghun页面进入. 这是一个地址列表页. 你想要找的那个她(他)是哪个城市的
2. 在用户的详情页,有推荐--猜你喜欢
-
爬虫总体算法
1. 城市列表,找到一个城市
2. 城市下面有用户列表. 点击某一个用户,进去查看用户的详情信息
需要注意的是,用户推荐,会出现重复推荐的情况. 第一个页面推荐了张三,从上三进来推荐了李四. 从李四进来有推荐到第一个页面了. 这就形成了死循环,重复推荐
我们完成爬虫,分为三个阶段
1. 单机版. 将所有功能在一个引用里完成
2. 并发版. 有多个连接同时访问,这里使用了go的协程
3. 分布式. 多并发演进就是分布式了. 削峰,减少服务器的压力.
下面开始项目阶段
项目
一. 单任务版网络爬虫
目标: 抓取珍爱网中的用户信息.
1. 抓取用户所在的城市列表信息
2. 抓取某一个城市的某一个人的基本信息,把信息存到我们自己的数据库中
分析:
1. 通过url获取网站数据. 拿到我们想要的地址,以及点击地址跳转的url. 把地址信息保存到数据库. 数据量预估300
2. 通过url循环获取用户列表. 拿到页面详情url,在获取用户详情信息. 把用户信息保存到数据库. 数据量会比较大. 一个城市如果有10000个人注册了,那么就有300w的数据量.
3. 所以,数据库选择的是elasticSearch
-------------------
抓取城市列表页,也就是目标把这个页面中我们要的内容抓取下来.
其实就两个内容,1. 城市名称,2. 点击城市名称跳转的url
第一步: 抓取页面内容
package main import ( "fmt" io/IoUtilnet/httpregexp" ) func main() { // 第一步,通过url抓取页面 resp,err := http.Get(http://www.zhenai.com/zhenghun) if err != nil { panic(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return } 读取出来body的所有内容 all,err := IoUtil.ReadAll(resp.Body) nil { panic(err) } fmt.Printf("%s\n",all) printCityList(all) }
第二步: 正则表达式,提取城市名称和跳转的url
nil { panic(err) } //fmt.Printf("%s\n",all) printCityList(all) } /** * 正则表达式提取城市名称和跳转的url */ func printCityList(content []byte) { re := regexp.MustCompile(`<a href=(http://www.zhenai.com/zhenghun/[a-z1-9]+)" data-v-5e16505f>([^<]+)</a>`) all := re.FindAllSubmatch(content,-1for _,line := range all { fmt.Printf(city: %s,url: %s\n",line[2],1)">]) } }
结果如下:
这样第一个页面就抓取完成了. 第二个和第三个页面可以了类似处理. 但这样不好,我们需要把结构进行抽象提取. 形成一个通用的模块
再来分析我们的单机版爬虫项目
项目结构---共有三层结构:
解析器抽象
既然都是解析器,那么我们就把解析器抽象出来.
每一个解析器,都有输入参数和输出参数
输入参数: 通过url抓取的网页内容.
输出参数: Request{URL,Parse}列表,Item列表
为什么输出的第一个参数是Request{URL,Parse}列表呢?
- 城市列表解析器,我们获取到城市名称和url,点解url,要进入的是城市解析器. 所以这里的解析器应该是城市解析器.
- 城市解析器. 我们进入城市以后,会获取用户的姓名和用户详情页的url. 所以这里的解析器,应该传的是用户解析器.
- 用户解析器. 用来解析用户的信息. 保存入库
项目架构
1. 有一个或多个种子页面,发情请求到处理引擎. 引擎不是马上就对任务进行处理的. 他首先吧种子页面添加到队列里去
2. 处理引擎从队列中取出要处理的url,交给提取器提取页面内容. 然后将页面内容返回
3. 将页面内容进行解析,返回的是Request{URL,Parse}列表和 Items列表
4. 我们将Request添加到任务队列中. 然后下一次依然从任务队列中取出一条记录. 这样就循环往复下去了
5. 队列什么时候结束呢? 有可能不会结束,比如循环推荐,也可能可以结束.
这样,结构都有了,入参出参也定义好了,接下来就是编码实现
我们先来改写上面的抓取城市列表
项目结构
1. 有一个提取器
2. 有一个解析器. 解析器里应该有三种类型的解析器
3. 有一个引擎来触发操作
4. 有一个main方法入口
第一步: Fetcher--提取器
package fetcher import ( ) 抓取器 func Fetch(url string) ([],error) { 页面 client := http.Client{} request,err := http.NewRequest(GETUser-AgentMozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/66.0.3359.181 Safari/537.36) resp,err := client.Do(request) resp,err := http.Get(url) nil { return nil,fmt.Errorf(http get error :%shttp get error errCode:%d 读取出来body的所有内容 IoUtil.ReadAll(resp.Body) }
第二步: 有一个城市解析器
package parser import ( aaa/crawler/zhenai/engine ) const cityListRegexp = `<a[^href]*href="[^>]*>([^<]+)</a>` func ParseCityList(content []) (engine.ParseResult) { re := regexp.MustCompile(cityListRegexp) all := re.FindAllSubmatch(content,1)">) pr := engine.ParseResult{} count := 1 range all { req := engine.Request{ Url:string(line[]),ParseFun: ParseCity,} pr.Req = append(pr.Req,req) pr.Items = append(pr.Items,1)">City: " + 2])) count -- if count <=0 { break } } pr }
第三步:定义引擎需要使用的结构体
package engine type Request struct { Url string ParseFun func(content []) ParseResult } type ParseResult { Req []Request Items []interface{} } func NilParse(content []) ParseResult{ ParseResult{} }
第四步: 抽象出引擎
package engine import ( aaa/crawler/fetchergithub.com/astaxie/beego/logs ) func Run(seeds ...Request) { var que []Request range seeds { que = append(que,seed) } for len(que) > { cur := que[] que = que[:] logs.Info(fetch url: fetcher.Fetch(cur.Url) if e != nil { logs.Info(解析页面异常 url:continue } resultParse := cur.ParseFun(cont) que = range resultParse.Items { fmt.Printf(内容项: %s \naaa/crawler/zhenai/parser ) func main() { req := engine.Request{ Url:])) } pr }const cityRe = `<a[^href]*href=(http://album.zhenai.com/u/[0-9]+)` func ParseCity(content []) engine.ParseResult{ cityRegexp:= regexp.MustCompile(cityRe) subs := cityRegexp.FindAllSubmatch(content,1)"> engine.ParseResult{}range subs { name := string(sub[]) 获取用户的详细地址 re := 注意,这里定义了一个函数来传递,这样可以吧name也传递过去 ParseFun: func(content []) engine.ParseResult { ParseUser(content,name) },re) pr.Items = append(pr.Items,1)">Name:
城市解析器和城市列表解析器基本类似. 返回的数据是request和用户名
第七步: 用户解析器
aaa/crawler/zhenai/modelstrconvstrings 个人基本信息 const userRegexp = `<div[^class]*class=m-btn purple"[^>]*>([^<]+)</div>` 个人隐私信息 const userPrivateRegexp = `<div data-v-8b1eac0c="" m-btn pink">([^<]+)</div> 择偶条件 const userPartRegexp = `<div data-v-8b1eac0c=m-btn` func ParseUser(content []byte,name ) engine.ParseResult { pro := model.Profile{} pro.Name = name 获取用户的年龄 userCompile := regexp.MustCompile(userRegexp) usermatch := userCompile.FindAllSubmatch(content,1)">) pr :=for i,userInfo := range usermatch { text := string(userInfo[if i == { pro.Marry = text } if strings.Contains(text,1)">岁) { age,_ := strconv.Atoi(strings.Split(text,1)">")[]) pro.Age = age 座) { pro.Xingzuo =cm) { height,1)">]) pro.Height = height } kg) { weight,1)">]) pro.Weight = weight 工作地:) { salary := strings.Split(text,1)">] pro.Salary = salary 月收入:7 { pro.Occuption =8 { pro.Education = } } pr.Items = append(pr.Items,pro) pr }
看一下抓取的效果吧
抓取的城市列表
抓取的某个城市的用户列表
具体某个人的详细信息
至此,完成了单机版爬虫. 再来回顾一下.
做完了感觉,这个爬虫其实很简单,之前用java都实现过.只不过这次是用go实现的
- 有一个种子页面,从这个页面进来,会获取到源源不断的用户信息
- 遇到一个403的问题. 需要使用自定义的http请求,设置header 的User-agent,否则服务器请求被拒绝
- 使用函数式编程. 函数的特点就是灵活. 灵活多变. 想怎么封装都行. 这里是在cityParse解析出user信息的时候,使用了函数式编程.把用户名传递过去了
二. 并发版网络爬虫
三. 分布式网络爬虫