重复数据删除大量数据?

时间:2017-06-29 19:04:51

标签: c# collections large-files

我们有一个包含数亿到数十亿网址的文件。

我想用c#重复数据删除网址,但是将其放入HashSet集合或类似内容会很快抛出OutOfMemoryException

如果我流式传输,我可以避免内存问题,但是我需要在集合中维护一些状态,我认为这也会导致内存问题。

如何使用c#快速删除重复数据,而不会遇到内存问题?我试图避免将其批量加载到数据库中,只是为了将其重新拉出来,并将所有内容保留在机器本地。

我不关心OutOfMemoryException;我只是解释为什么那不起作用。我想重复数据删除。

对于更多上下文,我们将此文件发送给在其上运行分析的大数据供应商,他们向他们收取他们处理的数据量。我们的系统管理小组不喜欢为最终的大量临时数据设置数据库的想法,并且已经问过我们,"你不能在代码中执行它吗?&# 34;我需要试一试。

2 个答案:

答案 0 :(得分:1)

假设您有十亿个网址,并且您可以在没有内存问题的情况下保留一百万个哈希集。

将第一百万个网址放在哈希集中。记录哈希集大小s1。将它们写入新文件。读取其余的URL,根据哈希集检查每个URL,并将它们写入新文件。您现在知道第一个s1网址是唯一的。

将新文件的位置s1的{​​{1}}放入新的哈希集中。记录大小s1 + 1m。写下您知道对新文件唯一的第一个s2网址。现在编写hashset内容。现在读取其余的URL,根据hashset检查它们。您现在知道第一个s1网址是唯一的。

将位置s1 + s2的网址放入新的哈希集中的s1 + s2。等等,直到你知道它们都是独特的。

答案 1 :(得分:1)

我认为你应该重新审视使​​URL独特的事情。与wezten的答案非常相似,我认为拆分数据是一个好主意,但是,通过将数据划分为较小的文件,您可以大大缩短路径,例如:

  • 网址
  • 网址的长度
  • 网址的第一个和最后一个字符

所以程序的流程就像(伪代码,只是考虑它):

while (url = largeFile.ReadLine()) {
    string directory = GetDirectoryForUrl( url );
    int wordLength = url.Length;
    string filename = url[0] + url[1];
    string fullName = Path.Combine( directory, wordLength.ToString(), fileName);

    var set = LoadSet( fullName );
    if (!set.Contains(url)) {
        AppendToFile( fullName, url );
    }
}

然后对于子方法,比如

string GetDirectoryForUrl( string url ) {
    return GetDomain(url);
}

ISet<string> LoadSet( string fullName ) {
    // check if directories exists...
    if (!File.Exists( fullName )) { 
        return new HashSet<string>();
    }
    // load the hashset based on the file
}

void AppendToFile(string fullName, string url) {
    // add or create the file (check if directories exist)
}

这当然会创建许多较小的文件,但它的优点是你只检查数据的小子集(虽然坦率地说,我不知道你的数据看起来如何,也许只有一些字符不同)

这里的优点是您可以按照您所知道的标准细分数据,并且可以及时调整数据

没有时间创建完整的代码(但因为你主要是在寻找算法:))

<强>更新

我创建了一个小型控制台程序,它既可以创建要测试的文件,也可以创建对大文件的分析。这是代码

有问题的测试会产生大约100万个网址,但是如果有这么有限的随机数据集,那么你描述的数据看起来可能会有20%以上的重复数据。

我的机器上的分析本身需要大约26分钟才能完成,我无法估计这段时间是否合适,因为我没有测试任何其他方式来编写它。

到目前为止,代码从我的初始设置改变了一点,因为我使用了部分url作为我的目录结构。到目前为止,使用数据集时,我没有看到程序结束时的减速,但我还要提到Comodo将我的程序保存在沙盒中。

将所有数据汇总回1个大文件也没有实现,但我没有看到任何大问题。

运行程序所需的类

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace analyzeduplicates {
  public interface IFileAnalyzer {
    IStringToDirectoryHelper StringToDirectoryHelper { set; }
    ISetFileHelper SetFileLoader { set; }
    void Analyze( string targetFile, string targetDirectory );
  }

  public interface IStringToDirectoryHelper {
    string[] GetPathFromString( string value );
  }

  public class StringToDirectoryHelper : IStringToDirectoryHelper {
    public string[] GetPathFromString( string value ) {
      string item = value.Trim();
      return item
        .Trim()
        .Split( new[] { "\\", "/", ":", "@", "%", ":", "?", "&", ";", "." }, StringSplitOptions.RemoveEmptyEntries )
        .Take( 3 )
        .Concat(new string[] { item.Length.ToString(), item[0].ToString() + item[value.Length-1].ToString() } )
        .ToArray();
    }
  }

  public interface ISetFileHelper {
    IReadOnlyCollection<string> GetSetFromFile( string path );
    void AddToSetFile( string path, string value );
  }

  public class SetFileHelper : ISetFileHelper {
    public IReadOnlyCollection<string> GetSetFromFile( string path ) {
      if (!Directory.Exists(Path.GetDirectoryName(path))) {
        return new List<string>();
      }
      if (!File.Exists(path)) {
        return new List<string>();
      }
      return File.ReadAllLines( path );
    }

    public void AddToSetFile( string path, string value) {
      if (!Directory.Exists(Path.GetDirectoryName(path))) {
        Directory.CreateDirectory( Path.GetDirectoryName( path ) );
      }
      File.AppendAllLines( path, new string[] { value } );
    }
  }

  public class FileAnalyzer: IFileAnalyzer {
    public IStringToDirectoryHelper StringToDirectoryHelper { get; set; }
    public ISetFileHelper SetFileLoader { get; set; }

    public FileAnalyzer() {

    }

    public FileAnalyzer(
      IStringToDirectoryHelper stringToDirectoryHelper, 
      ISetFileHelper setFileLoader) : this() {
      StringToDirectoryHelper = stringToDirectoryHelper;
      SetFileLoader = setFileLoader;
    }

    private void EnsureParametersSet() {
      if (StringToDirectoryHelper == null) {
        throw new InvalidOperationException( $"Cannot start analyzing without {nameof(StringToDirectoryHelper)}" );
      }
      if (SetFileLoader == null) {
        throw new InvalidOperationException( $"Cannot start analyzing without {nameof( SetFileLoader )}" );
      }
    }

    public void Analyze( string targetFile, string targetDirectory ) {
      EnsureParametersSet();
      using (var reader = new StreamReader(targetFile, true)) {
        long count = 0;
        while (!reader.EndOfStream) {
          if (count % 1000 == 0) {
            Console.WriteLine( $"Analyzing line {count}-{count + 1000}" );
          }
          count++;
          string line = reader.ReadLine();
          if (string.IsNullOrWhiteSpace(line)) {
            // nothing meaningfull can be done
            continue;
          }
          var path = StringToDirectoryHelper.GetPathFromString( line );
          string targetPath = Path.Combine( new[] { targetDirectory }.Concat( path ).ToArray() );
          var set = SetFileLoader.GetSetFromFile( targetPath );
          if (set.Contains(line)) {
            // duplicate, don't care for it
            continue;
          }
          SetFileLoader.AddToSetFile( targetPath, line );
        }
      }
    }
  }
}

控制台程序本身

using System;
using System.Diagnostics;
using System.IO;

namespace analyzeduplicates {
  class Program {

    static void Main( string[] args ) {
      string targetFile = Path.Combine( Environment.CurrentDirectory, "source.txt" );
      if (File.Exists(targetFile)) {
        File.Delete( targetFile );
      }
      if ( !File.Exists( targetFile ) ) {
        Console.WriteLine( "Generating extensive list of urls" );
        Stopwatch generateWatch = Stopwatch.StartNew();
        GenerateList( targetFile );
        generateWatch.Stop();
        Console.WriteLine( "Generating took {0:hh\\:mm\\:ss}", generateWatch.Elapsed );
      }
      Console.WriteLine( "Analyzing file" );
      Stopwatch analyzeWatch = Stopwatch.StartNew();
      IFileAnalyzer analyzer = new FileAnalyzer(new StringToDirectoryHelper(), new SetFileHelper());
      analyzer.Analyze( targetFile, Environment.CurrentDirectory );
      analyzeWatch.Stop();
      Console.WriteLine( "Analyzing took {0:hh\\:mm\\:ss}", analyzeWatch.Elapsed );
      Console.WriteLine( "done, press enter to clean up" );
      Console.ReadLine();
      File.Delete( targetFile );
      foreach (var dir in Directory.GetDirectories( Environment.CurrentDirectory )) {
        Directory.Delete( dir, true );
      }
      Console.WriteLine( "Cleanup completed, press enter to exit" );
      Console.ReadLine();
    }

    public static void GenerateList( string targetFile ) {
      string[] domains = new[] {
        "www.google.com",
        "www.google.de",
        "www.google.ca",
        "www.google.uk",
        "www.google.co.uk",
        "www.google.nl",
        "www.google.be",
        "www.google.fr",
        "www.google.sa",
        "www.google.me",
        "www.youtube.com",
        "www.youtube.de",
        "www.youtube.ca",
        "www.youtube.uk",
        "www.youtube.co.uk",
        "www.youtube.nl",
        "www.youtube.be",
        "www.youtube.fr",
        "www.youtube.sa",
        "www.youtube.me"
      };
      string[] paths = new[] {
        "search","indicate", "test", "generate", "bla", "bolognes", "macaroni", "part", "of", "web", "site", "index", "main", "nav"
      };
      string[] extensions = new[] {
        "", ".html", ".php", ".aspx", ".aspx", "htm"
      };
      string[] query = new[] {
        "", "?s=test", "?s=query&b=boloni", "?isgreat", "#home", "#main", "#nav"
      };
      string[] protocols = new[] {
        "http://", "https://", "ftp://", "ftps://"
      };
      using (var writer = new StreamWriter(targetFile)) {
        var rnd = new Random();
        for (long i = 0; i < 1000000; i++) {
          int pathLength = rnd.Next( 5 );
          string path = "/";
          if (pathLength > 0) {
            for (int j = 0; j< pathLength; j++ ) {
              path += paths[rnd.Next( paths.Length )] + "/";
            }
          }
          writer.WriteLine( "{0}{1}{2}{3}{4}{5}", protocols[rnd.Next( protocols.Length )], domains[rnd.Next(domains.Length)], path, paths[rnd.Next(paths.Length)], extensions[rnd.Next(extensions.Length)], query[rnd.Next(query.Length)] );
        }
      }
    }
  }
}

我不确定现在的完整答案是不是有点大,但我想我可以分享它。我不知道程序将如何在与您描述的数据集一样大的数据集上执行,我会感兴趣的是在它为您工作时获得时间:)