Запишіть один 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.