Viết một tệp CSV duy nhất bằng spark-csv


Câu trả lời:


168

Nó đang tạo một thư mục với nhiều tệp, vì mỗi phân vùng được lưu riêng lẻ. Nếu bạn cần một tệp đầu ra duy nhất (vẫn còn trong một thư mục), bạn có thể repartition(ưu tiên nếu dữ liệu ngược dòng lớn nhưng yêu cầu xáo trộn):

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

hoặc coalesce:

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

khung dữ liệu trước khi lưu:

Tất cả dữ liệu sẽ được ghi vào mydata.csv/part-00000. Trước khi sử dụng tùy chọn này, hãy chắc chắn rằng bạn hiểu điều gì đang xảy ra và chi phí chuyển tất cả dữ liệu cho một nhân viên . Nếu bạn sử dụng hệ thống tệp phân tán có tính năng sao chép, dữ liệu sẽ được chuyển nhiều lần - lần đầu tiên được tìm nạp cho một nhân viên và sau đó được phân phối qua các nút lưu trữ.

Ngoài ra, bạn có thể giữ nguyên mã của mình và sử dụng các công cụ mục đích chung như cathoặc HDFSgetmerge để đơn giản hợp nhất tất cả các phần sau đó.


6
bạn cũng có thể sử dụng liên kết: df.coalesce (1) .write.format ("com.databricks.spark.csv") .option ("tiêu đề", "true") .save ("mydata.csv")
ravi

spark 1.6 tạo ra một lỗi khi chúng tôi đặt .coalesce(1)nó cho biết một số FileNotFoundException trên thư mục _tempional. Nó vẫn còn là một lỗi trong spark: Problem.apache.org/jira/browse/SPARK-2984
Harsha

@Harsha Không có khả năng. Thay vì đơn giản là kết quả coalesce(1)đắt tiền và thường không thực tế.
zero323,

Đã đồng ý @ zero323, nhưng nếu bạn có yêu cầu đặc biệt để hợp nhất thành một tệp, bạn vẫn có thể có nhiều tài nguyên và thời gian.
Harsha

2
@Harsha Tôi không nói là không có. Nếu bạn điều chỉnh GC một cách chính xác, nó sẽ hoạt động tốt nhưng nó chỉ đơn giản là lãng phí thời gian và rất có thể sẽ ảnh hưởng đến hiệu suất tổng thể. Vì vậy, cá nhân tôi không thấy có lý do gì để bận tâm, đặc biệt vì việc hợp nhất các tệp bên ngoài Spark rất đơn giản mà không cần lo lắng về việc sử dụng bộ nhớ.
zero323,

36

Nếu bạn đang chạy Spark với HDFS, tôi đã giải quyết vấn đề bằng cách ghi tệp csv bình thường và tận dụng HDFS để thực hiện hợp nhất. Tôi đang làm điều đó trong Spark (1.6) trực tiếp:

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

Không thể nhớ tôi đã học thủ thuật này ở đâu, nhưng nó có thể hiệu quả với bạn.


Tôi đã không thử nó - và nghi ngờ nó có thể không thẳng về phía trước.
Minkymorgan

1
Cảm ơn. Tôi đã thêm một câu trả lời hoạt động trên Databricks
Josiah Yoder.

@Minkymorgan, tôi cũng gặp sự cố tương tự nhưng không thể làm đúng được .. Bạn có thể vui lòng xem câu hỏi này stackoverflow.com/questions/46812388/…
SUDARSHAN

4
@SUDARSHAN Hàm của tôi ở trên hoạt động với dữ liệu không nén. Trong ví dụ của bạn, tôi nghĩ rằng bạn đang sử dụng nén gzip khi bạn ghi tệp - và sau đó - cố gắng hợp nhất chúng lại với nhau nhưng không thành công. Điều đó sẽ không hiệu quả vì bạn không thể hợp nhất các tệp gzip với nhau. Gzip không phải là một thuật toán Nén có thể phân tách, vì vậy chắc chắn không phải là "có thể hợp nhất". Bạn có thể kiểm tra tính năng nén "snappy" hoặc "bz2" - nhưng cảm giác đặc biệt là điều này cũng sẽ thất bại khi hợp nhất. Có lẽ tốt nhất là xóa nén, hợp nhất các tệp thô, sau đó nén bằng codec có thể phân tách.
Minkymorgan

và nếu tôi muốn giữ lại tiêu đề thì sao? nó trùng lặp cho từng phần tệp
Bình thường

32

Tôi có thể đến trò chơi hơi muộn ở đây, nhưng việc sử dụng coalesce(1)hoặc repartition(1)có thể hoạt động đối với các tập dữ liệu nhỏ, nhưng các tập dữ liệu lớn sẽ được ném vào một phân vùng trên một nút. Điều này có thể gây ra lỗi OOM hoặc tốt nhất là xử lý chậm.

Tôi thực sự khuyên bạn nên sử dụng FileUtil.copyMerge()hàm từ API Hadoop. Điều này sẽ hợp nhất các đầu ra thành một tệp duy nhất.

EDIT - Điều này đưa dữ liệu đến trình điều khiển một cách hiệu quả hơn là một nút thực thi. Coalesce()sẽ ổn nếu một trình thực thi duy nhất có nhiều RAM để sử dụng hơn trình điều khiển.

CHỈNH SỬA 2 : copyMerge()đang bị xóa trong Hadoop 3.0. Xem bài viết tràn ngăn xếp sau để biết thêm thông tin về cách làm việc với phiên bản mới nhất: Làm thế nào để thực hiện CopyMerge trong Hadoop 3.0?


Bạn có suy nghĩ gì về cách lấy csv với hàng tiêu đề theo cách này không? Bạn sẽ không muốn tệp tạo ra một tiêu đề, vì điều đó sẽ xen kẽ các tiêu đề trong toàn bộ tệp, một tiêu đề cho mỗi phân vùng.
nojo

Có một lựa chọn mà tôi đã sử dụng trong quá khứ ghi nhận ở đây: markhneedham.com/blog/2014/11/30/...
etspaceman

@etspaceman Tuyệt. Tôi vẫn chưa thực sự có cách tốt để thực hiện việc này, thật không may, vì tôi cần có thể thực hiện việc này trong Java (hoặc Spark, nhưng theo cách không tiêu tốn nhiều bộ nhớ và có thể hoạt động với các tệp lớn) . Tôi vẫn không thể tin rằng họ đã xóa lệnh gọi API này ... đây là cách sử dụng rất phổ biến ngay cả khi không được sử dụng chính xác bởi các ứng dụng khác trong hệ sinh thái Hadoop.
woot

20

Nếu bạn đang sử dụng Databricks và có thể phù hợp tất cả dữ liệu vào RAM trên một worker (và do đó có thể sử dụng .coalesce(1)), bạn có thể sử dụng dbfs để tìm và di chuyển tệp CSV kết quả:

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)

Nếu tệp của bạn không vừa với RAM trên worker, bạn có thể muốn xem xét đề xuất sử dụng FileUtils.copyMerge () của Loạn cân bằng () . Tôi chưa thực hiện việc này và không biết liệu có khả thi hay không, ví dụ: trên S3.

Câu trả lời này được xây dựng dựa trên các câu trả lời trước đây cho câu hỏi này cũng như các thử nghiệm của riêng tôi đối với đoạn mã được cung cấp. Ban đầu tôi đã đăng nó lên Databricks và đang xuất bản lại ở đây.

Tài liệu tốt nhất cho tùy chọn đệ quy rm của dbfs mà tôi đã tìm thấy trên diễn đàn Databricks .


3

Một giải pháp hoạt động cho S3 được sửa đổi từ Minkymorgan.

Đơn giản chỉ cần chuyển đường dẫn thư mục được phân vùng tạm thời (với tên khác với đường dẫn cuối cùng) dưới srcPathdạng csv / txt cuối cùng duy nhất dưới dạng destPath Chỉ định cũng deleteSourcenhư nếu bạn muốn xóa thư mục gốc.

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

Việc triển khai copyMerge liệt kê tất cả các tệp và lặp lại chúng, điều này không an toàn trong s3. nếu bạn viết các tệp của mình và sau đó liệt kê chúng - điều này không đảm bảo rằng tất cả chúng sẽ được liệt kê. xem [cái này | docs.aws.amazon.com/AmazonS3/latest/dev/…
LiranBo

3

df.write()API của spark sẽ tạo nhiều tệp phần bên trong đường dẫn nhất định ... để buộc spark chỉ ghi một tệp phần duy nhất sử dụng df.coalesce(1).write.csv(...)thay vì df.repartition(1).write.csv(...)liên kết là một chuyển đổi hẹp trong khi phân vùng lại là một chuyển đổi rộng. Xem Spark - repartition () vs thanesce ()

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

sẽ tạo thư mục trong đường dẫn tệp nhất định với một lần part-0001-...-c000.csvsử dụng tệp

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

để có một tên tệp thân thiện với người dùng


Ngoài ra, nếu khung dữ liệu không quá lớn (~ GBs hoặc có thể vừa với bộ nhớ trình điều khiển), bạn cũng có thể sử dụng df.toPandas().to_csv(path)điều này để ghi một csv duy nhất với tên tệp ưa thích của bạn
pprasad009 10/12/19

1
Ugh, thật bực bội làm sao điều này chỉ có thể được thực hiện bằng cách chuyển đổi thành gấu trúc. Thật khó để viết một tệp mà không có một số UUID trong đó?
ijoseph

2

phân vùng lại / liên kết thành 1 phân vùng trước khi bạn lưu (bạn vẫn nhận được một thư mục nhưng nó sẽ có một tệp phần trong đó)


2

bạn có thể dùng rdd.coalesce(1, true).saveAsTextFile(path)

nó sẽ lưu trữ dữ liệu dưới dạng tệp đơn trong đường dẫn / 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._

Tôi đã giải quyết bằng cách sử dụng phương pháp dưới đây (hdfs đổi tên tên tệp): -

Bước 1: - (Xếp khung dữ liệu và ghi vào HDFS)

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

Bước 2: - (Tạo cấu hình Hadoop)

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

Bước 3: - (Lấy đường dẫn trong đường dẫn thư mục hdfs)

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

Bước 4: - (Lấy tên tệp spark từ thư mục hdfs)

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

setp5: - (tạo danh sách có thể thay đổi theo tỷ lệ để lưu tất cả các tên tệp và thêm nó vào danh sách)

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

Bước 6: - (lọc thứ tự tệp _SUCESS từ danh sách tỷ lệ tên tệp)

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

bước 7: - (chuyển đổi danh sách scala thành chuỗi và thêm tên tệp mong muốn vào chuỗi thư mục hdfs và sau đó áp dụng đổi tên)

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

1

Tôi đang sử dụng cái này bằng Python để lấy một tệp:

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

1

Câu trả lời này mở rộng trên câu trả lời được chấp nhận, cung cấp nhiều ngữ cảnh hơn và cung cấp các đoạn mã bạn có thể chạy trong Spark Shell trên máy của mình.

Thêm ngữ cảnh về câu trả lời được chấp nhận

Câu trả lời được chấp nhận có thể cho bạn ấn tượng rằng mã mẫu xuất ra một mydata.csvtệp duy nhất và không phải vậy. Hãy chứng minh:

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

Đây là những gì được xuất ra:

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

NB mydata.csvlà một thư mục trong câu trả lời được chấp nhận - nó không phải là một tệp!

Cách xuất một tệp với một tên cụ thể

Chúng ta có thể sử dụng spark-daria để viết ra một mydata.csvtệp duy nhất .

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

Điều này sẽ xuất ra tệp như sau:

Documents/
  better/
    mydata.csv

Đường dẫn S3

Bạn sẽ cần chuyển các đường dẫn s3a DariaWriters.writeSingleFileđể sử dụng phương thức này trong S3:

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

Xem ở đây để biết thêm thông tin.

Tránh copyMerge

copyMerge đã bị xóa khỏi Hadoop 3. Việc DariaWriters.writeSingleFiletriển khai sử dụng fs.rename, như được mô tả ở đây . Spark 3 vẫn sử dụng Hadoop 2 , vì vậy việc triển khai copyMerge sẽ hoạt động vào năm 2020. Tôi không chắc khi nào Spark sẽ nâng cấp lên Hadoop 3, nhưng tốt hơn hết là bạn nên tránh bất kỳ cách tiếp cận copyMerge nào khiến mã của bạn bị hỏng khi Spark nâng cấp Hadoop.

Mã nguồn

Tìm DariaWritersđối tượng trong mã nguồn spark-daria nếu bạn muốn kiểm tra việc triển khai.

Triển khai PySpark

Việc ghi ra một tệp với PySpark sẽ dễ dàng hơn vì bạn có thể chuyển đổi DataFrame thành một Pandas DataFrame được ghi ra dưới dạng một tệp theo mặc định.

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)

Hạn chế

Cách DariaWriters.writeSingleFiletiếp cận Scala và cách tiếp cận df.toPandas()Python chỉ hoạt động đối với các tập dữ liệu nhỏ. Tập dữ liệu khổng lồ không thể được viết ra dưới dạng các tệp đơn lẻ. Việc ghi dữ liệu dưới dạng một tệp duy nhất không phải là tối ưu từ góc độ hiệu suất vì dữ liệu không thể được ghi song song.


0

bằng cách sử dụng Listbuffer, chúng tôi có thể lưu dữ liệu vào một tệp:

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

Có một cách nữa để sử dụng 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)}

tên 'true' không được xác định
Arron
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.