HOWTO · Java

Java でファイルから改行を削除する方法

このチュートリアルでは、replace、line separator、replaceAll などの複数の方法で Java のファイルから改行を削除する方法を示します。

このページの内容

このチュートリアルでは、Java でファイルから改行を削除する方法を紹介します。小さなファイルや中程度のファイルであれば、最も短い方法は、ファイル全体を Files.readString() で String に読み込み、replaceAll("\\R", "") ですべての改行を削除し、結果を Files.writeString() で書き戻すことです。さらに、\r\n で終わる Windows ファイル、1 行ずつ処理する大きなファイル、Scanner を使う方法、そして末尾の改行だけを削除する方法も扱います。サンプルには Java 11 以降が必要です。どちらの Files メソッドも Java 11 で追加されました。

すべてのサンプルは、このサンプルファイル notes.txt を使います:

Remove line breaks
from this file
with Java

サンプルが「改行が消えた」と主張するときは、その主張を cat -A で確認します。このコマンドは各行の末尾に $ を表示し、キャリッジリターンを ^M と表示します:

$ cat -A notes.txt
Remove line breaks$
from this file$
with Java$

Files.readString() と replaceAll() ですべての改行を削除する

Files.readString() はファイル全体を 1 つの String に読み込み、UTF-8 としてデコードします。正規表現パターン \R は、あらゆる Unicode の改行にマッチします。ラインフィード、キャリッジリターン、あるいは 1 つの改行として扱われる \r\n の組み合わせです。すべてのマッチを削除すれば、3 種類の行末をすべて 1 回の処理で消せます。クリーニングされたテキストは、新しいファイル cleaned.txt に保存されます:

コード例:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class RemoveAllBreaks {
    public static void main(String[] args) throws IOException {
        Path input = Path.of("notes.txt");
        String content = Files.readString(input);
        String cleaned = content.replaceAll("\\R", "");
        Files.writeString(Path.of("cleaned.txt"), cleaned);
        System.out.println(cleaned);
    }
}

出力:

Remove line breaksfrom this filewith Java

breaks と from という単語がくっついていることに注目してください。改行を削除しても、その場所には何も挿入されません。cat -A による確認では $ が 1 つも残っていないことが分かります。つまり cleaned.txt は、末尾の改行もない 1 行だけのファイルです:

$ cat -A cleaned.txt
Remove line breaksfrom this filewith Java

Windows の CRLF 行末を処理する

Windows で作成されたテキストファイルは、各行が \r\n で終わります。一方、Linux や macOS のファイルは \n のみを使います。これが、単純な修正方法である replace("\n", "") を壊します。この方法は Windows の行末の \n の半分だけを取り除き、\r の半分を各行に残してしまうからです。印刷するとテキストは正しく見えますが、隠れたキャリッジリターンが後の処理を壊します。読み戻した値の末尾に \r が残るためです。次のサンプルは Windows 形式のファイル notes-crlf.txt を使い、単純な結果と修正された結果の両方をディスクに書き込みます:

コード例:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class RemoveCrlfBreaks {
    public static void main(String[] args) throws IOException {
        String content = Files.readString(Path.of("notes-crlf.txt"));

        String naive = content.replace("\n", "");
        Files.writeString(Path.of("trap.txt"), naive);
        System.out.println("replace only: " + naive.length() + " characters");

        String fixed = content.replaceAll("\\R", "");
        Files.writeString(Path.of("fixed.txt"), fixed);
        System.out.println("replaceAll \\R: " + fixed.length() + " characters");
    }
}

出力:

replace only: 39 characters
replaceAll \R: 37 characters

余分な 2 文字は、生き残ったキャリッジリターンであり、cat -A では ^M として見えます。それらが一切残っていないのは replaceAll() の結果だけです:

$ cat -A trap.txt
First Windows line^MSecond Windows line^M
$ cat -A fixed.txt
First Windows lineSecond Windows line

BufferedReader で大きなファイルを 1 行ずつ処理する

数百メガバイトに成長しうるファイルでは、ファイル全体をメモリに読み込むのは誤った方法です。代わりに、Files.newBufferedReader() と lines() ストリームを組み合わせると、1 行ずつ処理できます。ストリームは行末文字を除いた各行を渡し、Collectors.joining() が行を 1 つの String に連結します。メモリに構築されるのは、連結された結果だけです:

コード例:

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Collectors;

public class JoinLines {
    public static void main(String[] args) throws IOException {
        try (BufferedReader reader = Files.newBufferedReader(Path.of("notes.txt"))) {
            String joined = reader.lines().collect(Collectors.joining(" "));
            System.out.println(joined);
        }
    }
}

出力:

Remove line breaks from this file with Java

joining() に渡す区切り文字には注意が必要です。このサンプルでは " " を使い、隣り合う行の境界にある単語が分かれたままになるようにしています。最初のサンプルのくっついた単語と比べてください。境界に意味がない場合、たとえば固定長データレコードでは、結果が切れ目のない文字列でなければならないため、joining("") を使います。

代わりに Scanner でファイルを読み取る

Scanner は、ファイルがすでにトークンごとに解析されている場合に、同じ仕事をこなせます。区切り文字を \R に設定すると、各行が改行を含まない 1 つのトークンになるため、すべてのトークンを追加すれば、改行のない内容を再構築できます。文字セットは以下に示すように明示的に渡してください。そうしないと、Scanner は Files.readString() が UTF-8 を使う場面で、プラットフォームのデフォルト文字セットを黙って使ってしまいます:

コード例:

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Scanner;

public class ScannerRead {
    public static void main(String[] args) throws IOException {
        StringBuilder builder = new StringBuilder();
        try (Scanner scanner = new Scanner(Path.of("notes.txt"), StandardCharsets.UTF_8)) {
            scanner.useDelimiter("\\R");
            while (scanner.hasNext()) {
                builder.append(scanner.next());
            }
        }
        System.out.println(builder);
    }
}

出力:

Remove line breaksfrom this filewith Java

末尾の改行だけを削除する

場合によっては、内部の改行こそがデータであり、問題なのは最後の改行だけです。末尾にある目に見えない 1 つの \n のために、期待値との比較が失敗します。パターンを文字列の末尾に固定すれば解決します。replaceAll("\\R+$", "") は、内容のまさに末尾にある 1 つ以上の改行を削除し、それ以外には一切触れません:

コード例:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class TrimTrailingBreak {
    public static void main(String[] args) throws IOException {
        Path input = Path.of("notes.txt");
        String content = Files.readString(input);
        String trimmed = content.replaceAll("\\R+$", "");
        Files.writeString(Path.of("trimmed.txt"), trimmed);
        System.out.println("Before: " + content.length() + " characters");
        System.out.println("After: " + trimmed.length() + " characters");
    }
}

出力:

Before: 44 characters
After: 43 characters

消えた文字はちょうど 1 つ、最後の改行です。内部の 2 つの改行は生き残りました。それらの行は今も $ で終わり、最後の行にはありません:

$ cat -A trimmed.txt
Remove line breaks$
from this file$
with Java

結果を元のファイルに安全に書き戻す

クリーニングされたテキストで元のファイルを置き換える必要がある場合、読み取ったばかりのファイルに直接上書きしてはいけません。Files.writeString() はまず書き込み先を切り捨てるため、書き込み中のクラッシュで元の内容が失われます。安全な方法は、新しい内容を同じディレクトリ内の一時ファイルに書き込み、Files.move() で元のファイルの上に移動することです。1 つのディレクトリ内での移動は名前変更であり、読み取り側には古い内容か新しい内容のどちらかが見え、書きかけの混在状態が見えることはありません。サンプルファイルのコピー notes-copy.txt に適用した例です:

コード例:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

public class SafeWriteBack {
    public static void main(String[] args) throws IOException {
        Path target = Path.of("notes-copy.txt");
        String cleaned = Files.readString(target).replaceAll("\\R", "");
        Path temp = target.resolveSibling(target.getFileName() + ".tmp");
        Files.writeString(temp, cleaned);
        Files.move(temp, target, StandardCopyOption.REPLACE_EXISTING);
        System.out.println("notes-copy.txt now holds: " + Files.readString(target));
    }
}

出力:

notes-copy.txt now holds: Remove line breaksfrom this filewith Java