Splitting a string by multiple possible delimiters - python

I want to parse a str into a list of float values, however I want to be flexible regarding my delimiters. Specifically, I would like to be able to use any of these
s = '3.14; 42.2' # delimiter is '; '
s = '3.14;42.2' # delimiter is ';'
s = '3.14, 42.2' # delimiter is ', '
s = '3.14,42.2' # delimiter is ','
s = '3.14 42.2' # delimiter is ' '
I thought about removing all spaces, but this would disable the last version; I tried the re.split()-function by doing re.split('[;, ]', s) which would work using a single character as delimiter but fails otherwise.
I can however do
s.replace('; ', ';').replace(', ', ';').replace(',', ';').replace(' ', ';')
s.split(';')
which works but seems not really like a good practice or useful - especially if I would add even more delimiters in the future. What would be a good approach to do this?

You can use re.split and split on (The [ ] is a space and the brackets are for display only)
[;,] ?|[ ]
The pattern matches
[;,] ? Match either ; or , followed by an optional space
| or
[ ] Match a single space
Regex demo | Python demo
A bit more strict pattern with lookarounds could be asserting a digit on the left using lookarounds.
(?<=\d)(?:[;,] ?| )(?=\d)
The pattern matches:
(?<=\d) Positive lookbehind, assert a digit to the left
(?: Non capture group for the alternation
[;,] ? Match either ; or , followed by an optional space
| Or
Match a space
) Close non capture group
(?=\d) Positive lookahead, assert a digit to the right
Regex demo
Example code
import re
strings = [
"3.14; 42.2",
"3.14;42.2",
"3.14, 42.2",
"3.14,42.2",
"3.14 42.2"
]
for s in strings:
print(re.split(r"[;,] ?| ", s))
Output
['3.14', '42.2']
['3.14', '42.2']
['3.14', '42.2']
['3.14', '42.2']
['3.14', '42.2']

I think you can account for the last space(s) like this:
re.split(r'[;,]\s*', s)
Here \s* will capture the spaces after the separator, if any.

can also just do:
res = re.split('; |;|,|, | ', data)
see https://www.geeksforgeeks.org/python-split-multiple-characters-from-string/

Assuming you would know the delimiter of the input ahead of time, you could write a function that takes your delimiter as an argument, replaces with a space, and splits it:
def split_on_delim(strng, delim):
return strng.replace(delim, ' ').split()
for example:
>>> s = '3.14; 42.2'
>>> split_on_delim(s, '; ')
['3.14', '42.2']

Related

Python Split Regex not split what I need

I have this in my file
import re
sample = """Name: #s
Owner: #a[tag=Admin]"""
target = r"#[sae](\[[\w{}=, ]*\])?"
regex = re.split(target, sample)
print(regex)
I want to split all words that start with #, so like this:
["Name: ", "#s", "\nOwner: ", "#a[tag=Admin]"]
But instead it give this:
['Name: ', None, '\nOwner: ', '[tag=Admin]', '']
How to seperating it?
I would use re.findall here:
sample = """Name: #s
Owner: #a[tag=Admin]"""
parts = re.findall(r'#\w+(?:\[.*?\])?|\s*\S+\s*', sample)
print(parts) # ['Name: ', '#s', '\nOwner: ', '#a[tag=Admin]']
The regex pattern used here says to match:
#\w+ a tag #some_tag
(?:\[.*?\])? followed by an optional [...] term
| OR
\s*\S+\s* any other non whitespace term,
including optional whitespace on both sides
If I understand the requirements correctly you could do that as follows:
import re
s = """Name: #s
Owner: #a[tag=Admin]
"""
rgx = r'(?=#.*)|(?=\r?\n[^#\r\n]*)'
re.split(rgx, s)
#=> ['Name: ', '#s', '\nOwner: ', '#a[tag=Admin]\n']
Demo
The regular expression can be broken down as follows.
(?= # begin a positive lookahead
#.* # match '#' followed by >= 0 chars other than line terminators
) # end positive lookahead
| # or
(?= # begin a positive lookahead
\r?\n # match a line terminator
[^#\r\n]* # match >= 0 characters other than '#' and line terminators
) # end positive lookahead
Notice that matches are zero-width.
re.split expects the regular expression to match the delimiters in the string. It only returns the parts of the delimiters which are captured. In the case of your regex, that's only the part between the brackets, if present.
If you want the whole delimiter to show up in the list, put parentheses around the whole regex:
target = r"(#[sae](\[[\w{}=, ]*\])?)"
But you are probably better off not capturing the interior group. You can change it to a non-capturing group by using (?:…) instead of (…):
target = r"(#[sae](?:\[[\w{}=, ]*\])?)"
In your output, you keep the [tag=Admin] as that part is in a capture group, and using split can also return empty strings.
Another option is to be specific about the allowed data format, and instead of split capture the parts in 2 groups.
(\s*\w+:\s*)(#[sae](?:\[[\w{}=, ]*])?)
The pattern matches:
( Capture group 1
\s*\w+:\s* Match 1+ word characters and : between optional whitespace chars
) Close group
( Capture group 2
#[sae] Match # followed by either s a e
(?:\[[\w{}=, ]*])? Optionally match [...]
) Close group
Example code:
import re
sample = """Name: #s
Owner: #a[tag=Admin]"""
target = r"(\s*\w+:\s*)(#[sae](?:\[[\w{}=, ]*])?)"
listOfTuples = re.findall(target, sample)
lst = [s for tpl in listOfTuples for s in tpl]
print(lst)
Output
['Name: ', '#s', '\nOwner: ', '#a[tag=Admin]']
See a regex demo and a Python demo.

Python regex - substitute until certain character

I am looking to replace spaces with commas, but up to first / and tried the following:
import re
txt = "usera 28935 28876 0 Apr25 ? 00:07:20 /xxx/yyyy/foo/bar/zzzzz/Java/jdk-1.8.0_101/xxx/xxx -cp /xxx/yyyy/foo/bar/zzzzz"
rem = (re.sub(' +', ' ', txt)) # convert multiple spaces into single
print(re.sub(' ', ',', rem.lstrip()))
But the output is - inserts comma after every space!
usera,28935,28876,0,Apr25,?,00:07:20,/xxx/yyyy/foo/bar/zzzzz/Java/jdk-1.8.0_101/xxx/xxx,-cp,/xxx/yyyy/foo/bar/zzzzz
Expected Output:
usera,28935,28876,0,Apr25,?,00:07:20,/xxx/yyyy/foo/bar/zzzzz/Java/jdk-1.8.0_101/xxx/xxx -cp /xxx/yyyy/foo/bar/zzzzz
i.e. comma should be applied until the first /
I have tried lookahead, lookbehind but unable to work this out.
Could someone advise me on how to achieve this please?
Whenever you have a problem like this, consider splitting before using a regex
# split the text once at the first /
a, b = txt.split("/", 1)
# do the replacement in the first half
a = re.sub(" +", ",", a)
# join 'em back up
result = "{}/{}".format(a,b)
You can use lookbehind, but it needs to be variable length. So, you'll need third-party regex module:
>>> import regex
>>> txt = "usera 28935 28876 0 Apr25 ? 00:07:20 /xxx/yyyy/foo/bar/zzzzz/Java/jdk-1.8.0_101/xxx/xxx -cp /xxx/yyyy/foo/bar/zzzzz"
>>> regex.sub(r'(?<!/.*) +', ',', txt)
'usera,28935,28876,0,Apr25,?,00:07:20,/xxx/yyyy/foo/bar/zzzzz/Java/jdk-1.8.0_101/xxx/xxx -cp /xxx/yyyy/foo/bar/zzzzz'
# or you can use \G
>>> regex.sub(r'\G([^/ ]*+) +', r'\1,', txt)
'usera,28935,28876,0,Apr25,?,00:07:20,/xxx/yyyy/foo/bar/zzzzz/Java/jdk-1.8.0_101/xxx/xxx -cp /xxx/yyyy/foo/bar/zzzzz'
The first one replaces spaces only if / character is not present earlier in the string.
The second one defines a sequence of other than space or / characters followed by spaces to be matched as many times as possible from the start of the string.

Merging three regex patterns used for text cleaning to improve efficiency

Given a text I want to make some modifications:
replace uppercase chars at the beginning of a sentence.
remove chars like ’ or ' (without adding whitespace)
remove unwanted chars for example ³ or ? , ! . (and replace with whitespace)
def multiple_replace(text):
# first sub so words like can't will change to cant and not can t
first_strip=re.sub("[’']",'',text)
def cap(match):
return (match.group().lower())
p = re.compile(r'((?<=[\.\?!]\s)(\w+)|(^\w+))')
#second sub to change all words that begin a sentence to lowercase
second_strip = p.sub(cap,first_strip)
# third_strip is to remove all . from text unless they are used in decimal numbers
third_strip= re.sub(r'(?<!\d)\.|\.(?!\d)','',second_strip)
# fourth strip to remove unexpected char that might be in text for example !,?³ and replace with whitespace
forth_strip=re.sub('[^A-Za-z0-9##_$&%]+',' ', third_strip)
return forth_strip
I am wondering if there is a more efficient way of doing it? Because I am going over the text 4 times just so it can be in the right format for me to parse. This seems a lot especially if there are millions of documents. Is there a more efficient way of doing this?
You could make use of an alternation to match either an uppercase char A-Z at the start of the string, or after . ? or ! followed by a whitespace char.
I think you can also add a . to the negated character class [^A-Za-z0-9##_$&%.]+ to not remove the dot for a decimal value and change the order of operations to use cap first before removing any dots.
import re
def cap(match):
return match.group().lower()
p = re.compile(r'(?<=[.?!]\s)[A-Z]|^[A-Z]', re.M)
text = "A test here. this `` (*)is. Test, but keep 1.2"
first_strip = p.sub(cap, text)
second_strip = re.sub(r"[`']+|(?<!\d)\.|\.(?!\d)", '', first_strip)
third_strip = re.sub('[^A-Za-z0-9##_$&%.]+', ' ', second_strip)
print(third_strip)
Output
a test here this is test but keep 1.2
Python demo
You could also use a lambda with all 3 patterns and 2 capturing groups checking the group values in the callback, but I think that would not benefit the readability or making it easier to change or test.
import re
p = re.compile(r"(?:((?<=[.?!]\s)[A-Z]|^[A-Z])|[`']+|((?<!\d)\.|\.(?!\d))|[^A-Za-z0-9##_$&%.]+)", re.M)
text = "A test here. this `` (*)is. Test, but keep 1.2"
result = re.sub(p, lambda x: x.group(1).lower() if x.group(1) else ('' if x.group(2) else ' '), text)
print(result)
Output
a test here this is test but keep 1.2
Python demo

How to replace .. in a string in python

I am trying to replace this string to become this
import re
s = "haha..hehe.hoho"
s = re.sub('[..+]+',' ', s)
my output i get haha hehe hoho
desired output
haha hehe.hoho
What am i doing wrong?
Test on sites like regexpal: http://regexpal.com/
It's easier to get the output and check if the regex is right.
You should change your regex to something like: '\.\.' if you want to remove only double dots.
If you want to remove when there's at least 2 dots you can use '\.{2,}'.
Every character you put inside a [] will be checked against your expression
And the dot character has a special meaning on a regex, to avoid this meaning you should prefix it with a escape character: \
You can read more about regular expressions metacharacters here: https://www.hscripts.com/tutorials/regular-expression/metacharacter-list.php
[a-z] A range of characters. Matches any character in the specified
range.
. Matches any single character except "n".
\ Specifies the next character as either a special character, a literal, a back reference, or an octal escape.
Your new code:
import re
s = "haha..hehe.hoho"
#pattern = '\.\.' #If you want to remove when there's 2 dots
pattern = '\.{2,}' #If you want to remove when there's at least 2 dots
s = re.sub(pattern, ' ', s)
Unless you are constrained to use regex, then I find the replace() function much simpler:
s = "haha..hehe.hoho"
print s.replace('..',' ')
gives your desired output:
haha hehe.hoho
Change:
re.sub('[..+]+',' ', s)
to:
re.sub('\.\.+',' ', s)
[..+]+ , this meaning in regex is that use the any in the list at least one time. So it matches the .. as well as . in your input. Make the changes as below:
s = re.sub('\.\.+',' ', s)
[] is a character class and will match on anything in it (meaning any 1 .).
I'm guessing you used it because a simple . wouldn't work, because it's a meta character meaning any character. You can simply escape it to mean a literal dot with a \. As such:
s = re.sub('\.\.',' ', s)
Here is what your regex means:
So, you allow for 1 or more literal periods or plus symbols, which is not the case.
You do not have to repeat the same symbol when looking for it, you can use quantifiers, like {2}, which means "exactly 2 occurrences".
You can use split and join, see sample working program:
import re
s = "haha..hehe.hoho"
s = " ".join(re.split(r'\.{2}', s))
print s
Output:
haha hehe.hoho
Or you can use the sub with the regex, too:
s = re.sub(r'\.{2}', ' ', "haha..hehe.hoho")
In case you have cases with more than 2 periods, you should use \.{2,} regex.

regular expression to split

I try to understand the regex in python. How can i split the following sentence with regular expression?
"familyname, Givenname A.15.10"
this is like the phonebook in python regex http://docs.python.org/library/re.html. The person maybe have 2 or more familynames and 2 or more givennames. After the familynames exist ', ' and after givennames exist ''. the last one is the office of the person. What i did until know is
import re
file=open('file.txt','r')
data=file.readlines()
for i in range(90):
person=re.split('[,\.]',data[i],maxsplit=2)
print(person)
it gives me a result like this
['Wegner', ' Sven Ake G', '15.10\n']
i want to have something like
['Wegner', ' Sven Ake', 'G', '15', '10']. any idea?
In the regex world it's often easier to "match" rather than "split". When you're "matching" you tell the RE engine directly what kinds of substrings you're looking for, instead of concentrating on separating characters. The requirements in your question are a bit unclear, but let's assume that
"surname" is everything before the first comma
"name" is everything before the "office"
"office" consists of non-space characters at the end of the string
This translates to regex language like this:
rr = r"""
^ # begin
([^,]+) # match everything but a comma
(.+?) # match everything, until next match occurs
(\S+) # non-space characters
$ # end
"""
Testing:
import re
rr = re.compile(rr, re.VERBOSE)
print rr.findall("de Batz de Castelmore d'Artagnan, Charles Ogier W.12.345")
# [("de Batz de Castelmore d'Artagnan", ', Charles Ogier ', 'W.12.345')]
Update:
rr = r"""
^ # begin
([^,]+) # match everything but a comma
[,\s]+ # a comma and spaces
(.+?) # match everything until the next match
\s* # spaces
([A-Z]) # an uppercase letter
\. # a dot
(\d+) # some digits
\. # a dot
(\d+) # some digits
\s* # maybe some spaces or newlines
$ # end
"""
import re
rr = re.compile(rr, re.VERBOSE)
s = 'Wegner, Sven Ake G.15.10\n'
print rr.findall(s)
# [('Wegner', 'Sven Ake', 'G', '15', '10')]
What you want to do is first split the family name by ,
familyname, rest = text.split(',', 1)
Then you want to split the office with the first space from the right.
givenname, office = rest.rsplit(' ', 1)
Assuming that family names don't have a comma, you can take them easily. Given names are sensible to dots. For example:
Harney, PJ A.15.10
Harvey, P.J. A.15.10
This means that you should probably trim the rest of the record (family names are out) by a mask at the end (regex "maskpattern$").

Categories

Resources