以上是go语言中已经you封装好的爬虫库或者框架, 但我们写爬虫的目的是为了学习. 所以.....不使用框架了
1. 不用已有的爬虫库和框架
2. 数据库使用ElasticSearch
3. 页面展示使用标准库的http
这个练习的目的,就是使用go基础.之所以选择爬虫,是因为爬虫有一定的复杂性
哈哈, 要是还没有女盆友, 又不想花钱的童鞋, 可以自己学习一下爬虫技术
1. 通过http://www.zhenai.com/zhenghun页面进入. 这是一个地址列表页. 你想要找的那个她(他)是哪个城市的
2. 在用户的详情页, 有推荐--猜你喜欢
1. 城市列表, 找到一个城市
2. 城市下面有用户列表. 点击某一个用户, 进去查看用户的详情信息
3. 用户详情页右侧有猜你喜欢, 链接到一个新的用户详情页
需要注意的是, 用户推荐, 会出现重复推荐的情况. 第一个页面推荐了张三, 从上三进来推荐了李四. 从李四进来有推荐到第一个页面了. 这就形成了死循环, 重复推荐
我们完成爬虫, 分为三个阶段
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/ioutil" "net/http" "regexp" ) 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) if err != nil { panic(err) } //fmt.Printf("%s\n", all) printCityList(all) }
package main import ( "fmt" "io/ioutil" "net/http" "regexp" ) 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) if err != nil { panic(err) } //fmt.Printf("%s\n", all) printCityList(all) } /** * 正则表达式提取城市名称和跳转的url */ func printCityList(content []byte) { re := regexp.MustCompile(`"(http://www.zhenai.com/zhenghun/[a-z1-9]+)" data-v-5e16505f>([^<]+)`) all := re.FindAllSubmatch(content, -1) for _, line := range all { fmt.Printf("city: %s, url: %s\n", line[2], line[1]) } }
结果如下:
这样第一个页面就抓取完成了. 第二个和第三个页面可以了类似处理. 但这样不好, 我们需要把结构进行抽象提取. 形成一个通用的模块
既然都是解析器, 那么我们就把解析器抽象出来.
每一个解析器, 都有输入参数和输出参数
输入参数: 通过url抓取的网页内容.
输出参数: Request{URL, Parse}列表, Item列表
为什么输出的第一个参数是Request{URL, Parse}列表呢?
1. 有一个或多个种子页面, 发情请求到处理引擎. 引擎不是马上就对任务进行处理的. 他首先吧种子页面添加到队列里去
2. 处理引擎从队列中取出要处理的url, 交给提取器提取页面内容. 然后将页面内容返回
3. 将页面内容进行解析, 返回的是Request{URL, Parse}列表和 Items列表
4. 我们将Request添加到任务队列中. 然后下一次依然从任务队列中取出一条记录. 这样就循环往复下去了
5. 队列什么时候结束呢? 有可能不会结束, 比如循环推荐, 也可能可以结束.
我们先来改写上面的抓取城市列表
1. 有一个提取器
2. 有一个解析器. 解析器里应该有三种类型的解析器
3. 有一个引擎来触发操作
4. 有一个main方法入口
package fetcher import ( "fmt" "io/ioutil" "net/http" ) // 抓取器 func Fetch(url string) ([]byte, error) { // 第一步, 通过url抓取页面 client := http.Client{} request, err := http.NewRequest("GET", url, nil) request.Header.Set("User-Agent", "Mozilla/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) if err != nil { return nil, fmt.Errorf("http get error :%s", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("http get error errCode:%d", http.StatusOK) } // 读取出来body的所有内容 return ioutil.ReadAll(resp.Body) }
package parser import ( "aaa/crawler/zhenai/engine" "regexp" ) const cityListRegexp = `"(http://www.zhenai.com/zhenghun/[a-z1-9]+)"[^>]*>([^<]+)` func ParseCityList(content []byte) (engine.ParseResult) { re := regexp.MustCompile(cityListRegexp) all := re.FindAllSubmatch(content, -1) pr := engine.ParseResult{} count := 1 for _, line := range all { req := engine.Request{ Url:string(line[1]), ParseFun: ParseCity, } pr.Req = append(pr.Req, req) pr.Items = append(pr.Items, "City: " + string(line[2])) count -- if count <=0 { break } } return pr }
package engine type Request struct { Url string ParseFun func(content []byte) ParseResult } type ParseResult struct { Req []Request Items []interface{} } func NilParse(content []byte) ParseResult{ return ParseResult{} }
package engine import ( "aaa/crawler/fetcher" "fmt" "github.com/astaxie/beego/logs" ) func Run(seeds ...Request) { var que []Request for _, seed := range seeds { que = append(que, seed) } for len(que) > 0 { cur := que[0] que = que[1:] logs.Info("fetch url:", cur.Url) cont, e := fetcher.Fetch(cur.Url) if e != nil { logs.Info("解析页面异常 url:", cur.Url) continue } resultParse := cur.ParseFun(cont) que = append(que, resultParse.Req...) for _, item := range resultParse.Items { fmt.Printf("内容项: %s \n", item) } } }
package main import ( "aaa/crawler/zhenai/engine" "aaa/crawler/zhenai/parser" ) func main() { req := engine.Request{ Url:"http://www.zhenai.com/zhenghun", ParseFun: parser.ParseCityList, } engine.Run(req) }
package parser import ( "aaa/crawler/zhenai/engine" "regexp" ) const cityRe = `"(http://album.zhenai.com/u/[0-9]+)"[^>]*>([^<]+)` func ParseCity(content []byte) engine.ParseResult{ cityRegexp:= regexp.MustCompile(cityRe) subs := cityRegexp.FindAllSubmatch(content, -1) pr := engine.ParseResult{} for _, sub := range subs { name := string(sub[2]) // 获取用户的详细地址 re := engine.Request{ Url:string(sub[1]), // 注意, 这里定义了一个函数来传递, 这样可以吧name也传递过去 ParseFun: func(content []byte) engine.ParseResult { return ParseUser(content, name) }, } pr.Req = append(pr.Req, re) pr.Items = append(pr.Items, "Name: " + string(sub[2])) } return pr }
城市解析器和城市列表解析器基本类似. 返回的数据是request和用户名
package parser import ( "aaa/crawler/zhenai/engine" "aaa/crawler/zhenai/model" "regexp" "strconv" "strings" ) // 个人基本信息 const userRegexp = `class]*class="m-btn purple"[^>]*>([^<]+)` // 个人隐私信息 const userPrivateRegexp = `"" class="m-btn pink">([^<]+)` // 择偶条件 const userPartRegexp = `"" class="m-btn">([^<]+)` func ParseUser(content []byte, name string) engine.ParseResult { pro := model.Profile{} pro.Name = name // 获取用户的年龄 userCompile := regexp.MustCompile(userRegexp) usermatch := userCompile.FindAllSubmatch(content, -1) pr := engine.ParseResult{} for i, userInfo := range usermatch { text := string(userInfo[1]) if i == 0 { pro.Marry = text continue } if strings.Contains(text, "岁") { age, _ := strconv.Atoi(strings.Split(text, "岁")[0]) pro.Age = age continue } if strings.Contains(text, "座") { pro.Xingzuo = text continue } if strings.Contains(text, "cm") { height, _ := strconv.Atoi(strings.Split(text, "cm")[0]) pro.Height = height continue } if strings.Contains(text, "kg") { weight, _ := strconv.Atoi(strings.Split(text, "kg")[0]) pro.Weight = weight continue } if strings.Contains(text, "工作地:") { salary := strings.Split(text, "工作地:")[1] pro.Salary = salary continue } if strings.Contains(text, "月收入:") { salary := strings.Split(text, "月收入:")[1] pro.Salary = salary continue } if i == 7 { pro.Occuption = text continue } if i == 8 { pro.Education = text continue } } pr.Items = append(pr.Items, pro) return pr }
二. 并发版网络爬虫
三. 分布式网络爬虫