3
\$\begingroup\$

The problem I want to solve is to replace a given string inside tags.

For example, if I'm given:

Some text abc [tag]some text abc, more text abc[/tag] still some more text

I want to replace abc for def but only inside the tags, so the output would be:

Some text abc [tag]some text def, more text def[/tag] still some more text

We may assume that the tag nesting is well formed. My solution is the following:

def replace_inside(text):
    i = text.find('[tag]') 
    while i >= 0:
        j = text.find('[/tag]', i+1)
        snippet = text[i:j].replace('abc', 'def')
        text = text[:i] + snippet + text[j:]
        i = text.find('[tag]', j) 
    return text

I was wondering if it can be solved in a more elegant way, for example, using regex.

\$\endgroup\$
1

2 Answers 2

2
\$\begingroup\$

Your code is incorrect. See the following case:

print replace_inside("[tag]abc[/tag]abc[tag]abc[/tag]")

You can indeed use regular expressions

pattern = re.compile(r"\[tag\].*?\[/tag\]")
return pattern.sub(lambda match: match.group(0).replace('abc','def') ,text)
\$\endgroup\$
4
  • 1
    \$\begingroup\$ doesn't it need to be r"\[tag\].*?\[/tag\]" for a non-greedy *? \$\endgroup\$ Feb 17, 2012 at 1:54
  • \$\begingroup\$ @Winston Ewert♦: you're right, I fixed it. \$\endgroup\$
    – kunigami
    Feb 17, 2012 at 21:37
  • \$\begingroup\$ I agree with @James Khoury, it's necessary to use the non-greed version, otherwise it will fail in your test case too. \$\endgroup\$
    – kunigami
    Feb 17, 2012 at 21:38
  • 1
    \$\begingroup\$ @JamesKhoury, thanks. It seems I don't do regular expressions enough. \$\endgroup\$ Feb 18, 2012 at 6:12
0
\$\begingroup\$

Unless you have further restrictions on your input, your code cannot be correct. You're assuming there can be tag nesting, so you should be making a parser.

As far as I can tell, your grammar is:

text -> text tag_open text tag_close text
text -> char_sequence
tag_open -> '[' identifier ']'
tag_close -> '[/' identifier ']'

Where char_sequence is some (possibly empty) sequence of characters that don't contain [ and identifier is some sequence of (probably alphanumeric + underscore) characters.

Now you could use something like PLY to build a syntax tree. You'll have to define your terminals:

char_sequence = r"[^\[]*"
tag_open = r"\[[a-zA-Z0-9_]+\]"
tag_close = r"\[/[a-zA-Z0-9_]+\]"

Now you've got to build your abstract syntax tree. I would advise a class Node that contains a list of children, the associated tag, and the text in it. Thus, something like

def Node(object):
    def __init__(self, tag_name, text):
        self.tag_name = tag_name
        self.text = text
        self.children = []

    def add_child(self, child):
        self.children.append(child)

You'd want [tag]Hello![tag]Hi![/tag][/tag] to parse to a Node that has the tag_name tag, the text Hello!, and a single child.

Once you have that, you can go ahead and define the grammar rules. PLY lets you do that with

def p_tag_in_text(p): """text : text open_tag text close_tag text""" # You now have an object for each of text, text, open_tag, text, close_tag, text # and can put them together here.

Once you've defined all parser rules, you get an abstract syntax tree, which you can then traverse finding all relevant nodes and calling replace on the text you want.

As you can see, this method is a lot longer and a good deal more complicated. The token examples I showed don't match the grammar -- you'd probably want to split the [, [/ and ] out of the tokens, and make sure that close_tag and open_tag just contain the identifiers. I've also not entirely specified how the grammar rules should be implemented: the current setup leads to it being unclear where the text is in relation to the tags. You'd have to refine the system somewhat to make that work (probably making the text a kind of child, and appending it at a suitable time.

Because of this, you should first try to restrict your input. If you can ensure a number of things about tag nesting (especially tags containing themselves) it would allow you to do all of this with a regex, and in that case Winston Ewert's solution is what you should go with.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.