Програмиране с Python: Регулярни изрази
Author: Димитър Димитров

# Регулярни изрази

### „ Програмиране с Python“, ФМИ

#### 24.03.2008г.

# Задача

Искаме да се уверим, че подаденият ни телефонен номер е в следния формат: `[02] 5–7 цифри`

С други думи:

- В началото може да има 02, а може и да няма.
- Може да има дължина между 5 и 7 цифри.
- Разбира се, трябва да е съставен само от цифри.

Например, валидни номера са `02555469`, както и `987654`.

# Решение

    # 15-минутно
    # формат: [02] 5-7 цифри
    def validate_phone_str(number):
        if '02' == number[:2]:
            return validate_phone_str(number[2:])
        if all([c.isdigit() for c in number]):
            return 5 <= len(number) <= 7
        return False

Няма ли по-естествено описание на формати за валидация? Е, има.

    # шаблон, който се търси
    def validate_phone_re(number):
    	return bool(re.search(r'^(02)?\d{5,7}$', number))

# Накратко за регулярните изрази

- Търсят дадения шаблон (pattern) в низ.
- Изключително гъвкав синтаксис за задаване на шаблоните.
- За да ги използваме, ни е необходим модулът `re`.
- Пример:

      import re
      re.search('pattern', 'Find the pattern...')

- За запознатите с парадигмата регулярен израз, ще дадем една задачка.

# Задачката

Всичко е чудесно, но аз вече съм майстор на регулярните изрази.

За всички вас имаме следната задача:

> Да се провери дали дадено число е просто чрез регулярен израз. Разрешени операции са:
> - `re.search` с подходящ шаблон.
> - Употребата на низа '1'.
> - Операторa `*`.

# Големият тест

    def testRun():
        samples = "01234567 523456 02789a012 027890012 023456 02023456".split()
        for number in samples:
            print "%s %s %s" % (number.ljust(10),
                                str(validate_phone_str(number)).ljust(6),
                                validate_phone_re(number))

    >>> testRun()
    01234567   False  False
    523456     True   True
    02789a012  False  False
    027890012  True   True
    023456     False  True
    02023456   False  True

Хм. Я гледай ти... Къде сбъркахме?

# Решение

    # Грешното ни, 15-минутно решение
    def validate_phone_str(number):
        if '02' == number[:2]:
            return validate_phone_str(number[2:])
        if all([c.isdigit() for c in number]):
            return 5 <= len(number) <= 7
        return False

    # Едноминутното решение, работещо коректно.
    def validate_phone_re(number):
    	return bool(re.search(r'^(02)?\d{5,7}$', number))

Какво правим сега?

- Да си оправим нашата `validate_phone_str()`.
  Ами ако номерата имат вида `(+359|00359|02)?[0-9]{5,7}`?
- Да се запознаем с новата материя.

# Задаване на шаблон

- Всеки символ, освен някои специални, означава себе си.
- Специални символи: `. | ( ) [ ] { } + \ ^ $ * ?`.
- `\` пред специален символ го прави неспециален такъв.
- Пример (дефиницията на `matcher()` — по-късно):

      matcher('pat', 'Find a pattern.') # 'Find a pattern.'
      matcher('#', 'What ###?')         # 'What ###?'

# Повторения (quantifiers)

Важат за непосредствено предхождащия ги символ/клас/група. Нека го означим с `s`.

- `s*` означава нула или повече повторения на `s`.
- `s+` търси едно или повече повторения на `s`.
- `s?` съвпада с нула или едно повторение на `s`.
- `s{m,n}` означава между `m` и `n` повторения на `s`, където можем да пропуснем `m` или `n`. `s{,n}` има смисъл на нула до `n` повторения, а `s{m,}` — поне `m` повторения.
- Примери

    matcher('o+', 'Goooooooogle')           # 'Goooooooogle'
    matcher('[hH]o+', 'Hohohoho...')        # 'Hohohoho...'
    # Хм. Не искахме точно това. По-скоро:
    matcher('([hH]o)+', 'Hohohoho...')      # 'Hohohoho...'
    matcher('([hH]o){2,3}', 'Hohohoho...')  # 'Hohohoho...'

По подразбиране — алчно търсене за съвпадение (greedy). Деактивира се с `?` след квантора.

    matcher('[hH]o+', 'Hoooooohohooo...')   # 'Hoooooohohooo...'
    matcher('[hH]o+?', 'Hoooooohohooo...')  # 'Hoooooohohooo...'

# Значения на специалните символи

- `.` съвпада с един произволен символ
  По подразбиране символите за нов ред не се включват в тази група.
- `^` съвпада с началото на низ (или на ред, ако се работи в MULTILINE режим.)
- `$` съвпада с края на низ (или на ред, ако се работи в MULTILINE режим.)
- `|` има смисъл на или, например:

      matcher('day|nice', 'A nice dance-day.')   # 'A nice dance-day.'
      matcher('da(y|n)ce', 'A nice dance-day.')  # 'A nice dance-day.'

  NB! Единствено `|` се прилага не над непосредствените му символи/класове, а на целия низ отляво/отдясно. Например:

      matcher('m|c|n', 'abcdef')      # 'abcdef'
      matcher('(ab)|c|e', 'abcdef')   # 'abcdef'
      matcher('m[a-c]|c|e', 'abcdef') # 'abcdef'

# Символни класове

- Набор от символи, заграден от `[` и `]`, например `[aeoui]`.
- Съвпадат с точно един от символите, описани в класа, например:

      >>> matcher('[aeoui]', 'Google')
      'Google'

- Отрицание на клас — посредством `^` в началото на класа:

      >>> matcher('[^CBL][aeoui]', 'Cobol')
      'Cobol'

- Диапазони от символи:

      >>> matcher('[0-9]{1,3}-[a-z]', 'Figure 42-b')
      'Figure 42-b'
      >>> matcher('[^a-zA-Z-]', 'Figure-42-b')
      'Figure-42-b'

# Предефинирани класове

- `\d` — една цифра; същото като `[0-9]`.
- `\D` — един символ, който не е цифра; същото като `[^0-9]`.
- `\s` — един whitespace символ — `[\t\r\n\f\v]`.
- `\S` — един символ, който не е whitespace — `[^\t\r\n\f\v]`.
- `\w` — една буква или цифра.
- `\W` — един символ, който не е буква или цифра.
- `\b` — нула символа, но граница на дума.
- И други. Примери:

      matcher(r'\d+', 'Phone number: 5551234')  # 'Phone number: 5551234'
      matcher(r'\w+', 'Phone number: 5551234')  # 'Phone number: 5551234'
      matcher(r'\s+', 'Phone number: 5551234')  # 'Phone number: 5551234'

# Групи

Групите са частите от даден шаблон, оградени в `(` и `)`. Към тях можем да се обръщаме и от самия шаблон чрез специалните класове `\1` — първата група, `\2` — втората и така нататък. Няколко примера:

    matcher(r'(\w+).*\1', 'Matches str if str repeats one of its words.');
    'Matches str if str repeats one of its words.'

    # Хм. Не точно. Нека опитаме пак:
    matcher(r'(\b\w+\b).*\1', 'Matches str if str repeats one of its words.');
    'Matches str if str repeats one of its words.'

# Функции на модула `re`

- `re.search(pattern, string, flags=0)`
- `re.match(pattern, string, flags=0)`
- `re.findall(pattern, string, flags=0)`
- `re.split(pattern, string, maxsplit=0)`
- `re.sub(pattern, repl, string, count=0)`
- `re.compile(pattern, flags=0)`
- Примери

# Флагове

- `re.I` (re.IGNORECASE) — Perform case-insensitive matching.
- `re.L` (re.LOCALE) — Make \w, \W, \b, \B, dependent on the current locale.
- `re.M` (re.MULTILINE) — "^" matches the beginning of lines as well as the string.
  "$" matches the end of lines as well as the string.
- `re.S` (re.DOTALL) — "." matches any character at all, including the newline.
- `re.X` (re.VERBOSE) — Ignore whitespace and comments for nicer looking RE's.
- `re.U` (re.UNICODE) — Make \w, \W, \b, \B, dependent on the Unicode locale.
- Примери

# Кодът на `matcher()`

    def matcher(regex, string):
        match = re.search(regex, string)
        if match is None: return string
        start, end = match.span()
        return string[:start]
               + '«' + string[start:end] + '»' +
               string[end:]

# Въпроси?

- `help(re)`
- http://xkcd.com/208/