Я використовую https://github.com/databricks/spark-csv , я намагаюся написати єдиний CSV, але не в змозі, це робить папку.
Потрібна функція Scala, яка буде приймати такий параметр, як шлях і ім'я файлу, і записувати цей файл CSV.
Я використовую https://github.com/databricks/spark-csv , я намагаюся написати єдиний CSV, але не в змозі, це робить папку.
Потрібна функція Scala, яка буде приймати такий параметр, як шлях і ім'я файлу, і записувати цей файл CSV.
Відповіді:
Це створення папки з декількома файлами, оскільки кожен розділ зберігається окремо. Якщо вам потрібен один вихідний файл (все ще знаходиться в папці), ви можете 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
просто об'єднати всі частини згодом.
.coalesce(1)
її, говорить про деякий FileNotFoundException у _temporary каталозі. Це все ще помилка в іскрі: issues.apache.org/jira/browse/SPARK-2984
coalesce(1)
це дуже дорого і зазвичай не практично.
Якщо ви використовуєте 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()
Не можу пригадати, де я навчився цього фокусу, але він може працювати для вас.
Я, можливо, трохи запізнююся на грі тут, але використання coalesce(1)
або repartition(1)
може працювати для невеликих наборів даних, але великі набори даних будуть кинуті в один розділ на одному вузлі. Це може призвести до помилок OOM або в кращому випадку повільно оброблятись.
Я б настійно пропонував би скористатися FileUtil.copyMerge()
функцією API Hadoop. Це об'єднає виходи в один файл.
EDIT - Це ефективно приносить дані водію, а не вузлу виконавця. Coalesce()
було б добре, якщо у одного виконавця є більше оперативної пам’яті для використання, ніж у драйвера.
EDIT 2 : copyMerge()
видаляється в Hadoop 3.0. Дивіться таку статтю про переповнення стека для отримання додаткової інформації про те, як працювати з новітньою версією: Як зробити CopyMerge в Hadoop 3.0?
Якщо ви використовуєте 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 .
Рішення, яке працює для 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
)
}
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 з улюбленим іменем файлу
переділ / з’єднання з 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)
Я використовую це в Python, щоб отримати один файл:
df.toPandas().to_csv("/tmp/my.csv", sep=',', header=True, index=False)
Ця відповідь розширюється на прийняту відповідь, надає більше контексту та надає фрагменти коду, які можна запустити в іскровій оболонці на вашій машині.
Більше контексту на прийняту відповідь
Прийнята відповідь може створити враження, що зразок коду видає один 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.writeSingleFile
Scala та підхід df.toPandas()
Python працюють лише для невеликих наборів даних. Величезні набори даних не можна виписати як окремі файли. Запис даних у вигляді одного файлу не є оптимальним з точки зору продуктивності, оскільки дані не можна записувати паралельно.
За допомогою 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()
Є ще один спосіб використання 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)}