Python 3 如何使用f字符串将整数格式化为IPv6地址?

Python 3 how to format integer into IPv6 address using f string?

我正在尝试将整数格式化为 IPv6 地址。

这里的IPv6地址是指表示0到2^128-1之间整数的字符串(340282366920938463463374607431768211455),格式为32位十六进制数字,用冒号(':')分隔成8个字段,每个字段4位.

现在我知道了str(ipaddress.IPv6Address(n))int(ipaddress.IPv6Address(s)),但是我想以学习的名义写自己的功能,我已经写好了,正在努力完善。

我正在寻找一种使用 f 字符串或 str.format 将整数格式化为 IPv6 格式的方法,我目前使用这个单行代码:

ipv6 = ':'.join(hex(n).removeprefix('0x').zfill(32)[i:i+4] for i in range(0, 32, 4))

而且速度很慢,因为它使用了字符串切片。

我已经编写了使用正则表达式缩短 IPv6 地址的代码,我正在寻找使用字符串格式单行替换上述单行。

我已经实现了第一部分(将整数格式化为带前导零且不带 '0x' 前缀的 32 位十六进制数):

"{0:0>32x}".format(n)

但我无法实现第二部分,我已经 Google 搜索了一种使用 Python 将每 N 个字符插入字符串中的分隔符的方法,但无论我使用什么关键字,大多数都是无关紧要的,而且我只看到了两种相关的方法,一种是我自己想出的方法,另一种是这样的:

re.sub('([\da-fA-F]{4})', r':', s, 7)

但是正则表达式很慢:

In [221]: re.sub('([\da-fA-F]{4})', r':', 'c42d7a7d155b93f7658c20c9fea598ff', 7)
Out[221]: 'c42d:7a7d:155b:93f7:658c:20c9:fea5:98ff'

In [222]: s = 'c42d7a7d155b93f7658c20c9fea598ff'

In [223]: %timeit re.sub('([\da-fA-F]{4})', r':', s, 7)
11.6 µs ± 993 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [224]: %timeit ':'.join(s[i:i+4] for i in range(0, 32, 4))
2.11 µs ± 23.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

但我在 Google 搜索建议中找到了这个关键字:Python f 字符串千位分隔符,并找到了这个语法:"{:,d}"

In [226]: "{:,d}".format(1234567890)
Out[226]: '1,234,567,890'

它非常接近我所寻求的,但不幸的是它不是方法,首先它处理整数(d),其次它每 3 个字符而不是 4 个字符插入一个分隔符,最后它使用逗号而不是冒号,我无法在不使语法无效的情况下更改它...

那么在将整数格式化为 32 位十六进制字符串时,每 4 位插入一个冒号的正确 f 字符串语法是什么?最好没有 f 字符串嵌套。我正在寻找单线。

通过 f 字符串嵌套,我的意思是这样的:

f'{f"{n:0>32x}"}'

我不知道这是否有效,我不喜欢它。


目前我已经这样做了:

"{0:0>32_x}".format(n)
In [248]: "{0:0>32_x}".format(260764824896579434326633182196140447999)
Out[248]: 'c42d_7a7d_155b_93f7_658c_20c9_fea5_98ff'

但我无法将下划线更改为冒号,我知道我可以使用 str.replace 但我仍然希望在一个命令中完成它。


性能比较:

In [253]: %timeit "{0:0>32_x}".format(260764824896579434326633182196140447999).replace('_', ':')
988 ns ± 7.72 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [254]: %timeit ':'.join([hex(260764824896579434326633182196140447999).replace('0x',"").zfill(32)[i:i+4] for i in range(0, 32, 4)])
4.59 µs ± 493 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [255]: %timeit "{0:0>32_x}".format(260764824896579434326633182196140447999)
810 ns ± 48.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

字符串插值比字符串切片快得多。

但是正则表达式很慢:

请注意,如果您想减少所需时间,您可以利用 re.compile,请考虑以下示例

import timeit
timeit.timeit(stmt='re.sub("(....)","\1:",s)',setup='import re;s = "c42d7a7d155b93f7658c20c9fea598ff"') # 2.398639300000468
timeit.timeit(stmt='pat.sub("\1:",s)',setup='import re;s = "c42d7a7d155b93f7658c20c9fea598ff";pat=re.compile("(....)")') # 1.6385453000002599

我使用不同的模式,因为我假设输入始终是 32 个十六进制数字,将每 4 个字符替换为它们,然后是 :

我做到了。我正在寻找的格式说明符是 "{:_x}"

它将一个整数格式化为其十六进制表示形式,并在每四个十六进制数字中插入一个下划线。那么用str.replace把下划线换成冒号就很简单了

但不幸的是,单独使用 f-string 填充时无法插入分隔符,需要其他工具。

但它完成了最难的部分,即以十六进制格式表示整数并每四位插入一个冒号。剩下的唯一工作就是填充。

len(s)很便宜,字符串乘法也很便宜。

使用简单的算术我得到了这个:

ipv6 = "{:_x}".format(n).replace('_', ':'); l = len(ipv6)              
if l != 39: ipv6 = '0000:' * ((39 - l) // 5) + '0' * (4 - l % 5) + ipv6

这个:

((39 - l) // 5)

计算应向左侧插入多少个字段。

39 - l计算还需要多少个字符才能使字符串的长度为39,然后用floor除法计算需要的字段数

这个:

(4 - l % 5)

计算最后一个字段应插入多少个前导零,每个字段应有 4 个数字,每个字段的分隔符长度为 5 个字符,l % 5 计算最左边字段的位数, 并从 4 中减去它以获得所需的前导零数。

它比字符串切片快得多。


其实一行f个字符串,加上位移和mod操作,确实可以做到,只是比我预想的要麻烦和慢很多:

def format_ipv6(n):
    return f'{(n>>112)%65536:0>4x}:{(n>>96)%65536:0>4x}:{(n>>80)%65536:0>4x}:{(n>>64)%65536:0>4x}:{(n>>48)%65536:0>4x}:{(n>>32)%65536:0>4x}:{(n>>16)%65536:0>4x}:{(n)%65536:0>4x}'

但它确实在一行中完成,我尝试将它与具有范围和 str.join 的生成器表达式一起使用,但这样做会进一步减慢速度...