Запишіть один CSV-файл, використовуючи spark-csv


108

Я використовую https://github.com/databricks/spark-csv , я намагаюся написати єдиний CSV, але не в змозі, це робить папку.

Потрібна функція Scala, яка буде приймати такий параметр, як шлях і ім'я файлу, і записувати цей файл CSV.

Відповіді:


168

Це створення папки з декількома файлами, оскільки кожен розділ зберігається окремо. Якщо вам потрібен один вихідний файл (все ще знаходиться в папці), ви можете repartition(бажано, якщо дані вгорі за течією великі, але потребують перетасування):

df
   .repartition(1)
   .write.format("com.databricks.spark.csv")
   .option("header", "true")
   .save("mydata.csv")

або coalesce:

df
   .coalesce(1)
   .write.format("com.databricks.spark.csv")
   .option("header", "true")
   .save("mydata.csv")

кадр даних перед збереженням:

Усі дані будуть записані в mydata.csv/part-00000. Перш ніж скористатися цією опцією, переконайтеся, що ви розумієте, що відбувається і яка вартість передачі всіх даних одному працівникові . Якщо ви використовуєте розподілену файлову систему з реплікацією, дані будуть передані кілька разів - спочатку передаються одному працівникові, а потім розподіляються по вузлах зберігання даних.

Як альтернативи ви можете залишити свій код , як це і використовувати інструменти загального призначення , як catі HDFSgetmerge просто об'єднати всі частини згодом.


6
ви можете також використовувати coalesce також: df.coalesce (1) .write.format ("com.databricks.spark.csv") .option ("header", "true") .save ("mydata.csv")
— ravi

Іскра 1.6 видає помилку, коли ми встановлюємо .coalesce(1)її, говорить про деякий FileNotFoundException у _temporary каталозі. Це все ще помилка в іскрі: issues.apache.org/jira/browse/SPARK-2984
— Харша,

@Harsha Навряд чи. Досить простий результат - coalesce(1)це дуже дорого і зазвичай не практично.
— zero323

Погоджено @ zero323, але якщо у вас є особлива вимога консолідуватись в один файл, все одно це має бути можливим, враховуючи, що у вас є достатньо ресурсів та часу.
— Харша

2
@Harsha Я не кажу, що немає. Якщо ви правильно налаштуєте GC, він повинен працювати чудово, але це просто марна трата часу і, швидше за все, зашкодить загальній продуктивності. Тож особисто я не бачу жодних причин для занепокоєння, тим більше, що об’єднувати файли за межами Spark надзвичайно просто, не турбуючись про використання пам'яті.
— нуль323

36

Якщо ви використовуєте Spark з HDFS, я вирішував цю проблему, записуючи файли CSV, як правило, і використовуючи HDFS для об'єднання. Я роблю це в Spark (1.6) безпосередньо:

import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs._

def merge(srcPath: String, dstPath: String): Unit =  {
   val hadoopConfig = new Configuration()
   val hdfs = FileSystem.get(hadoopConfig)
   FileUtil.copyMerge(hdfs, new Path(srcPath), hdfs, new Path(dstPath), true, hadoopConfig, null) 
   // the "true" setting deletes the source files once they are merged into the new output
}


val newData = << create your dataframe >>


val outputfile = "/user/feeds/project/outputs/subject"  
var filename = "myinsights"
var outputFileName = outputfile + "/temp_" + filename 
var mergedFileName = outputfile + "/merged_" + filename
var mergeFindGlob  = outputFileName

    newData.write
        .format("com.databricks.spark.csv")
        .option("header", "false")
        .mode("overwrite")
        .save(outputFileName)
    merge(mergeFindGlob, mergedFileName )
    newData.unpersist()

Не можу пригадати, де я навчився цього фокусу, але він може працювати для вас.


Я не пробував цього - і підозрюю, що це може бути не прямо.
— Minkymorgan

1
Дякую. Я додав відповідь, яка працює над Databricks
— Йодер

@Minkymorgan у мене аналогічна проблема , але не в змозі правильно це зробити ..Can ви , будь ласка , подивіться на це питання stackoverflow.com/questions/46812388 / ...
— Sudarshan

4
@SUDARSHAN Моя функція вище працює з нестисненими даними. У вашому прикладі я думаю, що ви використовуєте стиснення gzip, коли ви пишете файли - а потім після - намагаєтеся об'єднати їх разом, що не вдається. Це не спрацює, оскільки ви не можете об'єднати файли gzip разом. Gzip не є алгоритмом компресії, що розділяється, тому, звичайно, не є "злитим". Ви можете перевірити стислий або bz2-стиснення, але відчуття кишок - це теж не вдасться при злитті. Напевно, найкраще - видалити компресію, об'єднати необроблені файли, а потім стиснути за допомогою кодека, що розділяється.
— Minkymorgan

а що робити, якщо я хочу зберегти заголовок? він дублюється для кожної файлової частини
— Звичайний

32

Я, можливо, трохи запізнююся на грі тут, але використання coalesce(1)або repartition(1)може працювати для невеликих наборів даних, але великі набори даних будуть кинуті в один розділ на одному вузлі. Це може призвести до помилок OOM або в кращому випадку повільно оброблятись.

Я б настійно пропонував би скористатися FileUtil.copyMerge()функцією API Hadoop. Це об'єднає виходи в один файл.

EDIT - Це ефективно приносить дані водію, а не вузлу виконавця. Coalesce()було б добре, якщо у одного виконавця є більше оперативної пам’яті для використання, ніж у драйвера.

EDIT 2 : copyMerge()видаляється в Hadoop 3.0. Дивіться таку статтю про переповнення стека для отримання додаткової інформації про те, як працювати з новітньою версією: Як зробити CopyMerge в Hadoop 3.0?


Будь-які думки про те, як отримати CSV із заголовком у такий спосіб? Не хотілося б, щоб файл створював заголовок, оскільки це буде перемежовувати заголовки у всьому файлі, по одному для кожного розділу.
— nojo

Є варіант, який я використовував у минулому задокументованим тут: markhneedham.com/blog/2014/11/30/…
— etspaceman

@etspaceman Cool. На жаль, я ще не маю гарного способу це зробити, на жаль, тому що мені потрібно вміти це робити на Java (або Spark, але таким чином, що не потрібно багато пам’яті та може працювати з великими файлами) . Я все ще не можу повірити, що вони видалили цей виклик API ... це дуже поширене використання, навіть якщо воно точно не використовується іншими програмами в екосистемі Hadoop.
— woot

20

Якщо ви використовуєте Databricks і можете вмістити всі дані в оперативну пам'ять на одному працівнику (і, таким чином, використовувати .coalesce(1)), ви можете використовувати dbfs для пошуку та переміщення отриманого CSV-файлу:

val fileprefix= "/mnt/aws/path/file-prefix"

dataset
  .coalesce(1)       
  .write             
//.mode("overwrite") // I usually don't use this, but you may want to.
  .option("header", "true")
  .option("delimiter","\t")
  .csv(fileprefix+".tmp")

val partition_path = dbutils.fs.ls(fileprefix+".tmp/")
     .filter(file=>file.name.endsWith(".csv"))(0).path

dbutils.fs.cp(partition_path,fileprefix+".tab")

dbutils.fs.rm(fileprefix+".tmp",recurse=true)

Якщо ваш файл не входить в оперативну пам’ять на робочому місці, ви можете розглянути пропозицію хаотичної рівноваги використовувати FileUtils.copyMerge () . Я цього не робив і досі не знаю, можливо чи ні, наприклад, на S3.

Ця відповідь побудована на попередніх відповідях на це питання, а також на моїх власних тестах наданого фрагмента коду. Я спочатку опублікував це в Databricks і перепублікував його тут.

Найкраща документація на рекурсивний варіант rm dbfs, який я знайшов, знаходиться на форумі Databricks .


3

Рішення, яке працює для S3, модифікованого від Minkymorgan.

Просто пропустіть тимчасовий розділений шлях до каталогу (з іншим іменем, ніж кінцевий шлях) як srcPathєдиний остаточний csv / txt, як destPath Укажіть також, deleteSourceякщо ви хочете видалити вихідний каталог.

/**
* Merges multiple partitions of spark text file output into single file. 
* @param srcPath source directory of partitioned files
* @param dstPath output path of individual path
* @param deleteSource whether or not to delete source directory after merging
* @param spark sparkSession
*/
def mergeTextFiles(srcPath: String, dstPath: String, deleteSource: Boolean): Unit =  {
  import org.apache.hadoop.fs.FileUtil
  import java.net.URI
  val config = spark.sparkContext.hadoopConfiguration
  val fs: FileSystem = FileSystem.get(new URI(srcPath), config)
  FileUtil.copyMerge(
    fs, new Path(srcPath), fs, new Path(dstPath), deleteSource, config, null
  )
}

Реалізація copyMerge перераховує всі файли та повторює їх, у s3 це не безпечно. якщо ви пишете свої файли, а потім перераховуєте їх, це не гарантує, що всі вони будуть у списку. див. [це | docs.aws.amazon.com/AmazonS3/latest/dev/…
— LiranBo

3

df.write()API іскри створить декілька файлів частин всередині заданого шляху ..., щоб змусити писати іскру лише одне частинне використання файлу df.coalesce(1).write.csv(...)замість того, df.repartition(1).write.csv(...)як coalesce - це вузьке перетворення, тоді як переділ - це широка трансформація, див. Spark - remartition () vs coalesce ()

df.coalesce(1).write.csv(filepath,header=True) 

створить папку в заданому part-0001-...-c000.csvфайловому шляху за допомогою одного використання файлу

cat filepath/part-0001-...-c000.csv > filename_you_want.csv 

мати зручне ім'я файлу


альтернативно, якщо фрейм даних не надто великий (~ ГБ або вміститься в пам'яті драйвера), ви також можете використовувати df.toPandas().to_csv(path)це, щоб записати один csv з улюбленим іменем файлу
— pprasad009

2
Тьфу, так засмучує, як це можна зробити, лише перетворившись на панди. Наскільки важко просто написати файл без якогось UUID у ньому?
— ійосеф

2

переділ / з’єднання з 1 розділом перед збереженням (ви все одно отримаєте папку, але в ній буде один файл частини)


2

ви можете використовувати rdd.coalesce(1, true).saveAsTextFile(path)

він буде зберігати дані як єдиний файл у шляху / part-00000


1
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs._
import org.apache.spark.sql.{DataFrame,SaveMode,SparkSession}
import org.apache.spark.sql.functions._

Я вирішив, використовуючи підхід нижче (перейменувати ім'я файлу hdfs): -

Крок 1: - (Скрийте кадру даних та запишіть у HDFS)

df.coalesce(1).write.format("csv").option("header", "false").mode(SaveMode.Overwrite).save("/hdfsfolder/blah/")

Крок 2: - (Створити конфігурацію Hadoop)

val hadoopConfig = new Configuration()
val hdfs = FileSystem.get(hadoopConfig)

Крок 3: - (Отримати шлях у шлях папки hdfs)

val pathFiles = new Path("/hdfsfolder/blah/")

Крок 4: - (Отримати імена файлів іскри з папки hdfs)

val fileNames = hdfs.listFiles(pathFiles, false)
println(fileNames)

setp5: - (створити змінний список масштабування, щоб зберегти всі назви файлів та додати його до списку)

    var fileNamesList = scala.collection.mutable.MutableList[String]()
    while (fileNames.hasNext) {
      fileNamesList += fileNames.next().getPath.getName
    }
    println(fileNamesList)

Крок 6: - (відфільтруйте порядок передачі файлів _SUCESS зі списку масштабованих імен файлів)

    // get files name which are not _SUCCESS
    val partFileName = fileNamesList.filterNot(filenames => filenames == "_SUCCESS")

крок 7: - (перетворити список масштабування в рядок і додати потрібне ім'я файлу до рядка папки hdfs, а потім застосувати перейменування)

val partFileSourcePath = new Path("/yourhdfsfolder/"+ partFileName.mkString(""))
    val desiredCsvTargetPath = new Path(/yourhdfsfolder/+ "op_"+ ".csv")
    hdfs.rename(partFileSourcePath , desiredCsvTargetPath)

1

Я використовую це в Python, щоб отримати один файл:

df.toPandas().to_csv("/tmp/my.csv", sep=',', header=True, index=False)

1

Ця відповідь розширюється на прийняту відповідь, надає більше контексту та надає фрагменти коду, які можна запустити в іскровій оболонці на вашій машині.

Більше контексту на прийняту відповідь

Прийнята відповідь може створити враження, що зразок коду видає один mydata.csvфайл, і це не так. Давайте продемонструємо:

val df = Seq("one", "two", "three").toDF("num")
df
  .repartition(1)
  .write.csv(sys.env("HOME")+ "/Documents/tmp/mydata.csv")

Ось що виводиться:

Documents/
  tmp/
    mydata.csv/
      _SUCCESS
      part-00000-b3700504-e58b-4552-880b-e7b52c60157e-c000.csv

NB mydata.csv- це папка у прийнятій відповіді - це не файл!

Як вивести один файл із конкретним іменем

Ми можемо використовувати spark-daria, щоб виписати один mydata.csvфайл.

import com.github.mrpowers.spark.daria.sql.DariaWriters
DariaWriters.writeSingleFile(
    df = df,
    format = "csv",
    sc = spark.sparkContext,
    tmpFolder = sys.env("HOME") + "/Documents/better/staging",
    filename = sys.env("HOME") + "/Documents/better/mydata.csv"
)

Це виведе файл таким чином:

Documents/
  better/
    mydata.csv

S3 шляхи

Вам потрібно буде пройти s3a шляхи, DariaWriters.writeSingleFileщоб використовувати цей метод у S3:

DariaWriters.writeSingleFile(
    df = df,
    format = "csv",
    sc = spark.sparkContext,
    tmpFolder = "s3a://bucket/data/src",
    filename = "s3a://bucket/data/dest/my_cool_file.csv"
)

Дивіться тут для отримання додаткової інформації.

Уникнення copyMerge

copyMerge був видалений з Hadoop 3. У DariaWriters.writeSingleFileреалізації використовується fs.rename, як описано тут . Spark 3 все ще використовується Hadoop 2 , тому впровадження copyMerge запрацює в 2020 році. Я не впевнений, коли Spark перейде до Hadoop 3, але краще уникати будь-якого підходу copyMerge, який призведе до того, що ваш код порушиться, коли Spark оновить Hadoop.

Вихідний код

Шукайте DariaWritersоб’єкт у вихідному коді spark-daria, якщо ви хочете перевірити реалізацію.

Реалізація PySpark

Простіше виписати один файл із PySpark, оскільки ви можете перетворити DataFrame в Pandas DataFrame, який за замовчуванням виписується як один файл.

from pathlib import Path
home = str(Path.home())
data = [
    ("jellyfish", "JALYF"),
    ("li", "L"),
    ("luisa", "LAS"),
    (None, None)
]
df = spark.createDataFrame(data, ["word", "expected"])
df.toPandas().to_csv(home + "/Documents/tmp/mydata-from-pyspark.csv", sep=',', header=True, index=False)

Обмеження

Підхід DariaWriters.writeSingleFileScala та підхід df.toPandas()Python працюють лише для невеликих наборів даних. Величезні набори даних не можна виписати як окремі файли. Запис даних у вигляді одного файлу не є оптимальним з точки зору продуктивності, оскільки дані не можна записувати паралельно.


0

За допомогою Listbuffer ми можемо зберегти дані в один файл:

import java.io.FileWriter
import org.apache.spark.sql.SparkSession
import scala.collection.mutable.ListBuffer
    val text = spark.read.textFile("filepath")
    var data = ListBuffer[String]()
    for(line:String <- text.collect()){
      data += line
    }
    val writer = new FileWriter("filepath")
    data.foreach(line => writer.write(line.toString+"\n"))
    writer.close()

-2

Є ще один спосіб використання Java

import java.io._

def printToFile(f: java.io.File)(op: java.io.PrintWriter => Unit) 
  {
     val p = new java.io.PrintWriter(f);  
     try { op(p) } 
     finally { p.close() }
  } 

printToFile(new File("C:/TEMP/df.csv")) { p => df.collect().foreach(p.println)}

ім'я 'true' не визначено
— Arron
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.