เขียนไฟล์ 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

spark 1.6 แสดงข้อผิดพลาดเมื่อเราตั้งค่า.coalesce(1)ว่า FileNotFoundException บางรายการในไดเร็กทอรี _tem Contemporary มันยังคงเป็นจุดบกพร่องในจุดประกาย: issue.apache.org/jira/browse/SPARK-2984
Harsha

@ ฮาร์ชาไม่น่าเลย ค่อนข้างเป็นผลง่ายๆจากcoalesce(1)การมีราคาแพงมากและมักใช้ไม่ได้จริง
zero323

ตกลง @ zero323 แต่ถ้าคุณมีความต้องการพิเศษในการรวมเป็นไฟล์เดียวก็ยังควรเป็นไปได้เนื่องจากคุณมีทรัพยากรและเวลาเพียงพอ
Harsha

2
@ ฮาร์ชาไม่ได้บอกว่าไม่มี หากคุณปรับแต่ง GC อย่างถูกต้องควรใช้งานได้ดี แต่เสียเวลาและส่วนใหญ่จะส่งผลเสียต่อประสิทธิภาพโดยรวม โดยส่วนตัวแล้วฉันไม่เห็นเหตุผลใด ๆ ที่จะรบกวนโดยเฉพาะอย่างยิ่งเนื่องจากการรวมไฟล์ภายนอก Spark นั้นง่ายมากโดยไม่ต้องกังวลเรื่องการใช้หน่วยความจำเลย
zero323

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
Josiah Yoder

@Minkymorgan ฉันมีปัญหาคล้าย ๆ กัน แต่ไม่สามารถทำได้อย่างถูกต้อง .. โปรดดูคำถามนี้ได้
ไหม

4
@SUDARSHAN ฟังก์ชันของฉันด้านบนใช้งานได้กับข้อมูลที่ไม่มีการบีบอัด ในตัวอย่างของคุณฉันคิดว่าคุณกำลังใช้การบีบอัด gzip ในขณะที่คุณเขียนไฟล์และหลังจากนั้น - พยายามรวมสิ่งเหล่านี้เข้าด้วยกันซึ่งล้มเหลว จะไม่ได้ผลเนื่องจากคุณไม่สามารถรวมไฟล์ gzip เข้าด้วยกันได้ Gzip ไม่ใช่อัลกอริธึมการบีบอัดแบบแยกส่วนดังนั้นจึงไม่ใช่ "รวมกันได้" อย่างแน่นอน คุณอาจทดสอบการบีบอัดแบบ "เร็ว" หรือ "bz2" แต่รู้สึกว่าการผสานจะล้มเหลวเช่นกัน วิธีที่ดีที่สุดคือการลบการบีบอัดรวมไฟล์ดิบจากนั้นบีบอัดโดยใช้ตัวแปลงสัญญาณที่แยกได้
Minkymorgan

และถ้าฉันต้องการรักษาส่วนหัวไว้ล่ะ? มันซ้ำกันสำหรับแต่ละส่วนของไฟล์
ปกติ

32

ฉันอาจจะเล่นเกมช้าเล็กน้อยที่นี่ แต่การใช้coalesce(1)หรือrepartition(1)อาจใช้ได้กับชุดข้อมูลขนาดเล็ก แต่ชุดข้อมูลขนาดใหญ่ทั้งหมดจะถูกโยนลงในพาร์ติชันเดียวในโหนดเดียว สิ่งนี้มีแนวโน้มที่จะทำให้เกิดข้อผิดพลาด OOM หรืออย่างดีที่สุดก็คือการประมวลผลช้า

ฉันขอแนะนำให้คุณใช้FileUtil.copyMerge()ฟังก์ชันจาก Hadoop API การดำเนินการนี้จะรวมเอาต์พุตเป็นไฟล์เดียว

แก้ไข - นำข้อมูลไปยังไดรเวอร์ได้อย่างมีประสิทธิภาพแทนที่จะเป็นโหนดตัวดำเนินการ Coalesce()จะดีถ้าตัวดำเนินการเดียวมี RAM สำหรับใช้งานมากกว่าไดรเวอร์

แก้ไข 2 : copyMerge()กำลังถูกลบออกใน Hadoop 3.0 ดูบทความสแตกล้นต่อไปนี้สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีการทำงานกับเวอร์ชันใหม่ล่าสุด: จะทำ CopyMerge ใน Hadoop 3.0 ได้อย่างไร


มีความคิดเกี่ยวกับวิธีรับ csv ด้วยแถวส่วนหัวด้วยวิธีนี้หรือไม่? ไม่ต้องการให้ไฟล์สร้างส่วนหัวเนื่องจากจะสลับส่วนหัวไปทั่วทั้งไฟล์หนึ่งส่วนสำหรับแต่ละพาร์ติชัน
nojo

มีตัวเลือกที่ฉันเคยใช้ในอดีตมีบันทึกไว้ที่นี่: markhneedham.com/blog/2014/11/30/…
etspaceman

@etspaceman เจ๋ง. ฉันยังไม่มีวิธีที่ดีในการทำสิ่งนี้น่าเสียดายที่ฉันต้องสามารถทำได้ใน Java (หรือ Spark แต่ด้วยวิธีที่ไม่ใช้หน่วยความจำมากและสามารถทำงานกับไฟล์ขนาดใหญ่ได้) . ฉันยังไม่อยากเชื่อเลยว่าพวกเขาลบการเรียก API นี้ ... นี่เป็นการใช้งานทั่วไปแม้ว่าจะไม่ได้ใช้กับแอปพลิเคชันอื่นในระบบนิเวศ Hadoop ก็ตาม
woot

20

หากคุณใช้ 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)

หากไฟล์ของคุณไม่พอดีกับแรมในผู้ปฏิบัติงานที่คุณอาจต้องการที่จะต้องพิจารณา ข้อเสนอแนะ chaotic3quilibrium ที่จะใช้ FileUtils.copyMerge () ฉันยังไม่ได้ทำและยังไม่รู้ว่าเป็นไปได้หรือไม่เช่นใน S3

คำตอบนี้สร้างขึ้นจากคำตอบก่อนหน้าสำหรับคำถามนี้รวมทั้งการทดสอบข้อมูลโค้ดที่ให้มา เดิมฉันโพสต์ไว้ที่ Databricksและกำลังเผยแพร่ใหม่ที่นี่

เอกสารที่ดีที่สุดสำหรับตัวเลือก recursive RM dBFS ของของฉันได้พบอยู่ในฟอรั่ม Databricks


3

โซลูชันที่ใช้ได้กับ S3 ที่แก้ไขจาก Minkymorgan

เพียงแค่ส่งพา ธ ไดเร็กทอรีที่แบ่งพาร์ติชันชั่วคราว (ที่มีชื่อแตกต่างจากพา ธ สุดท้าย) เป็นsrcPathcsv / 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 ของ spark จะสร้างไฟล์ชิ้นส่วนหลายไฟล์ภายในพา ธ ที่กำหนด ... เพื่อบังคับให้ spark เขียนเฉพาะไฟล์ส่วนเดียวใช้df.coalesce(1).write.csv(...)แทนdf.repartition(1).write.csv(...)เนื่องจาก coalesce เป็นการแปลงที่แคบในขณะที่ repartition เป็นการแปลงแบบกว้างดูSpark - repartition () vs coalesce ()

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

จะสร้างโฟลเดอร์ใน filepath ที่กำหนดโดยpart-0001-...-c000.csvใช้ไฟล์เดียว

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

มีชื่อไฟล์ที่ใช้งานง่าย


หรือถ้า dataframe ไม่ใหญ่เกินไป (~ GBs หรือพอดีกับหน่วยความจำไดรเวอร์) คุณสามารถใช้df.toPandas().to_csv(path)สิ่งนี้จะเขียน csv เดียวด้วยชื่อไฟล์ที่คุณต้องการ
pprasad009

2
ฮึน่าผิดหวังมากที่สามารถทำได้โดยการแปลงเป็นหมีแพนด้าเท่านั้น การเขียนไฟล์โดยไม่มี UUID บางไฟล์นั้นยากแค่ไหน?
ijoseph

2

แบ่งพาร์ติชั่นใหม่ / รวมกันเป็น 1 พาร์ติชันก่อนที่คุณจะบันทึก (คุณยังคงได้รับโฟลเดอร์ แต่จะมีไฟล์ส่วนหนึ่งอยู่ในนั้น)


2

คุณสามารถใช้ได้ rdd.coalesce(1, true).saveAsTextFile(path)

มันจะจัดเก็บข้อมูลเป็นไฟล์ singile ใน 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 Config)

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

ขั้นตอนที่ 3: - (รับเส้นทางในเส้นทางโฟลเดอร์ hdfs)

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

ขั้นตอนที่ 4: - (รับชื่อไฟล์ spark จากโฟลเดอร์ 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 จากรายการชื่อไฟล์ scala)

    // 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)

1

ฉันใช้สิ่งนี้ใน Python เพื่อรับไฟล์เดียว:

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

1

คำตอบนี้ขยายจากคำตอบที่ยอมรับให้บริบทเพิ่มเติมและให้ข้อมูลโค้ดที่คุณสามารถเรียกใช้ใน 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วิธี Scala และdf.toPandas()งูหลามวิธีการทำงานเฉพาะสำหรับชุดข้อมูลขนาดเล็ก ไม่สามารถเขียนชุดข้อมูลขนาดใหญ่เป็นไฟล์เดียวได้ การเขียนข้อมูลเป็นไฟล์เดียวไม่ดีที่สุดจากมุมมองด้านประสิทธิภาพเนื่องจากไม่สามารถเขียนข้อมูลพร้อมกันได้


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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.