当前位置: 首页 > news >正文

大文件去重 (上)

在我们日常的开发中,可能会经常遇到对大数据量的处理,所以我在这里去简单模拟一下这个场景,我们假设去输入100w条url,然后我们来对这个url进行操作,比如去重,比如排序,代码生成如下:

import java.io.BufferedWriter;  
import java.io.File;  
import java.io.FileWriter;  
import java.io.IOException;  public class Main {  public static void main(String[] args) throws IOException {  long startTime = System.currentTimeMillis();  System.out.println("start write : " + startTime + " ");  WriteUrl writeUrl = new WriteUrl();  File file = new File("preUrl1.txt");  try(BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file))){  for (int i = 0; i < 100_0000; i++) {  //写入URL  String randomUrl = writeUrl.getRandomUrl();  bufferedWriter.write(randomUrl);  bufferedWriter.newLine();  //构建有重复的URL,如果为10000,那么每个URL假设他为5位数字  }  }  long endTime = System.currentTimeMillis();  System.out.println("end write : " + endTime + " ");  System.out.println("the time taken is " + (endTime - startTime) + " ms");  }  
}
import java.util.concurrent.ThreadLocalRandom;  public class WriteUrl {  //往文件中写入100_0000条数据,这些数据中可能有重复  public static final int URL_PER_FILE = 10_000;  public String getRandomUrl(){  // 生成 5 位数字,范围 00000~99999        int num = ThreadLocalRandom.current().nextInt(100_000);  String url = String.format("https://example.com/%05d", num);  return url;  }  }

在运行后,结果变成为the time taken is 676 ms,然后看一下文件大小:为 25M Sep 30 09:28 preUrl1.txt,我们来简单grep一下,命令为 grep -hF 'https://example.com/77242' preUrl1.txt | wc -l,最终的结果为6,可以看到确实是有重复的。或者说使用cat和sort来对他进行操作,即

cat preUrl1.txt \
| sort | uniq -c \
| sort -nr \
> repeat_stat.txtawk '$1>=2' repeat_stat.txt

就可以看到所有的重复的内容。所以如果想要对一个文件去进行去重,排序等操作,不妨去试试command,这里就有一个对应的思考,如果文件非常大,哪些操作会导致内存爆掉,这里简单回答,即全局排序/去重/计数的命令(sortuniq 等)。由于我们的操作依赖于他们底层的实现,且他们的实现有可能不够满足我们的需求,核心就是因为他们可能把文件中的数据一次性梭哈加载到内存中,无论你用什么东西,一次性梭哈加载到内存都会导致内存爆掉,这和语言无关,受内存物理大小限制。所以接下来我们来看一些对大文件的处理。当前,前文的使用command来对数据进行操作,只是实现方式中的一种,我们还可以去实现更多的方式。下文将给出一些相关的,文件大小小于内存和文件大小大于内存处理的具体实现。

大文件去重

一般来说,这种对数据进行处理的操作,肯定是越快越好,但是更多占比的还是去实现我们的功能。所以我们在实现自己功能的基础上才会去考虑对操作进行优化。这里给一个简单场景,将文件A的内容去重后放到文件B。

内存可以一次放下所有的内容

1. 使用命令行

如上文所说,最简单的可以为使用command来组成管道,然后对数据进行操作。一些常见的处理有cat(内容输出到标准输出),grep(按行扫描,将匹配的内容挑出来),pipe(也就是" | ",将管道前的输出作为管道后内容的输入),xargs(一般和管道一起用,区别是让前边的输出作为后边的参数,主要是让一些无法处理标准输入的操作来处理前边的内容,比如rm),wc(word counter,统计用的),sort(排序),uniq(uinque缩写,用来去重),awk(对数据进行更强大的操作,更像是对列的操作),sed(对行的操作)。常用的数据操作查不多就这些,有关更详细的内容,可以看看The Missing Semester of Your CS Education 。

具体的指令为

cat fileName.txt \
| sort | uniq -c \
| sort -nr \
> result.txt

先进行排序,然后使用unique聚合相同的内容实用-c来统计数量,然后对统计后的内容继续进行排序,-n为按照数值(number),-r为反转(reverse)

2. 使用布隆过滤器+HashSet去重 or 直接用HashSet ?

我们都知道,如果布隆过滤器中存在元素,那么有可能误判,如果布隆过滤器中不存在这个元素,那么一定不会去误判。所以我们可以根据这个性质,可以进行判断。对于布隆过滤器的误判,我们去用HashSet来做一个兜底。那么你可能会问,为什么我们要用布隆过滤器而不是直接去全部使用如HashSet这种数据结构呢?因为相比于布隆过滤器,HashSet还是一个相对于更大的操作,因为HashSet的底层仍然为HashMap,而HashMap底层是一个K-V的数组。这相比于布隆过滤器这种hash后用位来表示的结构来说,无疑是开销更大的。并且由于HashMap的一些额外操作,比如扩容等内容,他的操作成本也更大,即时间消耗也会更多。所以我们选择先用布隆过滤器扫一遍,然后再用HashSet再处理那些漏的内容。也就是在数据操作之前,先做一" 预筛"。

那么代码如下

import org.redisson.Redisson;  
import org.redisson.api.RBloomFilter;  
import org.redisson.api.RedissonClient;  
import org.redisson.config.Config;  import java.io.BufferedReader;  
import java.io.BufferedWriter;  
import java.io.IOException;  
import java.nio.file.Files;  
import java.nio.file.Path;  
import java.nio.file.Paths;  public class RemoveDuplicatesByBloom {  private static final long EXPECTED_INSERTIONS = 100_000_000L;  private static final double FALSE_PROBABILITY  = 0.001;  public static void main(String[] args) throws IOException{  //使用布隆过滤器进行初步的过滤  Config config = new Config();  config.useSingleServer()  .setAddress("redis://127.0.0.1:6379")  .setConnectionPoolSize(64);  RedissonClient redissonClient = Redisson.create(config);  RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("urlBloom");  if(bloomFilter.isExists()) {  bloomFilter.delete();  }  bloomFilter.tryInit(EXPECTED_INSERTIONS, FALSE_PROBABILITY);  Path in = Paths.get("preUrl1.txt");  Path out = Paths.get("new.txt");  System.out.println("begin operation");  try(BufferedReader reader = Files.newBufferedReader(in);  BufferedWriter writer = Files.newBufferedWriter(out)) {  String line;  long count = 0;  while((line = reader.readLine()) != null) {  if(!bloomFilter.contains(line)) {  bloomFilter.add(line);  writer.write(line);  writer.newLine();  }  if(++count % 100_0000 == 0){  System.out.printf("processed %,d lines %n" , count);  }  }  System.out.println("Done!");  }  redissonClient.shutdown();  }  
}

通过简单的使用布隆过滤器来进行判断,我们得到的新的文件,重复的都被干掉了,但是布隆过滤器有误判的风险,我们不妨把值搞小一点来看看他的误判,新的数据分别为100的预计插入数量和0.5的误判率。那么文件中的元素就只有100多条了,这肯定是不符合我们的需求的。既然他有误判的风险,那么我们需要有个兜底,可以再加一个HashSet来对他进行判断。那么你可能会问,既然都用HashSet了,那么为什么不全用HashSet呢?那么这就有一个很大的问题,HashSet存储的是原来的值,而布隆过滤器存储的是位,前者的大小随元素单个大小,以及元素总数量成正相关,而后者基本和数量有关。其实这里的核心就是原始的值占位大小是可以远远大于hash后的位大小的。所以原来占120G的内存,可能会变为12G。所以这里使用布隆过滤器是通过他的哈希后用位表示来降低内存占用。

但是有一个问题,即布隆过滤器会“漏”过去一部分的内容,如果想要确保所有的元素都不漏,那么就需要额外的拖底,比如说HashSet,但是这样的话,和直接用hashSet来判断存储有什么区别?因为HashSet要存储所有的内容,只为了去提防那很小的漏元素的可能。所以对于允许漏一定元素的情况下,可以使用布隆过滤器,否则直接用HashSet就可以了

3.使用位图

因为我们这里只是去判断他是否重复,而不是去判断其他的内容,所以我们可以用每一个位去表示一个特定的URL,0代表不存在,1代表存在那么假设我们有100_0000的数据,就只需要122KB来去对他进行表示。所以我们就可以用一个int数组来进行所有的表示。大体思路是这样的一个int可以表示32位,即32个数,我们用多个int数组来组成总共的大的位图。通过

//value为目标数,index为数组下标,除32来找到他对应的bucket
int index = value >> 5;  
//取模找对应的bucket的位
int mod = value & 31;
//将对应bucket的对应下标转换为1
list[index] |= 1 << mod;

通过这样,我们简单的将值对应到不同的位上。这种方式和布隆过滤器思路的区别就是,这种方式不会有误判的风险,而布隆过滤器是在添加时进行多次哈希然后将哈希位上对应的值变为1,随后在比较时多次哈希后来比较,哈希后的位对应的值是否都为1,如果是,则认为是重复已存在,这里的操作就会出现误判。而这里是让每一位去代表一个数。

这里有一个问题,就是我们的value怎么选取?如果原始要操作的数据是int类型的值,那再好不过,但是事实上往往我们要操作的是一个字符串。所以我们需要让不同的字符串去对应map到不同的数大小,这又带来很痛苦的问题:

  • 我们怎么去将String映射为唯一确定的数?
  • 如果一个String很大,那么在操作过程中,怎么尽量的去避免因为大量映射而造成的时间损耗?
    这里有那么几种考虑,第一种就是分配对应的key-value,来对应的标识。第二种就是通过哈希函数。
    对于第一种来说,由于我们需要去维护k-v这个map,就导致我们需要大量内存去的维护状态,这占的内存空间过高,所以我们需要借助外部存储,比如存在数据库里。对于第二种,一个好的哈希函数很重要,但是如果要自己去设计,那么就要考虑字符串,同时也有误判的风险,既然都有误判的风险了,那为什么不直接用布隆过滤器?

总结

由于篇幅问题,这里只去考虑内存放得下的部分,整体思想就是通过HashSet来去标识唯一存在。但是这样不够省内存,为了去省更多的内存,我们考虑用布隆过滤器,但是这样会带来精度丢失的问题。视具体情况而定。

http://www.dtcms.com/a/435773.html

相关文章:

  • 自建企业网站教程有没有做网站的高手
  • 网上做家教的网站知名seo电话
  • 线程中信号量与条件变量详解
  • 做网站的收获wordpress怎样恢复数据库
  • 泉州专门做网站怎么确定网站关键词
  • 七台河北京网站建设电子商务具体是做什么的
  • 网站页面设计稿流量推广平台
  • 高效网站推广费用网站建设 营业执照 经营范围
  • 进程和线程间的通信方式有哪些?
  • 铁威马内置wordpress目录长春网络优化哪个公司在做
  • 哪个网站建设公司好济南网站建设公司熊掌号
  • 企业建站系统免费白云外贸型网站建设
  • 天津个人专业做网站wordpress分享有礼
  • 安新网站建设网站服务器到期为什么要网站备案
  • 哈希和加密
  • 济南seo网站排名优化工具公司简介宣传文案
  • 正规的网站优化推广公司广告牌模板图片
  • 那家公司做网站比较好微信公众号文章 转wordpress
  • 龙岗网站设计资讯怎么做素材设计网站
  • FastAPI 深度剖析:从异步原理到高级应用
  • AIGC(生成式AI)试用 37 -- 辅助测试 Browser-use, Playwright
  • 做视频网站收入wordpress与discuz整合
  • oracle 网站开发箱包商城网站建设
  • [crackme]018-crackme_0006
  • 滨海专业做网站wordpress博客分页
  • 如何做衣服销售网站淄博网站制作制作
  • 东台建设局网站公司信息查询网
  • 建站套餐和定制网站的区别2013电子商务网站建设考试试卷
  • 中山币做网站公司网站的建设不包括什么
  • CSP 复赛入门组高频算法:典型例题、代码模板与实战题号