Is there a way to check for two different patterns in the same string in regex python?

Viewed 149

I want to extract certain digits from a string. The problem is that the string can contain the digits in two different patterns. How can I create a regex pattern in re.search such that I can have both patterns to search in a single string?

For e.g.,

## extract 65.45 from this string
string = '1112 (65.45%)'

So, if I do the following, it works

re.search('.*?\((.*)%\)', string).group(1)

and I get the expected result 65.45.

Now, I have another kind of string in the same text that I need to look for.

## from this string, extract 4.00 which appears before [
string = '4.00 [3.00 - 4.50]'

re.search('^(\S+)\s\[.*', string).group(1)

gives me the desired result: 4.00

But if I combine them like the following, it only extracts the one that matches first.

re.search('^(\S+)\s\[.*|.*?\((.*)%\)', string).group(1)

in which case, only the string that contains the square bracket extracts the value, not if the string has a % sign. How can I fix this?

For e.g., If I have a list of strings like the following:

['73 (1.40%)', '38 (1.55%)', '27 (2.17%)', '32 (1.46%)', '10 (1.46%)', '11 (1.04%)', '11 (1.41%)', '7 (1.34%)', '4 (1.24%)', '28 (1.27%)', '750 (14.41%)', '381 (15.54%)', '182 (14.60%)', '313 (14.27%)', '4.10 [3.73 - 4.45]', '4.08 [3.70 - 4.42]', '4.13 [3.77 - 4.47]', '4.13 [3.78 - 4.47]', '4.07 [3.70 - 4.42]', '4.07 [3.70 - 4.43]', '4.07 [3.70 - 4.40]', '4.09 [3.73 - 4.42]', '4.03 [3.63 - 4.40]', '4.10 [3.70 - 4.47]']

I want to do certain things with each value that is extracted and compare with a specific threshold value.

Using the for-loop, I did something like this:

for val in string: 
    match = re.search('^(\S+)\s\[.*|.*?\((.*)%\)', val)
    print(match)

which results in the following:

<re.Match object; span=(0, 10), match='73 (1.40%)'>
<re.Match object; span=(0, 10), match='38 (1.55%)'>
<re.Match object; span=(0, 10), match='27 (2.17%)'>
<re.Match object; span=(0, 10), match='32 (1.46%)'>
<re.Match object; span=(0, 10), match='10 (1.46%)'>
<re.Match object; span=(0, 10), match='11 (1.04%)'>
<re.Match object; span=(0, 10), match='11 (1.41%)'>
<re.Match object; span=(0, 9), match='7 (1.34%)'>
<re.Match object; span=(0, 9), match='4 (1.24%)'>
<re.Match object; span=(0, 10), match='28 (1.27%)'>
<re.Match object; span=(0, 12), match='750 (14.41%)'>
<re.Match object; span=(0, 12), match='381 (15.54%)'>
<re.Match object; span=(0, 12), match='182 (14.60%)'>
<re.Match object; span=(0, 12), match='313 (14.27%)'>
<re.Match object; span=(0, 18), match='4.10 [3.73 - 4.45]'>
<re.Match object; span=(0, 18), match='4.08 [3.70 - 4.42]'>
<re.Match object; span=(0, 18), match='4.13 [3.77 - 4.47]'>
<re.Match object; span=(0, 18), match='4.13 [3.78 - 4.47]'>
<re.Match object; span=(0, 18), match='4.07 [3.70 - 4.42]'>
<re.Match object; span=(0, 18), match='4.07 [3.70 - 4.43]'>
<re.Match object; span=(0, 18), match='4.07 [3.70 - 4.40]'>
<re.Match object; span=(0, 18), match='4.09 [3.73 - 4.42]'>
<re.Match object; span=(0, 18), match='4.03 [3.63 - 4.40]'>
<re.Match object; span=(0, 18), match='4.10 [3.70 - 4.47]'>

But not sure how to extract the exact value.

I have to do the .group() to extract the value, but it requires me to know the exact location. And I'm struggling to figure out how to do that.

If I do match.group(2), then I get the following result:

1.40
1.55
2.17
1.46
1.46
1.04
1.41
1.34
1.24
1.27
14.41
15.54
14.60
14.27
None
None
None
None
None
None
None
None
None
None
4 Answers

Here is an approach which works with your exact input, in which each list entry always would have one of the two matching patterns:

inp = ['73 (1.40%)', '38 (1.55%)', '27 (2.17%)', '32 (1.46%)', '10 (1.46%)', '11 (1.04%)', '11 (1.41%)', '7 (1.34%)', '4 (1.24%)', '28 (1.27%)', '750 (14.41%)', '381 (15.54%)', '182 (14.60%)', '313 (14.27%)', '4.10 [3.73 - 4.45]', '4.08 [3.70 - 4.42]', '4.13 [3.77 - 4.47]', '4.13 [3.78 - 4.47]', '4.07 [3.70 - 4.42]', '4.07 [3.70 - 4.43]', '4.07 [3.70 - 4.40]', '4.09 [3.73 - 4.42]', '4.03 [3.63 - 4.40]', '4.10 [3.70 - 4.47]']
matches = [re.findall(r'\b\d+ \((\d+(?:\.\d+)?%)\)|(\d+(?:\.\d+)?) \[\d+(?:\.\d+)? - \d+(?:\.\d+)?\]', x) for x in inp]
matches = [x[0][0] + x[0][1] for x in matches]
print(matches)

This prints:

['1.40%', '1.55%', '2.17%', '1.46%', '1.46%', '1.04%', '1.41%', '1.34%',
 '1.24%', '1.27%', '14.41%', '15.54%', '14.60%', '14.27%', '4.10', '4.08',
 '4.13', '4.13', '4.07', '4.07', '4.07', '4.09', '4.03', '4.10']

The strategy used above is to match, in two separate groups, either the first digit in the percentage input, or the number outside the square brackets. Then, in a list comprehension, we concatenate the two capture groups together. Since one of the two groups is guaranteed to be empty, the concatenated result always corresponds to the desired match.

I would just use a list of simple regexs and iterate through them for each string I want to test. The first regex that gets a hit will be used. I would also compile the regex upfront to save CPU cycles. This is easier to follow readability wise and easy to add new patterns to:

import re

regexs = [
    re.compile(r".*?\((.*)%\)"), 
    re.compile(r"^(\S+)\s\[.*"),
]

data = [
    "73 (1.40%)",
    "38 (1.55%)",
    "27 (2.17%)",
    "750 (14.41%)",
    "381 (15.54%)",
    "4.10 [3.73 - 4.45]",
    "4.08 [3.70 - 4.42]",
    "4.13 [3.77 - 4.47]",
    "this shouldn't match"
]


for val in data:
    for regex in regexs:
        if match := regex.search(val):
            print("Matched: " + match.group(1))
            break
    else:
        print("No match: " + val)

Outputs:

Matched: 1.40
Matched: 1.55
Matched: 2.17
Matched: 14.41
Matched: 15.54
Matched: 4.10
Matched: 4.08
Matched: 4.13
No match: this shouldn't match

.group returns captured groups, so .group(1) always returns the first captured group.

To get the other capture group, use .group(2)

Another option is to use lookarounds to get a match only:

(?<=\()\d+(?:\.\d+)?(?=%\))|\d+(?:\.\d+)?(?=\s*\[[^][]*])

The pattern matches

  • (?<=\() Positive lookbehind, assert ( to the left
  • \d+(?:\.\d+)? Match 1+ digits with an optional decimal part
  • (?=%\)) Positive lookahead, assert ) to the right
  • | Or
  • \d+(?:\.\d+)? Match 1+ digits with an optional decimal part
  • (?=\s*\[[^][]*]) Positive lookahead, assert an opening till closing square bracket to the right (You could make it more specific by specifying the exact format between the square brackets)

Regex demo | Python demo

import re

pattern = r"(?<=\()\d+(?:\.\d+)?(?=%\))|\d+(?:\.\d+)?\b(?=\s*\[[^][]*\])"
strings = ['73 (1.40%)', '38 (1.55%)', '27 (2.17%)', '32 (1.46%)', '10 (1.46%)', '11 (1.04%)', '11 (1.41%)', '7 (1.34%)', '4 (1.24%)', '28 (1.27%)', '750 (14.41%)', '381 (15.54%)', '182 (14.60%)', '313 (14.27%)', '4.10 [3.73 - 4.45]', '4.08 [3.70 - 4.42]', '4.13 [3.77 - 4.47]', '4.13 [3.78 - 4.47]', '4.07 [3.70 - 4.42]', '4.07 [3.70 - 4.43]', '4.07 [3.70 - 4.40]', '4.09 [3.73 - 4.42]', '4.03 [3.63 - 4.40]', '4.10 [3.70 - 4.47]']
for val in strings:
    match = re.search(pattern, val)
    print(match)

Output

<re.Match object; span=(4, 8), match='1.40'>
<re.Match object; span=(4, 8), match='1.55'>
<re.Match object; span=(4, 8), match='2.17'>
<re.Match object; span=(4, 8), match='1.46'>
<re.Match object; span=(4, 8), match='1.46'>
<re.Match object; span=(4, 8), match='1.04'>
<re.Match object; span=(4, 8), match='1.41'>
<re.Match object; span=(3, 7), match='1.34'>
<re.Match object; span=(3, 7), match='1.24'>
<re.Match object; span=(4, 8), match='1.27'>
<re.Match object; span=(5, 10), match='14.41'>
<re.Match object; span=(5, 10), match='15.54'>
<re.Match object; span=(5, 10), match='14.60'>
<re.Match object; span=(5, 10), match='14.27'>
<re.Match object; span=(0, 4), match='4.10'>
<re.Match object; span=(0, 4), match='4.08'>
<re.Match object; span=(0, 4), match='4.13'>
<re.Match object; span=(0, 4), match='4.13'>
<re.Match object; span=(0, 4), match='4.07'>
<re.Match object; span=(0, 4), match='4.07'>
<re.Match object; span=(0, 4), match='4.07'>
<re.Match object; span=(0, 4), match='4.09'>
<re.Match object; span=(0, 4), match='4.03'>
<re.Match object; span=(0, 4), match='4.10'>
Related