{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "ce1f4bc9-0627-41c3-a8bc-3ff1ef055d74",
   "metadata": {},
   "source": [
    "<table align=right>\n",
    "    <tr>\n",
    "        <td align=center>\n",
    "            <font size=6><b>Программирование $-$ 2026.</b></font>  \n",
    "        </td>\n",
    "        <td align=right>\n",
    "<img src=\"logoMM.png\" align=right width=120>\n",
    "        </td>\n",
    "    </tr>\n",
    "    </table>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d690deda",
   "metadata": {},
   "source": [
    "# Введение в регулярные выражения в Python. Модуль `re`\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "544a0bde",
   "metadata": {},
   "source": [
    "---\n",
    "## 1. Зачем нужны регулярные выражения\n",
    "\n",
    "Мы уже умеем искать в строке **конкретный** фрагмент: `in`, `find()`, `replace()`.\n",
    "\n",
    "А как ответить на вопросы:\n",
    "- состоит ли строка только из цифр и является ли записью целого числа со знаком?\n",
    "- есть ли в тексте **какое-нибудь** число, и где оно?\n",
    "- как заменить **любую** последовательность пробелов одним пробелом?\n",
    "\n",
    "Мы ищем не конкретную строку, а строку **определённого вида**. Описание вида строки называется **шаблоном**, или **регулярным выражением**.\n",
    "\n",
    "> Регулярное выражение — это строка, задающая **множество** строк, элементы которого обладают общей структурой (например, соответствуют шаюлону номера телефона, даты рождения, адреса электронно почты и т.п.). "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "17439a4d",
   "metadata": {},
   "source": [
    "***Пример 1***"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "7bbec3a0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "True\n",
      "6\n"
     ]
    }
   ],
   "source": [
    "text = \"Заказ 145 оплачен, доставка через 3 дня\"\n",
    "\n",
    "print(\"145\" in text)      # ищем конкретную строку — легко\n",
    "print(text.find(\"145\"))   # и её позицию\n",
    "\n",
    "# А как найти ЛЮБОЕ число, не зная его заранее?\n",
    "# Пришлось бы писать цикл по символам и вручную проверять isdigit()..."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "211419ca-a5c9-4d08-8f18-5245fb56f25d",
   "metadata": {},
   "source": [
    "***Пример 2***\n",
    "\n",
    "Задача. Пользователь вводит дату. Нужно проверить, что она записана в формате ДД.ММ.ГГГГ."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "f6c140cd-865c-4495-ae32-f6422a00622c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "True False\n"
     ]
    }
   ],
   "source": [
    "# правильно, но громоздко\n",
    "def is_date(s):\n",
    "    if len(s) != 10: \n",
    "        return False\n",
    "    if s[2] != '.' or s[5] != '.': \n",
    "        return False\n",
    "    if not s[0:2].isdigit(): \n",
    "        return False\n",
    "    if not s[3:5].isdigit(): \n",
    "        return False\n",
    "    if not s[6:10].isdigit(): \n",
    "        return False        \n",
    "    return True  \n",
    "\n",
    "print(is_date('17.10.2026'), is_date('1.2.3'))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "73b67a5b-84fc-4935-9ab4-5c8b3f2000fe",
   "metadata": {},
   "source": [
    "С \"регулярками\" — одна строка описания **шаблона**:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "736c3efb-56c5-42d1-8db9-ee32da5ff496",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "True False\n"
     ]
    }
   ],
   "source": [
    "import re\n",
    "\n",
    "def is_date(s):\n",
    "    if re.fullmatch(r'\\d{2}\\.\\d{2}\\.\\d{4}', s):\n",
    "        return True\n",
    "    return False    \n",
    "    \n",
    "print(is_date('17.10.2026'), is_date('1.2.3'))    "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9725bd5c",
   "metadata": {},
   "source": [
    "---\n",
    "## 2. Первый шаблон и модуль `re`\n",
    "\n",
    "Модуль `re` входит в стандартную библиотеку — устанавливать ничего не нужно.\n",
    "\n",
    "В этой лекции рассмотриv **три функции**:\n",
    "\n",
    "| Задача | Функция | Результат |\n",
    "|---|---|---|\n",
    "| Проверить, что **вся** строка соответствует шаблону | `re.fullmatch(шаблон, строка)` | объект совпадения или `None` |\n",
    "| Найти **первый** подходящий фрагмент | `re.search(шаблон, строка)` | объект совпадения или `None` |\n",
    "| Заменить **все** подходящие фрагменты | `re.sub(шаблон, замена, строка)` | новая строка |\n",
    "\n",
    "> Объект совпадения в условии `if` ведёт себя как `True`, а `None` — как `False`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "c4f5b508",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<re.Match object; span=(0, 3), match='кот'>\n",
      "None\n"
     ]
    }
   ],
   "source": [
    "import re\n",
    "\n",
    "print(re.fullmatch(\"кот\", \"кот\"))\n",
    "print(re.fullmatch(\"кот\", \"котик\"))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "653658e4",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Совпадает\n"
     ]
    }
   ],
   "source": [
    "word = \"кот\"\n",
    "if re.fullmatch(\"кот\", word):\n",
    "    print(\"Совпадает\")\n",
    "else:\n",
    "    print(\"Не совпадает\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a4a7366a",
   "metadata": {},
   "source": [
    "Пока шаблон `\"кот\"` ничем не лучше сравнения `word == \"кот\"`. Сила шаблонов — в **специальных символах**."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5fc248d5",
   "metadata": {},
   "source": [
    "---\n",
    "## 3. Из чего составляется шаблон\n",
    "\n",
    "### 3.1. Обычные символы и наборы\n",
    "\n",
    "Обычный символ (буква, цифра) означает сам себя. Квадратные скобки задают **один** символ из набора.\n",
    "\n",
    "| Запись | Значение | Пример подходящих строк |\n",
    "|---|---|---|\n",
    "| `abc` | ровно строка «abc» | `abc` |\n",
    "| `[abc]` | один символ: `a`, `b` или `c` | `a`, `c` |\n",
    "| `[0-9]` | одна цифра (диапазон) | `0`, `7` |\n",
    "| `[a-z]` | одна строчная латинская буква | `q` |\n",
    "| `[а-яё]` | одна строчная русская буква | `ж`, `ё` |\n",
    "| `[a-zA-Z]` | одна латинская буква любого регистра | `G` |\n",
    "| `[^0-9]` | один символ, **не** являющийся цифрой | `x`, `-`, ` ` |\n",
    "| `.` | **любой** один символ (кроме перевода строки) | `a`, `5`, `%` |\n",
    "\n",
    "⚠️ Буква `ё` не входит в диапазон `а-я` — её добавляют отдельно."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "cddf5a00",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<re.Match object; span=(0, 3), match='кот'>\n",
      "<re.Match object; span=(0, 3), match='рот'>\n",
      "None\n",
      "<re.Match object; span=(0, 3), match='кит'>\n",
      "<re.Match object; span=(0, 1), match='a'> None\n"
     ]
    }
   ],
   "source": [
    "print(re.fullmatch(\"[кр]от\", \"кот\"))\n",
    "print(re.fullmatch(\"[кр]от\", \"рот\"))\n",
    "print(re.fullmatch(\"[кр]от\", \"лот\"))\n",
    "print(re.fullmatch(\"к.т\", \"кит\"))\n",
    "print(re.fullmatch(\"[^0-9]\", \"a\"), re.fullmatch(\"[^0-9]\", \"5\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b72e0746",
   "metadata": {},
   "source": [
    "### 3.2. Классы символов\n",
    "\n",
    "Для часто встречающихся наборов есть краткие обозначения:\n",
    "\n",
    "| Запись | Значение | Эквивалент (приблизительно) |\n",
    "|---|---|---|\n",
    "| `\\d` | цифра | `[0-9]` |\n",
    "| `\\D` | не цифра | `[^0-9]` |\n",
    "| `\\s` | пробельный символ (пробел, `\\t`, `\\n`) | `[ \\t\\n]` |\n",
    "| `\\S` | не пробельный символ | |\n",
    "| `\\w` | буква (в т.ч. русская), цифра, `_` | |\n",
    "| `\\W` | инверсия правил для \\w | |\n",
    "| `\\b` | граница слова | |\n",
    "\n",
    "Правило: **заглавная буква — отрицание** строчной."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7b49e66b",
   "metadata": {},
   "source": [
    "### 3.3. Напоминание: сырые строки `r\"...\"` и экранирование\n",
    "\n",
    "Обратная косая черта `\\` это символ экранирования и Python должен его обрабатывать. Чтобы он этого не делал, шаблоны записывают как **сырые строки**: `r\"\\d\"`.\n",
    "\n",
    "Если нужен сам специальный символ (точка, скобка, плюс…), его **экранируют** обратной чертой:\n",
    "\n",
    "| Запись | Значение |\n",
    "|---|---|\n",
    "| `\\.` | символ точки |\n",
    "| `\\+`, `\\*`, `\\?` | символы `+`, `*`, `?` |\n",
    "| `\\(`, `\\)`, `\\[`, `\\]` | скобки |\n",
    "| `\\\\` | обратная косая черта |\n",
    "\n",
    "Внутри `[...]` большинство спецсимволов теряет особый смысл: `[.+]` — это точка или плюс."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "cdb8ccb0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<re.Match object; span=(0, 2), match='42'>\n",
      "None\n",
      "<re.Match object; span=(0, 4), match='3x14'>\n",
      "None\n",
      "<re.Match object; span=(0, 4), match='3.14'>\n"
     ]
    }
   ],
   "source": [
    "print(re.fullmatch(r\"\\d\\d\", \"42\"))\n",
    "print(re.fullmatch(r\"\\d\\d\", \"4a\"))\n",
    "print(re.fullmatch(r\"3.14\", \"3x14\"))    # точка — любой символ!\n",
    "print(re.fullmatch(r\"3\\.14\", \"3x14\"))   # экранированная точка\n",
    "print(re.fullmatch(r\"3\\.14\", \"3.14\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5634595f",
   "metadata": {},
   "source": [
    "🟨 **Вопрос аудитории.** Какие из строк подходят под шаблон `r\"[A-Z]\\d\\d\"`?\n",
    "\n",
    "`\"A12\"`, `\"a12\"`, `\"B7\"`, `\"Z00\"`, `\"Q123\"`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "46186ed3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "A12 True\n",
      "a12 False\n",
      "B7 False\n",
      "Z00 True\n",
      "Q123 False\n"
     ]
    }
   ],
   "source": [
    "# проверим\n",
    "for s in \"A12\", \"a12\", \"B7\", \"Z00\", \"Q123\":\n",
    "    print(s, bool(re.fullmatch(r\"[A-Z]\\d\\d\", s)))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b87020cf",
   "metadata": {},
   "source": [
    "---\n",
    "## 4. Повторения и варианты\n",
    "\n",
    "### 4.1. Квантификаторы\n",
    "\n",
    "Квантификатор ставится **после** символа (набора, класса, группы) и указывает, сколько раз он повторяется.\n",
    "\n",
    "| Запись | Число повторений | Пример | Подходит |\n",
    "|---|---|---|---|\n",
    "| `?` | 0 или 1 | `-?\\d` | `5`, `-5` |\n",
    "| `*` | 0 и более | `\\d*` | ``, `7`, `123` |\n",
    "| `+` | 1 и более | `\\d+` | `7`, `123` |\n",
    "| `{n}` | ровно n | `\\d{3}` | `123` |\n",
    "| `{m,n}` | от m до n | `\\d{2,4}` | `12`, `1234` |\n",
    "| `{m,}` | не менее m | `\\d{2,}` | `12`, `12345` |\n",
    "\n",
    "⚠️ Квантификатор относится **только к предыдущему элементу**: `ab+` — это `a`, затем одна или более `b`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "21e35063-5cdd-4adb-abc7-d6ddafce1a1d",
   "metadata": {},
   "source": [
    "#### Вспомогательная функция `check`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "e4a5eb3f-6478-4417-ab37-aae3921e9763",
   "metadata": {},
   "outputs": [],
   "source": [
    "def check(pattern, strings):\n",
    "    \"\"\"Печатает, какие строки целиком соответствуют шаблону.\"\"\"\n",
    "    for s in strings:\n",
    "        print(f\"{s!r:20}\", \"✓\" if re.fullmatch(pattern, s) else \"✗\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "822a687a-2d39-44a7-9a59-7ddc0626035e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "123\n",
      "'123'\n",
      "'123'\n"
     ]
    }
   ],
   "source": [
    "# пояснение 1\n",
    "s = '123'\n",
    "print(s)\n",
    "print(repr(s))\n",
    "print(f'{s!r}')      "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "ec474980-8805-4c2a-9c7a-e0e49d641612",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "3"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# пояснение 2 - тернарный оператор\n",
    "x = 1\n",
    "x = 3 if x>0 else -3\n",
    "x "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "85d71c63",
   "metadata": {},
   "source": [
    "Построим **шаблон целого числа** постепенно."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "f05dc4ff",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Шаг 1: r'\\d+'\n",
      "'7'                  ✓\n",
      "'42'                 ✓\n",
      "'-42'                ✗\n",
      "'+5'                 ✗\n",
      "''                   ✗\n",
      "'4-2'                ✗\n",
      "'--3'                ✗\n"
     ]
    }
   ],
   "source": [
    "tests = \"7\", \"42\", \"-42\", \"+5\", \"\", \"4-2\", \"--3\"\n",
    "\n",
    "print(r\"Шаг 1: r'\\d+'\") \n",
    "check(r\"\\d+\", tests)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "4e4bad4e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Шаг 2: r'-?\\d+'\n",
      "'7'                  ✓\n",
      "'42'                 ✓\n",
      "'-42'                ✓\n",
      "'+5'                 ✗\n",
      "''                   ✗\n",
      "'4-2'                ✗\n",
      "'--3'                ✗\n"
     ]
    }
   ],
   "source": [
    "print(r\"Шаг 2: r'-?\\d+'\")\n",
    "check(r\"-?\\d+\", tests)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "08543bdd",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Шаг 3: r'[+-]?\\d+'\n",
      "'7'                  ✓\n",
      "'42'                 ✓\n",
      "'-42'                ✓\n",
      "'+5'                 ✓\n",
      "''                   ✗\n",
      "'4-2'                ✗\n",
      "'--3'                ✗\n"
     ]
    }
   ],
   "source": [
    "print(r\"Шаг 3: r'[+-]?\\d+'\"); check(r\"[+-]?\\d+\", tests)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e6387bcb",
   "metadata": {},
   "source": [
    "### 4.2. Скобки и варианты\n",
    "\n",
    "| Запись | Значение | Пример | Подходит |\n",
    "|---|---|---|---|\n",
    "| `(...)` | объединить часть шаблона в единое целое | `(ab)+` | `ab`, `abab` |\n",
    "| `A\\|B` | вариант: A **или** B | `кот\\|пёс` | `кот`, `пёс` |\n",
    "| `(A\\|B)` | вариант внутри большего шаблона | `(ко\\|ки)т` | `кот`, `кит` |\n",
    "\n",
    "`|` имеет самый низкий приоритет: `ab|cd` означает «`ab` или `cd`», а не «`a`, затем `b` или `c`, затем `d`»."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "28417fe8",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "'ha'                 ✓\n",
      "'hahaha'             ✓\n",
      "'hah'                ✗\n",
      "''                   ✗\n",
      "\n",
      "'котик'              ✓\n",
      "'пёсик'              ✓\n",
      "'кот'                ✗\n",
      "'кит'                ✗\n"
     ]
    }
   ],
   "source": [
    "check(r\"(ha)+\", (\"ha\", \"hahaha\", \"hah\", \"\"))\n",
    "print()\n",
    "check(r\"(кот|пёс)ик\", (\"котик\", \"пёсик\", \"кот\", \"кит\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f036d3fe",
   "metadata": {},
   "source": [
    "🟩 **Задача.** Опишите шаблон десятичного числа: необязательный знак, целая часть, необязательная дробная часть через точку. \n",
    "\n",
    "Подходят: `3`, `-3.14`, `+0.5`. Не подходят: `3.`, `.5`, `3,14`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "41bfcdcb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "'3'                  ✓\n",
      "'-3.14'              ✓\n",
      "'+0.5'               ✓\n",
      "'3.'                 ✗\n",
      "'.5'                 ✗\n",
      "'3,14'               ✗\n",
      "'1.2.3'              ✗\n"
     ]
    }
   ],
   "source": [
    "number = r\"[+-]?\\d+(\\.\\d+)?\"\n",
    "check(number, (\"3\", \"-3.14\", \"+0.5\", \"3.\", \".5\", \"3,14\", \"1.2.3\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0deede8b",
   "metadata": {},
   "source": [
    "Разбор:\n",
    "- `[+-]?` — знак, может отсутствовать;\n",
    "- `\\d+` — хотя бы одна цифра;\n",
    "- `(\\.\\d+)?` — группа «точка и цифры» целиком необязательна."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1b1d1219",
   "metadata": {},
   "source": [
    "---\n",
    "## 5. Поиск фрагмента: `re.search()`\n",
    "\n",
    "`re.fullmatch()` требует совпадения **всей** строки. `re.search()` ищет **первый** подходящий фрагмент в любом месте.\n",
    "\n",
    "Методы объекта совпадения:\n",
    "\n",
    "| Метод | Результат |\n",
    "|---|---|\n",
    "| `m.group()` | найденный текст |\n",
    "| `m.start()` | индекс начала фрагмента |\n",
    "| `m.end()` | индекс за последним символом фрагмента |\n",
    "\n",
    "Значит, `text[m.start():m.end()] == m.group()`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "a718d675",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "None\n",
      "<re.Match object; span=(6, 9), match='145'>\n",
      "145 6 9\n",
      "145\n"
     ]
    }
   ],
   "source": [
    "text = \"Заказ 145 оплачен, доставка через 3 дня\"\n",
    "\n",
    "print(re.fullmatch(r\"\\d+\", text))   # вся строка — не число\n",
    "m = re.search(r\"\\d+\", text)         # но число в ней есть\n",
    "print(m)\n",
    "print(m.group(), m.start(), m.end())\n",
    "print(text[m.start():m.end()])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3039ab87",
   "metadata": {},
   "source": [
    "⚠️ Если ничего не найдено, результат — `None`, и вызов `m.group()` приведёт к ошибке. Всегда сначала проверяйте результат."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "8ee25a28",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Чисел нет\n"
     ]
    }
   ],
   "source": [
    "text = \"В этой строке нет цифр\"\n",
    "m = re.search(r\"\\d+\", text)\n",
    "if m:\n",
    "    print(\"Найдено:\", m.group())\n",
    "else:\n",
    "    print(\"Чисел нет\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fb892142",
   "metadata": {},
   "source": [
    "**ВАЖНО:** `search` находит **первое** совпадение и захватывает **как можно больше** символов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "3cee00a3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "12345\n"
     ]
    }
   ],
   "source": [
    "m = re.search(r\"\\d+\", \"abc 12345 и 678\")\n",
    "print(m.group())     # не \"1\", а \"12345\", и не \"678\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ccd511aa",
   "metadata": {},
   "source": [
    "🟩 **Задача.** Найти в строке первое время в формате `ЧЧ:ММ` и вывести его отдельно часы и минуты."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "5959d6b7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Часы: 9, минуты: 45\n",
      "Время не найдено\n"
     ]
    }
   ],
   "source": [
    "def first_time(text):\n",
    "    m = re.search(r\"\\d\\d:\\d\\d\", text)\n",
    "    if m is None:\n",
    "        return \"Время не найдено\"\n",
    "    t = m.group()\n",
    "    return f\"Часы: {int(t[:2])}, минуты: {int(t[3:])}\"\n",
    "\n",
    "print(first_time(\"Лекция начнётся в 09:45 в аудитории 211\"))\n",
    "print(first_time(\"Лекция завтра\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "645c2ba9",
   "metadata": {},
   "source": [
    "**Важно:** шаблон `\\d\\d:\\d\\d` пропустит и строку `99:99`, не имеющую смысла `время`. \\\n",
    "Регулярное выражение проверяет **форму** строки; проверку смысла (часы < 24 и тд) часто удобнее делать обычным кодом."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b56fa6d6",
   "metadata": {},
   "source": [
    "---\n",
    "## 6. Замена: `re.sub()`\n",
    "\n",
    "`re.sub(шаблон, замена, строка)` возвращает **новую** строку, в которой **все** непересекающиеся совпадения заменены. Исходная строка не меняется (строки неизменяемы)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "34873cb6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Телефон ***********, код ****\n",
      "Телефон #, код #\n",
      "Телефон 89001234567, код 4512\n"
     ]
    }
   ],
   "source": [
    "text = \"Телефон 89001234567, код 4512\"\n",
    "print(re.sub(r\"\\d\", \"*\", text))     # каждая цифра\n",
    "print(re.sub(r\"\\d+\", \"#\", text))    # каждое число целиком\n",
    "print(text)                          # исходная строка не изменилась"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "8727d4ca",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Слишком много пробелов здесь\n",
      "строка с разными пробелами\n"
     ]
    }
   ],
   "source": [
    "messy = \"Слишком    много     пробелов  здесь\"\n",
    "print(re.sub(r\" +\", \" \", messy))\n",
    "print(re.sub(r\"\\s+\", \" \", \"строка\\tс\\n разными   пробелами\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "008b0fbe",
   "metadata": {},
   "source": [
    "### Итог: три операции\n",
    "\n",
    "| Вопрос | Функция |\n",
    "|---|---|\n",
    "| Вся строка имеет нужный вид? | `re.fullmatch()` |\n",
    "| Есть ли в строке такой фрагмент и где он? | `re.search()` |\n",
    "| Заменить все такие фрагменты | `re.sub()` |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "506c7a2d",
   "metadata": {},
   "source": [
    "---\n",
    "## 7. Как проверять шаблон. Типичные ошибки\n",
    "\n",
    "Шаблон — это описание множества строк. Проверять его нужно на двух наборах:\n",
    "1. строки, которые **должны** подходить;\n",
    "2. строки, которые **не должны** подходить — особенно граничные случаи: пустая строка, лишний символ в начале/конце, минимальная и максимальная длина.\n",
    "\n",
    "| Ошибка | Пример | Что не так |\n",
    "|---|---|---|\n",
    "| `search` вместо `fullmatch` при проверке | `re.search(r\"\\d+\", \"ab12\")` | находит фрагмент, но строка — не число |\n",
    "| Неэкранированная точка | `r\"3.14\"` | подходит `3x14` |\n",
    "| `*` вместо `+` | `r\"\\d*\"` | подходит пустая строка |\n",
    "| Квантификатор не к тому элементу | `r\"ab+\"` вместо `r\"(ab)+\"` | повторяется только `b` |\n",
    "| Обычная строка вместо сырой | `\"\\b\"` | Python превращает `\\b` в символ backspace |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5cbafea6",
   "metadata": {},
   "source": [
    "---\n",
    "## 8. Решение задач\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "31a21a0f",
   "metadata": {},
   "source": [
    "### Задача 1. Прочитать шаблон\n",
    "\n",
    "Опишите словами, какие строки подходят под каждый шаблон, и приведите по одному подходящему и неподходящему примеру.\n",
    "\n",
    "1. `r\"\\d{6}\"`\n",
    "2. `r\"[A-Z][a-z]+\"`\n",
    "3. `r\"(\\+7|8)\\d{10}\"`\n",
    "4. `r\"\\w+\\.py\"`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "2372bcce",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "'344090'             ✓\n",
      "'34409'              ✗\n",
      "'3440900'            ✗\n",
      "\n",
      "'Python'             ✓\n",
      "'python'             ✗\n",
      "'P'                  ✗\n",
      "'PyThon'             ✗\n",
      "\n",
      "'+79001234567'       ✓\n",
      "'89001234567'        ✓\n",
      "'79001234567'        ✗\n",
      "\n",
      "'main.py'            ✓\n",
      "'my_lab2.py'         ✓\n",
      "'main.pyc'           ✗\n",
      "'.py'                ✗\n"
     ]
    }
   ],
   "source": [
    "check(r\"\\d{6}\", (\"344090\", \"34409\", \"3440900\"))                 # почтовый индекс\n",
    "print(); check(r\"[A-Z][a-z]+\", (\"Python\", \"python\", \"P\", \"PyThon\"))   # слово с заглавной\n",
    "print(); check(r\"(\\+7|8)\\d{10}\", (\"+79001234567\", \"89001234567\", \"79001234567\")) # номер телефона\n",
    "print(); check(r\"\\w+\\.py\", [\"main.py\", \"my_lab2.py\", \"main.pyc\", \".py\"]) # файл Python"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b474231c",
   "metadata": {},
   "source": [
    "### Задача 2. Номер группы\n",
    "\n",
    "Номер учебной группы имеет вид: **число курса** (1–6), **точка**, **номер группы** из одной или двух цифр. Например: `1.3`, `4.12`.\n",
    "\n",
    "Составьте шаблон и проверьте его на строках:\n",
    "- должны подходить: `1.3`, `4.12`, `6.1`\n",
    "- не должны: `7.1`, `1.123`, `13`, `1,3`, `0.5`, `1.`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "fc4e4b97",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Должны подходить:\n",
      "'1.3'                ✓\n",
      "'4.12'               ✓\n",
      "'6.1'                ✓\n",
      "Не должны подходить:\n",
      "'7.1'                ✗\n",
      "'1.123'              ✗\n",
      "'13'                 ✗\n",
      "'1,3'                ✗\n",
      "'0.5'                ✗\n",
      "'1.'                 ✗\n"
     ]
    }
   ],
   "source": [
    "pattern = r\"[1-6]\\.\\d{1,2}\"   \n",
    "\n",
    "good = \"1.3\", \"4.12\", \"6.1\"\n",
    "bad = \"7.1\", \"1.123\", \"13\", \"1,3\", \"0.5\", \"1.\"\n",
    "print(\"Должны подходить:\"); check(pattern, good)\n",
    "print(\"Не должны подходить:\"); check(pattern, bad)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "720995cc",
   "metadata": {},
   "source": [
    "### Задача 3. Идентификатор Python\n",
    "\n",
    "Напишите функцию `is_identifier(s)`, которая возвращает `True`, если `s` — допустимое имя переменной (латинские буквы, цифры, `_`; не начинается с цифры), иначе `False`.\n",
    "\n",
    "Проверка: `x`, `_tmp`, `value2`, `MAX_N` → `True`; `2x`, `my-var`, `''`, `a b` → `False`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "8f3a49e7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "'x'        True\n",
      "'_tmp'     True\n",
      "'value2'   True\n",
      "'MAX_N'    True\n",
      "'2x'       False\n",
      "'my-var'   False\n",
      "''         False\n",
      "'a b'      False\n"
     ]
    }
   ],
   "source": [
    "def is_identifier(s):\n",
    "    if re.fullmatch(r\"[A-Za-z_][A-Za-z0-9_]*\", s):\n",
    "        return True \n",
    "    return False    \n",
    "\n",
    "for s in \"x\", \"_tmp\", \"value2\", \"MAX_N\", \"2x\", \"my-var\", \"\", \"a b\":\n",
    "    print(f\"{s!r:10}\", is_identifier(s))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c246c3b1",
   "metadata": {},
   "source": [
    "### Задача 4. Первая дата в тексте\n",
    "\n",
    "Найти в тексте первую дату в формате `ДД.ММ.ГГГГ` и вывести её вместе с позицией. Если даты нет — сообщить об этом."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "60187d75",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Дата 25.01.2027 найдена на позициях 18–28\n",
      "Дата не найдена\n",
      "Дата не найдена\n"
     ]
    }
   ],
   "source": [
    "def find_date(text):\n",
    "    m = re.search(r\"\\d{2}\\.\\d{2}\\.\\d{4}\", text)\n",
    "    if m:\n",
    "        print(f\"Дата {m.group()} найдена на позициях {m.start()}–{m.end()}\")\n",
    "    else:\n",
    "        print(\"Дата не найдена\")\n",
    "\n",
    "find_date(\"Экзамен состоится 25.01.2027 в 10:00, пересдача 05.02.2027\")\n",
    "find_date(\"Экзамен скоро\")\n",
    "find_date(\"Версия 1.2.3456\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "70d4e70f",
   "metadata": {},
   "source": [
    "Еще раз: выведена только **первая** дата — `search` останавливается на первом совпадении. Как найти все совпадения, узнаем позже, после темы «Списки»."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fb19aec8",
   "metadata": {},
   "source": [
    "### Задача 5. Маскировка номера карты\n",
    "\n",
    "В строке встречается номер банковской карты: 16 цифр, возможно разбитых пробелами на группы по 4. Замените **весь** номер на строку `**** **** **** ****`.\n",
    "\n",
    "```\n",
    "\"Оплата картой 1234 5678 9012 3456 прошла\"  →  \"Оплата картой **** **** **** **** прошла\"\n",
    "\"Карта 1234567890123456\"                    →  \"Карта **** **** **** ****\"\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "ea5751d6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Оплата картой **** **** **** **** прошла\n",
      "Карта **** **** **** ****\n",
      "Клиент ввел 12345678 90123456\n",
      "Номер заказа 42 0101 0234 1234 98\n",
      "Оплата картой **** **** **** **** прошла\n",
      "Карта **** **** **** ****\n",
      "Клиент ввел **** **** **** ****\n",
      "Номер заказа 42 0101 0234 1234 98\n"
     ]
    }
   ],
   "source": [
    "# Решение зависит от трактовки условия.\n",
    "# 1) Если комбинация вида 12345678 90123456 недопустима\n",
    "def mask_card1(text):\n",
    "    return re.sub(r'\\d{16}|(\\d{4} ){3}\\d{4}'  ,\n",
    "    '**** **** **** ****', text) \n",
    "# 2) \"возможно\" может означать, что где-то есть, где-то нет\n",
    "def mask_card2(text):\n",
    "    return re.sub(r\"(\\d{4} ?){3}\\d{4}\", \"**** **** **** ****\", text)\n",
    "\n",
    "print(mask_card1(\"Оплата картой 1234 5678 9012 3456 прошла\"))\n",
    "print(mask_card1(\"Карта 1234567890123456\"))\n",
    "print(mask_card1(\"Клиент ввел 12345678 90123456\"))\n",
    "print(mask_card1(\"Номер заказа 42 0101 0234 1234 98\"))\n",
    "\n",
    "print(mask_card2(\"Оплата картой 1234 5678 9012 3456 прошла\"))\n",
    "print(mask_card2(\"Карта 1234567890123456\"))\n",
    "print(mask_card2(\"Клиент ввел 12345678 90123456\"))\n",
    "print(mask_card2(\"Номер заказа 42 0101 0234 1234 98\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "32b30a28",
   "metadata": {},
   "source": [
    "### Задача 6. Нормализация текста\n",
    "\n",
    "Привести строку к аккуратному виду: \\\n",
    "заменить любые последовательности пробельных символов одним пробелом, \\\n",
    "убрать пробелы перед знаками препинания `, . ! ?`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "4fe607db",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Привет, мир! Как дела?\n"
     ]
    }
   ],
   "source": [
    "def normalize(text):\n",
    "    text = re.sub(r\"\\s+\", \" \", text)          # 1) сжать пробелы\n",
    "    text = re.sub(r\" ([,.!?])\", r\"\\1\", text)  # 2) убрать пробел перед знаком\n",
    "    return text.strip()                       # убрать начальные и конечные пробелы\n",
    "\n",
    "print(normalize(\"  Привет  ,   мир !\\n\\nКак   дела ?  \"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5b924e1b",
   "metadata": {},
   "source": [
    "**Внимание:** во второй замене используется `\\1` — ссылка на текст, найденный в скобках. \\\n",
    "Это «заглядывание вперёд» в тему групп, которая будет подробно разобрана позже. \\\n",
    "Альтернатива без групп — несколько вызовов `re.sub(r\" ,\", \",\", text)` и т.д."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ec8c5473",
   "metadata": {},
   "source": [
    "---\n",
    "## 9. Итоги\n",
    "\n",
    "**Шаблон** задаёт множество строк. Он строится из:\n",
    "\n",
    "| Элемент | Примеры |\n",
    "|---|---|\n",
    "| обычные символы | `a`, `7`, `:` |\n",
    "| наборы | `[abc]`, `[0-9]`, `[^a-z]` |\n",
    "| классы | `.`, `\\d`, `\\w`, `\\s`, `\\D`, `\\W`, `\\S` |\n",
    "| квантификаторы | `?`, `*`, `+`, `{n}`, `{m,n}` |\n",
    "| группировка и варианты | `(...)`, `\\|` |\n",
    "| экранирование | `\\.`, `\\+`, `\\(` |\n",
    "\n",
    "**Функции:** `re.fullmatch` — проверка всей строки, `re.search` — поиск первого фрагмента (`.group()`, `.start()`, `.end()`), `re.sub` — замена всех фрагментов.\n",
    "\n",
    "**Возможные вопросы на экзамене:**\n",
    "1. Чем отличаются `re.fullmatch(r\"\\d+\", s)` и `re.search(r\"\\d+\", s)`?\n",
    "2. Почему шаблоны записывают в виде `r\"...\"`?\n",
    "3. Можно ли регулярным выражением проверить, что дата `29.02.2027` существует? \n",
    "\n",
    "**Дальше:** `re.findall`, `re.split`, `re.finditer` (после темы «Списки»), извлечение групп, флаги, `re.compile`.\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bd2e3a1a-7090-49f8-9c3e-0256817fa96d",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
