How to split a string by commas positioned outside of parenthesis?

Posted on

Question :

How to split a string by commas positioned outside of parenthesis?

I got a string of such format:

"Wilbur Smith (Billy, son of John), Eddie Murphy (John), Elvis Presley, Jane Doe (Jane Doe)"

so basicly it’s list of actor’s names (optionally followed by their role in parenthesis). The role itself can contain comma (actor’s name can not, I strongly hope so).

My goal is to split this string into a list of pairs – (actor name, actor role).

One obvious solution would be to go through each character, check for occurances of '(', ')' and ',' and split it whenever a comma outside occures. But this seems a bit heavy…

I was thinking about spliting it using a regexp: first split the string by parenthesis:

import re
x = "Wilbur Smith (Billy, son of John), Eddie Murphy (John), Elvis Presley, Jane Doe (Jane Doe)"
s = re.split(r'[()]', x) 
# ['Wilbur Smith ', 'Billy, son of John', ', Eddie Murphy ', 'John', ', Elvis Presley, Jane Doe ', 'Jane Doe', '']

The odd elements here are actor names, even are the roles. Then I could split the names by commas and somehow extract the name-role pairs. But this seems even worse then my 1st approach.

Are there any easier / nicer ways to do this, either with a single regexp or a nice piece of code?

Asked By: kender


Answer #1:

One way to do it is to use findall with a regex that greedily matches things that can go between separators. eg:

>>> s = "Wilbur Smith (Billy, son of John), Eddie Murphy (John), Elvis Presley, Jane Doe (Jane Doe)"
>>> r = re.compile(r'(?:[^,(]|([^)]*))+')
>>> r.findall(s)
['Wilbur Smith (Billy, son of John)', ' Eddie Murphy (John)', ' Elvis Presley', ' Jane Doe (Jane Doe)']

The regex above matches one or more:

  • non-comma, non-open-paren characters
  • strings that start with an open paren, contain 0 or more non-close-parens, and then a close paren

One quirk about this approach is that adjacent separators are treated as a single separator. That is, you won’t see an empty string. That may be a bug or a feature depending on your use-case.

Also note that regexes are not suitable for cases where nesting is a possibility. So for example, this would split incorrectly:

"Wilbur Smith (son of John (Johnny, son of James), aka Billy), Eddie Murphy (John)"

If you need to deal with nesting your best bet would be to partition the string into parens, commas, and everthing else (essentially tokenizing it — this part could still be done with regexes) and then walk through those tokens reassembling the fields, keeping track of your nesting level as you go (this keeping track of the nesting level is what regexes are incapable of doing on their own).

Answered By: kender

Answer #2:

s = re.split(r',s*(?=[^)]*(?:(|$))', x) 

The lookahead matches everything up to the next open-parenthesis or to the end of the string, iff there’s no close-parenthesis in between. That ensures that the comma is not inside a set of parentheses.

Answered By: Laurence Gonsalves

Answer #3:

I think the best way to approach this would be to use python’s built-in csv module.

Because the csv module only allows a one character quotechar, you would need to do a replace on your inputs to convert () to something like | or ". Then make sure you are using an appropriate dialect and off you go.

Answered By: Alan Moore

Answer #4:

An attempt on human-readable regex:

import re

regex = re.compile(r"""
    # name starts and ends on word boundary
    # no '(' or commas in the name
    # everything inside parentheses is a role
    ))? # role is optional
    """, re.VERBOSE)

s = ("Wilbur Smith (Billy, son of John), Eddie Murphy (John), Elvis Presley,"
     "Jane Doe (Jane Doe)")
print re.findall(regex, s)


[('Wilbur Smith', 'Billy, son of John'), ('Eddie Murphy', 'John'), 
 ('Elvis Presley', ''), ('Jane Doe', 'Jane Doe')]
Answered By: Wogan

Answer #5:

My answer will not use regex.

I think simple character scanner with state “in_actor_name” should work. Remember then state “in_actor_name” is terminated either by ‘)’ or by comma in this state.

My try:

s = 'Wilbur Smith (Billy, son of John), Eddie Murphy (John), Elvis Presley, Jane Doe (Jane Doe)'

in_actor_name = 1
role = ''
name = ''
for c in s:
    if c == ')' or (c == ',' and in_actor_name):
        in_actor_name = 1
        name = name.strip()
        if name:
            print "%s: %s" % (name, role)
        name = ''
        role = ''
    elif c == '(':
        in_actor_name = 0
        if in_actor_name:
            name += c
            role += c
if name:
    print "%s: %s" % (name, role)


Wilbur Smith: Billy, son of John
Eddie Murphy: John
Elvis Presley: 
Jane Doe: Jane Doe
Answered By: jfs

Answer #6:

Here’s a general technique I’ve used in the past for such cases:

Use the sub function of the re module with a function as replacement argument. The function keeps track of opening and closing parens, brackets and braces, as well as single and double quotes, and performs a replacement only outside of such bracketed and quoted substrings. You can then replace the non-bracketed/quoted commas with another character which you’re sure doesn’t appear in the string (I use the ASCII/Unicode group-separator: chr(29) code), then do a simple string.split on that character. Here’s the code:

import re
def srchrepl(srch, repl, string):
    """Replace non-bracketed/quoted occurrences of srch with repl in string"""

    resrchrepl = re.compile(r"""(?P<lbrkt>[([{])|(?P<quote>['"])|(?P<sep>["""
                            + srch + """])|(?P<rbrkt>[)]}])""")
    return resrchrepl.sub(_subfact(repl), string)

def _subfact(repl):
    """Replacement function factory for regex sub method in srchrepl."""
    level = 0
    qtflags = 0
    def subf(mo):
        nonlocal level, qtflags
        sepfound ='sep')
        if  sepfound:
            if level == 0 and qtflags == 0:
                return repl
            level += 1
        elif'quote') == "'":
            qtflags ^= 1            # toggle bit 1
            return "'"
        elif'quote') == '"':
            qtflags ^= 2            # toggle bit 2
            return '"'
            level -= 1
    return subf

If you don’t have nonlocal in your version of Python, just change it to global and define level and qtflags at the module level.

Here’s how it’s used:

>>> GRPSEP = chr(29)
>>> string = "Wilbur Smith (Billy, son of John), Eddie Murphy (John), Elvis Presley, Jane Doe (Jane Doe)"
>>> lst = srchrepl(',', GRPSEP, string).split(GRPSEP)
>>> lst
['Wilbur Smith (Billy, son of John)', ' Eddie Murphy (John)', ' Elvis Presley', ' Jane Doe (Jane Doe)']
Answered By: Micha? Niklas

Answer #7:

This post helped me a lot. I was looking to split a string by commas positioned outside quotes. I used this as a starter. My final line of code was regEx = re.compile(r'(?:[^,"]|"[^"]*")+') This did the trick. Thanks a ton.

Answered By: Don O’Donnell

Answer #8:

I certainly agree with @Wogan above, that using the CSV moudle is a good approach. Having said that if you still want to try a regex solution give this a try, but you will have to adapt it to Python dialect


Leave a Reply

Your email address will not be published. Required fields are marked *