web-dev-qa-db-ja.com

Scalaストリームクラスで大きなCSVファイルを読み取るにはどうすればよいですか?

Scala Stream)で大きなCSVファイル(> 1 Gb)を読み取るにはどうすればよいですか?コード例はありますか?または、ロードせずに大きなCSVファイルを読み取る別の方法を使用しますか?最初にメモリに?

40
Jan Willem Tulp

すでに述べたようにSource.fromFile(...).getLinesを使用してください。

これは、すでにレイジーなイテレーターを返します(以前に取得した値をメモしておくために、ストリームをレイジーコレクションとして使用するため、再度読み取ることができます)。

メモリの問題が発生している場合、問題の原因は実行中のafter getLinesにあります。厳密な収集を強制するtoListのような操作は問題を引き起こします。

71
Kevin Wright

Scalaのcollection.immutable.Streamストリーム付き。これはnotです。ストリームは怠惰ですが、メモ化を行います。

私はあなたが何をしようとしているのかわかりませんが、大量のメモリを使用しなくても、ファイルを1行ずつ読み取るだけで十分機能します。

getLinesは遅延評価される必要があり、クラッシュすることはありません(ファイルに2³²を超える行がない限り、afaik)。存在する場合は、#scalaで質問するか、バグチケットを提出します(または両方を実行します)。

13
soc

ファイルの内容全体を一度にメモリにロードする必要がないようにしながら、大きなファイルを1行ずつ処理する場合は、_scala.io.Source_から返されるIteratorを使用できます。

小さな関数tryProcessSource(2つのサブ関数を含む)があり、これらのタイプのユースケースに正確に使用しています。この関数は最大4つのパラメーターを受け取り、そのうち最初のパラメーターのみが必要です。他のパラメータには、適切なデフォルト値が提供されています。

これが関数プロファイルです(完全な関数の実装は下部にあります)。

_def tryProcessSource(
  file: File,
  parseLine: (Int, String) => Option[List[String]] =
    (index, unparsedLine) => Some(List(unparsedLine)),
  filterLine: (Int, List[String]) => Option[Boolean] =
    (index, parsedValues) => Some(true),
  retainValues: (Int, List[String]) => Option[List[String]] =
    (index, parsedValues) => Some(parsedValues),
): Try[List[List[String]]] = {
  ???
}
_

最初のパラメーター_file: File_は必須です。そして、CSVのような行指向のテキストファイルを指す_Java.io.File_の有効なインスタンスです。

2番目のパラメーターparseLine: (Int, String) => Option[List[String]]はオプションです。また、指定する場合は、2つの入力パラメーターを受け取ることを期待する関数でなければなりません。 _index: Int_、_unparsedLine: String_。そして、_Option[List[String]]_を返します。関数は、有効な列値で構成されるSomeラップされた_List[String]_を返す場合があります。または、ストリーミングプロセス全体が早期に中止されていることを示すNoneを返す場合があります。このパラメーターが指定されていない場合、_(index, line) => Some(List(line))_のデフォルト値が提供されます。このデフォルトでは、行全体が単一のString値として返されます。

3番目のパラメーターfilterLine: (Int, List[String]) => Option[Boolean]はオプションです。また、指定する場合は、2つの入力パラメーターを受け取ることを期待する関数でなければなりません。 _index: Int_、_parsedValues: List[String]_。そして、_Option[Boolean]_を返します。関数は、この特定の行を出力に含めるかどうかを示すSomeラップされたBooleanを返す場合があります。または、ストリーミングプロセス全体が早期に中止されていることを示すNoneを返す場合があります。このパラメーターが指定されていない場合、_(index, values) => Some(true)_のデフォルト値が提供されます。このデフォルトでは、すべての行が含まれます。

4番目の最後のパラメーターretainValues: (Int, List[String]) => Option[List[String]]はオプションです。また、指定する場合は、2つの入力パラメーターを受け取ることを期待する関数でなければなりません。 _index: Int_、_parsedValues: List[String]_。そして、_Option[List[String]]_を返します。関数は、既存の列の値のサブセットまたは変更、あるいはその両方で構成されるSomeラップされた_List[String]_を返す場合があります。または、ストリーミングプロセス全体が早期に中止されていることを示すNoneを返す場合があります。このパラメーターが指定されていない場合、_(index, values) => Some(values)_のデフォルト値が提供されます。このデフォルトでは、2番目のパラメーターparseLineによって解析された値になります。

次の内容(4行)のファイルを考えます。

_street,street2,city,state,Zip
100 Main Str,,Irving,TX,75039
231 Park Ave,,Irving,TX,75039
1400 Beltline Rd,Apt 312,Dallas,Tx,75240
_

次の呼び出しプロファイル...

_val tryLinesDefaults =
  tryProcessSource(new File("path/to/file.csv"))
_

...tryLinesDefaults(ファイルの変更されていない内容)の結果は次のとおりです。

_Success(
  List(
    List("street,street2,city,state,Zip"),
    List("100 Main Str,,Irving,TX,75039"),
    List("231 Park Ave,,Irving,TX,75039"),
    List("1400 Beltline Rd,Apt 312,Dallas,Tx,75240")
  )
)
_

次の呼び出しプロファイル...

_val tryLinesParseOnly =
  tryProcessSource(
      new File("path/to/file.csv")
    , parseLine =
        (index, unparsedLine) => Some(unparsedLine.split(",").toList)
  )
_

...tryLinesParseOnlyの結果は次のようになります(各行が個々の列の値に解析されます):

_Success(
  List(
    List("street","street2","city","state","Zip"),
    List("100 Main Str","","Irving,TX","75039"),
    List("231 Park Ave","","Irving","TX","75039"),
    List("1400 Beltline Rd","Apt 312","Dallas","Tx","75240")
  )
)
_

次の呼び出しプロファイル...

_val tryLinesIrvingTxNoHeader =
  tryProcessSource(
      new File("C:/Users/Jim/Desktop/test.csv")
    , parseLine =
        (index, unparsedLine) => Some(unparsedLine.split(",").toList)
    , filterLine =
        (index, parsedValues) =>
          Some(
            (index != 0) && //skip header line
            (parsedValues(2).toLowerCase == "Irving".toLowerCase) && //only Irving
            (parsedValues(3).toLowerCase == "Tx".toLowerCase)
          )
  )
_

...tryLinesIrvingTxNoHeaderの結果は次のようになります(各行は個々の列値に解析され、ヘッダーはなく、Irving、Txの2行のみ):

_Success(
  List(
    List("100 Main Str","","Irving,TX","75039"),
    List("231 Park Ave","","Irving","TX","75039"),
  )
)
_

次に、tryProcessSource関数の実装全体を示します。

_import scala.io.Source
import scala.util.Try

import Java.io.File

def tryProcessSource(
  file: File,
  parseLine: (Int, String) => Option[List[String]] =
    (index, unparsedLine) => Some(List(unparsedLine)),
  filterLine: (Int, List[String]) => Option[Boolean] =
    (index, parsedValues) => Some(true),
  retainValues: (Int, List[String]) => Option[List[String]] =
    (index, parsedValues) => Some(parsedValues)
): Try[List[List[String]]] = {
  def usingSource[S <: Source, R](source: S)(transfer: S => R): Try[R] =
    try {Try(transfer(source))} finally {source.close()}
  def recursive(
    remaining: Iterator[(String, Int)],
    accumulator: List[List[String]],
    isEarlyAbort: Boolean =
      false
  ): List[List[String]] = {
    if (isEarlyAbort || !remaining.hasNext)
      accumulator
    else {
      val (line, index) =
        remaining.next
      parseLine(index, line) match {
        case Some(values) =>
          filterLine(index, values) match {
            case Some(keep) =>
              if (keep)
                retainValues(index, values) match {
                  case Some(valuesNew) =>
                    recursive(remaining, valuesNew :: accumulator) //capture values
                  case None =>
                    recursive(remaining, accumulator, isEarlyAbort = true) //early abort
                }
              else
                recursive(remaining, accumulator) //discard row
            case None =>
              recursive(remaining, accumulator, isEarlyAbort = true) //early abort
          }
        case None =>
          recursive(remaining, accumulator, isEarlyAbort = true) //early abort
      }
    }
  }
  Try(Source.fromFile(file)).flatMap(
    bufferedSource =>
      usingSource(bufferedSource) {
        source =>
          recursive(source.getLines().buffered.zipWithIndex, Nil).reverse
      }
  )
}
_

このソリューションは比較的簡潔ですが、ようやくここにたどり着くまでにかなりの時間と多くのリファクタリングパスが必要でした。改善の余地がある場合はお知らせください。


更新:以下の問題を それ自体のStackOverflowの質問 として質問しました。そして今それは エラーを修正する答えがあります 下記のとおりです。

私はこれをさらに汎用的にして、retainValuesパラメータをtransformLineに変更して、以下の新しいジェネリックス化された関数定義を作成するというアイデアを思いつきました。ただし、IntelliJでハイライトエラーが発生し続けます。「Some [List [String]]タイプの式は予期されるタイプOption [A]に準拠していません」というエラーが発生し、デフォルト値を変更する方法を理解できなかったため、エラー消えます。

_def tryProcessSource2[A <: AnyRef](
  file: File,
  parseLine: (Int, String) => Option[List[String]] =
    (index, unparsedLine) => Some(List(unparsedLine)),
  filterLine: (Int, List[String]) => Option[Boolean] =
    (index, parsedValues) => Some(true),
  transformLine: (Int, List[String]) => Option[A] =
    (index, parsedValues) => Some(parsedValues)
): Try[List[A]] = {
  ???
}
_

この作業を行う方法に関する支援があれば、大歓迎です。

3