regex lookahead AND look behind

Viewed 103

I have the following 2 variations of scraped data:

   txt =  '''Käuferprovision: 3 % zzgl. gesetzl. MwSt.''' # variation 1

and

    txt = '''Käuferprovision: Die Courtage i.H.v. % 3,57 inkl. MwSt. ist''' # variation 2

I'd like to make one regular expression that gets the percentage as a float, so in the first instance 3.0 and in the second 3.57

I've tried this so far:

m = re.search(r'.{3}.%.{5}',txt)
txt = m.group().split("%")[1:]
txt = ("".join(txt)).replace(",",".")
print(txt)

Which works for the variation 2 but not variaton 1.

5 Answers

You might use an alternation with 2 capture groups, and check which group exists.

\b(\d+(?:\,\d+)?)\s*%|%\s*(\d+(?:\,\d+)?)\b

See a regex demo.

The pattern matches:

  • \b A word boundary
  • (\d+(?:\,\d+)?)\s*% Capture group 1 - match a digit with optional decimal, optional whitespace chars and %
  • | Or
  • %\s*(\d+(?:\,\d+)?) Capture group 2 - \b A word boundary - the other way around as in group 1
  • \b A word boundary

For example

import re

regex = r"\b(\d+(?:\,\d+)?)\s*%|%\s*(\d+(?:\,\d+)?)\b"
test_str = ("Käuferprovision: 3 % zzgl. gesetzl. MwSt.\n"
            "Käuferprovision: Die Courtage i.H.v. % 3,57 inkl. MwSt. ist")

matches = re.finditer(regex, test_str, re.MULTILINE)
for matchNum, match in enumerate(matches, start=1):
    if match.group(1):
        print(match.group(1).replace(',', '.'))
    else:
        print(match.group(2).replace(',', '.'))

Output

3
3.57

If the spaces between the percentage signs are fixed, you could also use lookarounds to get a match only without groups.

(?<=% )\b\d+(?:,\d+)\b|\b\d+(?:,\d+)?(?= %)

See another regex demo.

Example

import re

pattern = r"(?<=% )\b\d+(?:,\d+)\b|\b\d+(?:,\d+)?(?= %)"
test_str = ("Käuferprovision: 3 % zzgl. gesetzl. MwSt.\n"
            "Käuferprovision: Die Courtage i.H.v. % 3,57 inkl. MwSt. ist")

for s in re.findall(pattern, test_str):
    print(s.replace(",", "."))

Output

3
3.57

You may try this code to grab your percent values and convert them into float:

>>> import re
>>> arr = ['Käuferprovision: 3 % zzgl. gesetzl. MwSt.', 'Käuferprovision: Die Courtage i.H.v. % 3,57 inkl. MwSt. ist']
>>> rx = re.compile(r'\d+(?:[.,]\d+)*(?=\s*%)|(?<=%)\s*\d+(?:[.,]\d+)*')
>>> for s in arr:
...     for m in rx.finditer(s): print (float(m.group().replace(',', '.')))
...
3.0
3.57

RegEx Demo

Online Code Demo

Yet another one - using a branch reset with PyPi regex:

import regex as re

data = """
Käuferprovision: 3 % zzgl. gesetzl. MwSt.
Käuferprovision: Die Courtage i.H.v. % 3,57 inkl. MwSt. ist
"""
rx = re.compile(r'(?|(?P<value>\d+(?:,\d+)?)\s*%|%\s*(?P<value>\d+(?:,\d+)?))')

for m in rx.finditer(data):
    print(float(m.group('value').replace(',', '.')))

Which yields

3.0
3.57

See a demo on regex101.com.


If you want to go completely crazy, use a subroutine and a branch reset (this is way over the top, admittedly):

(?(DEFINE)
    (?<value>\d+(?:,\d+)?)
    (?<before>%\s+)
    (?<after>\s+%)
)

(?|(?P<mwst>(?&value))(?&after)|(?&before)(?P<mwst>(?&value)))

See the other demo on regex101.com.

I would recommend using \d inside the regex which matches a digit character. ? can also be of aid to you which matches one or zero instances. The below regex would match both of the strings for you:

re.search(r'([\d,]+)? % ([\d,]+)?',txt)

[\d,] simply matches a digit character or the character ,. [\d,]+ matches one or more of these characters. ([\d,]+)? will match the characters if they exist, otherwise it will not attempt to capture them.

My two cents:

(%?\s*(\d+(?:,\d+)?)\s*%?)

See an online demo

  • (%?\s* - Open 1st capture group holding an optional percentage sign and 0+ whitespace characters;
    • (\d+(?:,\d+)?) - A 2nd capture group holding your numerical value.
  • \s*%?) - Close 1st capture group right after 0+ whitespace characters followed by an optional percentage sign.

Now you could try:

import re

data = """
Käuferprovision: 3 % zzgl. gesetzl. MwSt. 5,44
Käuferprovision: Die Courtage i.H.v. % 3,57 inkl. MwSt. ist
"""

for i in re.findall(r'(%?\s*(\d+(?:,\d+)?)\s*%?)', data):
    if '%' in i[0]:
      print(float(i[1].replace(',', '.')))

Prints:

3.0
3.57

I believe you could cram the above in some list-comprehension.

import re

data = """
Käuferprovision: 3 % zzgl. gesetzl. MwSt. 5,44
Käuferprovision: Die Courtage i.H.v. % 3,57 inkl. MwSt. ist
"""

print([float(i[1].replace(',', '.')) for i in re.findall(r'(%?\s*(\d+(?:,\d+)?)\s*%?)', data) if '%' in i[0]])

Prints:

[3.0, 3.57]
Related