二つのファイルを比較して同一の行を削除する方法

いろいろミスがあったので、修正。

##やりたいこと

新しく生成された CSV ファイルの、「前回から何が違うか」を調べるコードを書かなければならなかったので、メモ。

当初、片方のファイルの各行を、もう一方のファイルの全行と比較する処理を書いたが、800,000 行くらいの CSV ファイルを処理させたら MacBookPro Retina 15" (core i7) で結構な時間がかかったので、StackOverFlow あたりを這いずり回って調査した結果、下記の方法にたどり着く。下記の方法だと 800,000 行のファイルの比較に ~~40秒くらい。~~ 1秒くらい。

----

##実装方針

ファイルをふたつ (A, B) 受け取り、A と B で共通している行を削除する。

このコード例では、A から「B と重複する行」を取り除いた後、残った行（差異のある行）を HashSet として受け取る。

##サンプルコード

```
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;

/**
 * Created by kanoloa on 16/03/29.
 *
 * sample of diff.
 *
 */
public class FileDiff {

    protected HashSet<String> getUpdatedLines(Path file_A, Path file_B) {
        HashSet<String> hash_A = null;
        HashSet<String> hash_B = null;

        if (Files.exists(file_A)) {
            hash_A = getHashSet(file_A);
        }

        if (Files.exists(file_B)) {
            hash_B = getHashSet(file_B);
        }

        // ファイル B が存在しない場合は、A の全行が残る。
        if (hash_A != null && hash_B != null) {
            hash_A.removeAll(hash_B);
        }

        return hash_A;
    }

    protected HashSet<String> getHashSet(Path file) {
        if (Files.notExists(file)) {
            return null;
        } else {
            HashSet<String> hashSet = new HashSet<>();
            try (BufferedReader br = Files.newBufferedReader(file)) {
                br.lines().forEach(hashSet::add);
                return hashSet;
            } catch (IOException ioe) {
                ioe.printStackTrace();
                return null;
            }
        }
    }

    public static void main(String[] args) {

        if (args.length != 2) {
            System.out.println("please specify two files.");
            return;
        }

        Path file_A = Paths.get(args[0]);
        Path file_B = Paths.get(args[1]);

        if (Files.notExists(file_A)) {
            System.out.println("file does not exist: " + file_A.toString());
            return;
        }

        if (Files.notExists(file_B)) {
            System.out.println("file does not exist: " + file_B.toString());
            return;
        }

        FileDiff diff = new FileDiff();

        long start = System.currentTimeMillis();
        HashSet<String> lines = diff.getUpdatedLines(file_A, file_B);
        long end = System.currentTimeMillis();


        if (lines.size() > 0) {
            lines.forEach(System.out::println);
        }
        System.out.println("found " + lines.size() + " lines.");
        System.out.println("took + " + (end - start) + " msec.");
    }
}
```

二つ目の引数として渡されたファイルが存在しない場合は、一つ目の引数の全行が戻される。

一つ目の引数として渡されたファイルが存在しない場合は、null が戻されるので、呼び出し側で対処する。

##実行結果

800,800行 (80万とんで800行) あり、23メガくらいある二つのファイルを比較した場合（差異は全部で 8,000 行ある)の実行時間は 1秒強。

```
X000252211,,,yes,,,BOLT,BOLT
X000000111,,,yes,,,BOLT,BOLT
(中略)
X000268511,,,yes,,,BOLT,BOLT
X000058211,,,yes,,,BOLT,BOLT
found 8000 lines.
took + 1081 msec.
```

# 処理方式を変えてみる

コメントにアドバイスをいただいたので、両方のファイルを HashSet に取るのをやめ、片方は通常のループ処理で回してみる。変更するのは getUpdatedLines() メソッド。

下記のコードでは、一番目の引数として渡される file_A を HashSet に取り、二番目の引数の file_B を forEach で回しながら、読み込んだ一行が HashSet に存在すれば、その行に該当するエントリーを HashSet から取り除く。

```
   protected HashSet<String> getUpdatedLines(Path file_A, Path file_B) {
        HashSet<String> hash_A;

        if (Files.exists(file_A)) {
            hash_A = getHashSet(file_A);
        } else {
            return null;
        }

        if (Files.exists(file_B)) {
            try (BufferedReader br = Files.newBufferedReader(file_B)) {
                br.lines().forEach(hash_A::remove);
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }
        return hash_A;
    }
```

このコードで、5回実行した平均は 710ミリ秒。

一番最初のコードでの 5回実行平均が 1,093ミリ秒だったので、最初のコードの 60% の時間で処理できるようになった。