2019
做。不是。采用。正则表达式!!!(带 3 个感叹号)
来自 cmets,我必须强调 PeteWiFi 的评论:
只是一个友好的警告,如果你尝试和
以这种方式匹配特定的方案和卡片长度。例如,开关
自 2002 年以来不存在,激光于 2014 年被撤回,签证到期
发行 19 位数字卡,万事达卡现在发行 2xxxx
范围,只是为了强调这种方法的几个问题。一种
正则表达式适用于基本的“它看起来像卡号”,但不是
远不止于此。
如果您想使用正则表达式来了解卡品牌用于视觉用途(例如显示 Visa 徽标或标签),那很好。但是如果你的代码逻辑依赖于它,那么就不要使用正则表达式,也不要使用 3rd 方插件/库!
检测卡号的正则表达式既快速又简单(我知道,这太吸引人了!)。但是,从长远来看,您的项目会遇到许多严重且难以解决的错误。发卡机构不断推出新的卡号模式,或退出旧卡号模式,或可能彻底倒闭。谁知道呢。
解决方案
根据一些经常更新的官方页面(例如this page on wikipedia)构建您自己的解决方案(最好是非正则表达式)。
至于“-”、“.”、“空格”等所有杂音,只要去掉所有这些非数字,就可以使用这个(基于这个answer):
$number = preg_replace("/[^0-9]/", "", "4111-1111 1111.1111");
// Output: 4111111111111111
还不相信?
This page goes into deep technical details why regex is hell。 (注意文章使用了“地狱”这个词,因为一旦你进去了就不能出去了)
编辑
这是我开发的一个解决方案(用 PHP):
// Based on https://en.wikipedia.org/wiki/Payment_card_number
// This constant is used in get_card_brand()
// Note: We're not using regex anymore, with this approach way we can easily read/write/change bin series in this array for future changes
// Key (string) brand, keep it unique in the array
// Value (array) for each element in the array:
// Key (string) prefix of card number, minimum 1 digit maximum 6 digits per prefix. You can use "dash" for range. Example: "34" card number starts with 34. Range Example: "34-36" (which means first 6 digits starts with 340000-369999) card number starts with 34, 35 or 36
// Value (array of strings) valid length of card number. You can set multiple ones. You can also use "dash" for range. Example: "16" means length must be 16 digits. Range Example: "15-17" length must be 15, 16 or 17. Multiple values example: ["12", "15-17"] card number can be 12 or 15 or 16 or 17 digits
define('CARD_NUMBERS', [
'american_express' => [
'34' => ['15'],
'37' => ['15'],
],
'diners_club' => [
'36' => ['14-19'],
'300-305' => ['16-19'],
'3095' => ['16-19'],
'38-39' => ['16-19'],
],
'jcb' => [
'3528-3589' => ['16-19'],
],
'discover' => [
'6011' => ['16-19'],
'622126-622925' => ['16-19'],
'624000-626999' => ['16-19'],
'628200-628899' => ['16-19'],
'64' => ['16-19'],
'65' => ['16-19'],
],
'dankort' => [
'5019' => ['16'],
//'4571' => ['16'],// Co-branded with Visa, so it should appear as Visa
],
'maestro' => [
'6759' => ['12-19'],
'676770' => ['12-19'],
'676774' => ['12-19'],
'50' => ['12-19'],
'56-69' => ['12-19'],
],
'mastercard' => [
'2221-2720' => ['16'],
'51-55' => ['16'],
],
'unionpay' => [
'81' => ['16'],// Treated as Discover cards on Discover network
],
'visa' => [
'4' => ['13-19'],// Including related/partner brands: Dankort, Electron, etc. Note: majority of Visa cards are 16 digits, few old Visa cards may have 13 digits, and Visa is introducing 19 digits cards
],
]);
/**
* Pass card number and it will return brand if found
* Examples:
* get_card_brand('4111111111111111'); // Output: "visa"
* get_card_brand('4111.1111 1111-1111'); // Output: "visa" function will remove following noises: dot, space and dash
* get_card_brand('411111######1111'); // Output: "visa" function can handle hashed card numbers
* get_card_brand('41'); // Output: "" because invalid length
* get_card_brand('41', false); // Output: "visa" because we told function to not validate length
* get_card_brand('987', false); // Output: "" no match found
* get_card_brand('4111 1111 1111 1111 1111 1111'); // Output: "" no match found
* get_card_brand('4111 1111 1111 1111 1111 1111', false);// Output: "visa" because we told function to not validate length
* Implementation Note: This function doesn't use regex, instead it compares digit by digit.
* Because we're not using regex in this function, it's easier to add/edit/delete new bin series to global constant CARD_NUMBERS
* Performance Note: This function is extremely fast, less than 0.0001 seconds
* @param String|Int $cardNumber (required) Card number to know its brand. Examples: 4111111111111111 or 4111 1111-1111.1111 or 411111###XXX1111
* @param Boolean $validateLength (optional) If true then will check length of the card which must be correct. If false then will not check length of the card. For example you can pass 41 with $validateLength = false still this function will return "visa" correctly
* @return String returns card brand if valid, otherwise returns empty string
*/
function get_card_brand($cardNumber, $validateLength = true) {
$foundCardBrand = '';
$cardNumber = (string)$cardNumber;
$cardNumber = str_replace(['-', ' ', '.'], '', $cardNumber);// Trim and remove noise
if(in_array(substr($cardNumber, 0, 1), ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'])) {// Try to find card number only if first digit is a number, if not then there is no need to check
$cardNumber = preg_replace('/[^0-9]/', '0', $cardNumber);// Set all non-digits to zero, like "X" and "#" that maybe used to hide some digits
$cardNumber = str_pad($cardNumber, 6, '0', STR_PAD_RIGHT);// If $cardNumber passed is less than 6 digits, will append 0s on right to make it 6
$firstSixDigits = (int)substr($cardNumber, 0, 6);// Get first 6 digits
$cardNumberLength = strlen($cardNumber);// Total digits of the card
foreach(CARD_NUMBERS as $brand => $rows) {
foreach($rows as $prefix => $lengths) {
$prefix = (string)$prefix;
$prefixMin = 0;
$prefixMax = 0;
if(strpos($prefix, '-') !== false) {// If "dash" exist in prefix, then this is a range of prefixes
$prefixArray = explode('-', $prefix);
$prefixMin = (int)str_pad($prefixArray[0], 6, '0', STR_PAD_RIGHT);
$prefixMax = (int)str_pad($prefixArray[1], 6, '9', STR_PAD_RIGHT);
} else {// This is fixed prefix
$prefixMin = (int)str_pad($prefix, 6, '0', STR_PAD_RIGHT);
$prefixMax = (int)str_pad($prefix, 6, '9', STR_PAD_RIGHT);
}
$isValidPrefix = $firstSixDigits >= $prefixMin && $firstSixDigits <= $prefixMax;// Is string starts with the prefix
if($isValidPrefix && !$validateLength) {
$foundCardBrand = $brand;
break 2;// Break from both loops
}
if($isValidPrefix && $validateLength) {
foreach($lengths as $length) {
$isValidLength = false;
if(strpos($length, '-') !== false) {// If "dash" exist in length, then this is a range of lengths
$lengthArray = explode('-', $length);
$minLength = (int)$lengthArray[0];
$maxLength = (int)$lengthArray[1];
$isValidLength = $cardNumberLength >= $minLength && $cardNumberLength <= $maxLength;
} else {// This is fixed length
$isValidLength = $cardNumberLength == (int)$length;
}
if($isValidLength) {
$foundCardBrand = $brand;
break 3;// Break from all 3 loops
}
}
}
}
}
}
return $foundCardBrand;
}