数据结构与算法笔记:高级篇 - 位图:如何实现网页爬虫中的URL去重功能?

概述

网页爬虫是搜索引擎中的非常重要的系统,复杂爬取几十亿、上百亿额度网页。爬虫的工作原理是,通过解析已经爬取网页中的网页链接,然后再爬取这些链接对应地网页。而同一个网页链接有可能被包含在多个页面中,这就会导致爬虫在爬取的过程中,重复爬取相同的网页。如果你是一名负责爬虫的工程师,你会如何避免这些重复的爬取呢?

最容易想到的方法就是,我们记录已经爬取的网页链接(也就是 URL),再派去一个新的网页之前,我们拿它的链接,在已经爬取的网页链接中搜索。如果存在,那就说明这个网页已经被爬取过了;如果不存在,那就说明这个网页还没有被爬取过,可以继续爬取。等爬取到这个网页之后,我们将这个网页的链接添加到已经爬取的网页链接列表。

思路非常简单,你应该很容易就能想到。不过,我们该如何记录已经爬取的网页链接呢?需要用什么样的数据结构?


算法解析

关于这个问题,可以先回想下,是否可以用我们之前学过的数据结构来解决呢?

这个问题要处理的对象是网页链接,也就是 URL,需要支持的操作有两个,添加一个 URL 和查询一个 URL。除了这两个功能性要求之外,在非功能性方面,我们还要求这两个操作的执行效率要尽可能高。此外,因为我们处理的是上亿的网页链接,内存消耗会非常大,所以在存储效率上,我们要尽可能地高效。

回想一下,满足这些条件的数据结构有哪些呢?显然,散列表、红黑树、跳表这些动态数据结构,都能够支持快速地插入、查找数据,但是在内存消耗方面,是否可以接受?

我们拿散列表举例。假设我们要爬取 10 亿个网页,为了判重,我们把这 10 亿网页链接存储在散列表中。你来估算下,大约需要多少内存?

假设一个 URL 的平均长度是 64 字节,那单纯存储这 10 亿个 URL,需要大约 60 GB 的内存。因为散列表必须维持较小的装载因子,才能保证不会出现过多的散列表冲突,导致操作的性能下降。而且,用链表法解决冲突,还会存储链表指针。所以,如果将这 10 亿个 URL 构建成散列表,那需要的内存空间会远大于 60GB,有可能会超过 100GB。

当然,对于一个大型的搜索引擎来说,即便是 100GB 的内存要求,其实也不算太高,我们可以采用分治的思想,用多态机器(比如 20 台内存是 8GB 的机器)来存储这 10 亿网页链接。

对于爬虫的 URL 去重这个问题,刚刚讲的分治加散列表的思路,已经是可以实实在在地工作了。不过,作为一个有追求的工程师,我们应该考虑,在添加、查询数据的效率以及内存消耗方面,是否还有进一步优化的空间呢?

你可能会说,散列表中添加、查找数据的时间复杂度已经是 O ( 1 ) O(1) O(1),还能有进一步优化的空间吗?实际上,前面也讲过,时间复杂度并不能完全代表代码的执行时间。大 O 时间复杂度表示法,会忽略掉常数、系数和低阶,并且共计的对象是语句的频度。不同的语句,执行时间也是不同的。时间复杂度知识表示执行时间的变化趋势,并不能度量在特定的数据规模下,代码执行时间的多少?

如果时间复杂度中原来的系数是 10,我们现在能通过优化,将系数降为 1,那再时间复杂度没有变化的情况下,执行效率就提高了 10 倍。对于实际的软件开发来说,10 倍效率的提升,显然是非常值得的优化。

如果我们用基于链表的方法解决冲突问题,散列表中存储的是 URL,那查询的时候,通过哈希函数定位到某个链表之后,我们还需要依次比对每个链表中的 URL。这个操作是比较耗时的,主要原因有两点。

  • 一方面,链表中的结点在内存中不是连续存储的,所以不能已下载加载的 CPU 缓存中,没法很好地利用 CPU 高速缓存,所以数据访问性能方面会打折扣。
  • 另一方面,链表中的每个数据都是 URL,而 URL 不是简单的数字,是平均长度为 64 字节的字符串。也就是说,我们要让带判重的 URL,跟链表中的每个 URL,做字符串匹配。显然,这样一个字符串匹配操作,比起单纯的数字比对,要满很多。

所以,基于这两点,除了刚刚这种基于散列表的解决方案,貌似没有更好的办法了。实际上,如果要想内存方面有明显的节省,那就得换一种解决方案,也就是我们本章要着重讲的这种存储结构,布隆过滤器(Bloom Filter)。

在讲布隆过滤器之前,要先将另一种存储结构,位图(BitMap)。因为布隆过滤器本身就是基于位图的,是对位图的一种改进。

我们先来看一个根开篇问题类似、但比那个稍微简单的问题。我们有 1 千万个整数,整数的范围在 1 到 1 亿之间。我和快速查找某个整数是否在这 1 千万个整数中呢?

当然,这个问题还是可以用散列表来解决。不过,我们可以使用一种比较 “特殊” 的散列表,那就是位图。我们申请一个大小为 1 亿、数据类型为布尔类型的数组。我们将这 1 千万个整数作为数组下标,将对应地数组值设置成 true。比如,整数 5 对应下标为 5 的数组值设置为 true,也就是 array[5]=true

当我们查询某个整数 K 是否在这 1 千万个整数中的时候,我们只需要将对应的数组值 array[K] 取出来,看是否等于 true。如果等于 true,那就说明 1 千万个整数中包含这个整数 K;相反,就表示不包含这个整数 K。

不过,很多语言中提供的布尔类型,大小是一个字节,并不能节省太多内存空间。实际上,表示 true、false 两个值,我们只需要一个二进制位(bit)就可以了。那如何通过编程语言,表示一个二进制位呢?

这里就要用到位运算了。我们可以借助编程语言中提供的数据类型,比如 int、long、char 等类型,通过位运算,用其中的某个位表示某个数组。文字描述起来有点不好理解,我把位图代码实现写了出来,你可以对照代码看下。

public class BitMap { // java中的char占16bit,也就是2个字节
    private char[] bytes;
    private int nbits;

    public BitMap(int nbits) {
        this.nbits = nbits;
        this.bytes = new char[nbits];
    }

    public void set(int k) {
        if (k > nbits) return;
        int byteIndex = k / 16;
        int bitIndex = k % 16;
        bytes[byteIndex] |= (1 << bitIndex);
    }
    
    public boolean get(int k) {
        if (k > nbits) return false;
        int byteIndex = k / 16;
        int bitIndex = k % 16;
        return (bytes[byteIndex] & (1 << bitIndex)) != 0;
    }
}

从刚刚的位图结构的讲解中,你应该可以发现,位图通过数组下标来定位数据,所以访问效率非常高。而且,每个数字用一个二进制位来表示,在数字范围不大的情况下,所需要的内存空间非常节省。

比如刚刚那个例子,如果用散列表存储这 1 千万的数据,数据是 32 位整型数,也就是需要 4 个字节的存储空间,那总共至少需要 40MB 的存储空间。如果我们通过位图的话,数字范围在 1 到 1 亿之间,只需要 1 亿个二进制位,也就是 12 MB 左右的存储空间就足够了。

关于位图,就讲完了,不是挺简单的?不过,这里我们有个假设,就是数字所在的范围不是很大。如果数字的范围恨到,比如刚刚那个问题,数字范围不是 1 到 1 亿,而是 1 到 10 亿,那位图的大小就是 10 亿个二进制位,也就是 120MB 的大小,消耗的内存空间,不降反增。

这个时候,布隆过滤器就要出场了。布隆过滤器就是为了解决刚刚这个问题,对位图这种数据结构的一种改进。

还是刚刚那个里子,数据是 1 千万,数据的范围是 1 到 10 亿。布隆过滤器的做法是,我们仍然使用 1 亿个二进制位,然后通过哈希函数,对数字进行处理,让它落到这 1 到 1 亿范围内。比如我们把哈希函数设计成 f(x)=x%n,其中 x 表示数组,n 表示位图的大小(1 亿),也就是,对数字跟位图的大小进行取模求余。

不过,你肯定会说,哈希函数会存在冲突的问题啊,一亿零一和 1 两个数字,经过刚刚的求余取模的哈希函数处理之后,最后的结果都是 1。这样就无法区分,位图存储的是 1 还是一亿零一了。

为了降低这种冲突的概率,当然我们可以设计一个复杂点、随机点的哈希函数。此外,还有其他方法吗?我们来看布隆过滤器处理方法。既然一个哈希函数可能会存在冲突,那用多个哈希函数一块儿定位一个数据,是否能降低冲突的概率呢?我来具体解释下布隆过滤器是怎么说的?

我们使用 k 个哈希函数,对同一个数字进行求哈希值,那会得到 k 个不同的哈希值,分别记作 $X_{1}$$X_{2}$$X_{3}$、…、$X_{K}$。我们把这 K 个数字作为位图中的下标,将对应的 BitMap$X_{1}$BitMap$X_{2}$BitMap$X_{3}$、…、BitMap$X_{K}$ 都设置成 true,也就是说,我们用 K 个二进制位,来表示一个数字的存在。

当我们要查询某个数字是否存在的时候,同样用 K 个哈希哈数,对这个数求哈希值,分别得到 $Y_{1}$$Y_{2}$$Y_{3}$、…、$Y_{K}$。我们看这 K 个哈希值,对应位图中的数值是否为 true,如果都是 true,则说明,这个数字存在,如果有其中任意一个不为 true,那就说明这个数字不存在。

在这里插入图片描述

对于两个不同的数字来说,经过一个哈希函数处理之后,可能会产生相同的哈希值。但是经过 k 个函数处理之后,K 个哈希值都相同的概率就非常低了。尽管采用 K 个哈希函数之后,两个数字哈希冲突的概率降低了,但是,这种处理方式又带来了新的问题,那就是容易误判。我们看下面这个例子。

在这里插入图片描述

布隆过滤器的误判有一个特点,那就是,它只会存在的情况有误判。如果某个数字经过布隆过滤器判断不存在,那说明这个数字真的不存在,不会发生误判;如果某个数字经过布隆过滤器判断存在,这个时候才会有误判,有可能并不存在。不过,只要我们调整哈希函数的个数、位图大小跟要存储数字的个数之间的比例,那就可以将这种误判的概率降到非常低。

尽管布隆过滤器会存在误判,但是,这并不影响它发挥大作用。很多场景对误判有一定的容忍度。比如本章讲的解决爬虫判重这个问题,即便一个网页没有被爬取过,被误判为已经爬取,对于搜素引擎来说,也并不是什么大事,是可以容忍的,毕竟网页太多了,搜搜引擎也不可能 100% 都爬取到。

弄懂了布隆过滤器,本章的爬虫网页去重的问题,就很简单了。

我们用布隆过滤器来记录已经爬取过的网页链接,假设要判重的网页有 10 亿,那我们可以用一个 10 倍大小的位图来存储,也就是 100 亿个二进制位,换算成字节,那就是大约 1.2GB。之前用散列表判重,需要至少 100GB 的空间。相比来讲,布隆过滤器在存储空间消耗上,降低了非常多。

我们再来看下,利用布隆过滤器,在执行效率方面,是否比散列表更加高效呢?

布隆过滤器用多个哈希函数对同一个网页链接进行处理,CPU 只需要将网页链接从内存中读取一次,进行多次哈希计算,理论上将组操作是 CPU 密集型的。而在散列表的处理方式中,需要读取散列值相同(散列冲突)的多个网页链接,分别跟待排重的网页链接,进行字符串匹配。这个操作设计很多内存数据的读取,所以是内存密集型的。我们知道 CPU 计算可能是要比内存访问更快速的,所以,理论上讲,布隆过滤器的判重方式,更加快速。

总结

本章,关于搜索引擎爬虫网页去重问题的解决,我们从散列表讲到位图,再讲到布隆过滤器。布隆过滤器非常适合这种不需要 100% 准确、允许存在小概率误判的大规模判重场景。除了爬虫去重这个例子,还有比如统计一个大型网址每条的 UV 数,也就是每天有多少用户访问了网站,我们可以使用布隆过滤器,对重复访问的用户进行去重。

前面讲到,布隆过滤器的误判率,主要跟哈希函数的个数、位图的大小有关。当我们往布隆过滤器中不停地加入数据之后,位图中不是 true 的位置就越来越少了,误判率就越来越高了。所以,对于无法事先知道要判重的数据个数的情况,我们需要支持自动扩容的功能。

当布隆过滤器中,数据个数与位图大小的比例超过某个阈值时,我们就重新申请一个新的位图。后面来的数据,会被放置到新的位图中。但是,如果我们要判断某个数据是否在布隆过滤器中已经存在,我们就需要查看多个位图,相应的执行效率就降低了一些。

位图、布隆过滤器应用如此广泛,很多编程语言都已经实现了。比如 Java 中的 BitSet 类就是一个位图,Redis 也提供了 BitMap 位图,Google 的 Guava 工具包提供了 BloomFilter 布隆过滤器的实现。如果你感兴趣,可以自己去研究下这些实现的源码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/745760.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

测试开发是什么?为什么现在那么多公司都要招聘测试开发?

测试开发是一种软件开发过程中的一种角色&#xff0c;旨在提高软件质量并确保软件功能完善和稳定。测试开发人员负责编写和执行自动化测试脚本&#xff0c;创建测试工具和框架&#xff0c;以及与开发人员紧密合作&#xff0c;提供实时反馈和改进。 为什么现在那么多公司都要招…

RISC-V异常处理流程概述

RISC-V异常处理流程概述 一、RISC-V异常处理流程和异常委托1.1 异常处理流程1.2 异常委托二、RISC-V异常处理中软件相关内容2.1 异常处理准备工作2.2 异常处理函数2.3 Opensbi系统调用的注册三、参考资料一、RISC-V异常处理流程和异常委托 1.1 异常处理流程 发生异常时,首先…

聚乙烯醇(PVA)涂布型薄膜是高阻隔性包装材料 我国市场增长快速

聚乙烯醇&#xff08;PVA&#xff09;涂布型薄膜是高阻隔性包装材料 我国市场增长快速 聚乙烯醇&#xff08;PVA&#xff09;涂布型薄膜&#xff0c;是以其他塑料薄膜&#xff08;主要是双向拉伸薄膜&#xff09;为基材&#xff0c;以聚乙烯醇为涂料&#xff0c;经表面涂布后制…

如何从0构建一款类jest工具

Jest工作原理 Jest 是一个流行的 JavaScript 测试框架&#xff0c;特别适用于 React 项目&#xff0c;但它也可以用来测试任何 JavaScript 代码。Jest 能够执行用 JavaScript 编写的测试文件的原因在于其设计和内部工作原理。下面是 Jest 的工作原理及其内部机制的详细解释&…

C语言的学习发展路线(都是干货)

哈喽&#xff0c;大家好呀~我又回来了&#xff0c;前期比较忙&#xff0c;没有时间来更文&#xff0c;现在给大家推荐了一个C语言的学习路线&#xff0c;供大家一起学习啦&#xff01; 1. 环境搭建与工具篇 选择编译器&#xff1a;常用的编译器有gcc、Clang、Visual Studio等。…

第一个Java程序--HalloWorld(记事本版)

一、开发步骤 1.编写 将 Java 代码编写到扩展名为 .java 的源文件中 class HelloChina{public static void main(String[] args){System.out.println("HelloWorld!");} } 2.编译 winr进入DOS操作系统&#xff0c;进入当前目录。&#xff08;操作命令见《JAVA概述…

红酒哲学:品味流转时光,探寻生活之深邃奥秘

在繁华的都市中&#xff0c;我们时常被各种声音和色彩所包围&#xff0c;追求着速度与激情。然而&#xff0c;在这喧嚣之中&#xff0c;总有那么一刻&#xff0c;我们渴望静下心来&#xff0c;品味一份不同的宁静与深度。这时&#xff0c;一杯雷盛红酒便成了我们与内心对话的桥…

Ubuntu磁盘分区和挂载 虚拟机扩容 逻辑卷的创建和扩容保姆及教程

目录 1、VMware虚拟机Ubuntu20.04系统磁盘扩容 2、Linux的磁盘分区和挂载 3、创建逻辑卷和逻辑卷的扩容 1、VMware虚拟机Ubuntu20.04系统磁盘扩容 通过下图可以看出我们的根磁盘一共有20G的大小&#xff0c;现在我们把它扩容为30G 注&#xff1a;如果你的虚拟机有快照是无…

鸿萌数据迁移业务案例:为医药客户成功迁移重要科研数据

天津鸿萌科贸发展有限公司对 Windows 及 Linux 系统下的各类型备份及数据迁移业务积累了丰富的业务经验&#xff0c;可提供针对性的解决方案。 医药科研数据迁移成功案例 2024年6月初&#xff0c;天津某医药厂家埃尔法 workstation2020 服务器硬盘老化&#xff0c;为保证服务…

小白上手AIGC-基于PAI-DSW部署Stable Diffusion文生图Lora模型

小白上手AIGC-基于PAI-DSW部署Stable Diffusion文生图Lora模型 前言资源准备开启体验服务创建工作空间 部署服务创建DSW实例安装Diffusers启动WebUI 写在最后 前言 在上一篇博文小白上手AIGC-基于FC部署stable-diffusion 中&#xff0c;说到基于函数计算应用模板部署AIGC文生图…

这么精彩的排序算法,你确定不来看一下?

目录 1.交换函数&#xff1a; 2.三数取中&#xff1a; 一.插入排序&#xff1a; 二.希尔排序&#xff1a; 三.选择排序&#xff1a; 四.快速排序&#xff1a; 1.霍尔法&#xff08;递归版&#xff09;&#xff1a; 2.挖坑法&#xff08;递归版&#xff09;&#xff1a; 3.双指针…

智领全栈,模力全开|2024中国智算中心全栈技术大会,锐捷网络引爆智算网络新风潮

6月25日至27日&#xff0c;2024中国智算中心全栈技术大会暨展览会、第5届中国数据中心绿色能源大会暨第10届中国(上海)国际数据中心产业展览会在上海新国际博览中心隆重开幕。此次大会由CDCC和益企研究院主办&#xff0c;以“AI赋能&#xff0c;重构未来”为主题&#xff0c;吸…

重温react-06

开始 函数组件必然成为未来发展的趋势(个人见解),总之努力的去学习,才能赚更多的钱.加油呀! 函数组件的格式 import React from reactexport default function LearnFunction01() {return (<div>LearnFunction01</div>) }以上是函数式组件的组基本的方式 快捷生…

如何提高工业交换机的电源功耗?

工业交换机的电源功耗是指在工作状态下所消耗的能量。随着工业自动化技术的发展&#xff0c;工业交换机在生产和制造领域中扮演着至关重要的角色。它们通过连接各种设备和系统&#xff0c;实现信息的传输和处理&#xff0c;提高生产效率和质量。然而&#xff0c;工业交换机的大…

springAI孵化(二)

目录 一、spring AI 目的 二、spring AI 来源 三、sprig AI 是什么&#xff1f; 四、spring AI中的 概念 4.1、模型&#xff08;Models&#xff09; 4.2、提示&#xff08;Prompts&#xff09; 4.3、提示模板&#xff08;Prompt Templates&#xff09; 4.4、令 牌&…

你的企业“赚钱能力”,银行怎么看?聊聊税贷与票贷背后的门道

大家都听过“税贷”和“票贷”吧&#xff1f;特别是这两年&#xff0c;国家扶持中小微企业&#xff0c;这些名词更是火得不行。但你知道吗&#xff0c;税贷和票贷并不是只看税和票那么简单。今天&#xff0c;咱就来聊聊这背后的门道&#xff08;最后附上&#xff1a;企业信用贷…

ChatGPT的Mac客户端正式发布了!Mac用户有福了

ChatGPT的Mac客户端正式发布了&#xff01;Mac用户有福了 &#x1f389; 大家好&#xff0c;我是猫头虎&#xff0c;科技自媒体博主。今天我带来了一个超级重磅的消息 &#x1f4e2;&#xff0c;就是 ChatGPT 的客户端终于来了&#xff01;这对我们所有 Mac 用户&#xff0c;尤…

可穿戴式手持气象仪

TH-SQ17在快节奏的现代生活中&#xff0c;我们越来越依赖各种智能设备来辅助我们的决策和行动。其中&#xff0c;气象信息的重要性不言而喻&#xff0c;它不仅关系到我们的出行安全&#xff0c;更影响着我们的日常生活安排。如今&#xff0c;一款革命性的产品——可穿戴式手持气…

GPT-4o背后的秘密:深入了解它的运作方式

GPT-4o是OpenAI最新推出的多模态大模型&#xff0c;它在语言处理、图像识别和音频处理方面都实现了重大突破。GPT-4o的"o"代表"omni"&#xff0c;意为全能&#xff0c;能够处理文本、音频、图像和视频输入&#xff0c;是一种高度集成的神经网络。这篇文章将…

精打细算用好 LLMs :LLM 落地应用成本及响应延迟优化

前言 高成本和延迟是将大语言模型应用于生产环境中的主要障碍之一&#xff0c;二者均与提示词信息的体量&#xff08;prompt size&#xff09;紧密相连。 鉴于大语言模型&#xff08;LLM&#xff09;展现出极强的广泛适用性&#xff0c;不少人视其为解决各类问题的灵丹妙药。…