Separate only at first instance with multiple delimiters using regex

Viewed 200

I have some strings which are in a format

lorem ipsum, dolor sit - amet, consectetur : adipiscing elit. Praesent vitae orc

I want it to get split at the first instance of each separator, to return

['lorem ipsum',
'dolor sit', 
'amet, consectetur', 
'adipiscing elit. Praesent vitae orc']

Right now my output is

['lorem ipsum',
'dolor sit',
'amet',
'consectetur ',
'adipiscing elit. Praesent vitae orc']

Right now I'm using re.split(', | - |: ', txt) but it separates at all the instances in the string. Any suggestions on how I can achieve the required output?

Edit:

I realised my question isn't clear, so for an example, if the string is

"abc: def: ijk, lmno: pqr - stu, wx"

the output should be

["abc",
"def: ijk",
"lmno: pqr",
"stu, wxy"]

and not

["abc",
"def",
"ijk",
"lmno",
"pqr",
"stu",
"wxy"]
3 Answers

If all separators have to be present at least once, instead of using split you could use 4 capturing groups with a backreference matching 1 of the 3 options except what is already matched.

^(.*?)(, | - |: )(.*?)(?!\2)(, | - |: )(.*?)(?!\2|\4)(, | - |: )(.*)

The pattern will match

  • ^ Start of string
  • (.*?) Group 1, match as least as possible
  • (, | - |: ) Group 2, match any of the listed
  • (.*?) Group 3, match as least as possible
  • (?!\2) Negative lookahead, assert what is on the right is not what is matched in group 2 (pick one of 2 valid options)
  • (, | - |: ) Group 4, match any of the listed
  • (.*?) Group 5, match as least as possible
  • (?!\2|\4) Negative lookahead, assert what is on the right is not what is matched in group 2 or group 4 (Pick the only valid option left)
  • (, | - |: ) Group 6, match any of the listed
  • (.*) Group 7, match any char as much as possible

Regex demo

For example

import re

regex = r"^(.*?)(, | - |: )(.*?)(?!\2)(, | - |: )(.*?)(?!\2|\4)(, | - |: )(.*)"

test_str = ("lorem ipsum, dolor sit - amet , consectetur : adipiscing elit. Praesent vitae orc\n\n"
    "abc: def: ijk, lmno: pqr - stu, wx\n\n")

matches = re.search(regex, test_str, re.MULTILINE)

if matches:
    print(matches.group(1))
    print(matches.group(3))
    print(matches.group(5))
    print(matches.group(7))

Output

lorem ipsum
dolor sit
amet , consectetur 
adipiscing elit. Praesent vitae orc

See Python demo1 and demo2

Just food for thought, not sure if it's a valued answer but maybe if you can use regex instead of re module to utilize the capability of a negative lookbehind with a non-fixed width. For example:

\s*([,:-])(?<!\1.*\1)\s*

In Python:

import regex as re
string1 = "abc: def: ijk, lmno: pqr - stu, wx"
lst1 = re.sub(r'\s*([,:-])(?<!\1.*\1)\s*', '|' , string1).split('|')
print(lst1)

Result:

['abc', 'def: ijk', 'lmno: pqr', 'stu, wx']

You could use a small class that counts the replacements:

import re

text = "lorem ipsum, dolor sit - amet, consectetur : adipiscing elit. Praesent vitae orc"
# text = "abc: def: ijk, lmno: pqr - stu, wx"
rx = re.compile(r'[-,:]')


class Replacer:
    def __init__(self, *args, **kwargs):
        for key in args:
            setattr(self, key, 0)
        self.needle = kwargs.get("needle")

    def __call__(self, match):
        key = match.group(0)
        setattr(self, key, getattr(self, key, 0) + 1)
        cnt = getattr(self, key, 0)
        return self.needle if cnt == 1 else key


rpl = Replacer("-", ",", ":", needle="#@#")

result = [item.strip() for item in re.split("#@#", rx.sub(rpl, text))]
print(result)

Which yields

['lorem ipsum', 'dolor sit', 'amet, consectetur', 'adipiscing elit. Praesent vitae orc']
Related