二维码分为很多种,其中最常用的是QR Code
二维码。
QR码的纠错容量有四个级别,分别是:
- ERROR_CORRECT_L ,L级别,7%或更少的错误能修正;
- ERROR_CORRECT_M ,M级别,15%或更少的错误能修正,是qrcode的默认级别;
- ERROR_CORRECT_Q, Q级别,25%或更少的错误能修正;
- ERROR_CORRECT_H ,H级别,30%或更少的错误能修正。
那么把讨论范围限定在QR码,在4种容错等级下,最多能够包含多少字符呢?
这个问题非常简单,测试一下就知道了:
import qrcode
for level in 'LMQH':
for i in range(200, 1000):
try:
qrcode.make('he11o' * i, error_correction=getattr(qrcode, 'ERROR_CORRECT_' + level))
except Exception:
print('%s: %d, ' % (level, 5 * (i - 1)), end='')
break
输出结果:
L: 2950, M: 2330, Q: 1660, H: 1270
根据上面的测试结果,当容错等级为M
,可以生成长度不超过2330的字符串二维码。
我们可以手动测试一下超过这个长度的情况:
>>> qrcode.make('x' * 2350)
Traceback (most recent call last):
...
File "D:\Python36\lib\site-packages\qrcode\main.py", line 167, in best_fit
raise exceptions.DataOverflowError()
qrcode.exceptions.DataOverflowError
根据报错提示,发生在qrcode.main
库的line 167
,报出了exceptions.DataOverflowError
类型的错误。
追查这段代码的位置:
self.version = bisect_left(util.BIT_LIMIT_TABLE[self.error_correction], needed_bits, start)
if self.version == 41:
raise exceptions.DataOverflowError()
其中self.version
是二维码的等级大小,合法值应在1-40
的范围内。
很显然,当version
等于41的时候就非法了,二维码太大了,发生报错。
进入qrcode.util
库里面查看BIT_LIMIT_TABLE
的定义,是一个比较复杂的列表推导公式。
但是这个数据不依赖于输入,我们可以直接打印出来看看,是一个二维列表,里面总共有4个子列,每个子列里有41个元素。
那就很显然了,4个子表对应二维码的4种纠错等级,41个元素对应二维码的41种尺寸等级。
把这4个子列的最大值打印出来、并且按从大到小的顺序排序:
>> sorted([row[-1] for row in qrcode.util.BIT_LIMIT_TABLE], reverse=True)
[23648, 18672, 13328, 10208]
好像和之前测试出来的四个数字L: 2953, M: 2331, Q: 1663, H: 1273
没有什么关系?
不,等等,这不是差了8倍多一点的关系吗?
咱再看看BIT_LIMIT_TABLE
这个参数命名,BIT
…1个byte
等于8个bit
。这个语法命名还是非常清晰易懂的嘛。
那么我们把BIT_LIMIT_TABLE
里的数值除以8以后,再次测试:
limit_table = sorted([row[-1] >> 3 for row in qrcode.util.BIT_LIMIT_TABLE], reverse=True)
print('LIMIT_TABLE:', limit_table)
string = 'he11o' * 600
for st, lv in zip(limit_table, 'LMQH'):
print('%s: ' % lv, end='')
for i in range(st - 10, st + 10):
try:
qrcode.make(string[:i], error_correction=getattr(qrcode, 'ERROR_CORRECT_' + lv))
except Exception:
print('%d, ' % (i - 1), end='')
break
输出结果:
LIMIT_TABLE: [2956, 2334, 1666, 1276]
L: 2953, M: 2331, Q: 1663, H: 1273
发现了很显然的对应关系,和LIMIT_TABLE
的每一个级别都相差了3个byte
。
当我把前面测试文本中的he11o
换成12345
,同样长度的文本却没有发生报错。
推测当二维码中只有数字时,可能会进行某种压缩,以提高信息的传递量。
但是同样的代码测试速度太慢了,改成了二分法测试加速运算:
string = '12345' * 2000
for level in 'LMQH':
a = 0
for i in range(16, -1, -1):
b = 1 << i
try:
qrcode.make(string[:a+b], error_correction=getattr(qrcode, 'ERROR_CORRECT_' + level))
a += b
except Exception:
pass
print('%s: %d, ' % (level, a), end='')
输出结果:
L: 7089, M: 5596, Q: 3993, H: 3057
可以发现规律,这组数字等于LIMIT_TABLE
对应数字的2.4倍减5取整。
2.4倍是从哪里出现的?我在Python库的源代码中未能追查到线索。
但是我在另一个开源的QR-Code-generator项目的qrcodegen.c文件中,看到这样一段代码:
其中主要调用的函数qrcodegen_encodeText
:
bool qrcodegen_encodeText(const char *text, uint8_t tempBuffer[], uint8_t qrcode[],
...
size_t textLen = strlen(text);
...
size_t bufLen = (size_t)qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion);
struct qrcodegen_Segment seg;
if (qrcodegen_isNumeric(text)) {
if (qrcodegen_calcSegmentBufferSize(qrcodegen_Mode_NUMERIC, textLen) > bufLen)
goto fail;
...
fail:
...
return false;
}
当调用函数qrcodegen_calcSegmentBufferSize
的返回值大于bufLen
时,生成二维码失败。
而在函数qrcodegen_calcSegmentBufferSize
中,简单调用了函数calcSegmentBitLength
:
testable int calcSegmentBitLength(enum qrcodegen_Mode mode, size_t numChars) {
// All calculations are designed to avoid overflow on all platforms
if (numChars > (unsigned int)INT16_MAX)
return -1;
long result = (long)numChars;
if (mode == qrcodegen_Mode_NUMERIC)
result = (result * 10 + 2) / 3; // ceil(10/3 * n)
else if (mode == qrcodegen_Mode_ALPHANUMERIC)
result = (result * 11 + 1) / 2; // ceil(11/2 * n)
else if (mode == qrcodegen_Mode_BYTE)
result *= 8;
else if (mode == qrcodegen_Mode_KANJI)
result *= 13;
else if (mode == qrcodegen_Mode_ECI && numChars == 0)
result = 3 * 8;
else { // Invalid argument
assert(false);
return -1;
}
assert(result >= 0);
if (result > INT16_MAX)
return -1;
return (int)result;
}
result
来自于textLen
,是字符串的字节长度。
很显然,当二维码的模式属于NUMERIC
时,对字符串长度进行了10/3
倍的向上取整转换。表达的含义是一个NUMERIC
类型的字符等于10/3
个bit
。
而bit
数目本身是byte
的8倍,所以8/(10/3)=2.4,就是这么来的。
那么以此类推,当模式属于ALPHANUMERIC
时,倍数等于16/11
。
模式属于BYTE
时,倍数等于1
。
for j in range(256):
c = bytes([j])
print()
print(j, c, end=': ')
for level in 'LMQH':
a = 0
for i in range(16, -1, -1):
b = 1 << i
try:
qrcode.make(c * (a + b), error_correction=getattr(qrcode, 'ERROR_CORRECT_' + level))
a += b
except Exception:
pass
print('%s: %d, ' % (level, a), end='')
在qrcode.util
库中,可以看到参数ALPHA_NUM
的定义:
ALPHA_NUM = b’0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:’
显而易见,当字符集属于这个范围时,模式将被判定为前面提到的NUMERIC
。
而当字符属于0-9
的时候,存在额外的规则可将模式判定为MODE_ALPHA_NUM
。
如果用一个公式来表示,其中limit
表示4种容错等级在LIMIT_TABLE
中的对应值,k
表示不同字符模式的压缩系数,在3种模式下分别为2.4
和16/11
和1
。公式可以表示为:
int(((limit - 2) - 1 / 8) * k)
生成QR-Code二维码,LMQH四个容错等级的二维码最大字符容量是:
字符类别 | L | M | Q | H |
---|---|---|---|---|
0-9 | 7089 | 5596 | 3993 | 3057 |
ALPHA_NUM | 4296 | 3391 | 2420 | 1852 |
其他 | 2953 | 2331 | 1663 | 1273 |
LIMIT_TABLE | 2956 | 2334 | 1666 | 1276 |
在Python中运行的测试程序符合理论计算结果。