regex in python - how to understand this ip lable without parentheses

Viewed 71

I have this code to check if a string is a valid IPv4 address:

import re

def is_ip4(IP):
    label = "([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])"
    pattern = re.compile("(" + label + "\.){3}" + label + "$")
    if pattern.match(IP):
        print("matched!")
    else:
        print("No!")

it works fine. but if I remove the parentheses from the label, as this

import re

def is_ip4(IP):
    label = "[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]"
    pattern = re.compile("(" + label + "\.){3}" + label + "$")
    if pattern.match(IP):
        print("matched!")
    else:
        print("No!")

it show valid ip for "2090.1.11.0", "20.1.11.0", but not for "2.1.11.0". I'm actually a bit confused for the cases with vs without parentheses. Can someone explain this for me? thanks

1 Answers

The reason you need the parentheses is because of the two-step process you're using. By itself, the parentheses don't do anything (other than capturing in a group). But you're also doing this:

pattern = re.compile("(" + label + "\.){3}" + label + "$")

The label regex is copied twice, first for three repetitions followed by a period. That copy is fine (almost), because in the statement, it is enclosed in parentheses once more. However, the second copy is outside any parentheses, so you end up with a regex like (simplified):

pattern == '(a|ab|abc\.){3}a|ab|abc$'

This matches if either (a|ab|abc\.){3}a matches, or ab or abc. With parentheses, it would be like:

pattern == '((a|ab|abc)\.){3}(a|ab|abc)$'

So, although the parentheses appear superfluous, they are not for two reasons. They are keeping the period separate from the last option abc and they are keeping the final choices together and apart from the first bit.

However, you shouldn't be doing this in the first place. Just use:

from ipaddress import ip_address

def is_ip4(ip):
    try:
        ip_address(ip)
        return True
    except ValueError:
        return False

No installation required, it's a standard library.

The reason you get a match for '2090.1.11.0' is because matching it to this:

'([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]\\.){3}[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]$'

Comes down to matching it to this:

'([0-9]){3}[0-9]'

Since, [0-9] is the first option in the 'or' expression in parentheses, repeated three times and the second [0-9] is the first option in the 'or' expression after the {3}.

Note that the $ you put in to ensure the entire string was matches is lumped in with the final 'or' option, so that doesn't do anything here.

Try running the below and note the identical first match:

import re

print(re.findall('([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]\\.){3}[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]$', '2090.1.11.0'))
print(re.findall('([0-9]){3}[0-9]', '2090.1.11.0'))

(ignore the second match on the first line, not as relevant)

Related