spark-csv를 사용하여 단일 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)은 _temporary 디렉토리에 FileNotFoundException을 설정하면 오류가 발생 합니다. 여전히 스파크의 버그입니다. issues.apache.org/jira/browse/SPARK-2984
Harsha

@Harsha 가능성이 없습니다. 오히려 coalesce(1)매우 비싸고 일반적으로 실용적이지 않은 단순한 결과입니다 .
zero323

@ zero323에 동의했지만 하나의 파일로 통합해야하는 특별한 요구 사항이있는 경우 충분한 리소스와 시간이 있다면 가능합니다.
Harsha jul.

2
@Harsha 나는 없다고 말하지 않는다. GC를 올바르게 조정하면 제대로 작동하지만 단순히 시간 낭비이며 전반적인 성능에 영향을 미칠 가능성이 높습니다. 그래서 개인적으로 저는 메모리 사용량에 대해 전혀 걱정하지 않고 Spark 외부의 파일을 병합하는 것이 사소한 일이기 때문에 특별히 신경 쓸 이유가 없습니다.
zero323

36

HDFS로 Spark를 실행하는 경우 정상적으로 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에 작품
요시야 Yoder 보낸

@Minkymorgan 나는 비슷한 문제가 있지만 올바르게 할 수 없습니다 ..이 질문을
봐주세요

4
@SUDARSHAN 위의 내 기능은 압축되지 않은 데이터로 작동합니다. 귀하의 예에서는 파일을 작성할 때 gzip 압축을 사용하고 있다고 생각합니다. gzip 파일을 함께 병합 할 수 없으므로 작동하지 않습니다. Gzip은 분할 가능한 압축 알고리즘이 아니므로 "병합 가능"하지 않습니다. "snappy"또는 "bz2"압축을 테스트 할 수 있습니다.하지만 직감적으로 이것은 병합시에도 실패 할 것입니다. 아마도 가장 좋은 방법은 압축을 제거하고 원시 파일을 병합 한 다음 분할 가능한 코덱을 사용하여 압축하는 것입니다.
Minkymorgan

헤더를 보존하려면 어떻게해야합니까? 각 파일 부분에 대해 복제
Normal

32

여기서 게임에 조금 늦었을 수 있지만 작은 데이터 세트를 사용 coalesce(1)하거나 repartition(1)사용할 수 있지만 큰 데이터 세트는 모두 하나의 노드에서 하나의 파티션에 던져집니다. 이로 인해 OOM 오류가 발생하거나 기껏해야 처리 속도가 느려질 수 있습니다.

FileUtil.copyMerge()Hadoop API 의 기능 을 사용하는 것이 좋습니다 . 그러면 출력이 단일 파일로 병합됩니다.

편집 -이것은 효과적으로 데이터를 실행기 노드가 아닌 드라이버로 가져옵니다. Coalesce()단일 실행기가 드라이버보다 더 많은 RAM을 사용하면 괜찮을 것입니다.

편집 2 : copyMerge()Hadoop 3.0에서 제거됩니다. 최신 버전으로 작업하는 방법에 대한 자세한 내용은 다음 스택 오버플로 문서를 참조하십시오. Hadoop 3.0에서 CopyMerge를 수행하는 방법은 무엇입니까?


이런 식으로 헤더 행이있는 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)

파일이 작업자의 RAM에 맞지 않으면 chaotic3quilibrium의 FileUtils.copyMerge () 사용 제안을 고려할 수 있습니다 . 나는 이것을하지 않았으며, 예를 들어 S3에서 가능한지 아직 알지 못합니다.

이 답변은이 질문에 대한 이전 답변과 제공된 코드 조각에 대한 자체 테스트를 기반으로 작성되었습니다. 나는 원래 그것을 Databricks에 게시했고 여기에 다시 게시하고 있습니다.

내가 찾은 dbfs의 rm의 재귀 옵션에 대한 최고의 문서 는 Databricks 포럼에 있습니다.


3

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

copyMerge 구현은 모든 파일을 나열하고 반복하며 s3에서는 안전하지 않습니다. 파일을 작성한 다음 나열하는 경우 모든 파일이 나열되는 것은 아닙니다. [this | docs.aws.amazon.com/AmazonS3/latest/dev/…
LiranBo

3

불꽃의 df.write()API는 힘 스파크 쓰기에 ... 주어진 경로 내에서 단지 하나의 파트 파일 사용 여러 부품 파일이 생성됩니다 df.coalesce(1).write.csv(...)대신 df.repartition(1).write.csv(...)다시 파티션 넓은 변환 참조 반면 유착이 좁은 변환 같이 ) (재분할 () 유착 대 - 스파크

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

하나의 part-0001-...-c000.csv파일 사용으로 주어진 파일 경로에 폴더를 생성합니다.

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

사용자 친화적 인 파일 이름을 가지려면


또는 데이터 프레임이 너무 크지 않은 경우 (~ GBs 또는 드라이버 메모리에 맞을 수 있음) df.toPandas().to_csv(path)원하는 파일 이름으로 단일 csv를 작성할 수도 있습니다
pprasad009

1
으, 팬더로 변환해야만이 작업을 수행 할 수 있다는 점이 실망 스럽습니다. UUID없이 파일을 작성하는 것이 얼마나 어렵습니까?
ijoseph

2

저장하기 전에 파티션을 1 개로 재분할 / 합체합니다 (폴더는 여전히 있지만 그 안에 하나의 부분 파일이 있음).


2

당신이 사용할 수있는 rdd.coalesce(1, true).saveAsTextFile(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 구성 생성)

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

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

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 경로

DariaWriters.writeSingleFileS3에서이 메서드를 사용하려면 s3a 경로를 전달해야합니다 .

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는 하둡 제으로부터 제거 DariaWriters.writeSingleFile구현 용도 fs.rename, 여기에 설명 된 바와 같이 . Spark 3는 여전히 Hadoop 2를 사용 했기 때문에 copyMerge 구현은 2020 년에 작동 할 것입니다. Spark가 Hadoop 3으로 언제 업그레이드 될지는 확실하지 않지만 Spark가 Hadoop을 업그레이드 할 때 코드가 손상되는 copyMerge 접근 방식을 피하는 것이 좋습니다.

소스 코드

DariaWriters구현을 검사하려면 spark-daria 소스 코드에서 객체를 찾으십시오 .

PySpark 구현

DataFrame을 기본적으로 단일 파일로 기록되는 Pandas DataFrame으로 변환 할 수 있으므로 PySpark로 단일 파일을 작성하는 것이 더 쉽습니다.

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()파이썬은 작은 데이터 세트에 대해서만 작업을 접근. 거대한 데이터 세트는 단일 파일로 쓸 수 없습니다. 데이터를 병렬로 쓸 수 없기 때문에 데이터를 단일 파일로 쓰는 것은 성능 측면에서 최적이 아닙니다.


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

이름을 '사실'은 정의되어 있지 않습니다
애런
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.