用于银行交易解析的正则表达式

Regex for bank transaction parsing

如何从以下格式的文本 table 中解析和提取 4 个重要的列?这些是使用 Ruby 的 pdf-reader 包从 PDF 中提取的银行交易行项目 - 如您所见,各列之间的间距非常不规则。

11/4                      Stripe Transfer St-XYZ Agnostic Computers                      582.30
11/4                      Recurring Payment authorized on 11/01 Digitalocean.Com                                           12.00
11/4                      Purchase authorized on 11/01 Google *Gsuite_Get                                                  24.00
11/4                      Purchase authorized on 11/02 Amazon Web Service                                                 460.15
11/4                      Purchase authorized on 11/02 Amazon Web Service                                                   8.07           2,903.09
11/5                      Recurring Payment authorized on 11/03 Atlassian                                                  15.00           2,888.09
11/6                      Recurring Payment authorized on 11/04 Pipedrive Inc NY NY                                        24.00           2,864.09
11/12                     Foobar Retail Dis 211011 ABCDEFGH                            8,031.44
11/12                     Wire Trans Svc Charge - Sequence: 999999999999 Srf#                                              45.00
11/12                     WT 211012-999999 ABCD Bank Limited /Bnf=FOOBARINC                                             5,000.00           5,850.53
11/14                      Purchase authorized on 11/13 Microconf Microconf.Com MN                                            100.00           5,702.53

以上交易是从具有以下视觉布局的银行 PDF 中提取的

需要通过正则表达式解析 粗体 列:

  1. 日期 - dd/mm 格式 - 始终存在
  2. 支票号码 - 始终为空,可能会被忽略(字母数字单字?)
  3. 描述 - 带有日期、数字、特殊字符的文本 - 始终存在
  4. Credits - 货币金额(仅适用于存款)
  5. 借记 - 货币金额(仅用于付款)
  6. 余额 - 货币金额(偶尔出现,不重要)

我只能提取 /^(\d{1,2}\/\d{1,2})\s+/mg 来提取 mm/dd。我应该从右边开始咀嚼数量,但没有明确的分隔符模式!

假设您的目标是 csv/spreadsheet 个条目

最好分阶段完成任务,我的首选目标格式是电子表格的 CSV

TL;DR 查看最后一条评论

11/4                      Stripe Transfer St-XYZ Agnostic Computers                      582.30
11/4                      Recurring Payment authorized on 11/01 Digitalocean.Com                                                 12.00
11/4                      Purchase authorized on 11/01 Google *Gsuite_Get                                                        24.00
11/4                      Purchase authorized on 11/02 Amazon Web Service                                                       460.15
11/4                      Purchase authorized on 11/02 Amazon Web Service                                                         8.07           2,903.09
11/5                      Recurring Payment authorized on 11/03 Atlassian                                                        15.00           2,888.09
11/6                      Recurring Payment authorized on 11/04 Pipedrive Inc NY NY                                              24.00           2,864.09
11/12                     Foobar Retail Dis 211011 ABCDEFGH                            8,031.44
11/12                     Wire Trans Svc Charge - Sequence: 999999999999 Srf#                                                    45.00
11/12                     WT 211012-999999 ABCD Bank Limited /Bnf=FOOBARINC                                                   5,000.00           5,850.53
11/14                      Purchase authorized on 11/13 Microconf Microconf.Com MN                                                  100.00           5,702.53

1st我们可以瞄准更大的间隙,所以选择一个合适的宽度,不用担心以后会解决的交错。 成为 ??.?? 我们要么需要保护现有的逗号,以便用另一个未使用的符号替换它们,比如 ~,要么为了货币最好从数字之间删除它们。

用虚拟扩展名替换所有行尾,如果不是数字,如果列太多也无所谓,所以使用 ??.?? ??.??(是的,在这种情况下我们假设小于 1000,不能使用 、# 或 *)

因此11/4 Stripe Transfer St-XYZ Agnostic Computers 582.30 变成 11/4 Stripe Transfer St-XYZ Agnostic Computers 582.30 ??.?? ??.??

11/4                      Stripe Transfer St-XYZ Agnostic Computers                      582.30    ??.??    ??.??
11/4                      Recurring Payment authorized on 11/01 Digitalocean.Com            ??.??                                12.00    ??.??    ??.??
11/4                      Purchase authorized on 11/01 Google *Gsuite_Get            ??.??                                       24.00    ??.??    ??.??
11/4                      Purchase authorized on 11/02 Amazon Web Service            ??.??                                      460.15    ??.??    ??.??
11/4                      Purchase authorized on 11/02 Amazon Web Service            ??.??                                        8.07           2903.09    ??.??    ??.??
11/5                      Recurring Payment authorized on 11/03 Atlassian            ??.??                                       15.00           2888.09    ??.??    ??.??
11/6                      Recurring Payment authorized on 11/04 Pipedrive Inc NY NY            ??.??                             24.00           2864.09    ??.??    ??.??
11/12                     Foobar Retail Dis 211011 ABCDEFGH                            8031.44    ??.??    ??.??
11/12                     Wire Trans Svc Charge - Sequence: 999999999999 Srf#            ??.??                                   45.00    ??.??    ??.??
11/12                     WT 211012-999999 ABCD Bank Limited /Bnf=FOOBARINC            ??.??                                  5000.00           5850.53    ??.??    ??.??
11/14                      Purchase authorized on 11/13 Microconf Microconf.Com MN            ??.??                           100.00           5702.53    ??.??    ??.??

现在我们可以瞄准剩余的不规则白色 space 所以用 2 或 3 个 spaces 替换所有较大的 spaces 视情况而定(通常 2 个就可以,但要注意任何描述双 spaces.)

11/4   Stripe Transfer St-XYZ Agnostic Computers   582.30   ??.??  ??.??
11/4   Recurring Payment authorized on 11/01 Digitalocean.Com  ??.??   12.00   ??.??  ??.??
11/4   Purchase authorized on 11/01 Google *Gsuite_Get  ??.??   24.00   ??.??  ??.??
11/4   Purchase authorized on 11/02 Amazon Web Service  ??.??   460.15   ??.??  ??.??
11/4   Purchase authorized on 11/02 Amazon Web Service  ??.??   8.07  2903.09   ??.??  ??.??
11/5   Recurring Payment authorized on 11/03 Atlassian  ??.??   15.00  2888.09   ??.??  ??.??
11/6   Recurring Payment authorized on 11/04 Pipedrive Inc NY NY  ??.??   24.00  2864.09   ??.??  ??.??
11/12   Foobar Retail Dis 211011 ABCDEFGH   8031.44   ??.??  ??.??
11/12   Wire Trans Svc Charge - Sequence: 999999999999 Srf#  ??.??   45.00   ??.??  ??.??
11/12   WT 211012-999999 ABCD Bank Limited /Bnf=FOOBARINC  ??.??   5000.00  5850.53   ??.??  ??.??
11/14   Purchase authorized on 11/13 Microconf Microconf.Com MN  ??.??   100.00  5702.53   ??.??  ??.??

最后添加headers,将 替换为逗号分隔符并删除??.??

Date,Description,Credits,Debits,Balance,,,

11/4,Stripe Transfer St-XYZ Agnostic Computers,582.30,,
11/4,Recurring Payment authorized on 11/01 Digitalocean.Com,,12.00,,
11/4,Purchase authorized on 11/01 Google *Gsuite_Get,,24.00,,
11/4,Purchase authorized on 11/02 Amazon Web Service,,460.15,,
11/4,Purchase authorized on 11/02 Amazon Web Service,,8.07,2903.09,,
11/5,Recurring Payment authorized on 11/03 Atlassian,,15.00,2888.09,,
11/6,Recurring Payment authorized on 11/04 Pipedrive Inc NY NY,,24.00,2864.09,,
11/12,Foobar Retail Dis 211011 ABCDEFGH,8031.44,,
11/12,Wire Trans Svc Charge - Sequence: 999999999999 Srf#,,45.00,,
11/12,WT 211012-999999 ABCD Bank Limited /Bnf=FOOBARINC,,5000.00,5850.53,,
11/14,Purchase authorized on 11/13 Microconf Microconf.Com MN,,100.00,5702.53,,

导入电子表格时,headers 和可能的货币需要样式。

事后看来,我意识到你需要做的就是

  1. 删除逗号
  2. 在大白spaces中插入一个虚拟列3(即使是单个~)
  3. 将 spaces 减少到 2x space 然后用逗号
  4. 替换那 2 spaces
  5. 删除虚拟条目,例如~
  6. 添加headerDate,Description,Credits,Debits,Balance

剩下的事情自己解决。

列之间的间距是不规则的,但似乎总是大于 2。在这种情况下,您可以使用 3 个捕获组和一个可选的第 4 部分,还有一个捕获组用于借方部分。

^(\d{1,2}\/\d{1,2})\s{2,}(\S.*?)\s{2,}(\d{1,3}(?:,\d{3})*\.\d{2})(?:\s{2,}(\d{1,3}(?:,\d{3})*\.\d{2}))?

部分模式匹配:

  • ^ 字符串开头
  • (\d{1,2}\/\d{1,2})\s{2,} 捕获 组 1 匹配 1,2 个数字 / 1,2 个数字和 2 个或更多空白字符
  • (\S.*?)\s{2,} 捕获 组 2 匹配至少一个非空白字符和尽可能少的字符,直到下一次出现 2 个或更多空白字符
  • (\d{1,3}(?:,\d{3})*\.\d{2})抓取第3组匹配号码格式
  • (?:非捕获组
    • \s{2,} 匹配 2 个或更多空白字符
    • (\d{1,3}(?:,\d{3})*\.\d{2})抓取第4组,匹配数字格式
  • )?关闭非捕获组并使其成为可选的

看到一个rubular regex demo

TL;DR

你的主要问题是,如果你处理的是字符串数据你已经从 PDF 解析它之后,那么很难确定哪个位置元素对应于哪个字段.你真的应该打开一个关于如何在 PDF 解析时解决这个问题的单独问题,而不是在 PDF 解析阶段之后尝试解析文本。也就是说,下面是一个适用于您提供的有限示例的解决方案,它至少应该让您开始尝试进行字符串解析。

假设和示例

从你的例子来看,你的格式似乎有一些隐含的业务规则:

  1. 某些字段始终存在(例如日期和描述)。
  2. 每行只有一笔借方或一笔贷方。
  3. 每行最多有 4/5 个填充字段。

然而,即使“余额”不重要,如果不参考某些现有余额或 clearly-defined 解析输出中的空格数,您也无法真正判断某项是贷方还是借方,因此您需要修复输入数据或 PDF 解析以确保您 总是 有一个平衡(您可以在 PDF 解析时计算)或确保您知道特定的字段宽度位于 PDF 布局或 PDF 的解析输出中。

虽然您需要针对实际用例更新部分解决方案,但您可以创建结构或其他对象来保存数据,然后根据字段数或字段之间的空格数做出额外的解析决策每笔交易都持有。下面是一个潜在的解决方案。

使用 PDF 解析中的字符串的示例

注意:只要不影响结果以减少 Whosebug 代码块中的 side-scrolling,下面的代码示例已被积极地包装到 60 个字符。随意重排代码以适合您自己的风格选择。

我们首先将您在原始 post 中提供的已解析文本存储在 here-document 中,以练习此代码示例的其余部分。

text_extracted_from_pdf = <<~'EXTRACTED_TEXT'
  11/4                      Stripe Transfer St-XYZ Agnostic Computers                      582.30
  11/4                      Recurring Payment authorized on 11/01 Digitalocean.Com                                           12.00
  11/4                      Purchase authorized on 11/01 Google *Gsuite_Get                                                  24.00
  11/4                      Purchase authorized on 11/02 Amazon Web Service                                                 460.15
  11/4                      Purchase authorized on 11/02 Amazon Web Service                                                   8.07           2,903.09
  11/5                      Recurring Payment authorized on 11/03 Atlassian                                                  15.00           2,888.09
  11/6                      Recurring Payment authorized on 11/04 Pipedrive Inc NY NY                                        24.00           2,864.09
  11/12                     Foobar Retail Dis 211011 ABCDEFGH                            8,031.44
  11/12                     Wire Trans Svc Charge - Sequence: 999999999999 Srf#                                              45.00
  11/12                     WT 211012-999999 ABCD Bank Limited /Bnf=FOOBARINC                                             5,000.00           5,850.53
  11/14                      Purchase authorized on 11/13 Microconf Microconf.Com MN                                            100.00           5,702.53
EXTRACTED_TEXT

我们还将定义一些常量,用于在 PDF 解析后解析您提取的文本,以及一个 Struct class 来保存每行文本的解析结果。您可能需要根据您的真实数据进行调整。

# This describes what a currency item looks like after your
# PDF parse.
MONEY_FMT = /\b[\d,]+\.\d{2}\b/

# Make some assumptions about fixed-width fields. These
# values seem reliable given the sample string data from
# your original post.
LN_START_TO_LAST_CRED_CHR = /^.{92}\.\d{2}$?/
LN_START_TO_END_OF_DEBIT  = /^.{93,}#{MONEY_FMT}$?/

Transaction = Struct.new(:date, :description, :credit,
                         :debit, :balance, keyword_init:
                         true)

现在我们读取 PDF 解析的输出以尝试分析生成的字符串。使用 Ruby 3.1.1,并积极包装代码以最小化 Whosebug 上的 side-scrolling:

transactions = []
text_extracted_from_pdf.each_line do
  fields = _1.split /\s{2,}/

  date, description = fields.shift 2
  balance = fields.pop.chomp if fields.count == 2

  # This violates our rule of 4/5 populated fields.
  raise "too many fields remaining: #{fields.count}" unless
    fields.count == 1

  # Match on characters from start of line to end of credit.
  credit =
    fields.pop.chomp if _1.match? LN_START_TO_LAST_CRED_CHR

  # Match on characters from start of line to end of debit.
  debit =
    fields.pop.chomp if _1.match? LN_START_TO_END_OF_DEBIT

  transactions << Transaction.new({date: date, description:
                                   description, credit:
                                   credit, debit: debit,
                                   balance: balance})
end

预期结果

transactions 数组现在应该包含一个 Transaction 对象的集合,您可以根据需要对其进行迭代。例如,上面的示例代码使用以下结构对象填充 transactions 数组:

transactions
#=> 
[#<struct Transaction date="11/4", description="Stripe Transfer St-XYZ Agnostic Computers", credit="582.30", debit=nil, balance=nil>,
 #<struct Transaction date="11/4", description="Recurring Payment authorized on 11/01 Digitalocean.Com", credit=nil, debit="12.00", balance=nil>,
 #<struct Transaction date="11/4", description="Purchase authorized on 11/01 Google *Gsuite_Get", credit=nil, debit="24.00", balance=nil>,
 #<struct Transaction date="11/4", description="Purchase authorized on 11/02 Amazon Web Service", credit=nil, debit="460.15", balance=nil>,
 #<struct Transaction date="11/4", description="Purchase authorized on 11/02 Amazon Web Service", credit=nil, debit="8.07", balance="2,903.09">,
 #<struct Transaction date="11/5", description="Recurring Payment authorized on 11/03 Atlassian", credit=nil, debit="15.00", balance="2,888.09">,
 #<struct Transaction date="11/6", description="Recurring Payment authorized on 11/04 Pipedrive Inc NY NY", credit=nil, debit="24.00", balance="2,864.09">,
 #<struct Transaction date="11/12", description="Foobar Retail Dis 211011 ABCDEFGH", credit="8,031.44", debit=nil, balance=nil>,
 #<struct Transaction date="11/12", description="Wire Trans Svc Charge - Sequence: 999999999999 Srf#", credit=nil, debit="45.00", balance=nil>,
 #<struct Transaction date="11/12", description="WT 211012-999999 ABCD Bank Limited /Bnf=FOOBARINC", credit=nil, debit="5,000.00", balance="5,850.53">,
 #<struct Transaction date="11/14", description="Purchase authorized on 11/13 Microconf Microconf.Com MN", credit=nil, debit="100.00", balance="5,702.53">]

验证您的字符串解析

当人们对格式或代码逻辑做出假设时,很多事情都会出错。如果您想验证您的 Struct 对象,您可以遍历集合以识别错误的解析,或者您可以选择在上面的解析循环中记录、警告或引发异常。

# If you have parsed both a credit and a debit on the same line,
# something's wrong.
transactions.map do
  warn "bad parse for #{_1}" if _1.credit && _1.debit
end.compact!
#=> []

除了在这里简单地发出警告之外,您还可以使用 Array#reject! 直接从 transactions 中删除未正确解析的项目,假设您没有只需跳过在上面的 #each_line 循环中首先将它们添加到集合中即可。您如何选择识别和处理错误的解析完全取决于您;这只是众多方法中的一种,旨在说明您需要验证代码中每个 PDF 或字符串解析 某处 的结果。