python regex how to capture all groups that may be optional?

Viewed 35
minutes_pattern = re.compile(
    r"""
(?:(?P<hours>\d+)H)?
(?:(?P<minutes>\d+)M)?
(?:(?P<seconds>\d+)S)?
""",
    re.VERBOSE,
)

I'm trying to capture all HOURS, MINUTES, SECONDS. However, these may or may not exist: Examples:

PT30H20M10S
PT20M10S
PT20M
PT3010S
PT30H

I'm thinking (?:(?P<hours>\d+)H)? says:

  1. ?: non capturing group
  2. ?P hours group name
  3. \d+ any number of digits
  4. H must exist
  5. )? this group may or may not exist

But this compile does not capture any of the 3 groups. Help please

regex = minutes_pattern.search(duration)
print(regex.groups())

This only returns None

(None, None, None)

Ideally this would show (for the 1st example):

group hours: 30
group minutes: 20
group seconds: 10

And for the 2nd example:

group hours: None
group minutes: 20
group seconds: 10
1 Answers

The problem is that your regex successfully matches an empty string, so when you use .search to search for the first match, the first match occurs at index 0 with a length of 0:

>>> minutes_pattern.search(duration)
<_sre.SRE_Match object; span=(0, 0), match=''>

The simplest way to fix this would be to include the letters PT at the start of your regex, since these are always present in your input string, and they prevent the regex returning an empty match:

>>> minutes_pattern = re.compile(r"PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?")
>>> minutes_pattern.search(duration)
<_sre.SRE_Match object; span=(0, 11), match='PT30H20M10S'>
>>> minutes_pattern.search(duration).groups()
('30', '20', '10')

Alternatively, if you don't want to include PT in your regex (perhaps some other inputs won't include that prefix), you could use .findall instead of .search, and then filter the list to find the "non-trivial" match:

>>> minutes_pattern = re.compile(r"(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?")
>>> minutes_pattern.findall(duration)
[('', '', ''), ('', '', ''), ('30', '20', '10'), ('', '', '')]
>>> next(filter(any, minutes_pattern.findall(duration)), None)
('30', '20', '10')
Related