热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

第十三章go实现分布式网络爬虫单机版爬虫

 网络爬虫分为两类1.通用爬虫:类似于baidu,google.他们会把大量的数据挖下来,保存到自己的服务器上.用户打开跳转的时候,其实先是跳转到他们自己的服务器. 2.聚焦爬虫:

第十三章   go实现分布式网络爬虫---单机版爬虫

 

网络爬虫分为两类

1. 通用爬虫: 类似于baidu, google. 他们会把大量的数据挖下来, 保存到自己的服务器上. 用户打开跳转的时候, 其实先是跳转到他们自己的服务器. 

2. 聚焦爬虫: 其实就是有目标的爬虫, 比如我只需要内容信息. 那我就只爬取内容信息. 

通常我们使用的爬虫都是聚焦爬虫 


 

  • 项目总体结构

 第十三章   go实现分布式网络爬虫---单机版爬虫

 

 爬虫的思想很简单.

1. 写一段程序, 从网络上把数据抓下来

2. 保存到我们的数据库中

3. 写一个前端页面, 展示数据


 

  • go语言的爬虫库/框架

 

 

 第十三章   go实现分布式网络爬虫---单机版爬虫

  

以上是go语言中已经you封装好的爬虫库或者框架, 但我们写爬虫的目的是为了学习. 所以.....不使用框架了


 

  • 本课程的爬虫项目

第十三章   go实现分布式网络爬虫---单机版爬虫

1. 不用已有的爬虫库和框架

2. 数据库使用ElasticSearch

3. 页面展示使用标准库的http

这个练习的目的,就是使用go基础.之所以选择爬虫,是因为爬虫有一定的复杂性


 

  • 爬虫的主题 

 第十三章   go实现分布式网络爬虫---单机版爬虫

哈哈, 要是还没有女盆友, 又不想花钱的童鞋, 可以自己学习一下爬虫技术


 

  • 如何发现用户

第十三章   go实现分布式网络爬虫---单机版爬虫

1. 通过http://www.zhenai.com/zhenghun页面进入. 这是一个地址列表页. 你想要找的那个她(他)是哪个城市的

2. 在用户的详情页, 有推荐--猜你喜欢

 


 

  • 爬虫总体算法 

第十三章   go实现分布式网络爬虫---单机版爬虫

 1. 城市列表, 找到一个城市

2. 城市下面有用户列表. 点击某一个用户, 进去查看用户的详情信息

3. 用户详情页右侧有猜你喜欢, 链接到一个新的用户详情页

需要注意的是, 用户推荐, 会出现重复推荐的情况. 第一个页面推荐了张三, 从上三进来推荐了李四. 从李四进来有推荐到第一个页面了. 这就形成了死循环, 重复推荐


 

 第十三章   go实现分布式网络爬虫---单机版爬虫

 

 第十三章   go实现分布式网络爬虫---单机版爬虫

我们完成爬虫, 分为三个阶段

1. 单机版. 将所有功能在一个引用里完成

2. 并发版. 有多个连接同时访问, 这里使用了go的协程

3. 分布式. 多并发演进就是分布式了. 削峰, 减少服务器的压力. 


 

下面开始项目阶段

项目

一. 单任务版网络爬虫

目标: 抓取珍爱网中的用户信息.

1.  抓取用户所在的城市列表信息

2. 抓取某一个城市的某一个人的基本信息, 把信息存到我们自己的数据库中

分析: 

1. 通过url获取网站数据. 拿到我们想要的地址,以及点击地址跳转的url. 把地址信息保存到数据库.  数据量预估300

2. 通过url循环获取用户列表. 拿到页面详情url, 在获取用户详情信息. 把用户信息保存到数据库. 数据量会比较大. 一个城市如果有10000个人注册了, 那么就有300w的数据量.

3. 所以, 数据库选择的是elasticSearch

 -------------------

抓取城市列表页, 也就是目标把这个页面中我们要的内容抓取下来.

其实就两个内容, 1. 城市名称, 2. 点击城市名称跳转的url

第十三章   go实现分布式网络爬虫---单机版爬虫

 

 

第一步: 抓取页面内容

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

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])

    }
}

 

 结果如下:

第十三章   go实现分布式网络爬虫---单机版爬虫

 这样第一个页面就抓取完成了. 第二个和第三个页面可以了类似处理. 但这样不好, 我们需要把结构进行抽象提取. 形成一个通用的模块


再来分析我们的单机版爬虫项目

项目结构---共有三层结构:

  • 城市列表解析器: 用来解析城市列表
  • 城市解析器: 用来解析某一个城市的页面内容, 城市里是用户列表和分页
  • 用户解析器: 从城市页面点击用户进入到用户的详情页, 解析用户的详情信息

解析器抽象

既然都是解析器, 那么我们就把解析器抽象出来.

每一个解析器, 都有输入参数和输出参数

输入参数: 通过url抓取的网页内容. 

输出参数: Request{URL, Parse}列表, Item列表

为什么输出的第一个参数是Request{URL, Parse}列表呢?

  •  城市列表解析器, 我们获取到城市名称和url, 点解url, 要进入的是城市解析器. 所以这里的解析器应该是城市解析器. 
  • 城市解析器. 我们进入城市以后, 会获取用户的姓名和用户详情页的url. 所以这里的解析器, 应该传的是用户解析器.
  • 用户解析器. 用来解析用户的信息. 保存入库

项目架构

第十三章   go实现分布式网络爬虫---单机版爬虫

 

 1. 有一个或多个种子页面, 发情请求到处理引擎. 引擎不是马上就对任务进行处理的. 他首先吧种子页面添加到队列里去

2. 处理引擎从队列中取出要处理的url, 交给提取器提取页面内容. 然后将页面内容返回

3. 将页面内容进行解析, 返回的是Request{URL, Parse}列表和 Items列表

4. 我们将Request添加到任务队列中. 然后下一次依然从任务队列中取出一条记录. 这样就循环往复下去了

5. 队列什么时候结束呢? 有可能不会结束, 比如循环推荐, 也可能可以结束. 

这样,结构都有了, 入参出参也定义好了, 接下来就是编码实现

我们先来改写上面的抓取城市列表

项目结构 

1. 有一个提取器

2. 有一个解析器. 解析器里应该有三种类型的解析器

3. 有一个引擎来触发操作

4. 有一个main方法入口

第一步: Fetcher--提取器

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 }

看一下抓取的效果吧

抓取的城市列表

第十三章   go实现分布式网络爬虫---单机版爬虫

 

 抓取的某个城市的用户列表

第十三章   go实现分布式网络爬虫---单机版爬虫

 

具体某个人的详细信息

第十三章   go实现分布式网络爬虫---单机版爬虫

 

 至此, 完成了单机版爬虫. 再来回顾一下. 

做完了感觉, 这个爬虫其实很简单, 之前用java都实现过.只不过这次是用go实现的

 

二. 并发版网络爬虫

 

 

 

 

三. 分布式网络爬虫

 


推荐阅读
  • Maven + Spring + MyBatis + MySQL 环境搭建与实例解析
    本文详细介绍如何使用MySQL数据库进行环境搭建,包括创建数据库表并插入示例数据。随后,逐步指导如何配置Maven项目,整合Spring框架与MyBatis,实现高效的数据访问。 ... [详细]
  • JUnit下的测试和suite
    nsitionalENhttp:www.w3.orgTRxhtml1DTDxhtml1-transitional.dtd ... [详细]
  • Requests库的基本使用方法
    本文介绍了Python中Requests库的基础用法,包括如何安装、GET和POST请求的实现、如何处理Cookies和Headers,以及如何解析JSON响应。相比urllib库,Requests库提供了更为简洁高效的接口来处理HTTP请求。 ... [详细]
  • 本文介绍了如何通过C#语言调用动态链接库(DLL)中的函数来实现IC卡的基本操作,包括初始化设备、设置密码模式、获取设备状态等,并详细展示了将TextBox中的数据写入IC卡的具体实现方法。 ... [详细]
  • Web动态服务器Python基本实现
    Web动态服务器Python基本实现 ... [详细]
  • 本文探讨了如何通过Service Locator模式来简化和优化在B/S架构中的服务命名访问,特别是对于需要频繁访问的服务,如JNDI和XMLNS。该模式通过缓存机制减少了重复查找的成本,并提供了对多种服务的统一访问接口。 ... [详细]
  • Jupyter Notebook多语言环境搭建指南
    本文详细介绍了如何在Linux环境下为Jupyter Notebook配置Python、Python3、R及Go四种编程语言的环境,包括必要的软件安装和配置步骤。 ... [详细]
  • 软件测试行业深度解析:迈向高薪的必经之路
    本文深入探讨了软件测试行业的发展现状及未来趋势,旨在帮助有志于在该领域取得高薪的技术人员明确职业方向和发展路径。 ... [详细]
  • 利用 Calcurse 在 Linux 终端高效管理日程与任务
    对于喜爱使用 Linux 终端进行日常操作的系统管理员来说,Calcurse 提供了一种强大的方式来管理日程安排、待办事项及会议。本文将详细介绍如何在 Linux 上安装和使用 Calcurse,帮助用户更有效地组织工作。 ... [详细]
  • 一、Advice执行顺序二、Advice在同一个Aspect中三、Advice在不同的Aspect中一、Advice执行顺序如果多个Advice和同一个JointPoint连接& ... [详细]
  • 如何在PHP中安装Xdebug扩展
    本文介绍了如何从PECL下载并编译安装Xdebug扩展,以及如何配置PHP和PHPStorm以启用调试功能。 ... [详细]
  • 在安装并配置了Elasticsearch后,我在尝试通过GET /_nodes请求获取节点信息时遇到了问题,收到了错误消息。为了确保请求的正确性和安全性,我需要进一步排查配置和网络设置,以确保Elasticsearch集群能够正常响应。此外,还需要检查安全设置,如防火墙规则和认证机制,以防止未经授权的访问。 ... [详细]
  • 本文介绍了如何在 Windows 系统上利用 Docker 构建一个包含 NGINX、PHP、MySQL、Redis 和 Elasticsearch 的集成开发环境。通过详细的步骤说明,帮助开发者快速搭建和配置这一复杂的技术栈,提升开发效率和环境一致性。 ... [详细]
  • 本文深入解析了Elasticsearch写入与查询的底层机制。在数据写入过程中,首先会将数据暂存至内存缓冲区,在此阶段数据尚不可被搜索。同时,为了保证数据的持久性和可靠性,系统会将这些数据同步记录到事务日志(translog)中。当内存缓冲区接近满载时,系统会触发刷新操作,将缓冲区中的数据写入到磁盘上的段文件中,从而使其可被搜索。此外,文章还探讨了查询过程中涉及的索引分片、倒排索引等关键技术,为读者提供了全面的技术理解。 ... [详细]
  • Elasticsearch 嵌套调用中动态类导致数据返回异常分析与解决方案 ... [详细]
author-avatar
鑫瑜Twinkle
这个家伙很懒,什么也没留下!
Tags | 热门标签
RankList | 热门文章
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有