Look behinds: all the rage in regex?

╄→尐↘猪︶ㄣ 提交于 2021-02-04 22:35:48

问题


Many regex questions lately have some kind of look-around element in the query that appears to me is not necessary to the success of the match. Is there some teaching resource that is promoting them? I am trying to figure out what kinds of cases you would be better off using a positive look ahead/behind. The main application I can see is when trying to not match an element. But, for example, this query from a recent question has a simple solution to capturing the .*, but why would you use a look behind?

(?<=<td><a href="\/xxx\.html\?n=[0-9]{0, 5}">).*(?=<\/a><span

And this one from another question:

$url = "www.example.com/id/1234";
preg_match("/\d+(?<=id\/[\d])/",$url,$matches);

When is it truly better to use a positive look-around? Can you give some examples?

I realize this is bordering on an opinion-based question, but I think the answers would be really instructive. Regex is confusing enough without making things more complicated... I have read this page and am more interested in some simple guidelines for when to use them rather than how they work.


Thanks for all the replies. In addition to those below, I recommend checking out m.buettner's great answer here.


回答1:


  1. You can capture overlapping matches, and you can find matches which could lie in the lookarounds of other matches.
  2. You can express complex logical assertions about your match (because many engines let you use multiple lookbehind/lookahead assertions which all must match in order for the match to succeed).
  3. Lookaround is a natural way to express the common constraint "matches X, if it is followed by/preceded by Y". It is (arguably) less natural to add extra "matching" parts that have to be thrown out by postprocessing.

Negative lookaround assertions, of course, are even more useful. Combined with #2, they can allow you do some pretty wizard tricks, which may even be hard to express in usual program logic.


Examples, by popular request:

  • Overlapping matches: suppose you want to find all candidate genes in a given genetic sequence. Genes generally start with ATG, and end with TAG, TAA or TGA. But, candidates could overlap: false starts may exist. So, you can use a regex like this:

    ATG(?=((?:...)*(?:TAG|TAA|TGA)))
    

    This simple regex looks for the ATG start-codon, followed by some number of codons, followed by a stop codon. It pulls out everything that looks like a gene (sans start codon), and properly outputs genes even if they overlap.

  • Zero-width matching: suppose you want to find every tr with a specific class in a computer-generated HTML page. You might do something like this:

    <tr class="TableRow">.*?</tr>(?=<tr class="TableRow">|</table>)
    

    This deals with the case in which a bare </tr> appears inside the row. (Of course, in general, an HTML parser is a better choice, but sometimes you just need something quick and dirty).

  • Multiple constraints: suppose you have a file with data like id:tag1,tag2,tag3,tag4, with tags in any order, and you want to find all rows with tags "green" and "egg". This can be done easily with two lookaheads:

    (.*):(?=.*\bgreen\b)(?=.*\begg\b)
    



回答2:


There are two great things about lookaround expressions:

  • They are zero-width assertions. They require to be matched, but they consume nothing of the input string. This allows to describe parts of the string which will not be contained in a match result. By using capturing groups in lookaround expressions, they are the only way to capture parts of the input multiple times.
  • They simplify a lot of things. While they do not extend regular languages, they easily allow to combine (intersect) multiple expressions to match the same part of a string.



回答3:


Well one simple case where they are handy is when you are anchoring the pattern to the start or finish of a line, and just want to make sure that something is either right ahead or behind the pattern you are matching.




回答4:


I try to address your points:

  • some kind of look-around element in the query that appears to me is not necessary to the success of the match

    Of course they are necessary for the match. As soon as a lookaround assertions fails, there is no match. They can be used to ensure conditions around the pattern, that have additionally to be true. The whole regex does only match, if:

    1. The pattern does fit and

    2. The lookaround assertions are true.

    ==> But the returned match is only the pattern.

  • When is it truly better to use a positive look-around?

    Simple answer: when you want stuff to be there, but you don't want to match it!

    As Bergi mentioned in his answer, they are zero width assertions, this means they don't match a character sequence, they just ensure it is there. So the characters inside a lookaround expression are not "consumed", the regex engine continues after the last "consumed" character.

  • Regarding your first example:

    (?<=<td><a href="\/xxx\.html\?n=[0-9]{0, 5}">).*(?=<\/a><span
    

    I think there is a misunderstanding on your side, when you write "has a simple solution to capturing the .*". The .* is not "captured", it is the only thing that the expression does match. But only those characters are matched that have a "<td><a href="\/xxx\.html\?n=[0-9]{0, 5}">" before and a "<\/a><span" after (those two are not part of the match!).

    "Captured" is only something that has been matched by a capturing group.

  • The second example

    \d+(?<=id\/[\d])
    

    Is interesting. It is matching a sequence of digits (\d+) and after the sequence, the lookbehind assertion checks if there is one digit with "id/" before it. Means it will fail if there is more than one digit or if the text "id/" before the digit is missing. Means this regex is matching only one digit, when there is fitting text before.

  • teaching resources

    • www.regular-expressions.info

    • perlretut on Looking ahead and looking behind




回答5:


I'm assuming you understand the good uses of lookarounds, and ask why they are used with no apparent reason.

I think there are four main categories of how people use regular expressions:

Validation
Validation is usually done on the whole text. Lookarounds like you describe are not possible.

Match
Extracting a part of the text. Lookarounds are used mainly due to developer laziness: avoiding captures.
For example, if we have in a settings file with the line Index=5, we can match /^Index=(\d+)/ and take the first group, or match /(?<=^Index=)\d+/ and take everything.
As other answers said, sometimes you need overlapping between matches, but these are relatively rare.

Replace
This is similar to match with one difference: the whole match is removed and is being replaced with a new string (and some captured groups).
Example: we want to highlight the name in "Hi, my name is Bob!".
We can replace /(name is )(\w+)/ with $1<b>$2</b>,
but it is neater to replace /(?<=name is )\w+/ with <b>$&</b> - and no captures at all.

Split
split takes the text and breaks it to an array of tokens, with your pattern being the delimiter. This is done by:

  • Find a match. Everything before this match is token.
    • The content of the match is discarded, but:
    • In most flavors, each captured group in the match is also a token (notably not in Java).
  • When there are no more matches, the rest of the text is the last token.

Here, lookarounds are crucial. Matching a character means removing it from the result, or at least separating it from its token.
Example: We have a comma separated list of quoted string: "Hello","Hi, I'm Jim."
Splitting by comma /,/ is wrong: {"Hello", "Hi, I'm Jim."}
We can't add the quote mark, /",/: {"Hello, "Hi, I'm Jim."}
The only good option is lookbehind, /(?<="),/: {"Hello", "Hi, I'm Jim."}

Personally, I prefer to match the tokens rather than split by the delimiter, whenever that is possible.

Conclusion

To answer the main question - these lookarounds are used because:

  • Sometimes you can't match text that need.
  • Developers are shiftless.



回答6:


Lookaround assertions can also be used to reduce backtracking which can be the main cause for a bad performance in regexes.

For example: The regex ^[0-9A-Z]([-.\w]*[0-9A-Z])*@(1) can also be written ^[0-9A-Z][-.\w]*(?<=[0-9A-Z])@(2) using a positive look behind (simple validation of the user name in an e-mail address).

Regex (1) can cause a lot of backtracking essentially because [0-9A-Z] is a subset of [-.\w] and the nested quantifiers. Regex (2) reduces the excessive backtracking, more information here Backtracking, section Controlling Backtracking > Lookbehind Assertions.

For more information about backtracking

  • Best Practices for Regular Expressions in the .NET Framework
  • Optimizing Regular Expression Performance, Part II: Taking Charge of Backtracking
  • Runaway Regular Expressions: Catastrophic Backtracking



回答7:


I typed this a while back but got busy (still am, so I might take a while to reply back) and didn't get around to post it. If you're still open to answers...


Is there some teaching resource that is promoting them?

I don't think so, it's just a coincidence I believe.

But, for example, this query from a recent question has a simple solution to capturing the .*, but why would you use a look behind?

(?<=<td><a href="\/xxx\.html\?n=[0-9]{0, 5}">).*(?=<\/a><span

This is most probably a C# regex, since variable width lookbehinds are not supported my many regex engines. Well, the lookarounds could be certainly avoided here, because for this, I believe it's really simpler to have capture groups (and make the .* lazy as we're at it):

(<td><a href="\/xxx\.html\?n=[0-9]{0,5}">).*?(<\/a><span)

If it's for a replace, or

<td><a href="\/xxx\.html\?n=[0-9]{0,5}">(.*?)<\/a><span

for a match. Though an html parser would definitely be more advisable here.

Lookarounds in this case I believe are slower. See regex101 demo where the match is 64 steps for capture groups but 94+19 = 1-3 steps for the lookarounds.

When is it truly better to use a positive look-around? Can you give some examples?

Well, lookarounds have the property of being zero-width assertions, which mean they don't really comtribute to matches while they contribute onto deciding what to match and also allows overlapping matches.

Thinking a bit about it, I think, too, that negative lookarounds get used much more often, but that doesn't make positive lookarounds less useful!

Some 'exploits' I can find browsing some old answers of mine (links below will be demos from regex101) follow. When/If you see something you're not familiar about, I probably won't be explaining it here, since the question's focused on positive lookarounds, but you can always look at the demo links I provided where there's a description of the regex, and if you still want some explanation, let me know and I'll try to explain as much as I can.

To get matches between certain characters:

In some matches, positive lookahead make things easier, where a lookahead could do as well, or when it's not so practical to use no lookarounds:

Dog sighed. "I'm no super dog, nor special dog," said Dog, "I'm an ordinary dog, now leave me alone!" Dog pushed him away and made his way to the other dog.

We want to get all the dog (regardless of case) outside quotes. With a positive lookahead, we can do this:

\bdog\b(?=(?:[^"]*"[^"]*")*[^"]*$)

to ensure that there are even number of quotes ahead. With a negative lookahead, it would look like this:

\bdog\b(?!(?:[^"]*"[^"]*")*[^"]*"[^"]*$)

to ensure that there are no odd number of quotes ahead. Or use something like this if you don't want a lookahead, but you'll have to extract the group 1 matches:

(?:"[^"]+"[^"]+?)?(\bdog\b)

Okay, now say we want the opposite; find 'dog' inside the quotes. The regex with the lookarounds just need to have the sign inversed, first and second:

\bdog\b(?!(?:[^"]*"[^"]*")*[^"]*$)

\bdog\b(?=(?:[^"]*"[^"]*")*[^"]*"[^"]*$)

But without the lookaheads, it's not possible. the closest you can get is maybe this:

"[^"]*(\bdog\b)[^"]*"

But this doesn't get all the matches, or you can maybe use this:

"[^"]*?(\bdog\b)[^"]*?(?:(\bdog\b)[^"]*?)?"

But it's just not practical for more occurrences of dog and you get the results in variables with increasing numbers... And this is indeed easier with lookarounds, because they are zero width assertions, you don't have to worry about the expression inside the lookaround to match dog or not, or the regex wouldn't have obtained all the occurrences of dog in the quotes.

Of course now, this logic can be extended to groups of characters, such as getting specific patterns between words such as start and end.

Overlapping matches

If you have a string like:

abcdefghijkl

And want to extract all the consecutive 3 characters possible inside, you can use this:

(?=(...))

If you have something like:

1A Line1 Detail1 Detail2 Detail3 2A Line2 Detail 3A Line3 Detail Detail

And want to extract these, knowing that each line starts with #A Line# (where # is a number):

1A Line1 Detail1 Detail2 Detail3
2A Line2 Detail
3A Line3 Detail Detail

You might try this, which fails because of greediness...

[0-9]+A Line[0-9]+(?: \w+)+

Or this, which when made lazy no more works...

[0-9]+A Line[0-9]+(?: \w+)+?

But with a positive lookahead, you get this:

[0-9]+A Line[0-9]+(?: \w+)+?(?= [0-9]+A Line[0-9]+|$)

And appropriately extracts what's needed.

Another possible situation is one where you have something like this:

#ff00fffirstword#445533secondword##008877thi#rdword#

Which you want to convert to three pairs of variables (first of the pair being a # and some hex values (6) and whatever characters after them):

#ff00ff and firstword
#445533 and secondword#
#008877 and thi#rdword#

If there were no hashes inside the 'words', it would have been enough to use (#[0-9a-f]{6})([^#]+), but unfortunately, that's not the case and you have to resort to .*? instead of [^#]+, which doesn't quite yet solve the issue of stray hashes. Positive lookaheads however make this possible:

(#[0-9a-f]{6})(.+?)(?=#[0-9a-f]{6}|$)


Validation & Formatting

Not recommended, but you can use positive lookaheads for quick validations. The following regex for instance allow the entry of a string containing at least 1 digit and 1 lowercase letter.

^(?=[^0-9]*[0-9])(?=[^a-z]*[a-z])

This can be useful when you're checking for character length but have patterns of varying length in the a string, for example, a 4 character long string with valid formats where # indicates a digit and the hyphen/dash/minus - must be in the middle:

##-#
#-##

A regex like this does the trick:

^(?=.{4}$)\d+-\d+

Where otherwise, you'd do ^(?:[0-9]{2}-[0-9]|[0-9]-[0-9]{2})$ and imagine now that the max length was 15; the number of alterations you'd need.

If you want a quick and dirty way to rearrange some dates in the 'messed up' format mmm-yyyy and yyyy-mm to a more uniform format mmm-yyyy, you can use this:

(?=.*(\b\w{3}\b))(?=.*(\b\d{4}\b)).*

Input:

Oct-2013
2013-Oct

Output:

Oct-2013
Oct-2013

An alternative might be to use a regex (normal match) and process separately all the non-conforming formats separately.

Something else I came across on SO was the indian currency format, which was ##,##,###.### (3 digits to the left of the decimal and all other digits groupped in pair). If you have an input of 122123123456.764244, you expect 1,22,12,31,23,456.764244 and if you want to use a regex, this one does this:

\G\d{1,2}\K\B(?=(?:\d{2})*\d{3}(?!\d))

(The (?:\G|^) in the link is only used because \G matches only at the start of the string and after a match) and I don't think this could work without the positive lookahead, since it looks forward without moving the point of replacement.)

Trimming

Suppose you have:

   this    is  a   sentence    

And want to trim all the spaces with a single regex. You might be tempted to do a general replace on spaces:

\s+

But this yields thisisasentence. Well, maybe replace with a single space? It now yields " this is a sentence " (double quotes used because backticks eats spaces). Something you can however do is this:

^\s*|\s$|\s+(?=\s)

Which makes sure to leave one space behind so that you can replace with nothing and get "this is a sentence".

Splitting

Well, somewhere else where positive lookarounds might be useful is where, say you have a string ABC12DE3456FGHI789 and want to get the letters+digits apart, that is you want to get ABC12, DE3456 and FGHI789. You can easily do use the regex:

(?<=[0-9])(?=[A-Z])

While if you use ([A-Z]+[0-9]+) (i.e. the captured groups are put back in the resulting list/array/etc, you will be getting empty elements as well.

Note that this could be done with a match as well, with [A-Z]+[0-9]+


If I had to mention negative lookarounds, this post would have been even longer :)




回答8:


Keep in mind that a positive/negative lookaround is the same for a regex engine. The goal of lookarounds is to perform a check somewhere in your "regular expression".

One of the main interest is to capture something without using capturing parenthesis (capturing the whole pattern), example:

string: aaabbbccc

regex: (?<=aaa)bbb(?=ccc)

(you obtain the result with the whole pattern)

instead of: aaa(bbb)ccc

(you obtain the result with the capturing group.)



来源:https://stackoverflow.com/questions/19105300/look-behinds-all-the-rage-in-regex

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!