使用 Python 正则表达式解析版本字符串并使用固定宽度的回顾

60 阅读2分钟

我正在尝试编写一个 Python 库来解析我们的版本格式字符串。简化的版本字符串格式如下:

<product>-<x>.<y>.<z>[-alpha|beta|rc[.<n>]][.<extra>]][.centos|redhat|win][.snb|ivb]

其中包括:

  • 产品,例如 foo
  • 数字版本,例如: 0.1.0
  • [可选] 预发布信息,例如: beta、rc.1、alpha.extrainfo
  • [可选] 操作系统,例如: centos
  • [可选] 平台,例如: snb、ivb

以下为有效的版本字符串示例:

  1. foo-1.2.3
  2. foo-2.3.4-alpha
  3. foo-3.4.5-rc.2
  4. foo-4.5.6-rc.2.extra
  5. withos-5.6.7.centos
  6. osandextra-7.8.9-rc.extra.redhat
  7. all-4.4.4-rc.1.extra.centos.ivb

以下正则表达式适用于上述所有示例:

^(?P<prod>\w+)-(?P<maj>\d).(?P<min>\d).(?P<bug>\d)(?:-(?P<pre>alpha|beta|rc)(?:.(?P<pre_n>\d))?(?:.(?P<pre_x>\w+))?)?(?:.(?P<os>centos|redhat|win))?(?:.(?P<plat>snb|ivb))?$

但此正则表达式在以下类型版本中会遇到问题(没有“extra”预发布信息,但包含操作系统和/或平台):

  1. issue-0.1.0-beta.redhat.snb

对于字符串 #8,正则表达式将 redhat 识别为预发布额外信息 pre_x,而不是操作系统组。

我尝试使用回顾来避免在 pre_x 中选择操作系统或平台字符串:

...(?:.(?P<pre_x>\w+))?(?<!centos|redhat|win|ivb|snb))...

即:

^(?P<prod>\w+)-(?P<maj>\d).(?P<min>\d).(?P<bug>\d)(?:-(?P<pre>alpha|beta|rc)(?:.(?P<pre_n>\d))?(?:.(?P<pre_x>\w+))?(?<!centos|redhat|win|ivb|snb))?(?:.(?P<os>centos|redhat|win))?(?:.(?P<plat>snb|ivb))?$

如果 Python 的标准模块 re 可以接受可变宽度的回顾,那么这将很好地解决问题。我更愿意坚持使用标准模块,而不是使用正则表达式,因为我的库很可能会分发到大量机器上,而我希望限制依赖项。

我已经查看了类似的问题:这个问题、这个问题和这个问题,但它们不适用。

有什么想法可以解决这个问题吗?

2、解决方案

方法一:负回顾断言

您可以使用负回顾断言来使 (?P<pre_x>\w+) 匹配除 centos 或 redhat 之外的任何内容。

^(?P<prod>\w+)-(?P<maj>\d).(?P<min>\d).(?P<bug>\d)(?:-(?P<pre>alpha|beta|rc)(?:.(?P<pre_n>\d))?(?:.(?:(?!centos|redhat)\w)+)?)?(?:.(?P<os>centos|redhat))?(?:.(?P<plat>snb|ivb))?$

DEMO:regex101.com/r/bH0qI7/4

方法二:手动解析版本字符串

您也可以选择使用手动的方式来解析版本字符串,这能更直观地进行解析,不会像正则表达式一样看着麻烦。

def extract(text):
    parts = text.split('-')
    ret = {}
    ret['name'] = parts.pop(0)
    ret['version'] = parts.pop(0).split('.')

    if len(parts) > 0:
        rest_parts = parts.pop(0).split('.')
        if rest_parts[-1] in ['snb', 'ivb']:
            ret['platform'] = rest_parts.pop(-1)
        if rest_parts[-1] in ['redhat', 'centos', 'win']:
            ret['os'] = rest_parts.pop(-1)
        ret['extra'] = rest_parts

    return ret

测试用例和结果:

tests = \
[
    'foo-1.2.3',
    'foo-2.3.4-alpha',
    'foo-3.4.5-rc.2',
    'foo-4.5.6-rc.2.extra',
    'withos-5.6.7.centos',
    'osandextra-7.8.9-rc.extra.redhat',
    'all-4.4.4-rc.1.extra.centos.ivb',
    'issue-0.1.0-beta.redhat.snb',
]

for test in tests:
    print(test, extract(test))

结果:

('foo-1.2.3', {'version': ['1', '2', '3'], 'name': 'foo'})
('foo-2.3.4-alpha', {'version': ['2', '3', '4'], 'name': 'foo', 'extra': ['alpha']})
('foo-3.4.5-rc.2', {'version': ['3', '4', '5'], 'name': 'foo', 'extra': ['rc', '2']})
('foo-4.5.6-rc.2.extra', {'version': ['4', '5', '6'], 'name': 'foo', 'extra': ['rc', '2', 'extra']})
('withos-5.6.7.centos', {'version': ['5', '6', '7', 'centos'], 'name': 'withos'})
('osandextra-7.8.9-rc.extra.redhat', {'version': ['7', '8', '9'], 'os': 'redhat', 'name': 'osandextra', 'extra': ['rc', 'extra']})
('all-4.4.4-rc.1.extra.centos.ivb', {'platform': 'ivb', 'version': ['4', '4', '4'], 'os': 'centos', 'name': 'all', 'extra': ['rc', '1', 'extra']})
('issue-0.1.0-beta.redhat.snb', {'platform': 'snb', 'version': ['0', '1', '0'], 'os': 'redhat', 'name': 'issue', 'extra': ['beta']})