我正在使用https://github.com/databricks/spark-csv,我正在尝试编写单个CSV,但是不能,它正在制作一个文件夹。
需要一个Scala函数,该函数将采用路径和文件名之类的参数并写入该CSV文件。
我正在使用https://github.com/databricks/spark-csv,我正在尝试编写单个CSV,但是不能,它正在制作一个文件夹。
需要一个Scala函数,该函数将采用路径和文件名之类的参数并写入该CSV文件。
Answers:
它正在创建一个包含多个文件的文件夹,因为每个分区都是单独保存的。如果您只需要一个输出文件(仍在文件夹中),则可以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
。使用此选项之前,请确保您了解发生了什么以及将所有数据传输到单个工作程序的成本是多少。如果将分布式文件系统用于复制,则数据将被多次传输-首先被提取到单个工作服务器,然后再分布在存储节点上。
.coalesce(1)
它在_temporary目录上显示某些FileNotFoundException 时,spark 1.6会引发错误。它仍然是火花中的错误:issue.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()
Hadoop API中的功能。这会将输出合并到一个文件中。
编辑 -这有效地将数据带给驱动程序而不是执行者节点。Coalesce()
如果单个执行程序具有比驱动程序更多的RAM供使用,那就很好了。
编辑2:copyMerge()
已在Hadoop 3.0中删除。有关如何使用最新版本的更多信息,请参见以下堆栈溢出文章:如何在Hadoop 3.0中执行CopyMerge?
如果您正在使用Databricks,并且可以将所有数据放入一个工作线程中的RAM中(因此可以使用.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)
如果您的文件不适合工作服务器上的RAM,则可能要考虑 chaotic3quilibrium的建议使用FileUtils.copyMerge()。我尚未执行此操作,并且尚不知道是否可行,例如在S3上。
该答案建立在该问题的先前答案以及我自己对提供的代码段的测试之上。我最初将其发布到Databricks,并在此重新发布。
我发现的有关dbfs rm rm递归选项的最佳文档在Databricks论坛上。
适用于Minkymorgan修改的S3的解决方案。
如果要删除原始目录,只需将临时分区目录路径(名称与最终路径不同)传递为,将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
)
}
spark的df.write()
API将在给定的路径内创建多个零件文件...强制spark仅使用单个零件文件,df.coalesce(1).write.csv(...)
而不是df.repartition(1).write.csv(...)
因为coacece是一个狭窄的转换,而repartition是一个广泛的转换,请参见Spark-repartition()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
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 Config)
val hadoopConfig = new Configuration()
val hdfs = FileSystem.get(hadoopConfig)
步骤3:-(在hdfs文件夹路径中获取路径)
val pathFiles = new Path("/hdfsfolder/blah/")
步骤4:-(从hdfs文件夹获取spark文件名)
val fileNames = hdfs.listFiles(pathFiles, false)
println(fileNames)
setp5 :-(创建scala可变列表以保存所有文件名并将其添加到列表中)
var fileNamesList = scala.collection.mutable.MutableList[String]()
while (fileNames.hasNext) {
fileNamesList += fileNames.next().getPath.getName
}
println(fileNamesList)
步骤6:-(从文件名Scala列表中过滤_SUCESS文件顺序)
// get files name which are not _SUCCESS
val partFileName = fileNamesList.filterNot(filenames => filenames == "_SUCCESS")
步骤7 :-(将scala列表转换为字符串,并将所需的文件名添加到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)
该答案扩展了已接受的答案,提供了更多上下文,并提供了可在计算机上的Spark Shell中运行的代码段。
有关接受答案的更多上下文
接受的答案可能会给您留下印象,示例代码将输出一个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
斯卡拉方法和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()