Tôi đang sử dụng https://github.com/databricks/spark-csv , tôi đang cố gắng viết một CSV duy nhất nhưng không được, nó đang tạo một thư mục.
Cần một hàm Scala sẽ nhận tham số như đường dẫn và tên tệp và ghi tệp CSV đó.
Tôi đang sử dụng https://github.com/databricks/spark-csv , tôi đang cố gắng viết một CSV duy nhất nhưng không được, nó đang tạo một thư mục.
Cần một hàm Scala sẽ nhận tham số như đường dẫn và tên tệp và ghi tệp CSV đó.
Câu trả lời:
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ư cat
hoặc HDFSgetmerge
để đơn giản hợp nhất tất cả các phần sau đó.
.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
coalesce(1)
đắt tiền và thường không thực tế.
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 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?
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 .
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 srcPath
dạng csv / txt cuối cùng duy nhất dưới dạng destPath
Chỉ định cũng deleteSource
như 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
)
}
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.csv
sử 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
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
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 đó)
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)
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)
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.csv
tệ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.csv
là 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.csv
tệ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.writeSingleFile
triể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.writeSingleFile
tiế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.
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()
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)}