거대한 텍스트 파일에서 중복 된 문자열을 제거

Dexxrey :

나는 텍스트 파일에서 중복 된 문자열을 제거 할. 나는 HashSet에의 모든 하나의 라인을 넣어 그렇게 한 다음 다른 파일로 작성하기 위해. 그리고 그것을 잘 작동합니다. 그것은 큰 파일 (180메가바이트 500 만 라인)에 올 때 그러나 그것은 매우 잘 작동하지 않습니다. HashSet에 또는 다른 컬렉션 5 백만 문자열을 저장하는 것이 가능하지 않다는 사실을 가정 할 때, 나는 다음, 그때까지 다시 HashSet의 명확하고 그것을에, 파일에 쓰기 내가 처음 100 000 선을 저장할 수 있도록 루프를 만들어가 파일에서 더 이상 줄입니다. 불행하게도,이 모든 중복을 제거하지 않습니다하지만 난 그들의 70-90%에 대해 제거 할 수 있다고 생각합니다. 그러나 그것은 작동하지 않습니다. 언제 500 만 개 라인과 함께 180메가바이트 파일을 테스트합니다. 나는 약 300 000 중복을 계산하고 새로운 파일 만 약 3 라인을 가지고있다. (300) 000 - 그것은 만 약 5 있어야합니다.

    public File removeDuplicates(File file) {
    System.out.println("file opened");
    Scanner sc;
    HashSet<String> set = new HashSet<String>();
    JFileChooser chooser = new JFileChooser();
    File createdFile = null;
    int returnVal = chooser.showSaveDialog(parent);
    if (returnVal == JFileChooser.APPROVE_OPTION) {
        BufferedWriter bufferedWriter = null;
        createdFile = chooser.getSelectedFile();
        try {           

            if (!createdFile.exists()) {
                createdFile.createNewFile();
            }
        }catch(Exception e) {
            e.printStackTrace();
        }
    }
    try {
        sc = new Scanner(file);
        boolean hasMore = true;
        while (hasMore) {
            hasMore = false;
            while (sc.hasNextLine() && set.size() < PERIOD) {
                set.add(sc.nextLine());
                repeated++;
            }
            createdFile = this.writeToFile(set,createdFile);
            set.clear();
            hasMore = true;
            if (sc.hasNextLine() == false)
                hasMore = false;
            set.clear();
        }
    } catch (FileNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    return createdFile;

}
private File writeToFile(HashSet<String> set, File f) {
        BufferedWriter bufferedWriter = null;
        try {           
            Writer writer = new FileWriter(f, true);
            bufferedWriter = new BufferedWriter(writer);
            for (String str : set) {
                bufferedWriter.write(str);
                bufferedWriter.newLine();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (bufferedWriter != null)
                try {
                    bufferedWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }


    return f;
}

반복을 카운트 변수는있다 반복했다. 이 코드에서 뭔가 아니면 그것은 RAM 소비에서 무엇입니까? 그리고 그것이 작동되도록 할 수있는 방법은 무엇입니까?

벤자민 마우어

중복 제거

당신은 단순히 그 파일을 해제 복제 할 것을, 이제 잠시 가정 해 보자. 나는 가장 빠른, 노 번거 로움 방법은 좋은 오래된 유닉스 유틸 될 것이다라고 말하고 싶지만 :

cat myfile.txt | sort -u > sorted.txt

당신의 솔루션을 개선

( TL; DR은 JVM 힙 크기, 초기화 HashSet의 크기를 증가시키고이 답변의 마지막 솔루션을 사용! )

경우 당신이 필요로 자바에서이 작업을 수행의 첫 번째 시도는이보다 효율적으로 할 수 있도록. 많은 사람들이 언급 한 것처럼, 1백80메가바이트은 그 정도가 전부는 아닙니다. 그냥 전체 파일, 체크 할 필요가 (플러스 다음 모든 중복을 제거하지 않습니다)를로드합니다. 예를 들어,이 라인을 가지고 :

HashSet<String> set = new HashSet<String>();

이것은 초기 용량 HashSet의 만듭니다 N을 -재 할당이 줄을 추가 한,가있을 것이라는 점을 의미하고, 0.75의 부하 계수 (I 16 개 요소를? 생각) 메모리를 이상 모든 것을 복사합니다. 여기에 읽기 유용한 무언가, 특히 "성능"입니다

그럼 피하기 할당에 그 크기를 늘릴 수 있습니다 :

Set<String> set = new HashSet<String>(5000000);

그대로 나는 부하 계수를 왼쪽하지만 75 % 가득하면 그 수단은 재 할당됩니다. 당신은 확실히 당신의 파일의 크기를 알고 있다면, 당신은 그 설정을 조정할 수 있습니다.

항상 먼저 측정 - 좋아, 내가 어려운 방법을 배워야했다! 즉 성능 작업의 규칙 번호 하나입니다. 나는 모든 것을 다음은 (16기가바이트 RAM과 빠른 멀티 코어 CPU와) 내 빠른 워크 스테이션에서 내 자신의 구현을 시험하고 내 편집의 모든 것을 요약 썼다. 지금은 (지금 당장 했어야) 솔루션을 시도 궁금했다. 나는 그래서 집에서 내 노트북 (8기가바이트 RAM, 4+ 세 CPU)에 다시 달렸다.

좋아, 여기에 단순화 된 코드는 다음과 같습니다

import java.io.*;
import java.util.*;

public class SortTest {

    public static void main(String[] args) throws IOException {
        if (args.length != 1) {
            System.err.println("Pass filename as argument!");
            System.exit(1);
        }

        Set<String> set = new HashSet<String>();
        File createdFile = new File("./outfile");
        createdFile.createNewFile();

        try (BufferedReader br = new BufferedReader(new FileReader(new File(args[0])))) {
            for (String line = br.readLine(); line != null; line = br.readLine()) {
                set.add(line);
            }
        } catch (IOException ex) {
            throw new RuntimeException("Fatal Error.",  ex);
        }

        try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(createdFile, true))) {
            for (String line : set) {
                bufferedWriter.write(line);
                bufferedWriter.newLine();
            }
        }
    }
}

변경 : 나는 한 번에 전체 파일을로드 청크를 제거했습니다. 나는 기원전, BufferedReader의를 사용하고 있습니다. 스캐너는 (등 정수를 읽기) 구문 분석에 더 유용하고 오버 헤드가 발생할 수 있습니다. 또한 마지막에 파일의 쓰기를 추가하고 I는 BufferedWriter의 때마다 다시 할 필요가 없습니다. 또한 그것은했다 여부 존재 반환하지 않는 경우 검사가 불필요하므로 File.createNewFile는 ()는, 파일을 생성 않습니다. (참고 I는 간결 적절한 에러 처리를 생략했다고)

I은 name.basics 사용 https://datasets.imdbws.com/ 8.837.960 선을 함유하는 5백9메가바이트 파일 (압축 해제)된다. 최종 결과는 동일합니다, 그래서 사람들은 실제로 독특합니다.

실제로 많은 자원의 지옥을 소비하고 내 시스템은 오히려 느려집니다. 처음에는 심지어에서 OutOfMemory 오류가 발생했습니다! 그러나 힙 공간이 일을 더 그것을 실행 : time java -Xmx4g SortTest ./name.basics.tsv저를 제공합니다 :

실제 0m44.289s

사용자 1m23.128s

SYS 0m2.856s

그래서 44 개의 초 나쁘지 않다. 이제이 할당 및 세트를 방지하자 :

Set<String> set = new HashSet<String>(9000000, 0.9f);

결과:

실제 0m38.443s

사용자 1m12.140s

SYS 0m2.376s

음, 외모보다. 나는 이러한 테스트를 여러 번 reran와 시간이 너무 현실에서, 그 결과는 매우 가까운 5 초까지 다양 할 수 있음을, 그러나 말해야한다.

그냥 재미를 위해, 또한 (다시, 아니 적절한 오류 처리) 더 현대적이고 간결한 자바를 사용하여 내 자신의 작은 구현을 보여줄 것이다 :

import java.nio.file.*;
import java.util.*;

public class SortTest2 {

    public static void main(String[] args) throws Exception {
        Set<String> uniq = new HashSet<>(100000, 0.9f);
        try (Stream<String> stream = Files.lines(Paths.get(args[0]))) {
            stream.forEach(uniq::add);
        }

        Files.write(Paths.get("./outfile2"), (Iterable<String>) uniq::iterator);
    }
}

결과 :

실제 0m38.321s

사용자 1m16.452s

SYS 0m2.828s

적은 코드는,하지만 결과는 거의 동일합니다. 참고 : LinkedHashSet의로 HashSet의를 교체하는 경우, 그것은 당신의 라인의 순서를 유지합니다! 이것은 당신이 가능한 가장 일반적인 유형으로 변수와 인수를 선언해야하는 이유 좋은 예이다. 당신이 사용하는 경우 Set<String> uniq에만 변경해야하는 변화의 구현에 라인 (HashSet의 대 LinkedHashSet의).

사실은 프로파일 러와 그것을 좀보고 싶었지만, 실행 시간은 프로그램이 종료하기 전에 나는 심지어 결과를 얻을 수 없었기 때문에 짧은했다.

파일 당신의 RAM에 적합하고 적절한 최대 힙 인수 (-Xmx)를 사용하는 경우, 그것은 문제가되지 않습니다.

그런데 : 나는 다시 테스트 cat | sort -u버전을 - 그것은 55 초 걸렸습니다!

참고 : 많이 편집 된 글을 더 많은 테스트 후

편집하다

사용자 DodgyCodeException의 제안 제거 불필요한 이어 .stream()두 번째 버전에서 전화를.

OK,이는 최적의 솔루션 ™ - 나는 그것이 공동 노력이었다 말할 것입니다, 사용자 헐크와 vlaz에 감사합니다.

import java.nio.file.*;
import java.util.stream.*;

public class SortTest3 {

    public static void main(String[] args) throws Exception {
        try (Stream<String> stream = Files.lines(Paths.get(args[0]))) {
            Files.write(Paths.get("./outfile3"), (Iterable<String>) stream.distinct()::iterator);
        }
    }
}

뿐만 아니라 (아마도 너무 많은 정도) 매우 간결이 솔루션은, 빨리 다른 하나로서,하지만 무엇보다도 그것은 순서를 유지 . 에 대한 모든 감사합니다 .distinct().

대체 솔루션

나는 위의 솔루션은 대부분의 사용 사례에 대한 충분하고 오히려 간단한다고 생각합니다. 그러나하자 당신이 RAM에 맞지 않는 파일, 다룰 필요가 말을하거나 라인 순서를 유지해야합니다. 우리는이 솔루션 뒤에 생각을하고 조금 그것을 변경할 수 있습니다.

의 평균 길이의 가정 해 봅시다 - 당신은 항상 메모리에 한 줄거야, 그래서 당신은 라인으로 파일 라인을 읽을 m를 . 그러면 저장소 일부의 식별자를 필요로하고 바람직하게는 일정 크기와 비교 후 KK << m . 당신은 해시 함수,하지만 많은 충돌과 빠른 일이지만 암호화 해시 함수를 필요 그래서 더 충돌 방지 (예를 들어, SHA1, 2 또는 3). 그러나 참고 : 저항하는 더 많은 충돌의 해시 더 크고 더 큰 당신이 둘 필요가 계산 작업.

  1. 읽기 라인
  2. 계산 해시
  3. 링크 된 목록에서 값을 찾습니다
    • 당신은 하나의 큰 발견하면, 이전에 삽입
    • 당신은 하나의 동일, 폐기 라인을 찾을 경우
  4. 폐기하지 않을 경우 출력 파일에 라인을 쓰기

당신은 삽입 싼 (그리고 그 목록은 성장한다) 유지하는 연결리스트가 필요합니다. 이 목록은 즉시 라인을 작성하여 순서를 유지합니다 삽입 전략과 출력 파일의 지시 유지됩니다.

이 약 차지할 n * k + m공간에 있지만, 해시 함수를 계산 비싼 것입니다 계산.

이 충돌을 처리하지 않습니다. 당신이 좋은 해쉬 함수를 사용하는 경우 (그들은 매우 어렵다으로), 당신은 단지 그들이 일어나지 않을 것입니다 척 수 있습니다. 이 중요한 경우, 예를 들어, 확인 고유성에 다른 메커니즘을 추가 해시와 함께 행 번호를 저장하고 비교를 위해 이전에 본 라인을 가져올 필요가 있습니다. 그런 다음 충돌 해시와 라인을 저장하는 방식을 찾아야합니다.

추천

출처http://43.154.161.224:23101/article/api/json?id=138230&siteId=1