web-dev-qa-db-ja.com

ScalaまたはJavaでエンコードが混在したテキストファイルを読み取る方法

理想的にはweka.core.converters.CSVLoaderを使用して、CSVファイルを解析しようとしています。しかし、私が持っているファイルは有効なUTF-8ファイルではありません。ほとんどはUTF-8ファイルですが、フィールド値の一部は異なるエンコーディングになっているため、ファイル全体が有効なエンコーディングはありませんが、とにかく解析する必要があります。 WekaのようなJavaライブラリを使用する以外は、私は主にScalaで作業しています。scala.io.Sourceのファイルを読むことすらできません。出典:たとえば

Source.
  fromFile(filename)("UTF-8").
  foreach(print);

スロー:

    Java.nio.charset.MalformedInputException: Input length = 1
at Java.nio.charset.CoderResult.throwException(CoderResult.Java:277)
at Sun.nio.cs.StreamDecoder.implRead(StreamDecoder.Java:337)
at Sun.nio.cs.StreamDecoder.read(StreamDecoder.Java:176)
at Java.io.InputStreamReader.read(InputStreamReader.Java:184)
at Java.io.BufferedReader.fill(BufferedReader.Java:153)
at Java.io.BufferedReader.read(BufferedReader.Java:174)
at scala.io.BufferedSource$$anonfun$iter$1$$anonfun$apply$mcI$sp$1.apply$mcI$sp(BufferedSource.scala:38)
at scala.io.Codec.wrap(Codec.scala:64)
at scala.io.BufferedSource$$anonfun$iter$1.apply(BufferedSource.scala:38)
at scala.io.BufferedSource$$anonfun$iter$1.apply(BufferedSource.scala:38)
at scala.collection.Iterator$$anon$14.next(Iterator.scala:150)
at scala.collection.Iterator$$anon$25.hasNext(Iterator.scala:562)
at scala.collection.Iterator$$anon$19.hasNext(Iterator.scala:400)
at scala.io.Source.hasNext(Source.scala:238)
at scala.collection.Iterator$class.foreach(Iterator.scala:772)
at scala.io.Source.foreach(Source.scala:181)

無効なキャラクターをすべて捨てるか、ダミーに置き換えてとてもうれしいです。このような多くのテキストをさまざまな方法で処理し、データをさまざまなサードパーティライブラリに渡す必要がある場合があります。理想的な解決策は、すべての低レベルJavaライブラリがテキスト内の無効なバイトを無視するようにするグローバル設定のようなものです。これにより、変更せずにこのデータでサードパーティライブラリを呼び出すことができます。

解決:

import Java.nio.charset.CodingErrorAction
import scala.io.Codec

implicit val codec = Codec("UTF-8")
codec.onMalformedInput(CodingErrorAction.REPLACE)
codec.onUnmappableCharacter(CodingErrorAction.REPLACE)

val src = Source.
  fromFile(filename).
  foreach(print)

私を正しい方向に向けてくれた+ Esailijaに感謝します。これは私を コアを提供するJava inputstream? で不正なUTF-8バイトシーケンスを検出してそれらを置き換える方法Java解決策。Scalaコーデックを暗黙的にすることでこれをデフォルトの動作にすることができます。パッケージオブジェクトに暗黙的なコーデック定義を追加することで、パッケージ全体のデフォルトの動作にすることができます。 。

49
Daniel Mahler

これは私がJavaでそれをどうやってやったかです:

    FileInputStream input;
    String result = null;
    try {
        input = new FileInputStream(new File("invalid.txt"));
        CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
        decoder.onMalformedInput(CodingErrorAction.IGNORE);
        InputStreamReader reader = new InputStreamReader(input, decoder);
        BufferedReader bufferedReader = new BufferedReader( reader );
        StringBuilder sb = new StringBuilder();
        String line = bufferedReader.readLine();
        while( line != null ) {
            sb.append( line );
            line = bufferedReader.readLine();
        }
        bufferedReader.close();
        result = sb.toString();

    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch( IOException e ) {
        e.printStackTrace();
    }

    System.out.println(result);

無効なファイルはバイトで作成されます:

0x68, 0x80, 0x65, 0x6C, 0x6C, 0xC3, 0xB6, 0xFE, 0x20, 0x77, 0xC3, 0xB6, 0x9C, 0x72, 0x6C, 0x64, 0x94

4つの無効なバイトが混在するUTF-8のhellö wörldです。

.REPLACEを使用すると、使用されている標準のユニコード置換文字が表示されます。

//"h�ellö� wö�rld�"

.IGNOREを使用すると、無効なバイトが無視されることがわかります。

//"hellö wörld"

.onMalformedInputを指定せずに、

Java.nio.charset.MalformedInputException: Input length = 1
    at Java.nio.charset.CoderResult.throwException(Unknown Source)
    at Sun.nio.cs.StreamDecoder.implRead(Unknown Source)
    at Sun.nio.cs.StreamDecoder.read(Unknown Source)
    at Java.io.InputStreamReader.read(Unknown Source)
    at Java.io.BufferedReader.fill(Unknown Source)
    at Java.io.BufferedReader.readLine(Unknown Source)
    at Java.io.BufferedReader.readLine(Unknown Source)
24
Esailija

Scalaのソースのソリューション(@Esailijaの回答に基づく):

def toSource(inputStream:InputStream): scala.io.BufferedSource = {
    import Java.nio.charset.Charset
    import Java.nio.charset.CodingErrorAction
    val decoder = Charset.forName("UTF-8").newDecoder()
    decoder.onMalformedInput(CodingErrorAction.IGNORE)
    scala.io.Source.fromInputStream(inputStream)(decoder)
}
14
raisercostin

Scalaのコーデックには、Java.nio.charset.CharsetDecoderを返すデコーダフィールドがあります。

val decoder = Codec.UTF8.decoder.onMalformedInput(CodingErrorAction.IGNORE)
Source.fromFile(filename)(decoder).getLines().toList
13
maxmc

失敗した場合、別のコーデックに切り替えています。

パターンを実装するために、 このもう1つのstackoverflowの質問 からインスピレーションを得ました。

デフォルトのコーデックのリストを使用し、再帰的に調べます。それらがすべて失敗した場合、私は怖いビットを印刷します:

private val defaultCodecs = List(
  io.Codec("UTF-8"),
  io.Codec("ISO-8859-1")
)

def listLines(file: Java.io.File, codecs:Iterable[io.Codec] = defaultCodecs): Iterable[String] = {
  val codec = codecs.head
  val fileHandle = scala.io.Source.fromFile(file)(codec)
  try {
    val txtArray = fileHandle.getLines().toList
    txtArray
  } catch {
    case ex: Exception => {
      if (codecs.tail.isEmpty) {
        println("Exception:  " + ex)
        println("Skipping file:  " + file.getPath)
        List()
      } else {
        listLines(file, codecs.tail)
      }
    }
  } finally {
    fileHandle.close()
  }
}

私はただScalaを学んでいるので、コードは最適ではないかもしれません。

2
Harry Pehkonen

無効なバイトを無視する際の問題は、それらが再び有効になるタイミングを決定することです。 UTF-8では文字の可変長バイトエンコーディングが許可されているため、バイトが無効な場合は、文字の有効なストリームを再度取得するには、どのバイトから読み取りを開始するかを理解する必要があります。

要するに、読みながら「修正」できるライブラリを見つけるとは思わない。もっと生産的なアプローチは、まずそのデータをクリーンアップすることだと思います。

2
Brian Agnew

簡単な解決策は、データストリームをASCIIとして解釈し、すべての非テキスト文字を無視することです。ただし、有効なエンコードされたUTF8文字でさえ失われます。それがあなたに受け入れられるかどうかわからない。

編集:どの列が有効なUTF-8であるかを事前に知っている場合、どの列でどの戦略を使用するかを構成できる独自のCSVパーサーを作成できます。

0
mbelow

ISO-8859-1をエンコーダーとして使用します。これにより、バイト値が文字列にパックされます。ほとんどのエンコーディングでCSVを解析するにはこれで十分です。 (8ビットブロックと16ビットブロックが混在している場合、問題が発生します。ISO-8859-1の行は引き続き読み取ることができますが、行をブロックとして解析できない場合があります。)

個々のフィールドを個別の文字列として取得したら、試すことができます

new String(oldstring.getBytes("ISO-8859-1"), "UTF-8")

適切なエンコーディングで文字列を生成します(知っている場合は、フィールドごとに適切なエンコーディング名を使用します)。

編集:エラーを検出する場合は、Java.nio.charset.Charset.CharsetDecoderを使用する必要があります。この方法でUTF-8にマッピングすると、エラーが発生したときに文字列に0xFFFFが表示されます。

val decoder = Java.nio.charset.Charset.forName("UTF-8").newDecoder

// By default will throw a MalformedInputException if encoding fails
decoder.decode( Java.nio.ByteBuffer.wrap(oldstring.getBytes("ISO-8859-1")) ).toString
0
Rex Kerr