go语言实现并发网络爬虫的示例代码

go语言做爬虫也是很少尝试,首先我的思路是看一下爬虫的串行实现,然后通过两个并发实现:一个使用锁,另一个使用通道

这里不涉及从页面中提取URL的逻辑(请查看Go框架colly的内容)。网络抓取只是作为一个例子来考察Go的并发性。

我们想从我们的起始页中提取所有的URL,将这些URL保存到一个列表中,然后对列表中的每个URL做同样的处理。页面的图很可能是循环的,所以我们需要记住哪些页面已经经历了这个过程(或者在使用并发时,处于这个过程的中间)。

串行爬虫首先检查我们是否已经在获取地图中获取了该页面。如果我们没有,那么它就在页面上找到的每个URL上调用自己。注意:map 在Go中是引用类型,所以每次调用都会得到相同的 map。

func Serial(url string, fetcher Fetcher, fetched map[string]bool) {
    if fetched[url] {
        return
    }
    fetched[url] = true
    urls, err := fetcher.Fetch(url)
    if err != nil {
        return
    }
    for _, u := range urls {
        Serial(u, fetcher, fetched)
    }
    return
}
func main() {
    Serial(<page>, fetcher, make(map[string]bool))
}

fetcher将包含提取URLs到列表中的逻辑(也可以对页面的内容做一些处理)。这个实现不是本讲的重点。

由于网络速度很慢,我们可以使用并发性来加快这个速度。为了实现这一点,我们需要使用锁(在读/写时锁定已经获取的页面地图)和 waitgroup(等待所有的goroutine完成)。

已经获取的页面的 map 只能由持有锁的线程访问,因为我们不希望多个线程开始处理同一个URL。如果在一个线程的读和写之间,另一个线程在第一个线程更新之前从 map 上得到了相同的读数,这就可能发生。

我们定义了fetchState结构,将 map 和锁组合在一起,并定义了一个方法来初始化它。

爬虫程序的开始是一样的,检查我们是否已经获取了URL,但这次使用sync.Mutex来锁定 map,如前所述。然后,对于页面上发现的每个URL,我们在一个新的goroutine中启动相同的函数。在启动之前,我们将WaitGroup的计数器增加1,done.Wait()在退出之前等待所有的抓取工作完成。

func ConcurrentMutex(url string, fetcher Fetcher, f *fetchState) {
    f.mu.Lock()
    already := f.fetched[url]
    f.fetched[url] = true
    f.mu.Unlock()
    if already {
        return
    }
    urls, err := fetcher.Fetch(url)
    if err != nil {
        return
    }
    var done sync.WaitGroup
    for _, u := range urls {
        done.Add(1)
        go func(u string) {
            defer done.Done()
            ConcurrentMutex(u, fetcher, f)
        }(u)
    }
    done.Wait()
    return
}
type fetchState struct {
    mu      sync.Mutex
    fetched map[string]bool
}
func makeState() *fetchState {
    f := &fetchState{}
    f.fetched = make(map[string]bool)
    return f
}
func main() {
    ConcurrentMutex(<page>, fetcher, makeState())
}

注意:

[1] done.Done()的调用被推迟了,以防我们在其中一个调用中出现错误,在这种情况下,我们仍然要递减WaitGroup的计数器。

[2] 这段代码的一个问题是,我们没有限制线程的数量。但值得一提的是,goroutines比其他语言的线程更轻量级,并且由Go运行时管理,系统调用更少。

[3] 我们把字符串u传给立即函数,以便制作一个URL的副本,然后才把它送到goroutine,因为变量u在外层for循环中发生了变化。要理解这样做的必要性,一个更简单的例子是,在没有WaitGroup的情况下。

func checkThisOut() {
  s := "abc"
  sec := time.Second
  go func() {time.Sleep(sec); fmt.Printf("s = %v\n", s)}()
  go func(u string) {time.Sleep(sec); fmt.Printf("u = %v\n", u)}(s)
  s = "def"
  time.Sleep(2 * sec)
}
// this prints out: u = abc, s = def

[4] 我们可以运行内置的数据竞赛检测器,通过运行go run -race .来帮助检测竞赛条件。它在这个例子中非常有效。

下一个并发版本在线程之间完全不共享内存!嗯,这并不准确。我们只是不会自己同步访问共享数据。相反,我们使用一个通道在goroutine之间进行通信。

在这个最后的版本中,我们有一个主函数在主线程上运行。只有这个函数能看到 map 并从通道中读取。channel ,像 map 一样,也是引用类型。所以这里只有一个通道。

在启动时,我们将第一个URL写到通道上。这是在一个goroutine中完成的,因为向一个没有缓冲的通道的写入会导致goroutine暂停,直到该值被另一个goroutine读取。

我们在一个for循环中从通道中读取URL的列表(从一个没有缓冲的通道中读取也会阻塞)。然后,我们以与之前的实现类似的方式浏览该列表。通过使用一个计数器,一旦没有更多的工作者,这个循环就会中断。

工作者获取URL的列表,将它们传递给通道。如果出现错误,会传递一个空列表,这样从通道读取的for循环最终会退出(计数器的设置方式是,我们等待从每个goroutine读取一个值)。

func ConcurrentChannel(url string, fetcher Fetcher) {
    ch := make(chan []string)
    go func() {
        ch <- []string{url}
    }()
    master(ch, fetcher)
}
func master(ch chan []string, fetcher Fetcher) {
    n := 1
    fetched := make(map[string]bool)
    for urls := range ch {
        for _, u := range urls {
            if fetched[u] == false {
                fetched[u] = true
                n += 1
                go worker(u, ch, fetcher)
            }
        }
        n -= 1
        if n == 0 {
            break
        }
    }
}
func worker(url string, ch chan []string, fetcher Fetcher) {
    urls, err := fetcher.Fetch(url)
    if err != nil {
        ch <- []string{}
    } else {
        ch <- urls
    }
}

到此这篇关于go语言实现并发网络爬虫的示例代码的文章就介绍到这了,更多相关go语言并发网络爬虫内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 一文带你了解Golang中的并发性

    目录 什么是并发性,为什么它很重要 并发性与平行性 Goroutines, the worker Mortys Channels, the green portal 总结 并发是一个很酷的话题,一旦你掌握了它,就会成为一笔巨大的财富.说实话,我一开始很害怕写这篇文章,因为我自己直到最近才对并发性不太适应.我已经掌握了基础知识,所以我想帮助其他初学者学习Go的并发性.这是众多并发性教程中的第一篇,请继续关注更多的教程. 什么是并发性,为什么它很重要 并发是指在同一时间运行多个事物的能力.你的电脑有

  • Go语言实现的可读性更高的并发神库详解

    目录 前言 WaitGroup的封装 worker池 Stream ForEach和map ForEach map 总结 前言 前几天逛github发现了一个有趣的并发库-conc,其目标是: 更难出现goroutine泄漏 处理panic更友好 并发代码可读性高 从简介上看主要封装功能如下: 对waitGroup进行封装,避免了产生大量重复代码,并且也封装recover,安全性更高 提供panics.Catcher封装recover逻辑,统一捕获panic,打印调用栈一些信息 提供一个并发执行

  • Golang并发编程重点讲解

    目录 1.通过通信共享 2.Goroutines 3.Channels 3.1 Channel都有哪些特性 3.2 channel 的最佳实践 4.Channels of channels 5.并行(Parallelization) 6.漏桶缓冲区(A leaky buffer) 1.通过通信共享 并发编程是一个很大的主题,这里只提供一些特定于go的重点内容. 在许多环境中,实现对共享变量的正确访问所需要的微妙之处使并发编程变得困难.Go鼓励一种不同的方法,在这种方法中,共享值在通道中传递,实际

  • 一文带你了解Go语言实现的并发神库conc

    目录 前言 worker池 Stream ForEach和map ForEach map 总结 前言 哈喽,大家好,我是asong:前几天逛github发现了一个有趣的并发库-conc,其目标是: 更难出现goroutine泄漏 处理panic更友好 并发代码可读性高 从简介上看主要封装功能如下: 对waitGroup进行封装,避免了产生大量重复代码,并且也封装recover,安全性更高 提供panics.Catcher封装recover逻辑,统一捕获panic,打印调用栈一些信息 提供一个并发

  • go语言实现并发网络爬虫的示例代码

    go语言做爬虫也是很少尝试,首先我的思路是看一下爬虫的串行实现,然后通过两个并发实现:一个使用锁,另一个使用通道 这里不涉及从页面中提取URL的逻辑(请查看Go框架colly的内容).网络抓取只是作为一个例子来考察Go的并发性. 我们想从我们的起始页中提取所有的URL,将这些URL保存到一个列表中,然后对列表中的每个URL做同样的处理.页面的图很可能是循环的,所以我们需要记住哪些页面已经经历了这个过程(或者在使用并发时,处于这个过程的中间). 串行爬虫首先检查我们是否已经在获取地图中获取了该页面

  • Go语言制作svg格式树形图的示例代码

    目录 什么是SVG SVG定义 SVG优点 预定义元素 圆形 <circle> 直线 <line> 文字 <text> 结点SVG格式 根结点 子树结点 叶结点 结点坐标 结点文本 二叉树转SVG 全部源代码 最近一直在刷二叉树题目,但在要验证结果时,通常用中序遍历.层序遍历查看结果,验证起来没有画图来得直观,所有想到自己动手制作二叉树的树形图. 直接开干,先从svg入手: 什么是SVG SVG定义 SVG是可伸缩矢量图形 (Scalable Vector Graphi

  • 利用Go语言实现流量回放工具的示例代码

    目录 前言 goreplay介绍与安装 使用示例 流量放大.缩小 流量写入到ElastichSearch goreplay基本实现原理 总结 前言 哈喽,大家好,我是asong. 今天给大家推荐一款使用Go语言编写的流量回放工具 -- goreplay:工作中你一定遇到过需要在服务器上抓包的场景,有了这个工具就可以助你一臂之力,goreplay的功能十分强大,支持流量的放大.缩小,并且集成了ElasticSearch,将流量存入ES进行实时分析: 废话不多,我们接下来来看一看这个工具: gore

  • python爬虫scrapy框架之增量式爬虫的示例代码

    scrapy框架之增量式爬虫 一 .增量式爬虫 什么时候使用增量式爬虫: 增量式爬虫:需求 当我们浏览一些网站会发现,某些网站定时的会在原有的基础上更新一些新的数据.如一些电影网站会实时更新最近热门的电影.那么,当我们在爬虫的过程中遇到这些情况时,我们是不是应该定期的更新程序以爬取到更新的新数据?那么,增量式爬虫就可以帮助我们来实现 二 .增量式爬虫 概念: 通过爬虫程序检测某网站数据更新的情况,这样就能爬取到该网站更新出来的数据 如何进行增量式爬取工作: 在发送请求之前判断这个URL之前是不是

  • JavaScript/TypeScript 实现并发请求控制的示例代码

    场景 假设有 10 个请求,但是最大的并发数目是 5 个,并且要求拿到请求结果,这样就是一个简单的并发请求控制 模拟 利用 setTimeout 实行简单模仿一个请求 let startTime = Date.now(); const timeout = (timeout: number, ret: number) => { return (idx?: any) => new Promise((resolve) => { setTimeout(() => { const compa

  • C语言实现无头单向链表的示例代码

    目录 一.易错的接口实现 1.1 新节点开辟函数 1.2 尾插 1.3 尾删 二.常见简单接口 2.1 打印链表 2.2 节点计数器 2.3 判断是否为空链表 2.4 通过值查找节点 2.5 头插 2.6 头删 2.7 在任意节点后插入节点 2.8 在任意节点后删除节点 2.9 销毁链表 三.头文件相关内容 3.1 引用的库函数 3.2 结构体声明 一.易错的接口实现 1.1 新节点开辟函数 由于创建一个新节点是频繁的操作,所以封装为一个接口最佳. 链表节点的属性有:(1)数值.(2)指向下一个

  • C语言编程入门必背的示例代码整理大全

    目录 一.C语言必背代码前言 二.一部分C语言必背代码 一.C语言必背代码前言 对于c语言来说,要记得东西其实不多,基本就是几个常用语句加一些关键字而已.你所看到的那些几千甚至上万行的代码,都是用这些语句和关键词来重复编写的.只是他们逻辑功能不一样,那如何快速的上手C语言代码,建议多看多写,下面是小编整理的C语言必背代码. 二.一部分C语言必背代码 1.输出9*9成法口诀,共9行9列,i控制行,j控制列. #include "stdio.h" main() {int i,j,resul

  • Java 实现网络爬虫框架详细代码

    目录 Java 实现网络爬虫框架 一.每个类的功能介绍 二.每个类的源代码 Java 实现网络爬虫框架 最近在做一个搜索相关的项目,需要爬取网络上的一些链接存储到索引库中,虽然有很多开源的强大的爬虫框架,但本着学习的态度,自己写了一个简单的网络爬虫,以便了解其中的原理.今天,就为小伙伴们分享下这个简单的爬虫程序!! 一.每个类的功能介绍 DownloadPage.java的功能是下载此超链接的页面源代码. FunctionUtils.java 的功能是提供不同的静态方法,包括:页面链接正则表达式

  • C语言实现可排序通讯录的示例代码

    目录 1.目的 2.分部流程 1.初始化通讯录 2.添加联系人 3.判断联系人是否存在 4.判断通讯录是否已满 5.判断通讯录是否为空 6.通讯录扩容 7.核心函数 8.查找联系人 9.修改联系人 10.清空通讯录 11.删除联系人 12.显示通讯录 13.比较联系人 14.通讯录排序 3.总代码展示 1.目的 写一个实用型通讯录,它有如下功能: 显示目录 void ShowMenu() { printf("#######################\n"); printf(&qu

  • C语言模拟实现密码输入的示例代码

    目录 引言 思路分析 代码实现 代码分析 引言 登录账号时我们要输入密码,密码输入错误时会提示密码错误.有时密码的输入次数会被限制,例如银行卡,当我们3次密码都输入错误时卡会被冻结.下面用C语言模拟实现密码输入. 思路分析 首先要确立一个正确密码,再确定密码输入限制次数,接着用一个scanf语句读取用户输入的密码.将用户输入的密码和先前确定的密码进行比较,如果密码输入正确就显示密码正确,如果密码输入错误就提示密码错误,并告诉用户还有几次输入机会. 代码实现 #include<stdio.h>

随机推荐