如何在 Scala 或 Java 中读取具有混合编码的文本文件?

Posted

技术标签:

【中文标题】如何在 Scala 或 Java 中读取具有混合编码的文本文件?【英文标题】:How to read a text file with mixed encodings in Scala or Java? 【发布时间】:2012-11-17 11:33:50 【问题描述】:

我正在尝试解析 CSV 文件,最好使用 weka.core.converters.CSVLoader。 但是,我拥有的文件不是有效的 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 为我指明了正确的方向。 这将我带到How to detect illegal UTF-8 byte sequences to replace them in java inputstream? 它提供了核心的 java 解决方案。在 Scala 中,我可以通过使编解码器隐式来使其成为默认行为。我想我可以通过将其隐式编解码器定义放在包对象中来使其成为整个包的默认行为。

【问题讨论】:

在混乱中的某个地方,CharsetDecoderCodingErrorAction 必须设置为 IGNOREREPLACE +Esailija 这就是我想到的解决方案。 Python scikit 库中的一些文本处理函数采用这个选项作为参数。我只是没有看到在 Java/Scala apis 中设置它的任何东西。 我在回答中使用了手工制作的解决方案,我对 java 或 scala 也一无所知 【参考方案1】:

忽略无效字节的问题是决定它们何时再次有效。请注意,UTF-8 允许对字符进行可变长度字节编码,因此如果一个字节无效,您需要了解从哪个字节开始读取才能再次获得有效的字符流。

简而言之,我认为您不会找到一个在阅读时可以“纠正”的库。我认为更有效的方法是先尝试清理这些数据。

【讨论】:

AFAIK 组成多字节字符的字节具有初始位,表示 我是多字节字符的第一个字节我是多字节字符的第二个字节 等,因此应该可以丢弃字节,直到您获得有效的单字节字符或多字节字符的第一个字节。我可以在 R & Python 中读取这些文件。 这是一个很好的观点。但是,您知道它们是正确的吗? 如果您的意思是在 R & Python 中解析的文件,那么是的。【参考方案2】:

一个简单的解决方案是将您的数据流解释为 ASCII,忽略所有非文本字符。但是,您甚至会丢失有效的编码 UTF8 字符。不知道你能不能接受。

编辑:如果您事先知道哪些列是有效的 UTF-8,您可以编写自己的 CSV 解析器,该解析器可以配置在哪些列上使用哪种策略。

【讨论】:

【参考方案3】:

使用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

【讨论】:

这对我来说也是一个可行的解决方案。我不知道 ISO-8859-1 编解码器会接受任意字节。我认为在 Python 中(或我使用过的其他语言 ISO-8859-1 会在 ascii 字符上引发错误。【参考方案4】:

这就是我设法用 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

UTF-8 中的hellö wörld 混入了 4 个无效字节。

使用.REPLACE,您会看到正在使用的标准 unicode 替换字符:

//"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)

【讨论】:

这就是我的想法。当我看到您最初的评论时,我搜索了 CodingErrorAction 和 CharsetDecoder 并在另一个问题中找到了类似的解决方案。我真的希望这是我的包中的默认行为,而在 Scala 中,我可以使用隐式来做到这一点(不确定在 Java 中是否可行)。感谢您的帮助!【参考方案5】:

scala 的 Source 的解决方案(基于@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)

【讨论】:

【参考方案6】:

如果出现故障,我将切换到其他编解码器。

为了实现这个模式,我从this other *** question那里得到了灵感。

我使用默认的编解码器列表,并递归地遍历它们。如果它们都失败了,我会打印出可怕的部分:

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,所以代码可能不是最优的。

【讨论】:

【参考方案7】:

Scala 的 Codec 有一个返回 java.nio.charset.CharsetDecoder 的解码器字段:

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

【讨论】:

对。这就是 Esailija 和 raisercostin 答案的精髓。这是最简洁的。

以上是关于如何在 Scala 或 Java 中读取具有混合编码的文本文件?的主要内容,如果未能解决你的问题,请参考以下文章

混合(Scala/Java)项目的单一文档?

gradle项目中如何支持java与scala混合使用?

IDEA下基于Maven的Java和Scala混合编程

Scala混合具有相同包名的依赖项

window下安装scala步骤

如何在 Scala 中读取环境变量