如何使用Spark DataFrames防止两次处理文件

时间:2019-03-21 00:50:54

标签: apache-spark amazon-s3 apache-spark-sql aws-glue

我正在使用AWS Glue将一些S3 TSV转换为S3 Parquet。由于非UTF-8传入文件,我不得不使用DataFrames而不是DynamicFrames来处理我的数据(这是一个已知的问题,没有解决方法,DynamicFrames会因任何非UTF8字符而完全失败)。这似乎也意味着我无法使用Glue中的Job Bookmarks来跟踪我已经处理过的S3 TSV文件。

我的代码如下:

# pylint: skip-file
# flake8: noqa
import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from pyspark.sql.types import *
from awsglue.context import GlueContext
from awsglue.job import Job
from pyspark.sql.functions import split
from awsglue.dynamicframe import DynamicFrame

# @params: [JOB_NAME, s3target]
args = getResolvedOptions(sys.argv, ['JOB_NAME', 's3target', 's3source'])

sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args)

# Define massive list of fields in the schema
fields = [
    StructField("accept_language", StringType(), True),
    StructField("browser", LongType(), True),
    .... huge list ...
    StructField("yearly_visitor", ShortType(), True),
    StructField("zip", StringType(), True)
]

schema = StructType(fields)

# Read in data using Spark DataFrame and not an AWS Glue DynamicFrame to avoid issues with non-UTF8 characters
df0 = spark.read.format("com.databricks.spark.csv").option("quote", "\"").option("delimiter", u'\u0009').option("charset", 'utf-8').schema(schema).load(args['s3source'] + "/*.tsv.gz")

# Remove all rows that are entirely nulls
df1 = df0.dropna(how = 'all')

# Generate a partitioning column
df2 = df1.withColumn('date', df1.date_time.cast('date'))

# Write out in parquet format, partitioned by date, to the S3 location specified in the arguments
ds2 = df2.write.format("parquet").partitionBy("date").mode("append").save(args['s3target'])

job.commit()

我的问题是-每次运行时都没有工作书签,它将一遍又一遍地处理相同的s3文件。如何将源s3存储桶中的已处理文件移动到子文件夹或其他文件夹中,否则应避免对文件进行重复处理?

我不确定这里的窍门是什么,Spark是一个并行系统,甚至都不知道文件是什么。我想我可以使用Python Shell作业类型创建第二个Glue作业,并在此之后立即删除传入的文件,但是即使那样我也不确定要删除哪些文件,等等。

谢谢

克里斯

4 个答案:

答案 0 :(得分:1)

如果您不担心再次处理相同的源文件(相对于时间限制)并且用例中目标中没有重复的数据,则可以考虑在以下情况下将保存模式更新为“覆盖”编写数据框

https://spark.apache.org/docs/2.1.1/api/java/org/apache/spark/sql/DataFrameWriter.html

答案 1 :(得分:1)

要在输入源前缀之外标记已处理的文件,必须使用boto3(或直接awscli)移动或删除文件。

要确定要处理的文件,您可以采用两种不同的方式进行处理:

  • 在使用spark之前,将boto3与args['s3source'] + "/*.tsv.gz"一起使用来解析文件s3client.list_objects()。 您可以提供一组已解析的文件,而不是spark.read.load的全局文件。
import boto3
client = boto3.client('s3')

# get all the available files
# Note: if you expect a lot of files, you need to iterate on the pages of results

response = client.list_objects_v2(Bucket=your_bucket_name,Prefix=your_path_prefix)
files=['s3://'+your_bucket_name+obj['Key'] for obj in response['Contents'] if obj.endswith('tsv.gz')]

 ... initialize your job as before ...

df0 = df0 = spark.read.format("com.databricks.spark.csv").option("quote", "\"").option("delimiter", u'\u0009').option("charset", 'utf-8').schema(schema).load(files)

 ... do your work as before ...
  • 利用spark跟踪其所有输入文件这一事实,在成功保存后对其进行后处理:
 ... process your files with pyspark as before...

# retrieve the tracked files from the initial DataFrame
# you need to access the java RDD instances to get to the partitions information
# The file URIs will be in the following format: u's3://mybucket/mypath/myfile.tsv.gz'

files = [] 
for p in df0.rdd._jrdd.partitions(): 
    files.append([f.filePath() for f in p.files().array()])

有了文件列表后,将其删除,重命名或添加到元数据存储中以在下一个作业中将其过滤掉非常简单。

例如,删除它们:

# initialize a S3 client if not already done
from urlparse import urlparse # python 2
import boto3
client = boto3.client('s3')

# do what you want with the uris, for example delete them
for uri in files:
   parsed = urlparse(uri)
   client.delete_object(Bucket=parsed.netloc, Key=parsed.path)

答案 2 :(得分:0)

我用于通过AWS胶开发的ETL流程之一的解决方案是,首先使用boto3 API列出s3中的文件并将其移动到“ WORK”文件夹。此过程应该不会花费任何时间,因为您仅更改s3对象名称而不是任何物理移动。

完成上述步骤后,您可以将“ WORK”文件夹用作SPARK dataFrame的输入,而新文件可以继续推送到其他s3文件夹中。

我不确定您的用例,但是我们使用当前系统日期时间来创建“ WORK”文件夹,以便在几天后发现加载的过程或数据有问题时,我们可以调查或重新运行任何文件

答案 3 :(得分:0)

最终工作代码:

function onOpen() {//Drop Down Menu You will need to run this from the editor to get it to create a menu.  Or close and then open the spreadsheet.  If you have other menus then make sure you put this in another project.
  SpreadsheetApp.getUi().createMenu('Order Parts')
  .addItem('Show Order Dialog', 'getInventory')
  .addToUi();
}

function getInventory() {//show order dialog for selecting parts
  var ss=SpreadsheetApp.getActive();
  var sh=ss.getSheetByName('Inventory');
  var rg=sh.getRange(2,1,sh.getLastRow()-1,sh.getLastColumn());
  var pA=rg.getValues();
  var html="<style>td,th{border:1px solid black;}</style><table>";
  html+=Utilities.formatString('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td></tr>','Brand','Product Description','Item','Quantity')
  for(var i=0;i<pA.length;i++) {
    html+=Utilities.formatString('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td><input type="button" value="Order" onClick="moveToOrder(%s);" /></td></tr>',pA[i][0],pA[i][2],pA[i][2],pA[i][3],i+2)
  }
  html+='</table><br /><input type="button" value="Close" onClick="google.script.host.close();" />';
  html+='<script>function moveToOrder(row){google.script.run.moveToOrder(row);};console.log("MyCode");</script>';
  var userInterface=HtmlService.createHtmlOutput(html).setWidth(600).setHeight(300).setTitle('Inventory');
  SpreadsheetApp.getUi().showModelessDialog(userInterface,'Inventory');
}

function moveToOrder(row) { //moves selected part to active page so make sure your on the right order form
  var ss=SpreadsheetApp.getActive();
  var sh=ss.getSheetByName('Inventory');
  var rg=sh.getRange(row,1,1,3);
  var pA=rg.getValues();
  var osh=ss.getActiveSheet();
  osh.appendRow(pA[0]);
}