日期:2014-05-16  浏览次数:20424 次

如何让枯燥的表单数据也可以变得有趣
还有什么能比处理表格中的信用卡数据更枯燥的呢?恩,如果你打算对卡号进行加密就不会那么枯燥了,当然这需要应对一些挑战。然而,它不过只是一个数字文本框,数据会存储在一个数据库里——没什么特别,也用不到什么高深的技术。我有一些待处理数据,需要从中找到 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 = []