2013-03-19 2 views
1

Je lis dans un fichier csv pour être stocké dans une structure de données immuable. Chaque rangée est une entrée. Chaque entrée a une station. Chaque station peut avoir plusieurs entrées. Y a-t-il un moyen de faire cela en un seul passage au lieu du double passage que vous voyez ci-dessous?Optimisation d'une dénormalisation d'un fichier csv

object NYCSubwayEntrances { 
    def main(args: Array[String]) = { 
    import com.github.tototoshi.csv.CSVReader 
    //http://www.mta.info/developers/data/nyct/subway/StationEntrances.csv 
    val file = new java.io.File("StationEntrances.csv") 
    val reader = CSVReader.open(file) 
    reader.readNext //consume headers 
    val entranceMap = list2multimap(
     reader.all map { 
     case fields: List[String] => 
      // println(fields) 
      (
      fields(2), 
      Entrance(
       fields(14).toBoolean, 
       Option(fields(15)), 
       fields(16).toBoolean, 
       fields(17), 
       fields(18) match {case "YES" => true case _ => false}, 
       fields(19) match {case "YES" => true case _ => false}, 
       fields(20), 
       fields(21), 
       fields(22), 
       fields(23), 
       fields(24).toInt, 
       fields(25).toInt 
      ) 
     ) 
     } 
    ) 
    reader.close 
    val reader2 = CSVReader.open(file) 
    reader2.readNext //consume headers 
    val stations = reader2.all map { case fields: List[String] => 
     Station(
     fields(2), 
     fields(0), 
     fields(1), 
     colate(scala.collection.immutable.ListSet[String](
      fields(3), 
      fields(4), 
      fields(5), 
      fields(6), 
      fields(7), 
      fields(8), 
      fields(9), 
      fields(10), 
      fields(11), 
      fields(12), 
      fields(13) 
     )), 
     entranceMap(fields(2)).toList 
    ) 
    } 
    reader2.close 

    import net.liftweb.json._ 
    import net.liftweb.json.Serialization.write 
    implicit val formats = Serialization.formats(NoTypeHints) 
    println(pretty(render(parse(write(stations.toSet))))) 
    } 

    import scala.collection.mutable.{HashMap, Set, MultiMap} 

    def list2multimap[A, B](list: List[(A, B)]) = 
    list.foldLeft(new HashMap[A, Set[B]] with MultiMap[A, B]){(acc, pair) => acc.addBinding(pair._1, pair._2)} 

    def colate(set: scala.collection.immutable.ListSet[String]): List[String] = 
    ((List[String]() ++ set) diff List("")).reverse 
} 

case class Station(name: String, division: String, line: String, routes: List[String], entrances: List[Entrance]) {} 
case class Entrance(ada: Boolean, adaNotes: Option[String], freeCrossover: Boolean, entranceType: String, entry: Boolean, exitOnly: Boolean, entranceStaffing: String, northSouthStreet: String, eastWestStreet: String, corner: String, latitude: Integer, longitude: Integer) {} 

Un projet SBT avec toutes les dépendances correctes se trouvent à https://github.com/AEtherSurfer/NYCSubwayEntrances

StationEntrances.csv a été obtenu à partir http://www.mta.info/developers/sbwy_entrance.html

+0

Le fichier d'entrée semble être trié par 'Division, Line, Station_Name'. Est-ce une hypothèse sur laquelle vous êtes prêt à compter? Cela pourrait accélérer le traitement et réduire les besoins en mémoire (bien que le fichier ne semble pas très gros). – huynhjl

+0

Oui, je suis prêt à supposer que le csv est trié par 'Division, Line, Station_Name'. – Gabriel

Répondre

1

J'ai les extraits suivants. Cette première solution utilise groupBy pour regrouper les entrées liées à la même station. Il ne suppose pas que les lignes sont triées. Bien qu'il ne lise le fichier qu'une seule fois, il fait vraiment 3 passages (un pour tout lire en mémoire, un pour groupBy et un pour créer les stations). Voir à la fin le code de l'extracteur .

val stations = { 
    val file = new java.io.File("StationEntrances.csv") 
    val reader = com.github.tototoshi.csv.CSVReader.open(file) 
    val byStation = reader 
    .all  // read all in memory 
    .drop(1) // drop header 
    .groupBy { 
     case List(division, line, station, _*) => (division, line, station) 
    } 
    reader.close 
    byStation.values.toList map { rows => 
    val entrances = rows map { case Row(_, _, _, _, entrance) => entrance } 
    rows.head match { 
     case Row(division, line, station, routes, _) => 
     Station(
      division, line, station, 
      routes.toList.filter(_ != ""), 
      entrances) 
    } 
    } 
} 

Cette solution suppose que les lignes sont triées et devraient être plus rapides, comme il le fait qu'une seule passe et construire la liste des résultats car il lit le fichier. J'ai créé un extracteur dédié pour obtenir les routes/entrées d'une ligne donnée. Je pense que cela rend le code plus lisible, mais aussi si vous traitez avec la liste, appeler fields(0) à fields(25) n'est pas optimal puisque chaque appel doit traverser la liste. L'extracteur évite cela. Pour la plupart des parseurs Java csv, vous obtenez généralement Array[String], ce qui n'est généralement pas un problème. Enfin, l'analyse de csv ne renvoie généralement pas de chaînes nulles. Vous pouvez donc utiliser if (adaNotes == "") None else Some(adaNotes) au lieu de Option(adaNotes).

object Row { 
    def unapply(s: Seq[String]) = s match { 
    case List(division, line, station, rest @ _*) => 
     val (routes, 
     List(ada, adaNotes, freeCrossover, entranceType, 
      entry, exitOnly, entranceStaffing, northSouthStreet, eastWestStreet, 
      corner, latitude, longitude)) = rest splitAt 11 // 11 routes 
     Some((
     division, line, station, 
     routes, 
     Entrance(
      ada.toBoolean, Option(adaNotes), 
      freeCrossover.toBoolean, entranceType, 
      entry == "YES", exitOnly == "YES", 
      entranceStaffing, northSouthStreet, eastWestStreet, corner, 
      latitude.toInt, longitude.toInt))) 
    case _ => None 
    } 
}