本文最后更新于:星期四, 五月 28日 2020, 1:36 下午

Python 隐式行连接常被用于长行的分割,例如分割超长的字符串或者语句,而这个特性一不小心就会造成诡异的 bug。

两个例子 🌰

出题时间!

data = ['1111111111',
 '2222222222',
 '3333333333',
 '4444444444',
 '5555555555',
 '6666666666',
 '7777777777',
 '8888888888',
 '9999999999'
 '0000000000',
 'aaaaaaaaaa',
 'bbbbbbbbbb',
 'cccccccccc',
 'dddddddddd',
 'eeeeeeeeee',
 'ffffffffff',
 'AAAAAAAAAA',
 'BBBBBBBBBB',
 'CCCCCCCCCC',
 'DDDDDDDDDD',
 'EEEEEEEEEE',
 'FFFFFFFFFF']

print('0000000000' in data)
print('AAAAAAAAAA' in data)
rules = {
    '''
    2019-11-01 09:55:32
    todo: 新增关键字
    '''

    # git 泄露关键字
    '/.git/config': ['[core]', '[remote]', '[branch]', '[repositoryformatversion]'],

    # 其他
    '/config': ['debug']
}

print(rules.get('/.git/config', None))

上面两个例子,看看能不能正确地判断出上面的结果是什么?(答案在文章最后面,可以放心地往下看)

隐式行连接

示例如下:

In [1]: a = '1234567890abcdefABCDEF'

In [2]: a
Out[2]: '1234567890abcdefABCDEF'

In [3]: a = ('1234567890''abcdefABCDEF')

In [4]: a
Out[4]: '1234567890abcdefABCDEF'

In [5]: a = ('1234567890'    'abcdefABCDEF')

In [6]: a
Out[6]: '1234567890abcdefABCDEF'

In [7]: a = ('1234567890'
   ...:     'abcdefABCDEF')  # 分割长行,这样可读性会比较好

In [8]: a
Out[8]: '1234567890abcdefABCDEF'

过长的字符串先分割为多个短字符串,最后再外面套上括号即可。注意分割符只能是或者单/多个空白符(空格或者换行之类的),比如你用逗号分割那就变成元组了对吧?

很简单吧?看起来似乎也没什么问题。。。

坑点解析

Python 的隐式行连接不仅仅是小括号才能使用,中括号和大括号也是可以使用的。但是仅仅是这一句话,你可能会以为这三个相等:

In [25]: a = ('1234567890' 'abcdefABCDEF')
    ...: b = ['1234567890' 'abcdefABCDEF']
    ...: c = {'1234567890' 'abcdefABCDEF'}
    ...:
    ...: a == b == c

但是实际上,b 是个列表,c 是个字典,只有 a 是字符串。原因呢?如果你经常使用元组,你很快就能意识,在创建只有一个元素的元组时,需要在最后加个逗号,否则就变成这个元素本身了:

In [32]: ('1')
Out[32]: '1'

In [33]: ('1',)
Out[33]: ('1',)

道理是一样的。扯远了扯远了,回到这个坑本身来吧。

所以如果你在定义一个字符串列表的时候,忘记在某个元素之后加括号,那么 Python 就会帮你拼接上去:

In [34]: [
    ...: '1',
    ...: '2'
    ...: '3',
    ...: ]
Out[34]: ['1', '23']

你看,在大多数情况下,这个结果不是我们想要的吧?

你可能会想,这有什么呢?我一眼就可以看得出来少了个逗号。但是如果一个字符串列表比较大,元素又比较长,是很难直接发现少了个不起眼的逗号的。

第一个例子,这个例子我是在自己写的 CVE 爬虫里遇到过的。CVE 种类繁多,有些漏洞我不感兴趣,比如 Chrome、Safari 的漏洞,iOS 的漏洞、一些小众 CMS 的漏洞等等,我就写了个 list,只要在这个 list 里的就排除不做推送。当然这个 list 不是一次性就写好了,大概用了有一个月吧,过几天就会补几个关键字,慢慢的就有几百个元素了。然后有一天我发现一直在推送 iOS 的漏洞,找了半天没发现 bug,最后仔仔细细检查了大概 300 行的 list,终于发现问题所在。。。

第二个例子,是一个我负责的项目。简单来说,键 是路径,后面 值 中的列表是 关键字,逻辑是只要在这个路径中找到其中一个关键字,就视为匹配成功。然后我无意中发现一直没法匹配到 /.git/config 这个路径。这个例子如果没有经验的话,其实比第一个例子更难发现哪有问题:Python 把多行注释和 /.git/config 拼在了一起,所以根本没有 /.git/config 这个键。幸亏之前在 CVE 爬虫那踩过坑,定位并解决只花了 1 分钟左右。

答案

False
True
None

一些思考

Python 有很多很好使的特性,但是有一些实在是让人心情复杂。比如多行注释,别人家的语言一般都有特定的多行注释,比如 /**/<!----> 等,而 Python 使用三个连续的单引号 ''' 或者三个连续的双引号 """ 注释多行内容,实际上就是定义了一个字符串,只不过是没用上而已,照这个思路,单行注释还可以这样呢:"这是一个注释"。你说有什么问题吧,似乎一下也说不上来;说没问题吧,就是感觉怪怪的,至少本文提到的问题就是多行注释引起的。

来呀快活呀