当谈到数据丢失时,我们通常是指一些传统的数据丢失方式,其中许多与数据库有关。
TRUNCATE表,它删除了一个表中的所有数据,而你可能只是不小心截断了错误的数据。DELETE没有 子句--你本来只想删除一些行,但最后却删除了所有的行WHERE。- 在生产环境下运行,而不是在开发/暂存环境下运行。你认为你是在你的开发机器上,你把东西丢了又丢,然后意识到你把你的生产数据搞乱了。
- 在没有备份的情况下崩溃 - 这很常见,你遇到了系统故障,但没有备份可以恢复。
这些都是相当常见的,而且很好理解,我并不打算在这里谈论它们。相反,我想谈的是一类数据完整性问题,它比丢掉的表更不明显。我们实际上可以在一开始没有注意到的情况下丢失数据。我们可能仍然拥有它,只是由于我们的数据管道中的一个错误而不正确。
这怎么可能呢。好吧,让我们看看当一个程序遇到一些无效的输入时可以做什么。基本上有三件事它可以做。
- 失败(返回一个错误,抛出一个异常等)。
- 返回一些类似于NULL的值(None、NULL、nil或任何给定系统使用的)。
- 错误地解析它。
我大致按照我所期望的解析器行为的顺序来排列这些行为。我赞同我们应该尽早和经常失败的观点,有些系统不这样做,让我们看看。
第一课:解析所有的格式
当涉及到字符串化的日期时,我有一个最喜欢的格式,那就是ISO 8601,因为它被广泛使用,不容易被误解,而且,作为一个奖励,它是可排序的。
不幸的是,不是所有的输入数据都是这种格式,所以你需要适应这种情况。你通常可以指定你期望的字符串的格式,并将其传递给strptime或其等价物。
如果你的数据是同质的,或者你知道如何构造这样一个日期格式的字符串,这一切都能发挥作用。但是如果这些前提条件中的任何一个失败了,这就不再是小事了。幸运的是,有了dateutil。这个库有很多抽象的功能,可以让你在处理日期和时间时稍微友好一些。我最常用的功能是它的解析器,它的工作方式是这样的
from dateutil import parser
formats = ['2020-02-28', '2020/02/28', '28/2/2020', '2/28/2020', '28/2/20', '2/28/20']
[parser.parse(j).date().isoformat() for j in formats]
['2020-02-28', '2020-02-28', '2020-02-28', '2020-02-28', '2020-02-28', '2020-02-28']
无论你向它扔什么东西,它通常都会把事情弄清楚。
我的用例非常相似。我正在解析来自我们劳工部的几十个数据集,它有各种日期格式。有捷克的23. 4. 2020 ,但也有23. 4. 2020 12:34:33 ,23. 4. 2020 12 和其他一些。
问题是dateutil分析器假定m/d/y为默认值(如果输入不明确的话),所以使用非美国符号的7月4日(日期优先)会被检测为4月7日。
parser.parse('4. 7. 2020').date().isoformat()
'2020-04-07'
幸运的是dateutil有一个设置,你可以提示你的日期倾向于先有天。
parser.parse('4. 7. 2020', dayfirst=True).date().isoformat()
'2020-07-04'
然后它就像预期的那样工作了。
这在一段时间内工作得很好,但后来我注意到我的一些日期是在未来,但它们表示的是过去的事情。我钻研了一下代码,发现了发生的原因。
数据源从非标准的4. 7. 2020 切换到了ISO-8601,所以2020-07-04 很好!但由于dateutil被告知 。但由于dateutil被告知dayfirst ,它误解了一个完全有效的标准日期,只是因为我改变了一些dateutil的默认值。
parser.parse('2020-07-04', dayfirst=True).date().isoformat()
'2020-04-07'
我们最终得到了一个混乱的ISO日期,并得到了两个教训。
- 有时显式比隐式好--如果我们知道可能的源格式,我们可以显式地映射它们的转换,对它们有更多的控制(提示:使用strptime/fromisoformat)。
- 当有一个意外的输入时,你需要犯错,崩溃比试图估计可能的意思要安全得多(你在某些时候会犯错)。
这是一个已知的(而且尚未解决的)问题。
第二课:溢出不仅仅是针对整数的
在计算机科学中,如果你有一个可以容纳-128到127的数值的容器,计算100*2会溢出(环绕),你会得到一个负数。我一直以为这是一个数字问题,但我最近发现它也可以发生在日期和时间上。
有很多情况下你会得到无效的日期,可能是对其格式的混淆(例如,你在ISO-8601中交换了日和月),可能是一个错误(增加一个年份只是增加1,而不是检查该日是否存在,提示:闰年),可能是数据损坏,它可能只是许多事情。
正如我们在第1课中所学到的,解决这个问题的明智方法是不解决它,只是犯错。
在这一课中,我们需要Apache Spark,一个非常流行的具有Python绑定的数据处理引擎。
import os
import pyspark.sql.functions as f
import pyspark.sql.types as t
import pandas
import numpy
os.makedirs('data', exist_ok=True)
让我们写一些不存在的日期。我们将让Spark推断出这些类型,我们不会告诉它。由于这些日期在语法上没有问题,但在现实中不存在,我们可以期待以下三种情况之一。
- 错误
- 用NULL代替日期(可能还有一个警告)
- 日期将被识别为字符串,因为它们不符合日期格式。
with open('data/dates.csv', 'w') as fw:
fw.write('''name,date
john,2019-01-01
jane,2019-02-30
doe,2019-02-29
lily,2019-04-31''')
df = spark.read.option('inferSchema', True).option('header', True).csv('data/dates.csv')
df.printSchema()
df.show()
root
|-- name: string (nullable = true)
|-- date: timestamp (nullable = true)
+----+-------------------+
|name| date|
+----+-------------------+
|john|2019-01-01 00:00:00|
|jane|2019-03-02 00:00:00|
| doe|2019-03-01 00:00:00|
|lily|2019-05-01 00:00:00|
+----+-------------------+
结果是我们没有得到这三种合理的结果,日期溢出到下个月,对我来说,这是很疯狂的。
让我们试着暗示我们的数据中有日期。在这一点上,只有前两个选项适用:错误或空。
schema = t.StructType([
t.StructField('name', t.StringType(), True),
t.StructField('date', t.DateType(), True),
])
df = spark.read.schema(schema).option('header', True).csv('data/dates.csv')
df.printSchema()
df.show()
root
|-- name: string (nullable = true)
|-- date: date (nullable = true)
+----+----------+
|name| date|
+----+----------+
|john|2019-01-01|
|jane|2019-03-02|
| doe|2019-03-01|
|lily|2019-05-01|
+----+----------+
然而什么都没有,还是溢出。
让我们看看pandas是怎么做的(这里没有显示类型推理,但pandas在这种情况下只是假设它们是普通字符串)。它像预期的那样出错了。这样我们就不会有任何损失,而且我们不得不处理这个问题。
df = pandas.read_csv('data/dates.csv')
try:
pandas.to_datetime(df['date'])
except ValueError as e:
print(e) # just for output brevity
day is out of range for month: 2019-02-30
正如我在上面指出的,如果你交换了日和月,你就可以很容易地将年份移到未来。如果你在任何一个领域放置垃圾,你可以移动一个世纪。
with open('data/dates_swap.csv', 'w') as fw:
fw.write('''name,date
john,2019-31-12
jane,2019-999-02
doe,2019-02-22''')
# luckily, inferSchema will work fine (will think it's a string)
schema = t.StructType([t.StructField('name', t.StringType(), True), t.StructField('date', t.DateType(), True)])
df = spark.read.schema(schema).option('header', True).csv('data/dates_swap.csv')
df.printSchema()
df.show()
root
|-- name: string (nullable = true)
|-- date: date (nullable = true)
+----+----------+
|name| date|
+----+----------+
|john|2021-07-12|
|jane|2102-03-02|
| doe|2019-02-22|
+----+----------+
挖掘Spark的源代码,我们可以追溯到Java的标准库。被使用的API一般不被推荐。
scala> import java.sql.Date
import java.sql.Date
scala> Date.valueOf("2019-02-29")
val res0: java.sql.Date = 2019-03-01
这个问题正在Apache Spark内部得到解决,即将推出的主要版本(3)将使用不同的Java API,并将改变上述的行为。当让Spark自动推断类型时,它将不会假定日期,而是选择字符串(没有数据损失)。当提供模式时,它将扔掉所有无效的日期,也就是说,我们在没有任何警告的情况下丢失数据。
回到我们目前的Spark 2.4.5中,我们不仅要面对java.sql.Date ,还要面对其他的实现,因为例如直接使用Spark SQL不会触发这个问题。想来也是。
spark.sql('select timestamp("2019-02-29 12:59:34") as bad_timestamp').show()
+-------------+
|bad_timestamp|
+-------------+
| null|
+-------------+
在研究这些问题的时候,我偶然发现了Michael Armbrust(在Spark社区有巨大影响力的人,也是一位伟大的演讲者)的一句话:
我们对异常的一般政策是,对于与数据有关的问题(例如,不能解析的日期字符串),我们返回null,对于查询的静态问题(例如无效的格式字符串),我们抛出AnalysisException。
这正好总结了前面提出的问题,更常见的是返回NULL而不是处理问题。你自己在大规模运行Spark时就会看到它不会太经常失败,但它反而会丢失你的数据。
这样做的主要问题是,它不仅是错误的,其结果也是一个完全有效的日期,所以通常检测问题的机制:寻找空值、无限值、警告等。这些都不起作用,因为输出看起来很好。
有趣的花絮 Spark也会溢出时间
如果你想知道,你也可以把垃圾放到时间里,它将像日期一样溢出。(就像日期的问题一样,它将在Spark 3.0中得到解决)。
with open('data/times.csv', 'w') as fw:
fw.write('''name,date
john,2019-01-01 12:34:22
jane,2019-02-14 25:03:65
doe,2019-05-30 23:59:00''')
df = spark.read.option('inferSchema', True).option('header', True).csv('data/times.csv')
df.printSchema()
df.show()
root
|-- name: string (nullable = true)
|-- date: timestamp (nullable = true)
+----+-------------------+
|name| date|
+----+-------------------+
|john|2019-01-01 12:34:22|
|jane|2019-02-15 01:04:05|
| doe|2019-05-30 23:59:00|
+----+-------------------+
with open('data/times99.csv', 'w') as fw:
fw.write('''name,date
john,2019-01-01 12:34:22
jane,2019-02-14 13:303:65
doe,2019-05-30 984:76:44''')
df = spark.read.option('inferSchema', True).option('header', True).csv('data/times99.csv')
df.printSchema()
df.show()
root
|-- name: string (nullable = true)
|-- date: timestamp (nullable = true)
+----+-------------------+
|name| date|
+----+-------------------+
|john|2019-01-01 12:34:22|
|jane|2019-02-14 18:04:05|
| doe|2019-07-10 01:16:44|
+----+-------------------+
更有趣的花絮 铸造是不同的
当我们把数据直接读成日期时,确实会出现日期溢出的情况,如果我们读入字符串,然后才决定转换为日期呢?应该做同样的事情,对吗?(我猜你知道我在说什么了)。
fn = 'data/dates.csv'
df = spark.read.option('header', True).csv(fn)
df.show()
+----+----------+
|name| date|
+----+----------+
|john|2019-01-01|
|jane|2019-02-30|
| doe|2019-02-29|
|lily|2019-04-31|
+----+----------+
df.select(f.col('date').cast('date')).show()
+----------+
| date|
+----------+
|2019-01-01|
| null|
| null|
| null|
+----------+
整数溢出
我在前面已经谈到了溢出,让我们来测试一下。假设我们有几个数字,其中一个对于32位的整数来说太大。让我们告诉Spark它是一个整数,看看会发生什么。
fn = 'data/integers.csv'
with open(fn, 'w') as fw:
fw.write('id\n123123\n123123123\n123123123123')
schema = t.StructType([
t.StructField('id', t.IntegerType(), False)
])
spark.read.option('header', True).schema(schema).csv(fn).show()
+---------+
| id|
+---------+
| 123123|
|123123123|
| null|
+---------+
好吧,我们得到一个空的公平。这和上面Armbrust引用的原因一样Spark更喜欢空值而不是异常。(如果我们指定的是长数而不是整数,那么这个数字的加载就会很好。)
让我们来消化一下这意味着什么,如果你有一列整数,比如一个递增的ID,而你的数据源变得如此受欢迎,你不断摄入越来越多的数据,以至于它不再适合int32,Spark将开始抛出数据而不告诉你。你必须确保你有数据质量机制来捕捉这种情况。
请注意,如果你在加载数据时提供了一个模式,pandas也会以同样的方式失败,至少如果你使用古老的numpy类型的产品。
print(pandas.read_csv(fn, dtype={'id': numpy.int32})) # is this better? not really
id
0 123123
1 123123123
2 -1430928461
你必须求助于pandas 1.x的扩展数组类型注释来避免问题。
try:
pandas.read_csv(fn, dtype={'id': 'Int32'})
except Exception as e:
print(e)
cannot safely cast non-equivalent int64 to int32
(类型推理为Spark和pandas都 "解决 "了这个问题)。
第三课:局部问题可以变成全局问题
你可能会想:嘿,一些随机列中的问题与我无关,我只关心amount_paid 和client_id ,这两者都很好。
- 如果你对这些乱七八糟的列进行过滤(例如,"给我所有发票,其中
amount_paid是某某")如果这一列没有被正确解析,你将失去数据。 - 下面你要看的这个疯狂的东西。
让我们有一个由三个整数列组成的数据集。或者至少这些一直都是整数,但不知为何你在里面放了一个浮点数(别忘了这个值和整数是一样的)。我们来说明一下这在现实生活中是如何容易发生的。
schema = t.StructType([
t.StructField('a', t.IntegerType(), True),
t.StructField('b', t.IntegerType(), True),
t.StructField('c', t.IntegerType(), True),
])
with open('data/null_row.csv', 'w') as fw:
fw.write('''a,b,c
10,11,12
13,14,15.0''')
spark.read.schema(schema).option('header', True).csv('data/null_row.csv').show()
+----+----+----+
| a| b| c|
+----+----+----+
| 10| 11| 12|
|null|null|null|
+----+----+----+
刚刚发生的疯狂的事情是,仅仅因为c'的第二个值是一个浮点数而不是一个整数,Spark就决定扔掉整个行所以如果你在其他列上进行过滤/汇总,你的分析现在已经是错误的了(这确实在工作中狠狠地咬了我们一口)。
幸运的是,这个问题在即将到来的Spark的主要版本(3.0,现在找不到票据,但它确实得到了解决)中得到了修复,只有那一个单一的值被NULL。
另外,就像这里描述的一些(不幸的是,不是全部)问题一样,你可以通过降低允许性(在文件I/O的情况下,模式=FAILFAST)来捕捉这些讨厌的错误。
try:
spark.read.schema(schema).option('header', True).option('mode', 'FAILFAST').csv('data/null_row.csv').show()
except Exception as e:
print(str(e)[:500])
An error occurred while calling o114.showString.
: org.apache.spark.SparkException: Job aborted due to stage failure: Task 0 in stage 17.0 failed 1 times, most recent failure: Lost task 0.0 in stage 17.0 (TID 17, localhost, executor driver): org.apache.spark.SparkException: Malformed records are detected in record parsing. Parse Mode: FAILFAST.
at org.apache.spark.sql.execution.datasources.FailureSafeParser.parse(FailureSafeParser.scala:70)
at org.apache.spark.sql.execution.datasources.csv.Uni
第四课:CSV很有趣,直到它们不有趣为止
CSV看起来是一种很简单的格式。一堆由逗号和换行符分隔的数值,似乎并不复杂。但这只是故事的一半,还有两个格式方面没有得到那么多的关注。
- 如果你想在字段中使用引号,需要用另一个引号来转义,而不是像大多数其他地方那样用反斜杠。
- 你可以在字段中使用换行符。事实上,你想要多少就有多少。你只需要先引用你的字段。
本身并没有一个标准,但RFC 4180是我们最接近的。现在让我们利用这个事实,这些规则并不总是被遵守的。
import csv
fn = 'data/quote.csv'
with open(fn, 'w') as fw:
cw = csv.writer(fw)
cw.writerow(['name', 'age'])
cw.writerow(['Joe "analyst, among other things" Sullivan', '56'])
cw.writerow(['Jane Doe', '44'])
df = pandas.read_csv(fn)
print(df)
name age
0 Joe "analyst, among other things" Sullivan 56
1 Jane Doe 44
所以在这里我们有一个CSV,里面有一些人和他们的年龄,让我们问pandas他们的平均年龄是多少。
df.age.mean()
50.0
现在我们来问Spark同样的问题。
df = spark.read.option('header', True).option('inferSchema', True).csv(fn)
df.select(f.mean('age')).show()
+--------+
|avg(age)|
+--------+
| 44.0|
+--------+
原因是Spark在默认情况下没有正确地转义引号,所以它没有把第一行非标题解析为两个字段,而是把名字字段溢出到年龄字段中,从而丢掉了年龄信息。
df.show()
+--------------+--------------------+
| name| age|
+--------------+--------------------+
|"Joe ""analyst| among other thin...|
| Jane Doe| 44|
+--------------+--------------------+
它不仅完全破坏了文件,而且在求和过程中也没有抱怨!
而且,如果我们完成流水线,这种情况还会继续下去。如果我们真的把数据写到某个地方,它就会被进一步破坏。
df.write.mode('overwrite').option('header', True).csv('data/write1')
rfn = [j for j in os.listdir('data/write1') if j.endswith('.csv')][0]
print(pandas.read_csv(os.path.join('data/write1/', rfn)))
name age
0 \Joe \"\"analyst" among other things\\" Sullivan\""
1 Jane Doe 44
在这一点上,我们使用Spark来加载CSV并将其写回,在这个过程中,我们丢失了数据。
FAILFAST来不拯救
我在前面指出,FAILFAST 模式是相当有帮助的,虽然因为它不是默认的,所以并没有像我希望的那样被使用。让我们看看它在这里是否有帮助。
df = spark.read.option('header', True).option('mode', 'FAILFAST').option('inferSchema', True).csv(fn)
df.select(f.mean('age')).show()
+--------+
|avg(age)|
+--------+
| 44.0|
+--------+
你需要在Spark CSV阅读器中调整引号,以获得正确的结果。
df = spark.read.option('header', True).option('escape', '"').option('inferSchema', True).csv(fn)
df.show()
+--------------------+---+
| name|age|
+--------------------+---+
|Joe "analyst, amo...| 56|
| Jane Doe| 44|
+--------------------+---+
第五课:CSV真的不是小事
挑选一种技术很容易,所以让我们看看CSV I/O的另一种实现。Go中的encoding/csv ,那是他们的标准库实现。当你有一列数据的时候,问题就出现了。这种情况经常发生。哦,还有你有缺失的值。让我们来看看这个例子。
john
jane
jack
这是一个有四个值的简单数据集,第三个值是缺失的。如果我们用encoding/csv 写这个数据集,然后再读回来,我们就不会得到同样的东西。这就是所谓的往返测试。下面是我的长篇解释和一个代码片段。
现在失败了,因为encoding/csv 跳过了所有的空行--但在这种情况下,空行是一个数据点。这个实现以一种非常微妙的方式背离了RFC 4180,在我们的例子中,这导致了数据的丢失。
我报告了这个问题,并提交了一份PR,但被告知它可能不会(永远)被修复。
第6课:所有的赌注都没有了
到目前为止,我已经向你展示了我不幸亲身经历的数据丢失的实际案例。现在让我们看一个假设,如果有人知道这个问题并想利用它。利用一个CSV分析器?请告诉我们!
让我们看一下几个比萨饼订单。
df = pandas.read_csv(fn)
df['note'] = df['note'].str[:20]
print(df)
name date pizzas note
0 john 2019-09-30 3 cash payment
1 jane 2019-09-13 5 2nd floor, apt 2C
2 wendy 2019-08-30 1 no olives, moar chee
人们订购了多少披萨?
df.pizzas.sum()
9
正确答案是:这取决于你问谁😈。
sdf = spark.read.option('header', True).option('mode', 'FAILFAST').option('inferSchema', True).csv(fn)
sdf.agg(f.sum('pizzas')).show()
+-----------+
|sum(pizzas)|
+-----------+
| 127453|
+-----------+
所以,现在我们需要完成12.7万份订单,而不是9个披萨,这是一个很高的要求。
不知道发生了什么?当我们看数据集的时候,就不太清楚了。
sdf.show()
+--------+-------------------+------+--------------------+
| name| date|pizzas| note|
+--------+-------------------+------+--------------------+
| john|2019-09-30 00:00:00| 3| cash payment|
| jane|2019-09-13 00:00:00| 5| 2nd floor, apt 2C|
| wendy|2019-08-30 00:00:00| 1|no olives, moar c...|
| rick| null| 123| null|
| suzie| null|112233| null|
| jim| null| 13593| null|
|samantha| null| 29| null|
| james| null| 1000| null|
| roland| null| 135| null|
| ellie| null| 331| "|
+--------+-------------------+------+--------------------+
如果我们看一下文件本身,就比较清楚了。在第三单中,我决定玩玩音符的价值。我决定跨越多行(一个完全有效的CSV值),并利用Spark默认情况下逐行读取CSV的事实,并将把所有后续的行(我们的注释!)解释为新的记录。我可以通过这种方式编造出大量的数据。
with open('data/pizzas.csv') as fr:
print(fr.read())
name,date,pizzas,note
john,2019-09-30,3,cash payment
jane,2019-09-13,5,"2nd floor, apt 2C"
wendy,2019-08-30,1,"no olives, moar cheese
rick,,123,
suzie,,112233,
jim,,13593,
samantha,,29,
james,,1000,
roland,,135,
ellie,,331,"
我们通过利用最常见的数据格式和最常用的数据工程工具之一,成功地将数据悄悄地注入分析中。这是很危险的。
如果我们想在Spark中解决这个问题,我们需要强迫它考虑到多行值。
sdf = spark.read.option('header', True).option('multiLine', True).option('inferSchema', True).csv(fn)
sdf.show()
+-----+-------------------+------+--------------------+
| name| date|pizzas| note|
+-----+-------------------+------+--------------------+
| john|2019-09-30 00:00:00| 3| cash payment|
| jane|2019-09-13 00:00:00| 5| 2nd floor, apt 2C|
|wendy|2019-08-30 00:00:00| 1|no olives, moar c...|
+-----+-------------------+------+--------------------+
(注意,这破坏了像grep或cut这样的Unix工具,因为这些工具并不真正在CSV上操作,它们是读行器。)
奖励:一个魔术
说完了所有的标准例子,现在让我们来试试一个有趣的技巧,这是我在想演示一些完全不同的东西时无意中了解到的。
让我们看看一个小的数据集,它是一个孩子的列表,他们的平均年龄是多少?
fn = 'data/trick.csv'
with open(fn, 'w') as fw:
fw.write('''name,age
john,1
jane,2
joe,
jill,3
jim,4''')
让我们看看,我们猜测是2.5岁,对吗?也许是2,因为有一个缺失值。但是引擎倾向于跳过缺失值进行聚合。
schema = t.StructType([t.StructField('name', t.StringType(), True), t.StructField('age', t.IntegerType(), True)])
df = spark.read.option('header', True).schema(schema).csv(fn)
df.agg(f.mean('age')).show()
+--------+
|avg(age)|
+--------+
| 2.5|
+--------+
现在让我们把数据集缩小到只有1000行,再问一下Spark。为了确定,让我们做两次,同样的事情!我们从Spark那里得到三个不同的答案。
for _ in range(2):
pandas.read_csv('data/trick.csv').head(1000).to_csv('data/trick.csv') # lossless, right?
# this is the exact same code as above
df = spark.read.option('header', True).schema(schema).csv(fn)
df.agg(f.mean('age')).show()
+--------+
|avg(age)|
+--------+
| null|
+--------+
+--------+
|avg(age)|
+--------+
| 2.0|
+--------+
我们设法从一个看似相同的数据集中得到三个不同的答案。怎么做到的?
来自其他系统的花絮
现在我们只涵盖了少数几个工具,还有大量的工具需要剖析。这里只是快速列出了使用其他系统的注意事项。
- 在YAML中保存挪威的缩写时要小心,它可能会被变成
false(更多细节见挪威问题)。 - 一些scikit-learn的默认值受到了质疑我们知道我们的[复杂的软件]使用什么默认值吗?
- 当你采用分布式时,就会出现一大堆一致性问题。如果你喜欢在晚上睡觉,我不推荐Martin Kleppmann的《设计数据密集型应用》或Kyle Kingbury的Jepsen套件。
- 一些对账系统被设计为丢失数据,见《最后的写手赢了》(LWW)。
- 构建分布式数据存储真的很难,一些流行的数据存储在某些情况下会丢失一堆数据。参见Jepsen关于MongoDB或Elasticsearch的报告(并记住这些报告的有效版本)。
- 解析JSON比解析CSV要难得多,几十个主流解析器似乎并不同意对各种输入应该做什么(见解析JSON是一个雷区)。
未来
我很想说所有这些问题都会得到解决,但我并不那么肯定。其中一些技术有机会利用重大的版本升级来引入突破性的改进,但他们选择不这样做。
- pandas 1.0尽管提供了优越的功能,但默认情况下仍有可归零的int列(我们没有明确涉及这个问题,但它是我一个例子中的罪魁祸首)。
- Apache Spark已经改进了它的日期处理,但是CSV的情况仍然很糟糕,这个/我的问题已经存在多年了,v3.0是进行突破性改进的最佳人选,但是没有结果。
- Go的编码/CSV仍然会丢失数据,看来我改善这个问题的战斗已经失败了,所以唯一的机会就是分叉这个包或者使用其他的包。
总结
虽然我描述了许多非常具体的问题,但这篇文章的寓意更多的是关于软件的抽象性。当你做像pandas.read_csv 这样的事情时,你真的知道正在发生什么吗?你了解在Spark查询过程中会执行哪些事情吗?坦率地说,当你使用复杂的软件时,你应该不断问这些问题。
现在有大量的抽象,这也是Python变得如此流行的原因之一,但它也是一把非常方便的枪。这么多的抽象,这么多丢失数据的方法。
虽然我们确实谈到了各种乱七八糟的方法,但我们并没有完全涵盖如何摆脱困境,这也许是另一个话题。但有几个一般的提示可以给你。
- 正确性应该永远胜过性能。确保你只有在确保你的过程正确运行后才会追求性能。
- 明确的往往比隐含的好。听起来像是老生常谈,但我是认真的--抽象会给你带来很多麻烦,直到你达到一个点,即一个巨大的包内的隐含机制实际上对你不利。有时,弄脏你的手,从头开始编码是有意义的。
- 尽早失败,经常失败。不要捕捉所有的异常,不要记录它们而不是处理它们,不要丢弃无效的数据。崩溃、失败、错误,确保错误被暴露出来,以便你能对它们做出反应。
- 要有合理的默认值。你的软件能正确工作还不够,重要的是它从一开始就能正确工作。
- 数据工程师的工作是可靠地移动数据--终端用户并不关心它是Spark还是Cobol,他们希望他们的数字能够一致。在你的优先级列表中,总是把你的数据完整性放在你的技术之上。
- 了解你的工具。确保你至少了解引擎盖下发生的部分事情,这样当它出错时你就能更好地诊断它。
最后但同样重要的是这篇文章并不意味着阻止你使用抽象。请你这样做,但要小心,了解它们,理解它们是如何工作的,并祝你好运!