Text substitution depending on the length of the group matched

Viewed 38

Background: Absolute beginner working through the book "Automate the Boring Stuff with Python" and was curious as to how to make this substitution:

Input string:

"Agent Alice tells Agent Bob secret info about Agent Charlie."  

Desired Output:

"Agent A**** tells Agent B** secret info about Agent C******."

Note how the number of asterisks depends on the length of the name.

What I've tried:

import re
concealName = re.compile(r'(Agent)(\s)(\w)(\w)*')
sentence = "Agent Alice tells Agent Bob secret info about Agent Charlie."  
concealName.sub(r'\1\2\3' + '*' * len(r'\4'), sentence)

OUTPUT:

'Agent A** gave Agent B** info about Agent C**'

My logic was that it would calculate the length of each instance of a matched group 4, and multiply that length with the '*' string, which should've resulted in a string of ****** with the same length as the group 4 matched. Completely clueless as to why this didn't work, and how to fix it.

2 Answers

It didn't work because what you pass to sub is essentially a template string. It gets passed to the internals of the function and only then do the back-references get replaced with the real thing.

That is, r'\1\2\3' + '*' * len(r'\4') is first evaluated into r'\1\2\3' + '*' * 2 (the length of literal \4) and then into r'\1\2\3**', so the reference \4 is lost and you always end up with the first character of group 3 followed by two **.

Even if it worked that way, you got an error in your original pattern: The last part should be (\w*) with the asterisk within the group, otherwise it would repeat the whole group and \4 would only ever capture one single character.

Ultimately, however, I think you will have to use a different approach, i.e., by passing a function to sub. The closest to your solution is this:

import re

sentence = "Agent Alice tells Agent Bob secret info about Agent Charlie."

# note the * at the end - try to move it outside the group and see what happens!
concealName = re.compile(r'(Agent)(\s)(\w)(\w*)')

concealName.sub(lambda m: ''.join([m.group(1), # Agent
                                   m.group(2), # \s
                                   m.group(3), # first \w
                                   '*' * len(m.group(4)) # <- top secret
                                  ]), sentence)

which, as desired, gives 'Agent A**** tells Agent B** secret info about Agent C******.'.

From here, try and make some improvements. E.g., there's not really a reason to have separate groups for (Agent), (\s) and (\w) – you can merge them into a single group which would greatly simplify things.

You can do it this way:

import re

text = "Agent Alice tells Agent Bob secret info about Agent Charlie."


def repl(match_obj):
    name = match_obj.group()
    return name[0] + "*" * (len(name) - 1)


pattern = re.compile(r"(?<=Agent )\w+")
print(pattern.sub(repl, text))

output:

Agent A**** tells Agent B** secret info about Agent C******.

Explanation:

The pattern (?<=Agent )\w+, uses a positive look behind assertion so that it only matches the words (basically names) after the "Agent ".

Now .sub can accept a function as the repl parameter which it will call it on a found match object. Your match object is gonna be your names. The return value of this function is gonna be replaced.

Related