数据结构与算法之美学习笔记:45 | 位图:如何实现网页爬虫中的URL去重功能?

目录

  • 前言
  • 算法解析
  • 总结引申

前言

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

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

思路非常简单,不过,我们该如何记录已经爬取的网页链接呢?需要用什么样的数据结构呢?

算法解析

关于这个问题,我们可以先回想下,是否可以用我们之前学过的数据结构来解决呢?
这个问题要处理的对象是网页链接,也就是 URL,需要支持的操作有两个,添加一个 URL 和查询一个 URL。除了这两个功能性的要求之外,在非功能性方面,我们还要求这两个操作的执行效率要尽可能高。除此之外,因为我们处理的是上亿的网页链接,内存消耗会非常大,所以在存储效率上,我们要尽可能地高效。

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

我们拿散列表来举例。假设我们要爬取 10 亿个网页(像 Google、百度这样的通用搜索引擎,爬取的网页可能会更多),为了判重,我们把这 10 亿网页链接存储在散列表中。你来估算下,大约需要多少内存?

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

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

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

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

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

实际上,如果要想内存方面有明显的节省,那就得换一种解决方案,也就是我们今天要着重讲的这种存储结构,布隆过滤器(Bloom Filter)。

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

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

当然,这个问题还是可以用散列表来解决。不过,我们可以使用一种比较“特殊”的散列表,那就是位图。我们申请一个大小为 1 亿、数据类型为布尔类型(true 或者 false)的数组。我们将这 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/16+1];
  }

  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 亿个二进制位,也就是 12MB 左右的存储空间就够了。

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

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

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

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

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

我们使用 K 个哈希函数,对同一个数字进行求哈希值,那会得到 K 个不同的哈希值,我们分别记作 X1​,X2​,X3​,…,XK​。我们把这 K 个数字作为位图中的下标,将对应的 BitMap[X1​],BitMap[X2​],BitMap[X3​],…,BitMap[XK​]都设置成 true,也就是说,我们用 K 个二进制位,来表示一个数字的存在。

当我们要查询某个数字是否存在的时候,我们用同样的 K 个哈希函数,对这个数字求哈希值,分别得到 Y1​,Y2​,Y3​,…,YK​。我们看这 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 布隆过滤器的实现。

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

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

相关文章

Windows内存管理(一):Windows性能监视器(PerfMon)

一、什么是性能监视器 什么是性能监视器&#xff1f; (What is Performance Monitor? )很多时候&#xff0c;我们的计算机只是停止响应、意外关闭或行为异常。这种行为可能有多种原因&#xff0c;指出确切原因可能会有很大帮助。Windows有一个名为Performance Monitor的工具&…

国外高校对于ChatGPT的三种态度及正确使用方法

ChatGPT无疑是23年留学届的热门话题&#xff0c;也成为了不少留学生再也离不开的万能工具&#xff0c;从总结文献、润色论文、给教授写email似乎无所不能。 各大高校对于学生使用ChatGPT的态度也有所不同。例如&#xff0c;哈佛大学教育代理院长 Anne Harrington 在内部邮件中…

MySQL报错:Out of sort memory, consider increasing server sort buffer size

报错内容 ### Error querying database. Cause: java.sql.SQLException: Out of sort memory, consider increasing server sort buffer size ### The error may exist in class path resource [mapper/ProjectCaseReportMapper.xml] ### The error may involve defaultParam…

卸载流氓软件联软

这个流氓软件也是在更新的&#xff0c;下面是本人在联想邵阳笔记本下卸载流程&#xff0c;非常简单 注&#xff1a;按照本文卸载之后&#xff0c;我重新装了一次这个垃圾&#xff0c;但是发现重装完之后&#xff0c;系统启动之后就会进入黑屏&#xff0c;也就是说&#xff0c;…

2024年【熔化焊接与热切割】考试资料及熔化焊接与热切割考试试卷

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 熔化焊接与热切割考试资料根据新熔化焊接与热切割考试大纲要求&#xff0c;安全生产模拟考试一点通将熔化焊接与热切割模拟考试试题进行汇编&#xff0c;组成一套熔化焊接与热切割全真模拟考试试题&#xff0c;学员可…

java.lang.ClassNotFoundException: jakarta.servlet.Servlet

联系servlet的使用时&#xff0c;编写了servlet的处理器&#xff0c;但是浏览器报500错误&#xff0c;有时候是404错误 WebServlet("/mayikt") public class Servlet1 implements Servlet {Overridepublic void init(ServletConfig servletConfig) throws ServletExc…

畸变矫正-深度学习相关论文学习

目录 DocTr: Document Image Transformer for Geometric Unwarping and Illumination Correction SimFIR: A Simple Framework for Fisheye Image Rectification with Self-supervised Representation Learning Model-Free Distortion Rectification Framework Bridged by Di…

RockMQ面试题(1)

为什么要使用MQ 应用解耦&#xff1a;系统的耦合性越高&#xff0c;容错性就越低。以电商应用为例&#xff0c;用户创建订单后&#xff0c;如果耦合调用库存系统、物流 系统、支付系统&#xff0c;任何一个子系统出了故障或者因为升级等原因暂时不可用&#xff0c;都会造成下单…

tauri build打包问题-- wix, nsis下载

文章目录 1 更改配置里打包标识符identifier[附] 相关资源网盘链接&#xff08;适配tauri v1.5.9&#xff09;2 wix的下载3 nsis下载 1 更改配置里打包标识符identifier // tauri.config.json // 默认env -> build "bundle": { ... "identifier": &qu…

零售EDI:Petco EDI对接指南

Petco 始于1965年&#xff0c;是一家美国宠物零售商&#xff0c;提供各种宠物产品和服务以及某些类型的活体小动物。起初Petco只是一家邮购兽医用品公司&#xff0c;后发展为一家成熟的宠物食品和供应链的公司。Petco与其供应商之间是如何传输业务数据的呢&#xff1f; 通过EDI…

gitee创建远程仓库并克隆远程仓库到电脑

1、首先点加号新建一个仓库 2、输入仓库名&#xff0c;路径会自动填充&#xff0c;填写简单的仓库介绍&#xff0c;先选择私有&#xff0c;在仓库创建之后&#xff0c;可以改为开源 3、打开建好的仓库 4、复制仓库链接 5、打开一个文件夹(想要存储远程仓库的地址)&#xff0c;在…

【模拟IC学习笔记】 采样保持电路的设计

目录 采样保持工作原理 概念 时域响应-采保信号 采样网络的KT/C噪声 采样电容大小的选取 采样抖动(jitter) jitter对SNR的影响 法一 法二 采样开关的种类 单MOS管 实践&#xff1a;Nmos导通电阻 传输门 栅压自举开关 采样技术 上极板采样 下极板采样 采样保持…

.NET Framework 与 .NET Core 与 .NET Standard

介绍 在本文中&#xff0c;我们将探讨 .NET Framework、.NET Core 和 .NET Standard 之间的差异。 .NET Framework 与 .NET Core .NET框架.NET核心 历史 .NET Framework 是 .NET 的第一个实现。 .NET Core 是 .NET 的最新实现。 开源 .NET Framework 的某些组件是开源的。 .N…

普中STM32-PZ6806L开发板(有点悲伤的故事续-人灯还未了)

简介 继上篇 普中STM32-PZ6806L开发板(有点悲伤的故事) 说到 关于 普中STM32-PZ6806L开发板的LED流水灯也被烧坏掉了&#xff0c;再也无法玩流水灯, 内心充满了只会流水灯的不甘, 流水灯就是单片机的Hello World&#xff0c;怎么能没有呢&#xff1f; 事情发展 好巧不巧想起最近…

自动驾驶轨迹预测

目录 神经网络轨迹预测综述&#xff1a; 比较新的轨迹预测网络 Uber&#xff1a;LaneRCNN[5] Google&#xff1a;VectorNet[6] Huawei&#xff1a;HOME[7] Waymo&#xff1a;TNT[8] Aptive&#xff1a;Covernet[9] NEC&#xff1a;R2P2[10] 商汤&#xff1a;TPNet[11]…

python 正则分割字符串

python 正则分割字符串 文章目录 python 正则分割字符串方法1:方法2:方法3:方法4:方法5:测试代码总结 前段时间 对字符串的处理遇到了一个小问题,我希望在一个字符串中做特定的分割, 通过传入一个 pattern正则来分割字符串. 看例子 self.s 就是原始的字符串 ,字符串中包含一个…

工程管理系统功能设计与实践:实现高效、透明的工程管理

在现代化的工程项目管理中&#xff0c;一套功能全面、操作便捷的系统至关重要。本文将介绍一个基于Spring Cloud和Spring Boot技术的Java版工程项目管理系统&#xff0c;结合Vue和ElementUI实现前后端分离。该系统涵盖了项目管理、合同管理、预警管理、竣工管理、质量管理等多个…

TS 36.213 V12.0.0-随机接入过程

本文的内容主要涉及TS 36.213&#xff0c;版本是C00&#xff0c;也就是V12.0.0。

【深度学习每日小知识】Data Augmentation 数据增强

数据增强是通过对原始数据进行各种转换和修改来人工生成附加数据的过程&#xff0c;旨在增加机器学习模型中训练数据的大小和多样性。这对于计算机视觉领域尤为重要&#xff0c;因为图像经常被用作输入数据。 计算机视觉中的数据增强 数据增强的主要目标是解决过拟合问题&…

gazebo安装版本--公元2024年1月

不好意思我误导了各位&#xff0c;顺便也误导了我自己。。。。。。。。。 harmonic版本只适合单独使用&#xff0c;不适合与ros2配合仿真。 到2024年1月&#xff0c;只有fortress版本能与ros2配合使用