73
votes

Can a regular expression match whitespace or the start of a string?

I'm trying to replace currency the abbreviation GBP with a £ symbol. I could just match anything starting GBP, but I'd like to be a bit more conservative, and look for certain delimiters around it.

>>> import re
>>> text = u'GBP 5 Off when you spend GBP75.00'

>>> re.sub(ur'GBP([\W\d])', ur'£\g<1>', text) # matches GBP with any prefix
u'\xa3 5 Off when you spend \xa375.00'

>>> re.sub(ur'^GBP([\W\d])', ur'£\g<1>', text) # matches at start only
u'\xa3 5 Off when you spend GBP75.00'

>>> re.sub(ur'(\W)GBP([\W\d])', ur'\g<1>£\g<2>', text) # matches whitespace prefix only
u'GBP 5 Off when you spend \xa375.00'

Can I do both of the latter examples at the same time?

8
Which language is this? Is it Perl? - Hosam Aly
Yes Python, but the concept is the same regardless. - Mat
Then how about tagging the question with "python", and maybe include it somewhere in the question? It would help others who don't know the language, and can help people when searching google. - Hosam Aly
I do normally, but regular expressions trangress languages to an extent. The question was more about the regular expression than the Python syntax. - Mat
Regex capabilities and syntax vary a great deal from one flavor to the next, so you should always include that info. One thing that doesn't vary, though, is that \W matches anything that's not a word character, ie, [^A-Za-z0-9_]; the shorthand for whitespace is \s. - Alan Moore

8 Answers

72
votes

Use the OR "|" operator:

>>> re.sub(r'(^|\W)GBP([\W\d])', u'\g<1>£\g<2>', text)
u'\xa3 5 Off when you spend \xa375.00'
55
votes

\b is word boundary, which can be a white space, the beginning of a line or a non-alphanumeric symbol (\bGBP\b).

6
votes

This replaces GBP if it's preceded by the start of a string or a word boundary (which the start of a string already is), and after GBP comes a numeric value or a word boundary:

re.sub(u'\bGBP(?=\b|\d)', u'£', text)

This removes the need for any unnecessary backreferencing by using a lookahead. Inclusive enough?

4
votes

A left-hand whitespace boundary - a position in the string that is either a string start or right after a whitespace character - can be expressed with

(?<!\S)   # A negative lookbehind requiring no non-whitespace char immediately to the left of the current position
(?<=\s|^) # A positive lookbehind requiring a whitespace or start of string immediately to the left of the current position
(?:\s|^)  # A non-capturing group matching either a whitespace or start of string 
(\s|^)    # A capturing group matching either a whitespace or start of string

See a regex demo. Python 3 demo:

import re
rx = r'(?<!\S)GBP([\W\d])'
text = 'GBP 5 Off when you spend GBP75.00'
print( re.sub(rx, r'£\1', text) )
# => £ 5 Off when you spend £75.00

Note you may use \1 instead of \g<1> in the replacement pattern since there is no need in an unambiguous backreference when it is not followed with a digit.

BONUS: A right-hand whitespace boundary can be expressed with the following patterns:

(?!\S)   # A negative lookahead requiring no non-whitespace char immediately to the right of the current position
(?=\s|$) # A positive lookahead requiring a whitespace or end of string immediately to the right of the current position
(?:\s|$)  # A non-capturing group matching either a whitespace or end of string 
(\s|$)    # A capturing group matching either a whitespace or end of string
2
votes

I think you're looking for '(^|\W)GBP([\W\d])'

0
votes

You can always trim leading and trailing whitespace from the token before you search if it's not a matching/grouping situation that requires the full line.

0
votes

Yes, why not?

re.sub(u'^\W*GBP...

matches the start of the string, 0 or more whitespaces, then GBP...

edit: Oh, I think you want alternation, use the |:

re.sub(u'(^|\W)GBP...
0
votes

It works in Perl:

$text = 'GBP 5 off when you spend GBP75';
$text =~ s/(\W|^)GBP([\W\d])/$1\$$2/g;
printf "$text\n";

The output is:

$ 5 off when you spend $75

Note that I stipulated that the match should be global, to get all occurrences.