如何让枯燥的表单数据也可以变得有趣

还有什么能比处理表格中的信用卡数据更枯燥的呢?恩,如果你打算对卡号进行加密就不会那么枯燥了,当然这需要应对一些挑战。然而,它不过只是一个数字文本框,数据会存储在一个数据库里——没什么特别,也用不到什么高深的技术。我有一些待处理数据,需要从中找到 ABN——一种没有什么意思的数据。澳大利亚人肯定都知道ABN,对其他人而言,它代表的是政府为每个公司分配的11位澳大利亚商业编号(Australian Business Number )。这不是什么秘密(你可以在网上找到),所以你甚至不必为其加密,因为没有人会因此感到兴奋。当然,如果事情仅仅如此,那就不值得写一篇博客了。对了,正如你想象的那样,事情并不像它们看上去那么平淡无奇。
关于信用卡号一些有意思的事情
在 CrowdHired,我们并没有和信用卡打过很多交道,但ABN完全是另外一回事,因为企业客户是我们系统的用户(顺便说一下,正如你猜测的,过去几个月里我为一家创业公司工作。我真的应该谈一谈如何创业,那肯定会是一个有意思的故事)。对于任何数据,你都希望尽可能对用户输入进行验证。当我打算对ABN进行验证时,我发现了一些有意思的特性,信用卡号也具有同样的特性。正如你知道的那样,信用卡和ABN号码都是可自我验证的数据。
时至今日,我已经做web开发很多年了,但对于处理这些数据没有任何经验。所以自然地,作为一名好奇的开发者,我做了一些深入的调查。结果是,这种能够自我验证的数据非常普遍,其他一些广为人知的例子有ISBN、UPC和VIN。其中大多数都是用了基于校验数据位算法的一个变种进行验证和生成。可能这些算法中最有名的就是信用卡采用的Luhn算法。所以我们用信用卡作为示例。

验证和生成信用卡号码(及其他基于校验数据位的数字)
例如我们有下面信用卡号码:
1 4870696871788604
它有16个数字(维萨和万事达卡通常是16位,Amex是15位)。信用卡号可以分成下列部分:
1
2 发行编号|     账号   | 校验数据
487069 | 687178860 | 4
你可以找到很多关于信用卡号的结构分析,但我们想要做的是应用Luhn算法来检验信用卡号是否有效。接下来要这么处理:
1. 从后往前,每隔一个数字对数据加倍

2 4 | 8 | 7 | 0 | 6 | 9 | 6 | 8 | 7 | 1 | 7 | 8 | 8 | 6 | 0 | 4
8 | 8 |14 | 0 |12 | 9 |12 | 8 |14 | 1 |14 | 8 |16 | 6 |00 | 4
2.如果需要加倍的数字有两位,将这两个数字相加

3 4 | 8 | 7 | 0 | 6 | 9 | 6 | 8 | 7 | 1 | 7 | 8 | 8 | 6 | 0 | 4
8 | 8 |14 | 0 |12 | 9 |12 | 8 |14 | 1 |14 | 8 |16 | 6 |00 | 4
8 | 8 | 5 | 0 | 3 | 9 | 3 | 8 | 5 | 1 | 5 | 8 | 7 | 6 | 0 | 4
3.将所有的数字相加得到结果
1 8+8+5+0+3+9+3+8+5+1+5+8+7+6+0+4 = 80
4.如果相加之和和可以被10整除,那么就是一个有效的信用卡号码。举例的信用卡号就是有效的。
下面你可以看到我们是如何使用同样的算法来生成一个有效的信用卡号。我们所要做的就是把校验位值设置成X并且执行所有相似的步骤。在最后一步,我们只要将我们的校验位置为可以将所有数字之和可以被10整除。让我们在之前的信用卡号上稍微做一点修改(我们只要将校验位置为1,这样得到的就是一个无效的信用卡号)。
4 | 8 | 7 | 0 | 6 | 9 | 6 | 8 | 7 | 1 | 7 | 8 | 8 | 6 | 1 | X
8 | 8 |14 | 0 |12 | 9 |12 | 8 |14 | 1 |14 | 8 |16 | 6 | 2 | X
8 | 8 | 5 | 0 | 3 | 9 | 3 | 8 | 5 | 1 | 5 | 8 | 7 | 6 | 2 | X
8+8+5+0+3+9+3+8+5+1+5+8+7+6+2+X = 78+X
X = (78%10 == 0) ? 0 : 10 - 78%10
X=2
正如你看到的,无论其他15个数字是什么,我们总能够在0到9之前找到生成有效信用卡号码的校验数字。
当然,并不是每个子验证数字都是采用 Luhn算法。大多数不采用对10取余数生成校验位,像 IBAN一类的数据,校验位实际上由两个数字组成。并且,大多数奇怪的自我验证数据都和我第一次知道的ABN一样。因为,以我的经历而言,我不能指出ABN的校验位应该是什么。
ABN的奇怪之处
澳大利亚肯定不愿意使用基于校验位的算法。澳大利亚税收文件数据TFN(Tax File Number)和澳大利亚公司数据ACN(Australian Company Number)就是两个例子,但是ABN似乎与之不同。乍看上去,ABN验证算法似乎与之类似,只是在最后使用了一个更大的数字进行取模操作(对89取余数,mod(89))
• 从(左边)第一个数字开始逐个减一,得到一个新的11位数
• 对生成的新数据的每个数字乘以它的权重因子
• 将11个乘积加在一起
• 对乘积综合除以89,取余数
• 如果余数为0,那么该ABN有效
事实上,这里有一些用来验证ABN的ruby代码,这是我从 Ruby ABN gem中抽取出来的(并且很好地结合进了Rails3 ActiveRecord验证子,这样我们可以任意地调用 validates_abn_format_of )
def is_integer?(number)
Integer(number)
  true
rescue
  false
end

def abn_valid?(number)
  raw_number = number
  number = number.to_s.tr ' ',''
  return false unless is_integer?(number) && number.length == 11

  weights = [10, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
  sum = 0
  (0..10).each do |i|
    c = number[i,1]
    digit = c.to_i - (i.zero? ? 1 : 0)
    sum += weights[i] * digit
  end

  sum % 89 == 0 ? true : false
end
但是,尽管验证ABN数据很容易,但是生成却又是另外一回事了。正如我们看到的,基于校验位的算法,生成和验证数据的过程是一样的,只有在取模步骤中我们需要选择不同的数字来验证余数是否为0。但是,像ABN这样的数据,没有明显的校验位(也许我可能比较愚笨,所以如果你发现有明显的ABN校验位请不吝赐教),如果有效地生成一个有效的数据呢?事实上,为什么想要生成这些数据呢,仅仅验证数据的有效性还不够用吗?
恩,以 CrowdHired为例,我们试图生成一个很深的对象树,所以我们构建了一段维护基础架构的代码来允许我们创建伪数据来供开发使用(我们会在晚些时候讨论另一个有意思的事情)。在我们开始利用ABN数据的自我验证特性之前我们仅仅生成了任意的11为数字组成的数值作为伪ABN数据,但是一旦验证开始,我们就发现不能再这么干了。作为高效的开发者,我们(尽管我们这么称呼自己)使用一些真正的ABN(用我们掌握的那些),将他们放到一列数组中,然后随机地从中选取。但这种方式冒犯了开发者心中的上帝(换句话说触犯了我们的自尊——所以无论如何,我决定在周六花上几个小时来编写程序生成一些真正随机且有效的ABN数据)。下面是我的代码(现在这段代码成为伪数据生成脚本的核心部分):
def random_abn
  weights = [10,1,3,5,7,9,11,13,15,17,19]
  reversed_weights = weights.reverse
  initial_numbers = []
  final_numbers = []
  9.times {initial_numbers << rand(9)+1}
  initial_numbers = [rand(8)+1, rand(7)+2] + initial_numbers
  products = []
  weights.each_with_index do |weight, index|
    products << weight * initial_numbers[index]
  end
  product_sum = products.inject(0){|sum, value| sum + value}
  remainder = product_sum % 89
  if remainder == 0
    final_numbers = initial_numbers
  else
    current_remainder = remainder
    reversed_numbers = initial_numbers.reverse
    reversed_weights.each_with_index do |weight, index|
      next if weight > current_remainder
      if reversed_numbers[index] > 0
        reversed_numbers[index] -= 1
        current_remainder -= weight
        if current_remainder < reversed_weights[index+1]
          redo
        end
      end
    end
    final_numbers = reversed_numbers.reverse
  end
  final_numbers[0] += 1
  final_numbers.join
end
这个想法非常简单。让我们通过一个例子来讲解一下:
1.首先,我们随机成成11个0到9之间的数据,来组成我们未来的ABN(他们实际上并非都在0到9之间,后面很快会对此进行说明)
1 7 5 8 9 8 7 3 4 1 5 3
2.然后,我们对该数据执行验证步骤
• 对这些数据乘以他们的权重得到带权重的乘积
1 7x10=70 5x1=5 8x3=24 9x5=45 8x7=56 7x9=63 3x11=33 4x13=52 1x15=15 5x17=85 3x19=57
• 将所有的乘积相加
1 70+5+24+45+56+63+33+52+15+85+57 = 505
•  对89取模得到余数
1 505 mod 89 = 60
3.因为我们对89取模,所以最坏的情况得到的余数是88(尽管加入我们幸运地得到余数是0也就是直接得到了一个有效的ABN),我们现在可以使用带有权重的数字乘积来“进行变换”,与余数相减直到我们得到的结果是0。
我们从最后一位数字开始(权重是19),我们对这个数字减1,这就意味着我们从余数中减去了19。依次对下一个数字进行同样的操作,知道余数的变成0。
初始值 | 改变后的数值 | 余数
-------------------------------
  7x10=70 | 7x10=70 | 0
  5x1=5   | 5x1=5   | 0
  8x3=24  | 8x3=24  | 0
  9x5=45  | 9x5=45  | 0
  8x7=56  | 8x7=56  | 0
  7x9=63  | 6x9=63  | 0
  3x11=33 | 3x11=33 | 9
  4x13=52 | 4x13=52 | 9
  1x15=15 | 0x15=0  | 9
  5x17=85 | 4x17=68 | 24
  3x19=57 | 2x19=38 | 41
4.结果产生了我们的新数值
1 7 5 8 9 8 6 3 4 0 4 2
5.现在我们只要对每个数据的第一个数字加1(根据ABN的验证步骤)这样我们就得到了有效的ABN数据
1 85898634042
这些步骤之间有一些细微的差别。
• 我们生成的初始数据里没有0。因为我们“进行变化”的时候是通过对每个数字减一,那么就必须确保我们能够对他们减1(否则事情会变得更加复杂)。所以我们确保数据可以随机的在1和9之间选择,而不是0到9。
• 即使我们所有的初始数据都保证至少为1,我们任然可能会对一些余数“进行变化”时失败,最简单的例子就是当我们的余数是2时。唯一可以使用进行变化的数字权重为1(例如,ABN的第二个数字)。如果这个数字初始生成为1,我们只能进行一次变换并且得到的余数为1,这样我们就不能再进行任何的处理了。事实上,确切的场景还有余数为86, 77, 66, 53, 38, 21。客服这个问题最简单的办法就是保证生成的数值至少为2.这样我们至少可以进行两次变化,这样我们的问题余数都会被覆盖到。
• 最后,尽管我们在最后一步对每个数据的第一位加1,我们需要确保这个数据不能为9,所以我们需要确保生成的数字在1到8之间。
即使注意到了所有这些区别,这个算法还是不能够生成所有可能的ABN,但是对应我们的需求已经可以提供尽可能多的有效ABN。这个算法花费了我们1个小时(我们没有提到我忘记于是不能为0这个小bug,这个问题给我们的随机数据生成器带来了很大的悲剧:))但是这确实是一个有意思的小练习——就我而言在这个上面花费的时间为有所值。要知道,所有的这些关于子验证数据的学习以及算法编程的乐趣都是由表格里一段最常见的数据引起的。这也就说明了,无论你在哪里在做什么都可以学习和成长,你需要做的只是能够发现这些机会而已。

猜你喜欢

转载自peng631.iteye.com/blog/1405479