web-dev-qa-db-fra.com

Comment charger des CSV avec des horodatages au format personnalisé?

J'ai un champ d'horodatage dans un fichier csv que je charge dans un cadre de données à l'aide de la bibliothèque spark csv. Le même morceau de code fonctionne sur ma machine locale avec la version Spark 2.0 mais génère une erreur sur Azure Hortonworks HDP 3.5 et 3.6.

J'ai vérifié et Azure HDInsight 3.5 utilise également la même version de Spark, donc je ne pense pas que ce soit un problème avec la version de Spark.

import org.Apache.spark.sql.types._
val sourceFile = "C:\\2017\\datetest"
val sourceSchemaStruct = new StructType()
  .add("EventDate",DataTypes.TimestampType)
  .add("Name",DataTypes.StringType)
val df = spark.read
  .format("com.databricks.spark.csv")
  .option("header","true")
  .option("delimiter","|")
  .option("mode","FAILFAST")
  .option("inferSchema","false")
  .option("dateFormat","yyyy/MM/dd HH:mm:ss.SSS")
  .schema(sourceSchemaStruct)
  .load(sourceFile)

L'exception entière est la suivante:

Caused by: Java.lang.IllegalArgumentException: Timestamp format must be yyyy-mm-dd hh:mm:ss[.fffffffff]
  at Java.sql.Timestamp.valueOf(Timestamp.Java:237)
  at org.Apache.spark.sql.catalyst.util.DateTimeUtils$.stringToTime(DateTimeUtils.scala:179)
  at org.Apache.spark.sql.execution.datasources.csv.UnivocityParser$$anonfun$makeConverter$9$$anonfun$apply$13$$anonfun$apply$2.apply$mcJ$sp(UnivocityParser.scala:142)
  at org.Apache.spark.sql.execution.datasources.csv.UnivocityParser$$anonfun$makeConverter$9$$anonfun$apply$13$$anonfun$apply$2.apply(UnivocityParser.scala:142)
  at org.Apache.spark.sql.execution.datasources.csv.UnivocityParser$$anonfun$makeConverter$9$$anonfun$apply$13$$anonfun$apply$2.apply(UnivocityParser.scala:142)
  at scala.util.Try.getOrElse(Try.scala:79)
  at org.Apache.spark.sql.execution.datasources.csv.UnivocityParser$$anonfun$makeConverter$9$$anonfun$apply$13.apply(UnivocityParser.scala:139)
  at org.Apache.spark.sql.execution.datasources.csv.UnivocityParser$$anonfun$makeConverter$9$$anonfun$apply$13.apply(UnivocityParser.scala:135)
  at org.Apache.spark.sql.execution.datasources.csv.UnivocityParser.org$Apache$spark$sql$execution$datasources$csv$UnivocityParser$$nullSafeDatum(UnivocityParser.scala:179)
  at org.Apache.spark.sql.execution.datasources.csv.UnivocityParser$$anonfun$makeConverter$9.apply(UnivocityParser.scala:135)
  at org.Apache.spark.sql.execution.datasources.csv.UnivocityParser$$anonfun$makeConverter$9.apply(UnivocityParser.scala:134)
  at org.Apache.spark.sql.execution.datasources.csv.UnivocityParser.org$Apache$spark$sql$execution$datasources$csv$UnivocityParser$$convert(UnivocityParser.scala:215)
  at org.Apache.spark.sql.execution.datasources.csv.UnivocityParser.parse(UnivocityParser.scala:187)
  at org.Apache.spark.sql.execution.datasources.csv.UnivocityParser$$anonfun$5.apply(UnivocityParser.scala:304)
  at org.Apache.spark.sql.execution.datasources.csv.UnivocityParser$$anonfun$5.apply(UnivocityParser.scala:304)
  at org.Apache.spark.sql.execution.datasources.FailureSafeParser.parse(FailureSafeParser.scala:61)
  ... 27 more

Le fichier csv a une seule ligne comme suit:

"EventDate"|"Name"
"2016/12/19 00:43:27.583"|"adam"
6
jane

TL; DR Utilise l'option timestampFormat (pas dateFormat).


J'ai réussi à le reproduire dans la dernière version de Spark 2.3.0-SNAPSHOT (construit à partir du maître).

// OS Shell
$ cat so-43259485.csv
"EventDate"|"Name"
"2016/12/19 00:43:27.583"|"adam"

// spark-Shell
scala> spark.version
res1: String = 2.3.0-SNAPSHOT

case class Event(EventDate: Java.sql.Timestamp, Name: String)
import org.Apache.spark.sql.Encoders
val schema = Encoders.product[Event].schema

scala> spark
  .read
  .format("csv")
  .option("header", true)
  .option("mode","FAILFAST")
  .option("delimiter","|")
  .schema(schema)
  .load("so-43259485.csv")
  .show(false)
17/04/08 11:03:42 ERROR Executor: Exception in task 0.0 in stage 7.0 (TID 7)
Java.lang.IllegalArgumentException: Timestamp format must be yyyy-mm-dd hh:mm:ss[.fffffffff]
    at Java.sql.Timestamp.valueOf(Timestamp.Java:237)
    at org.Apache.spark.sql.catalyst.util.DateTimeUtils$.stringToTime(DateTimeUtils.scala:167)
    at org.Apache.spark.sql.execution.datasources.csv.UnivocityParser$$anonfun$makeConverter$9$$anonfun$apply$17$$anonfun$apply$6.apply$mcJ$sp(UnivocityParser.scala:146)
    at org.Apache.spark.sql.execution.datasources.csv.UnivocityParser$$anonfun$makeConverter$9$$anonfun$apply$17$$anonfun$apply$6.apply(UnivocityParser.scala:146)
    at org.Apache.spark.sql.execution.datasources.csv.UnivocityParser$$anonfun$makeConverter$9$$anonfun$apply$17$$anonfun$apply$6.apply(UnivocityParser.scala:146)
    at scala.util.Try.getOrElse(Try.scala:79)

La ligne correspondante dans les sources Spark est la "cause première" du problème:

Timestamp.valueOf(s)

Après avoir lu le javadoc de Timestamp.valueOf , vous pouvez apprendre que l’argument doit être:

horodatage au format yyyy-[m]m-[d]d hh:mm:ss[.f...]. Les fractions de secondes peuvent être omises. Le zéro initial pour mm et dd peut également être omis.

Remarque "Les secondes fractionnaires peuvent être omises", nous allons donc le couper en chargeant d'abord EventDate en tant que chaîne et seulement après avoir supprimé les fractions de secondes inutiles, convertissons-les en horodatage.

val eventsAsString = spark.read.format("csv")
  .option("header", true)
  .option("mode","FAILFAST")
  .option("delimiter","|")
  .load("so-43259485.csv")

Il s'avère que pour les champs de type TimestampType Spark utilise l'option timestampFormat first si elle est définie et que si elle n'utilise pas le code qu'il utilise Timestamp.valueOf .

Il s'avère que le correctif consiste simplement à utiliser l'option timestampFormat (et non pas dateFormat!).

val df = spark.read
  .format("com.databricks.spark.csv")
  .option("header","true")
  .option("delimiter","|")
  .option("mode","FAILFAST")
  .option("inferSchema","false")
  .option("timestampFormat","yyyy/MM/dd HH:mm:ss.SSS")
  .schema(sourceSchemaStruct)
  .load(sourceFile)
scala> df.show(false)
+-----------------------+----+
|EventDate              |Name|
+-----------------------+----+
|2016-12-19 00:43:27.583|adam|
+-----------------------+----+

Spark 2.1.0

Utilisez l'inférence de schéma dans CSV en utilisant l'option inferSchema avec votre timestampFormat personnalisé.

Il est important de déclencher l'inférence de schéma en utilisant inferSchema pour timestampFormat pour prendre effet.

val events = spark.read
  .format("csv")
  .option("header", true)
  .option("mode","FAILFAST")
  .option("delimiter","|")
  .option("inferSchema", true)
  .option("timestampFormat", "yyyy/MM/dd HH:mm:ss")
  .load("so-43259485.csv")

scala> events.show(false)
+-------------------+----+
|EventDate          |Name|
+-------------------+----+
|2016-12-19 00:43:27|adam|
+-------------------+----+

scala> events.printSchema
root
 |-- EventDate: timestamp (nullable = true)
 |-- Name: string (nullable = true)

Version initiale "incorrecte" laissée à des fins d'apprentissage

val events = eventsAsString
  .withColumn("date", split($"EventDate", " ")(0))
  .withColumn("date", translate($"date", "/", "-"))
  .withColumn("time", split($"EventDate", " ")(1))
  .withColumn("time", split($"time", "[.]")(0))    // <-- remove millis part
  .withColumn("EventDate", concat($"date", lit(" "), $"time")) // <-- make EventDate right
  .select($"EventDate" cast "timestamp", $"Name")

scala> events.printSchema
root
 |-- EventDate: timestamp (nullable = true)
 |-- Name: string (nullable = true)
    events.show(false)

scala> events.show
+-------------------+----+
|          EventDate|Name|
+-------------------+----+
|2016-12-19 00:43:27|adam|
+-------------------+----+

Spark 2.2.0

Depuis Spark 2.2, vous pouvez utiliser la fonction to_timestamp pour effectuer la conversion de chaîne en horodatage.

eventsAsString.select($"EventDate", to_timestamp($"EventDate", "yyyy/MM/dd HH:mm:ss.SSS")).show(false)

scala> eventsAsString.select($"EventDate", to_timestamp($"EventDate", "yyyy/MM/dd HH:mm:ss.SSS")).show(false)
+-----------------------+----------------------------------------------------+
|EventDate              |to_timestamp(`EventDate`, 'yyyy/MM/dd HH:mm:ss.SSS')|
+-----------------------+----------------------------------------------------+
|2016/12/19 00:43:27.583|2016-12-19 00:43:27                                 |
+-----------------------+----------------------------------------------------+
8
Jacek Laskowski

J'ai cherché ce problème et découvert la page officielle du problème Github https://github.com/databricks/spark-csv/pull/280 qui a corrigé un bug lié à l'analyse des données avec le format de date personnalisé. J’ai passé en revue certains codes sources et, selon le code code , pour connaître le motif de votre problème qui est défini par inferSchema avec la valeur par défaut false comme ci-dessous.

inferSchema: déduit automatiquement les types de colonne. Il nécessite un passage supplémentaire sur les données et est false par défaut

Veuillez changer inferSchema avec true pour votre format de date yyyy/MM/dd HH:mm:ss.SSS en utilisant SimpleDateFormat.

0
Peter Pan