web-dev-qa-db-fra.com

Comment lire un gros fichier CSV avec Scala Stream class?

Comment lire un gros fichier CSV (> 1 Go) avec un flux Scala? Avez-vous un exemple de code? Ou utiliseriez-vous une manière différente de lire un gros fichier CSV sans le charger en mémoire d'abord?

40
Jan Willem Tulp

Utilisez simplement Source.fromFile(...).getLines comme vous l'avez déjà dit.

Cela renvoie un Iterator, qui est déjà paresseux (vous utiliseriez stream comme une collection paresseuse où vous vouliez que les valeurs précédemment récupérées soient mémorisées, afin que vous puissiez les relire)

Si vous rencontrez des problèmes de mémoire, le problème réside dans ce que vous faites après getLines. Toute opération comme toList, qui force une collecte stricte, provoquera le problème.

71
Kevin Wright

J'espère que vous ne voulez pas dire collection.immutable.Stream De Scala avec Stream. C'est pas ce que vous voulez. Le flux est paresseux, mais fait la mémorisation.

Je ne sais pas ce que vous prévoyez de faire, mais la simple lecture du fichier ligne par ligne devrait très bien fonctionner sans utiliser de grandes quantités de mémoire.

getLines devrait évaluer paresseusement et ne devrait pas planter (tant que votre fichier n'a pas plus de 2³² lignes, afaik). Si c'est le cas, demandez sur #scala ou déposez un ticket de bug (ou faites les deux).

13
soc

Si vous cherchez à traiter le gros fichier ligne par ligne tout en évitant d'exiger que le contenu du fichier entier soit chargé en mémoire en une seule fois, vous pouvez utiliser le Iterator renvoyé par scala.io.Source.

J'ai une petite fonction, tryProcessSource, (contenant deux sous-fonctions) que j'utilise exactement pour ces types de cas d'utilisation. La fonction prend jusqu'à quatre paramètres, dont seul le premier est requis. Les autres paramètres ont des valeurs par défaut raisonnables fournies.

Voici le profil de fonction (l'implémentation complète de la fonction est en bas):

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]]] = {
  ???
}

Le premier paramètre, file: File, Est requis. Et ce n'est qu'une instance valide de Java.io.File Qui pointe vers un fichier texte orienté ligne, comme un CSV.

Le deuxième paramètre, parseLine: (Int, String) => Option[List[String]], est facultatif. Et si elle est fournie, ce doit être une fonction qui s'attend à recevoir deux paramètres d'entrée; index: Int, unparsedLine: String. Et puis retournez un Option[List[String]]. La fonction peut renvoyer un Some enveloppé List[String] Composé des valeurs de colonne valides. Ou il peut renvoyer un None qui indique que l'ensemble du processus de streaming est interrompu prématurément. Si ce paramètre n'est pas fourni, une valeur par défaut de (index, line) => Some(List(line)) Est fournie. Cette valeur par défaut entraîne le renvoi de la ligne entière sous la forme d'une seule valeur String.

Le troisième paramètre, filterLine: (Int, List[String]) => Option[Boolean], est facultatif. Et si elle est fournie, ce doit être une fonction qui s'attend à recevoir deux paramètres d'entrée; index: Int, parsedValues: List[String]. Et puis retournez un Option[Boolean]. La fonction peut renvoyer un Some enveloppé Boolean indiquant si cette ligne particulière doit être incluse dans la sortie. Ou il peut renvoyer un None qui indique que l'ensemble du processus de streaming est interrompu prématurément. Si ce paramètre n'est pas fourni, une valeur par défaut de (index, values) => Some(true) Est fournie. Cette valeur par défaut entraîne l'inclusion de toutes les lignes.

Le quatrième et dernier paramètre, retainValues: (Int, List[String]) => Option[List[String]], est facultatif. Et si elle est fournie, ce doit être une fonction qui s'attend à recevoir deux paramètres d'entrée; index: Int, parsedValues: List[String]. Et puis retournez un Option[List[String]]. La fonction peut renvoyer un Some enveloppé List[String] Composé d'un sous-ensemble et/ou d'une modification des valeurs de colonne existantes. Ou il peut renvoyer un None qui indique que l'ensemble du processus de streaming est interrompu prématurément. Si ce paramètre n'est pas fourni, une valeur par défaut de (index, values) => Some(values) Est fournie. Cette valeur par défaut se traduit par les valeurs analysées par le deuxième paramètre, parseLine.

Considérons un fichier avec le contenu suivant (4 lignes):

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

Le profil d'appel suivant ...

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

... donne cette sortie pour tryLinesDefaults (le contenu inchangé du fichier):

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

Le profil d'appel suivant ...

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

... donne cette sortie pour tryLinesParseOnly (chaque ligne analysée dans les valeurs de colonne individuelles):

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

Le profil d'appel suivant ...

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

... entraîne cette sortie pour tryLinesIrvingTxNoHeader (chaque ligne analysée dans les valeurs de colonne individuelles, pas d'en-tête et uniquement les deux lignes dans Irving, Tx):

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

Voici l'implémentation complète de la fonction 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
      }
  )
}

Bien que cette solution soit relativement succincte, il m'a fallu un temps considérable et de nombreuses passes de refactoring avant que je ne puisse enfin arriver ici. Veuillez me faire savoir si vous voyez des améliorations possibles.


MISE À JOUR: Je viens de poser le problème ci-dessous comme c'est sa propre question StackOverflow . Et maintenant a une réponse corrigeant l'erreur mentionné ci-dessous.

J'ai eu l'idée d'essayer de rendre cela encore plus générique en changeant le paramètre retainValues en transformLine avec la nouvelle définition de fonction générique ci-dessous. Cependant, je continue à obtenir l'erreur de surbrillance dans IntelliJ "L'expression de type Some [List [String]] n'est pas conforme à l'option de type attendue [A]" et je n'ai pas pu comprendre comment changer la valeur par défaut de sorte que l'erreur s'en va.

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]] = {
  ???
}

Toute aide sur la façon de faire ce travail serait grandement appréciée.

3
chaotic3quilibrium