web-dev-qa-db-ja.com

文字の行を読み取り、ファイルの位置を取得する

テキストファイルから文字の連続した行を読み取っています。ファイル内の文字のエンコードは、シングルバイトではない可能性があります。

ある時点で、次の行が始まるファイル位置を取得したいので、後でファイルを再度開いてその位置に戻ることができますquickly

ご質問

できれば標準のJavaライブラリを使用して、両方を行う簡単な方法はありますか?

そうでない場合、合理的な回避策は何ですか?

理想的なソリューションの属性

理想的なソリューションは、複数の文字エンコーディングを処理することです。これには、異なる文字が異なるバイト数で表される場合があるUTF-8が含まれます。理想的なソリューションは、主に信頼され、十分にサポートされているライブラリに依存します。最も理想的なのは標準のJavaライブラリです。2番目に最適なのはApacheまたはGoogleライブラリです。ソリューションはスケーラブルでなければなりません。ファイル全体をメモリに読み込むことはソリューションではありません。位置に戻る必要があります。以前のすべての文字を線形時間で読み取る必要はありません。

細部

最初の要件として、BufferedReader.readLine()が魅力的です。ただし、バッファリングは、意味のあるファイル位置の取得を明らかに妨げます。

それほど明白ではありませんが、InputStreamReaderは先読みも可能で、ファイル位置の取得を妨害します。 InputStreamReaderのドキュメント から:

バイトから文字への効率的な変換を可能にするために、現在の読み取り操作を満たすために必要な数より多くのバイトが、基礎となるストリームから先に読み取られる場合があります。

メソッドRandomAccessFile.readLine()文字ごとに1バイトを読み取る

文字の下位8ビットのバイト値を取得し、文字の上位8ビットをゼロに設定することにより、各バイトが文字に変換されます。したがって、このメソッドは完全なUnicode文字セットをサポートしていません。

22
Andy Thomas

BufferedReaderからFileReaderを作成し、FileReaderのインスタンスをコードからアクセスできるようにしておけば、次の呼び出しによって次の行の位置を取得できるはずです。

_fileReader.getChannel().position();
_

bufferedReader.readLine()の呼び出し後。

BufferedReaderは、パフォーマンスの向上と位置精度のトレードオフを考えている場合、サイズ1の入力バッファーで構築できます。

代替ソリューション自分でバイトを追跡することで何が問題になるのでしょうか。

_long startingPoint = 0; // or starting position if this file has been previously processed

while (readingLines) {
    String line = bufferedReader.readLine();
    startingPoint += line.getBytes().length;
}
_

これにより、基礎となるマーキングやバッファリングに関係なく、すでに処理したものに正確なバイト数が得られます。それらは取り除かれるので、あなたはあなたのタリーの行末を説明しなければなりません。

9
Jeff

このケースは、大きなXMLファイルをすばやく解析できるライブラリであるVTD-XMLによって解決されるようです。

最後のJava VTD-XML ximpleware実装、現在2.13 http://sourceforge.net/projects/vtd-xml/files/vtd-xml/ はいくつかのコードを提供しますIReader実装のgetChar()メソッドを呼び出すたびにバイトオフセットを維持します。

さまざまな文字エンコーディングのIReader実装がVTDGen.JavaおよびVTDGenHuge.Java内で利用可能

IReader実装は、次のエンコーディング用に提供されています

ASCII; ISO_8859_1 ISO_8859_10 ISO_8859_11 ISO_8859_12 ISO_8859_13 ISO_8859_14 ISO_8859_15 ISO_8859_16 ISO_8859_2 ISO_8859_3 ISO_8859_4 ISO_8859_5 ISO_8859_6 ISO_8859_7 ISO_8859_8 ISO_8859_16 UTF_16 UTF_16
WIN_1250 WIN_1251 WIN_1252 WIN_1253 WIN_1254 WIN_1255 WIN_1256 WIN_1257 WIN_1258

3
user1767316

この部分的な回避策は、7ビットASCIIまたはUTF-8でエンコードされたファイルのみに対処します。一般的な解決策の回答が望ましいです(この回避策に対する批判も同様です)。

UTF-8の場合:

  • すべてのシングルバイト文字は、マルチバイト文字のすべてのバイトと区別できます。マルチバイト文字のすべてのバイトの上位位置に「1」があります。特に、LFおよびCRを表すバイトは、マルチバイト文字の一部にすることはできません。
  • シングルバイト文字はすべて7ビットASCIIです。したがって、UTF-8デコーダーで7ビットASCII文字のみを含むファイルをデコードできます。

まとめると、これらの2つのポイントは、文字ではなくバイトを読み取る何かでラインを読み取り、そのラインをデコードできることを意味します。

バッファリングの問題を回避するために、RandomAccessFileを使用できます。そのクラスは、行を読み取り、ファイル位置を取得/設定するメソッドを提供します。

RandomAccessFileを使用して次の行をUTF-8として読み取るコードのスケッチを次に示します。

protected static String 
readNextLineAsUTF8( RandomAccessFile in ) throws IOException {
    String rv = null;
    String lineBytes = in.readLine();
    if ( null != lineBytes ) {
        rv = new String( lineBytes.getBytes(),
            StandardCharsets.UTF_8 );
    }
    return rv;
 } 

次に、そのメソッドを呼び出す直前にRandomAccessFileからファイル位置を取得できます。 inによって参照されるRandomAccessFileがあるとします:

    long startPos = in.getFilePointer();
    String line = readNextLineAsUTF8( in );
3
Andy Thomas

Java.io.LineNumberReader。行番号を設定して取得できるため、特定の行インデックスで続行できます。

BufferedReaderであるため、UTF-8も処理できます。

1
CoronA

ソリューションA

  1. ループで RandomAccessFile.readChar() または RandomAccessFile.readByte() を使用します。
  2. EOL文字を確認し、その行を処理します。

それ以外の問題は、EOL文字を超えて絶対に読まないようにする必要があることです。

readChar()は、バイトではなくcharを返します。したがって、文字幅を気にする必要はありません。

このファイルから文字を読み取ります。このメソッドは、現在のファイルポインターからファイルから2バイトを読み取ります。

[...]

このメソッドは、2バイトが読み取られるか、ストリームの終わりが検出されるか、または例外がスローされるまでブロックします。

ReaderではなくRandomAccessFileを使用すると、ファイル内の文字セットをデコードするJavaの機能を放棄します。 BufferedReaderはこれを自動的に行います。

これを克服する方法はいくつかあります。 1つは、自分でエンコードを検出してから、正しいread *()メソッドを使用することです。もう1つの方法は、BoundedInputストリームを使用することです。

この質問には1つあります Java:バッファされた入力でランダムアクセスファイルから文字列を読み取る

例えば。 https://stackoverflow.com/a/4305478/16549

1
kervin

最初に、私はAndy Thomas( https://stackoverflow.com/a/30850145/55646 )によって提案されたアプローチが最も適切であるとわかりました。

しかし、残念ながら、ファイル行に非ラテン文字が含まれている場合、バイト配列(_RandomAccessFile.readLine_から取得)を正しい文字列に変換できませんでした。

そこで、行から文字列ではなくバイト配列に直接データを収集する_RandomAccessFile.readLine_自体と同様の関数を記述してアプローチを作り直し、バイト配列から目的の文字列を構築しました。したがって、以下のコードは(Kotlinでの)私のニーズを完全に満たしました。

関数を呼び出した後、file.channel.position()は次の行の正確な位置(ある場合)を返します。

_fun RandomAccessFile.readEncodedLine(charset: Charset = Charsets.UTF_8): String? {
    val lineBytes = ByteArrayOutputStream()
    var c = -1
    var eol = false

    while (!eol) {
        c = read()
        when (c) {
            -1, 10 -> eol = true // \n
            13     -> { // \r
                eol = true
                val cur = filePointer
                if (read() != '\n'.toInt()) {
                    seek(cur)
                }
            }
            else   -> lineBytes.write(c)
        }
    }

    return if (c == -1 && lineBytes.size() == 0)
        null
    else
        Java.lang.String(lineBytes.toByteArray(), charset) as String
}
_
1
plinyar

RandomAccessFile には関数があります:seek(long pos)次の読み取りまたは書き込みが発生する、このファイルの先頭から測定したファイルポインターオフセットを設定します。

1
east.charm