利用 Power Query VBA 实现 CSV 数据清洗

相关背景

之前接到一个任务,要将旧系统中的业务数据导入到新的生产库(Oracle 11g)。由于大部分数据是按年份收集的CSV 文件,于是想到 PL/SQL Developer 中的文本导入器(Text Importer)。可惜历史数据质量很差,且年代越久远,数据越不规范,根本无法直接导入目标库。因此必须对原数据进行数据清洗。

这些数据质量问题主要包括:

  • 新业务字段的缺失;
  • 编码的大小写不统一;
  • 字段仅提供中文,缺乏入库必需的编码;
  • 长文本存在特殊字符(回车换行等);

由于每个 CSV 的数据量都在两万行以内,所以决定使用 Excel 中的 Power Query 完成数据清洗工作,中文转编码的工作由 VBA 完成,原因后面会说明。值得一提的是,若数据源是规范、标准的 CSV 导出文件,且数据量大,建议还是用传统方法,比如 Java。本文仅对 Power Query 的实践及遇到的问题作一个小结,以便后期查阅。

准备工作

  1. CSV 格式数据文件;
  2. ExcelOffice 365);

大致处理思路

1. 数据导入

注意:这里务必使用 Power Query 打开,而不是直接用 Excel 打开。因为后者会自动进行一些无用的格式转换,比如将长文本编号识别为数字(第一列),从而丢失源数据的精度。

2. 主要的数据转换

2.1. 转为【文本】型:

图中圈中部分可以单击,以快速转为某种格式。虽然方便,但当要处理的字段列较多时,频繁单击鼠标也很低效。这里推荐使用【快速访问工具栏】:将转为“文本”型的操作直接添加到该工具栏内,再批量选中要转换的字段列,按组合键【Alt 2】实现一键转换——

注意:这里可能是【Alt 1】,也可能【Alt 3】,具体加几,取决于命令添加后的相对位置(顺序为从左往右)。

2.2. 字段值的批量提取

对于目标数据前后存在冗余符号的情况(如下图所示):

Power Query 的【转换】选项卡提供了多种处理途径:既可以使用【替换值】的方式,也可以通过提取【分隔符之间的文本】。不过后者更适合数据较规范的场合,否则对于匹配失败的数据行,该列的提取内容 将变为空文本

2.3. 批量删除与撤销

选中若干列后,直接【Delete】删除选中列;任何情况下,想要撤回上一步操作,都可以通过【查询设置】工具栏内、【应用的步骤】列出的操作列表进行撤回,非常方便。

2.4. 其他转换问题

除了上述三种常规操作,实操过程中还遇到了新的转换问题,如字典值到字典码的转换(户籍类型的文字描述转为数据库中的类型码)、文本的拆分与转换(把形如 20岁 的数据拆成 20 和 岁 ,同时把单位部分转换为字典编码,比如 001)。

虽然 Power Query 也提供了添加条件列(【添加列–常规–条件列】),但设置起来相对费劲,遇到多个类似的列也很难快速套用前面的设置,无法批量操作。

起初的处理方案是使用 Excel 函数公式(如 =LEFT([@年龄], LEN([@年龄]) - 1)),但弊端也很明显:每一列都要批量复制公式,遇到字典编码转换的问题,手写公式本就很长,效率低下且容易出错。

于是想到 VBA 自定义函数,参数只要一个单元格的引用,函数名也可以任意指定。例如户籍地址类型的转换,刚开始效果就不错:

'Convert address type text into dictionary code Public Function cvtType(ByVal rng As Range) As String  Dim txt$, result$    txt = Trim(rng.Text)  Select Case txt    Case "本县区"      result = "01"    Case "本市其它县区", "本市其他县区"      result = "02"    Case "本省其它地市", "本省其他地市"      result = "03"    Case "其它省", "其他省"      result = "04"    Case "港澳台"      result = "05"    Case "外籍"      result = "06"    Case Else      result = ""  End Select    cvtType = resultEnd Function

这样,只需要使用公式 =cvtType([@地址类型]) 就能得出结果。

但使用公式会出现另一个性能问题:若遇到稍大一点的数据文件,公式批量复制、批量计算、后期批量选择性粘贴的执行速度将显著下降,严重时甚至卡死 Excel。这是频繁引用单元格区域造成的。

要想彻底解决这个性能问题,通常的做法是借助 VBA 中的数组,通过将参数批量放入 VBA 数组,实现一次读取、内存处理、一次写回,尽可能减少单元格的反复引用、计算结果的反复写入。最后,将处理逻辑写入宏过程,就能实现转换结果“一步到位”:

'Convert address type text into dictionary code:Sub cvtAddrType()    Dim rng As Range, col&, arr As Variant, i&, rowMax&, arr1 As Variant, str$        '1. Retrieve data from Range cells    Set rng = Range(ActiveCell.Address(0, 0)).Offset(0, -1)    col = rng.Column    rowMax = Cells(Rows.Count, col).End(xlUp).Row    arr = Range(rng, Cells(rowMax, col))    ReDim arr1(1 To rowMax - 1) As String    '2. Core conversion process    For i = 2 To rowMax        Select Case Trim(arr(i - 1, 1))            Case "本县区"                str = "01"            Case "本市其它县区", "本市其他县区", "本市其他区"                str = "02"            Case "本省其它地市", "本省其他地市", "本省其他市"                str = "03"            Case "其它省", "其他省"                str = "04"            Case "港澳台"                str = "05"            Case "外籍"                str = "06"            Case Else                str = ""        End Select        arr1(i - 1) = str    Next i    '3. Write back results    Set rng = Range(rng, Cells(rowMax, col))    rng.NumberFormatLocal = "@"    rng = Application.Transpose(arr1)    Set rng = NothingEnd Sub

3. 收尾工作

  1. 数据清洗结束后,应另存为新的 CSV 文件,作为文本导入工具的新数据源;
  2. 为确保导入发生未知异常时,数据库能迅速快速标识已导入的数据,还可以在主键上添加统一规则的前缀,如 jan2020_uuid 表示 2020 年 1 月的历史数据,all2020_uuid 表示 2020 年全年数据;
  3. 分批次生成导入数据的 SQL 文件,方便后期查阅,并设置统一规范;
  4. 编写数据导入说明文档,提示关键操作,避免重复采坑。

4. 小结

  1. 对于结构相同的 CSV 文件,还可以利用内置的 M 公式实现数据清洗的【格式刷】式操作;
  2. Power Query 虽然强大,但也不是万能。需要从实际出发,选择最合适的解决方案;
  3. 实际操作过程中,面临新的问题要多问几个为什么,抓住问题的本质(如公式对性能影响的根本原因等),这样才能在实战中逐步提高;
  4. 良好的习惯需从平时培养,如英文注释、代码缩进、多留文档等。

来源:https://www.icode9.com/content-4-798851.html

(0)

相关推荐