{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a8ce6d0d",
   "metadata": {},
   "source": [
    "# Парсинг данных с BeautifulSoup\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6f38a37c",
   "metadata": {},
   "source": [
    "## Блок 0. Установка и импорты"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "996fafe5",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Раскомментируйте при первом запуске\n",
    "# !pip install requests beautifulsoup4 lxml cssselect pandas matplotlib selectolax"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "0ad8a261",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "bs4: 4.13.5\n",
      "pandas: 2.3.3\n"
     ]
    }
   ],
   "source": [
    "import re\n",
    "import json\n",
    "import time\n",
    "import random\n",
    "import pathlib\n",
    "import os \n",
    "from datetime import datetime\n",
    "\n",
    "import pandas as pd\n",
    "import requests\n",
    "from bs4 import BeautifulSoup\n",
    "from requests.adapters import HTTPAdapter\n",
    "from urllib3.util.retry import Retry\n",
    "\n",
    "print(\"bs4:\", __import__(\"bs4\").__version__)\n",
    "print(\"pandas:\", pd.__version__)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "82238f41",
   "metadata": {},
   "source": [
    "### Создаём локальные HTML-фикстуры\n",
    "\n",
    "Это точные копии структуры реальных страниц, сохранённые локально.\n",
    "\n",
    "Локальные копии снимают эту проблему и делают результат воспроизводимым."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "db5a18ab",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Готово. Создано файлов: 14\n"
     ]
    }
   ],
   "source": [
    "DATA = pathlib.Path(\"data\")\n",
    "DATA.mkdir(exist_ok=True)\n",
    "\n",
    "# --- 1. Новостная лента (структура типового финансового медиа) --------------\n",
    "NEWS = [\n",
    "    (\"2026-08-24\", \"SBER\", \"Сбербанк повысил ставки по вкладам на фоне решения ЦБ\"),\n",
    "    (\"2026-08-24\", \"GAZP\", \"Газпром сократил экспорт: аналитики понизили прогноз\"),\n",
    "    (\"2026-08-23\", \"SBER\", \"Прибыль Сбербанка выросла на 12% по итогам полугодия\"),\n",
    "    (\"2026-08-23\", \"LKOH\", \"Лукойл объявил рекордные дивиденды за 2025 год\"),\n",
    "    (\"2026-08-22\", \"GAZP\", \"Убыток Газпрома оказался хуже ожиданий рынка\"),\n",
    "    (\"2026-08-22\", \"YDEX\", \"Яндекс запускает новый сервис: инвесторы настроены оптимистично\"),\n",
    "    (\"2026-08-21\", \"SBER\", \"Риски розничного кредитования растут, предупреждает ЦБ\"),\n",
    "    (\"2026-08-21\", \"LKOH\", \"Падение цен на нефть давит на выручку нефтяников\"),\n",
    "    (\"2026-08-20\", \"YDEX\", \"Выручка Яндекса превысила прогноз аналитиков\"),\n",
    "    (\"2026-08-20\", \"GAZP\", \"Санкции создают неопределённость для экспортёров\"),\n",
    "    (\"2026-08-19\", \"SBER\", \"Сбербанк улучшил прогноз по чистой процентной марже\"),\n",
    "    (\"2026-08-19\", \"LKOH\", \"Обвал акций нефтяного сектора продолжился\"),\n",
    "]\n",
    "\n",
    "cards = []\n",
    "for i, (d, ticker, title) in enumerate(NEWS, start=1):\n",
    "    cards.append(f\"\"\"\n",
    "    <article class=\"news-card\" data-id=\"{i}\">\n",
    "      <a class=\"news-card__link\" href=\"article_{i}.html\">\n",
    "        <span class=\"news-card__title\">{title}</span>\n",
    "      </a>\n",
    "      <time class=\"news-card__date\" datetime=\"{d}\">{d}</time>\n",
    "      <span class=\"news-card__ticker\">{ticker}</span>\n",
    "      <span class=\"news-card__views\">{100 + i * 37} просмотров</span>\n",
    "    </article>\"\"\")\n",
    "\n",
    "feed_html = f\"\"\"<!DOCTYPE html>\n",
    "<html lang=\"ru\">\n",
    "<head>\n",
    "  <meta charset=\"utf-8\">\n",
    "  <title>Лента новостей — учебный пример</title>\n",
    "</head>\n",
    "<body>\n",
    "  <header><h1>Финансовые новости</h1></header>\n",
    "  <div class=\"news-feed\">{''.join(cards)}\n",
    "  </div>\n",
    "  <footer class=\"page-footer\">Учебный пример. Данные вымышленные.</footer>\n",
    "</body>\n",
    "</html>\"\"\"\n",
    "(DATA / \"news_feed.html\").write_text(feed_html, encoding=\"utf-8\")\n",
    "\n",
    "# --- 2. Страницы отдельных новостей ----------------------------------------\n",
    "BODIES = {\n",
    "    \"рост\": \"Аналитики отмечают рост показателей и улучшение прогноза на следующий квартал.\",\n",
    "    \"падение\": \"Участники рынка фиксируют падение котировок и рост неопределённости.\",\n",
    "}\n",
    "for i, (d, ticker, title) in enumerate(NEWS, start=1):\n",
    "    body = BODIES[\"падение\"] if any(w in title.lower() for w in\n",
    "           (\"убыт\", \"падени\", \"обвал\", \"риск\", \"сократ\", \"санкц\", \"хуже\")) else BODIES[\"рост\"]\n",
    "    art = f\"\"\"<!DOCTYPE html>\n",
    "<html lang=\"ru\"><head><meta charset=\"utf-8\"><title>{title}</title></head>\n",
    "<body>\n",
    "  <article class=\"article\">\n",
    "    <h1 class=\"article__title\">{title}</h1>\n",
    "    <time class=\"article__date\" datetime=\"{d}\">{d}</time>\n",
    "    <div class=\"article__body\">\n",
    "      <p>{body}</p>\n",
    "      <p>Тикер: <span class=\"ticker\">{ticker}</span>. Объём торгов составил {i * 1_250} тыс. руб.</p>\n",
    "    </div>\n",
    "    <div class=\"article__tags\"><a href=\"#\" class=\"tag\">рынки</a><a href=\"#\" class=\"tag\">{ticker}</a></div>\n",
    "  </article>\n",
    "</body></html>\"\"\"\n",
    "    (DATA / f\"article_{i}.html\").write_text(art, encoding=\"utf-8\")\n",
    "\n",
    "# --- 3. Таблица курсов валют (структура страницы ЦБ РФ) ---------------------\n",
    "RATES = [(\"036\", 1, \"Австралийский доллар\", \"52,3145\"),\n",
    "         (\"826\", 1, \"Фунт стерлингов Соединенного королевства\", \"108,7712\"),\n",
    "         (\"978\", 1, \"Евро\", \"94,1583\"),\n",
    "         (\"156\", 10, \"Китайских юаней\", \"11,0421\"),\n",
    "         (\"840\", 1, \"Доллар США\", \"80,4419\"),\n",
    "         (\"392\", 100, \"Японских иен\", \"54,8830\")]\n",
    "rows = \"\".join(\n",
    "    f\"<tr><td>{c}</td><td>{u}</td><td>{n}</td><td>{v}</td></tr>\" for c, u, n, v in RATES)\n",
    "rates_html = f\"\"\"<!DOCTYPE html>\n",
    "<html lang=\"ru\"><head><meta charset=\"utf-8\"><title>Курсы валют</title></head>\n",
    "<body>\n",
    "  <h2>Официальные курсы валют на 26.08.2026</h2>\n",
    "  <table class=\"data\">\n",
    "    <thead><tr><th>Цифр. код</th><th>Единиц</th><th>Валюта</th><th>Курс</th></tr></thead>\n",
    "    <tbody>{rows}</tbody>\n",
    "  </table>\n",
    "</body></html>\"\"\"\n",
    "(DATA / \"cbr_rates.html\").write_text(rates_html, encoding=\"utf-8\")\n",
    "\n",
    "print(\"Готово. Создано файлов:\", len(list(DATA.glob(\"*.html\"))))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c483ca2e",
   "metadata": {},
   "source": [
    "---\n",
    "## Блок 1. Разминка: анатомия HTML-документа\n",
    "\n",
    "Прежде чем идти в интернет, разберём инструмент на маленьком куске разметки,\n",
    "который целиком помещается на экран. Всё, что вы здесь увидите, дальше работает\n",
    "точно так же на странице в сто тысяч строк."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "aeaecf03",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<html>\n",
      " <body>\n",
      "  <div class=\"news-feed\">\n",
      "   <article class=\"news-card\" data-id=\"1\">\n",
      "    <a class=\"news-card__link\" href=\"article_1.html\">\n",
      "     <span class=\"news-card__title\">\n",
      "      Прибыль Сбербанка выросла на 12%\n",
      "     </span>\n",
      "    </a>\n",
      "    <time class=\"news-card__date\" datetime=\"2026-08-23\">\n",
      "     2026-08-23\n",
      "    </time>\n",
      "    <span class=\"news-card__ticker\">\n",
      "     SBER\n",
      "    </span>\n",
      "   </article>\n",
      "   <ar\n"
     ]
    }
   ],
   "source": [
    "html_doc = \"\"\"\n",
    "<div class=\"news-feed\">\n",
    "  <article class=\"news-card\" data-id=\"1\">\n",
    "    <a class=\"news-card__link\" href=\"article_1.html\">\n",
    "      <span class=\"news-card__title\">Прибыль Сбербанка выросла на 12%</span>\n",
    "    </a>\n",
    "    <time class=\"news-card__date\" datetime=\"2026-08-23\">2026-08-23</time>\n",
    "    <span class=\"news-card__ticker\">SBER</span>\n",
    "  </article>\n",
    "  <article class=\"news-card\" data-id=\"2\">\n",
    "    <a class=\"news-card__link\" href=\"article_2.html\">\n",
    "      <span class=\"news-card__title\">Убыток Газпрома хуже ожиданий</span>\n",
    "    </a>\n",
    "    <time class=\"news-card__date\" datetime=\"2026-08-22\">2026-08-22</time>\n",
    "    <span class=\"news-card__ticker\">GAZP</span>\n",
    "  </article>\n",
    "</div>\n",
    "\"\"\"\n",
    "\n",
    "soup = BeautifulSoup(html_doc, \"lxml\")\n",
    "print(soup.prettify()[:400])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "89f7912e",
   "metadata": {},
   "source": [
    "**Словарь на три термина.** Дальше всё занятие мы говорим только этими словами.\n",
    "\n",
    "| Термин | Что это | В примере выше |\n",
    "|---|---|---|\n",
    "| **Тег** | Элемент разметки | `article`, `a`, `time`, `span` |\n",
    "| **Атрибут** | Свойство тега, пара «имя = значение» | `class=\"news-card\"`, `href=\"...\"`, `datetime=\"...\"` |\n",
    "| **Дерево (DOM)** | Вложенность тегов друг в друга | `article` → `a` → `span` |\n",
    "\n",
    "Парсинг — это ответ на вопрос **«по какому признаку я отличу нужный тег от всех остальных»**.\n",
    "Признаком почти всегда служит атрибут `class`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ad125e52",
   "metadata": {},
   "source": [
    "### Четыре способа найти один и тот же элемент\n",
    "\n",
    "Это главная путаница новичков: в туториалах встречаются разные способы,\n",
    "и кажется, что это разные вещи. Это один и тот же результат."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "b393c278",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<time class=\"news-card__date\" datetime=\"2026-08-23\">2026-08-23</time>\n",
      "<time class=\"news-card__date\" datetime=\"2026-08-23\">2026-08-23</time>\n",
      "<time class=\"news-card__date\" datetime=\"2026-08-23\">2026-08-23</time>\n",
      "<time class=\"news-card__date\" datetime=\"2026-08-23\">2026-08-23</time>\n"
     ]
    }
   ],
   "source": [
    "# 1) по тегу\n",
    "print(soup.find(\"time\"))\n",
    "\n",
    "# 2) по тегу и классу  (class_ с подчёркиванием: class — зарезервированное слово Python)\n",
    "print(soup.find(\"time\", class_=\"news-card__date\"))\n",
    "\n",
    "# 3) по произвольному атрибуту\n",
    "print(soup.find(\"time\", attrs={\"datetime\": \"2026-08-23\"}))\n",
    "\n",
    "# 4) CSS-селектором\n",
    "print(soup.select_one(\"time.news-card__date\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2867f1fd",
   "metadata": {},
   "source": [
    "### `find` и `find_all` — рабочая пара\n",
    "\n",
    "- `find(...)` → **один** тег (первый подошедший) или `None`\n",
    "- `find_all(...)` → **список** тегов (возможно пустой)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "741e0939",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'bs4.element.Tag'> → 1\n",
      "<class 'bs4.element.ResultSet'> → найдено: 2\n"
     ]
    }
   ],
   "source": [
    "first = soup.find(\"article\", class_=\"news-card\")\n",
    "print(type(first), \"→\", first.get(\"data-id\"))\n",
    "\n",
    "all_cards = soup.find_all(\"article\", class_=\"news-card\")\n",
    "print(type(all_cards), \"→ найдено:\", len(all_cards))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3de6ead8",
   "metadata": {},
   "source": [
    "### Как достать содержимое: текст и атрибуты\n",
    "\n",
    "Две операции, которые нужны в 95% случаев:\n",
    "\n",
    "- **текст** — `tag.get_text(strip=True)`\n",
    "- **атрибут** — `tag[\"href\"]` или, безопаснее, `tag.get(\"href\")`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "76536519",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2026-08-23 | SBER | Прибыль Сбербанка выросла на 12%\n",
      "ссылка: article_1.html\n"
     ]
    }
   ],
   "source": [
    "card = soup.find(\"article\", class_=\"news-card\")\n",
    "\n",
    "title = card.find(\"span\", class_=\"news-card__title\").get_text(strip=True)\n",
    "link  = card.find(\"a\", class_=\"news-card__link\")[\"href\"]\n",
    "date  = card.find(\"time\")[\"datetime\"]          # значение атрибута, а не видимый текст!\n",
    "ticker = card.find(\"span\", class_=\"news-card__ticker\").get_text(strip=True)\n",
    "\n",
    "print(f\"{date} | {ticker} | {title}\")\n",
    "print(\"ссылка:\", link)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a76e1357",
   "metadata": {},
   "source": [
    "> **Обратите внимание на `time`.** Видимый текст и атрибут `datetime` — разные вещи.\n",
    "> На реальных сайтах в тексте будет «вчера в 14:20», а в `datetime` — машинный ISO-формат.\n",
    "> Всегда предпочитайте атрибут: его не нужно парсить."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7880c262",
   "metadata": {},
   "source": [
    "### Собираем первый DataFrame\n",
    "\n",
    "Типовой паттерн парсинга — **цикл по карточкам, словарь на карточку, список словарей в `DataFrame`**.\n",
    "Запомните его: 90% ваших будущих парсеров будут выглядеть именно так."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "3ce7dcda",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>date</th>\n",
       "      <th>ticker</th>\n",
       "      <th>title</th>\n",
       "      <th>url</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2026-08-23</td>\n",
       "      <td>SBER</td>\n",
       "      <td>Прибыль Сбербанка выросла на 12%</td>\n",
       "      <td>article_1.html</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2026-08-22</td>\n",
       "      <td>GAZP</td>\n",
       "      <td>Убыток Газпрома хуже ожиданий</td>\n",
       "      <td>article_2.html</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "         date ticker                             title             url\n",
       "0  2026-08-23   SBER  Прибыль Сбербанка выросла на 12%  article_1.html\n",
       "1  2026-08-22   GAZP     Убыток Газпрома хуже ожиданий  article_2.html"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "records = []\n",
    "for card in soup.find_all(\"article\", class_=\"news-card\"):\n",
    "    records.append({\n",
    "        \"date\":   card.find(\"time\")[\"datetime\"],\n",
    "        \"ticker\": card.find(\"span\", class_=\"news-card__ticker\").get_text(strip=True),\n",
    "        \"title\":  card.find(\"span\", class_=\"news-card__title\").get_text(strip=True),\n",
    "        \"url\":    card.find(\"a\")[\"href\"],\n",
    "    })\n",
    "\n",
    "df_demo = pd.DataFrame(records)\n",
    "df_demo"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7a9df89f",
   "metadata": {},
   "source": [
    "### `select` — тот же поиск на языке CSS\n",
    "\n",
    "`select` понимает CSS-селекторы: то же, чем пользуются вёрстка и DevTools.\n",
    "Иногда это радикально короче, особенно когда нужен вложенный элемент."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "6d76bdb0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['Прибыль Сбербанка выросла на 12%', 'Убыток Газпрома хуже ожиданий']\n",
      "[<article class=\"news-card\" data-id=\"1\">\n",
      "<a class=\"news-card__link\" href=\"article_1.html\">\n",
      "<span class=\"news-card__title\">Прибыль Сбербанка выросла на 12%</span>\n",
      "</a>\n",
      "<time class=\"news-card__date\" datetime=\"2026-08-23\">2026-08-23</time>\n",
      "<span class=\"news-card__ticker\">SBER</span>\n",
      "</article>, <article class=\"news-card\" data-id=\"2\">\n",
      "<a class=\"news-card__link\" href=\"article_2.html\">\n",
      "<span class=\"news-card__title\">Убыток Газпрома хуже ожиданий</span>\n",
      "</a>\n",
      "<time class=\"news-card__date\" datetime=\"2026-08-22\">2026-08-22</time>\n",
      "<span class=\"news-card__ticker\">GAZP</span>\n",
      "</article>]\n",
      "['article_1.html', 'article_2.html']\n"
     ]
    }
   ],
   "source": [
    "# «внутри article.news-card найди span с классом news-card__title»\n",
    "titles = [t.get_text(strip=True) for t in soup.select(\"article.news-card span.news-card__title\")]\n",
    "print(titles)\n",
    "\n",
    "# > означает «непосредственный потомок», пробел — «любой потомок на глубине»\n",
    "print(soup.select(\"div.news-feed > article\"))\n",
    "\n",
    "# [attr] — поиск по наличию атрибута\n",
    "print([a[\"href\"] for a in soup.select(\"a[href]\")])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0dff6854",
   "metadata": {},
   "source": [
    "**Мини-шпаргалка по CSS-селекторам**\n",
    "\n",
    "| Селектор | Смысл |\n",
    "|---|---|\n",
    "| `div` | все теги `div` |\n",
    "| `.news-card` | все элементы с классом `news-card` |\n",
    "| `#main` | элемент с `id=\"main\"` |\n",
    "| `article.news-card` | `article`, у которого есть класс `news-card` |\n",
    "| `div.feed > article` | `article` — прямой потомок `div.feed` |\n",
    "| `div.feed article` | `article` где угодно внутри `div.feed` |\n",
    "| `a[href]` | `a`, у которого есть атрибут `href` |\n",
    "| `td:nth-of-type(4)` | четвёртая ячейка строки |\n",
    "\n",
    "`find_all` или `select` — вопрос вкуса. `find_all` нагляднее для простых случаев,\n",
    "`select` короче для вложенных."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "75b8df78",
   "metadata": {},
   "source": [
    "---\n",
    "## Блок 2. Навигация по дереву\n",
    "\n",
    "Ситуация, с которой вы столкнётесь на реальном сайте: **у нужного элемента нет своего класса.**\n",
    "Есть только соседний элемент, который опознать легко. Тогда мы «ходим» по дереву от него."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "d0ad75a7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Выручка → 1 240,5\n",
      "Чистая прибыль → 318,2\n"
     ]
    }
   ],
   "source": [
    "frag = BeautifulSoup(\"\"\"\n",
    "<table class=\"report\">\n",
    "  <tr><td class=\"label\">Выручка</td><td>1 240,5</td></tr>\n",
    "  <tr><td class=\"label\">Чистая прибыль</td><td>318,2</td></tr>\n",
    "</table>\n",
    "\"\"\", \"lxml\")\n",
    "\n",
    "# У ячейки со значением нет класса. Но она — следующий сосед ячейки с классом label.\n",
    "for cell in frag.find_all(\"td\", class_=\"label\"):\n",
    "    value = cell.find_next_sibling(\"td\")\n",
    "    print(cell.get_text(strip=True), \"→\", value.get_text(strip=True))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "50234db6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "parent      : tr\n",
      "родители    : ['tr', 'table', 'body', 'html', '[document]']\n",
      "след. сосед : 1 240,5\n",
      "вся строка  : Выручка | 1 240,5\n"
     ]
    }
   ],
   "source": [
    "label = frag.find(\"td\", class_=\"label\")\n",
    "\n",
    "print(\"parent      :\", label.parent.name)              # вверх на один уровень\n",
    "print(\"родители    :\", [p.name for p in label.parents if p.name])\n",
    "print(\"след. сосед :\", label.find_next_sibling(\"td\").get_text(strip=True))\n",
    "print(\"вся строка  :\", label.find_parent(\"tr\").get_text(\" | \", strip=True))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d825aff8",
   "metadata": {},
   "source": [
    "| Метод | Куда идёт |\n",
    "|---|---|\n",
    "| `.parent`, `.find_parent(tag)` | вверх по дереву |\n",
    "| `.children`, `.find_all(tag)` | вниз, к потомкам |\n",
    "| `.find_next_sibling(tag)` / `.find_previous_sibling(tag)` | вбок, к соседям |\n",
    "| `.find_next(tag)` / `.find_previous(tag)` | вперёд/назад по всему документу |\n",
    "\n",
    "Приём «найди якорь → сходи к соседу» выручает на страницах отчётности,\n",
    "где значения лежат в безымянных ячейках рядом с подписанными."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6ef48377",
   "metadata": {},
   "source": [
    "---\n",
    "## Блок 3. Первый настоящий запрос\n",
    "\n",
    "До сих пор HTML брался из строки. Теперь получим его с сервера.\n",
    "За это отвечает библиотека `requests` — BeautifulSoup сама в интернет не ходит,\n",
    "она умеет только разбирать уже полученный текст. Это разделение труда важно понимать:\n",
    "\n",
    "> **`requests` достаёт HTML. `BeautifulSoup` его разбирает.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "3159d064",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Код ответа  : 200\n",
      "Тип контента: text/html; charset=utf-8\n",
      "Размер      : 3739 символов\n",
      "<!DOCTYPE html>\n",
      "<html>\n",
      "  <head>\n",
      "  </head>\n",
      "  <body>\n",
      "      <h1>Herman Melville - Moby-Dick</h1>\n",
      "\n",
      "      <div>\n",
      "        <p>\n",
      "          Availing himself of the mild, summer-cool weather that now reigned in these latitudes, and in preparation for the peculiarly active pursuits shortly to be anticipated, Per\n"
     ]
    }
   ],
   "source": [
    "HEADERS = {\n",
    "    \"User-Agent\": \"Mozilla/5.0 (compatible; research-scraper/1.0; +mailto:you@university.edu)\",\n",
    "    \"Accept-Language\": \"ru-RU,ru;q=0.9\",\n",
    "}\n",
    "\n",
    "try:\n",
    "    resp = requests.get(\"https://httpbin.org/html\", headers=HEADERS, timeout=10)\n",
    "    print(\"Код ответа  :\", resp.status_code)\n",
    "    print(\"Тип контента:\", resp.headers.get(\"Content-Type\"))\n",
    "    print(\"Размер      :\", len(resp.text), \"символов\")\n",
    "    print(resp.text[:300])\n",
    "except requests.RequestException as e:\n",
    "    print(f\"Сеть недоступна ({type(e).__name__}). Это не страшно:\")\n",
    "    print(\"все ключевые блоки ноутбука работают на локальных копиях.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "77fd0946",
   "metadata": {},
   "source": [
    "**Четыре кода ответа, которые вам встретятся:**\n",
    "\n",
    "| Код | Что значит | Что делать |\n",
    "|---|---|---|\n",
    "| `200` | Всё хорошо | Парсить |\n",
    "| `403` | Доступ запрещён | Обычно не нравится ваш `User-Agent` — см. блок 7 |\n",
    "| `404` | Страницы нет | Проверить URL, возможно сайт поменял структуру |\n",
    "| `429` | Слишком много запросов | **Замедлиться.** Вы на грани бана — см. блок 7 |\n",
    "\n",
    "**`.text` или `.content`?** `.text` — строка, декодированная по кодировке из заголовков.\n",
    "Если сервер указал кодировку неверно (частая беда российских сайтов), получите кракозябры.\n",
    "Тогда либо `resp.encoding = \"utf-8\"` до обращения к `.text`, либо отдайте\n",
    "BeautifulSoup сырые байты `resp.content` — она определит кодировку сама."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "af567ffa",
   "metadata": {},
   "source": [
    "### Функция-обёртка с офлайн-запасом\n",
    "\n",
    "Всё остальное занятие мы ходим за страницами через эту функцию.\n",
    "Она делает три полезные вещи: ставит заголовки, ловит сетевые ошибки\n",
    "и подставляет локальную копию, если сеть недоступна."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "dba82229",
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_html(url, fallback=None, pause=1.0):\n",
    "    \"\"\"Скачать страницу; при любой проблеме вернуть локальную копию.\"\"\"\n",
    "    try:\n",
    "        r = requests.get(url, headers=HEADERS, timeout=10)\n",
    "        r.raise_for_status()\n",
    "        time.sleep(pause)                 # вежливая пауза — см. блок 7\n",
    "        print(f\"сеть: {url} → {r.status_code}\")\n",
    "        return r.content                  # байты: пусть кодировку определит парсер\n",
    "    except Exception as e:\n",
    "        if fallback is None:\n",
    "            raise\n",
    "            print(f\" {type(e).__name__}: {e}\\n   → работаем с локальной копией {fallback}\")\n",
    "        return pathlib.Path(fallback).read_bytes()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1984851e",
   "metadata": {},
   "source": [
    "---\n",
    "## Блок 4. Кейс А — [таблица курсов валют от ЦБ РФ](https://www.cbr.ru/currency_base/daily/)\n",
    "\n",
    "Разминка на реальной табличной структуре. Логика: найти таблицу → перебрать строки `<tr>` →\n",
    "из каждой достать ячейки `<td>`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "af6e672d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "сеть: https://www.cbr.ru/currency_base/daily/ → 200\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>code</th>\n",
       "      <th>iso</th>\n",
       "      <th>units</th>\n",
       "      <th>currency</th>\n",
       "      <th>rate</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>036</td>\n",
       "      <td>AUD</td>\n",
       "      <td>1</td>\n",
       "      <td>Австралийский доллар</td>\n",
       "      <td>61,5983</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>944</td>\n",
       "      <td>AZN</td>\n",
       "      <td>1</td>\n",
       "      <td>Азербайджанский манат</td>\n",
       "      <td>50,3534</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>012</td>\n",
       "      <td>DZD</td>\n",
       "      <td>100</td>\n",
       "      <td>Алжирских динаров</td>\n",
       "      <td>64,3673</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>051</td>\n",
       "      <td>AMD</td>\n",
       "      <td>100</td>\n",
       "      <td>Армянских драмов</td>\n",
       "      <td>23,4960</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>764</td>\n",
       "      <td>THB</td>\n",
       "      <td>10</td>\n",
       "      <td>Батов</td>\n",
       "      <td>25,9813</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "  code  iso units               currency     rate\n",
       "0  036  AUD     1   Австралийский доллар  61,5983\n",
       "1  944  AZN     1  Азербайджанский манат  50,3534\n",
       "2  012  DZD   100      Алжирских динаров  64,3673\n",
       "3  051  AMD   100       Армянских драмов  23,4960\n",
       "4  764  THB    10                  Батов  25,9813"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "raw = get_html(\"https://www.cbr.ru/currency_base/daily/\",\n",
    "               fallback=None)\n",
    "\n",
    "soup = BeautifulSoup(raw, \"lxml\")\n",
    "table = soup.find(\"table\", class_=\"data\")\n",
    "\n",
    "rows = []\n",
    "for tr in table.find_all(\"tr\")[1:]:              # [1:] — пропускаем строку заголовков\n",
    "    cells = [td.get_text(strip=True) for td in tr.find_all(\"td\")]\n",
    "    if cells:                                     # защита от пустых строк вёрстки\n",
    "        rows.append(cells)\n",
    "\n",
    "rates = pd.DataFrame(rows, columns=[\"code\", \"iso\", \"units\", \"currency\", \"rate\"])\n",
    "rates.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "34dcb5ad",
   "metadata": {},
   "source": [
    "Данные пришли строками — это стандартная ситуация при парсинге. Приводим к числам:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "04d41498",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>code</th>\n",
       "      <th>iso</th>\n",
       "      <th>units</th>\n",
       "      <th>currency</th>\n",
       "      <th>rate</th>\n",
       "      <th>rate_per_unit</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>036</td>\n",
       "      <td>AUD</td>\n",
       "      <td>1</td>\n",
       "      <td>Австралийский доллар</td>\n",
       "      <td>61.5983</td>\n",
       "      <td>61.598300</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>944</td>\n",
       "      <td>AZN</td>\n",
       "      <td>1</td>\n",
       "      <td>Азербайджанский манат</td>\n",
       "      <td>50.3534</td>\n",
       "      <td>50.353400</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>012</td>\n",
       "      <td>DZD</td>\n",
       "      <td>100</td>\n",
       "      <td>Алжирских динаров</td>\n",
       "      <td>64.3673</td>\n",
       "      <td>0.643673</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>051</td>\n",
       "      <td>AMD</td>\n",
       "      <td>100</td>\n",
       "      <td>Армянских драмов</td>\n",
       "      <td>23.4960</td>\n",
       "      <td>0.234960</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>764</td>\n",
       "      <td>THB</td>\n",
       "      <td>10</td>\n",
       "      <td>Батов</td>\n",
       "      <td>25.9813</td>\n",
       "      <td>2.598130</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>048</td>\n",
       "      <td>BHD</td>\n",
       "      <td>1</td>\n",
       "      <td>Бахрейнский динар</td>\n",
       "      <td>227.6123</td>\n",
       "      <td>227.612300</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>933</td>\n",
       "      <td>BYN</td>\n",
       "      <td>1</td>\n",
       "      <td>Белорусский рубль</td>\n",
       "      <td>28.1618</td>\n",
       "      <td>28.161800</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>068</td>\n",
       "      <td>BOB</td>\n",
       "      <td>10</td>\n",
       "      <td>Боливиано</td>\n",
       "      <td>72.3590</td>\n",
       "      <td>7.235900</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>986</td>\n",
       "      <td>BRL</td>\n",
       "      <td>1</td>\n",
       "      <td>Бразильский реал</td>\n",
       "      <td>16.5766</td>\n",
       "      <td>16.576600</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>410</td>\n",
       "      <td>KRW</td>\n",
       "      <td>1000</td>\n",
       "      <td>Вон</td>\n",
       "      <td>62.0160</td>\n",
       "      <td>0.062016</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>344</td>\n",
       "      <td>HKD</td>\n",
       "      <td>1</td>\n",
       "      <td>Гонконгский доллар</td>\n",
       "      <td>10.9171</td>\n",
       "      <td>10.917100</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>980</td>\n",
       "      <td>UAH</td>\n",
       "      <td>10</td>\n",
       "      <td>Гривен</td>\n",
       "      <td>19.2143</td>\n",
       "      <td>1.921430</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>208</td>\n",
       "      <td>DKK</td>\n",
       "      <td>1</td>\n",
       "      <td>Датская крона</td>\n",
       "      <td>13.3359</td>\n",
       "      <td>13.335900</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>784</td>\n",
       "      <td>AED</td>\n",
       "      <td>1</td>\n",
       "      <td>Дирхам ОАЭ</td>\n",
       "      <td>23.3086</td>\n",
       "      <td>23.308600</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>840</td>\n",
       "      <td>USD</td>\n",
       "      <td>1</td>\n",
       "      <td>Доллар США</td>\n",
       "      <td>85.6007</td>\n",
       "      <td>85.600700</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>704</td>\n",
       "      <td>VND</td>\n",
       "      <td>10000</td>\n",
       "      <td>Донгов</td>\n",
       "      <td>33.4247</td>\n",
       "      <td>0.003342</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>978</td>\n",
       "      <td>EUR</td>\n",
       "      <td>1</td>\n",
       "      <td>Евро</td>\n",
       "      <td>99.6820</td>\n",
       "      <td>99.682000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>818</td>\n",
       "      <td>EGP</td>\n",
       "      <td>10</td>\n",
       "      <td>Египетских фунтов</td>\n",
       "      <td>17.0458</td>\n",
       "      <td>1.704580</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>985</td>\n",
       "      <td>PLN</td>\n",
       "      <td>1</td>\n",
       "      <td>Злотый</td>\n",
       "      <td>23.0054</td>\n",
       "      <td>23.005400</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>19</th>\n",
       "      <td>392</td>\n",
       "      <td>JPY</td>\n",
       "      <td>100</td>\n",
       "      <td>Иен</td>\n",
       "      <td>53.7153</td>\n",
       "      <td>0.537153</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>20</th>\n",
       "      <td>356</td>\n",
       "      <td>INR</td>\n",
       "      <td>100</td>\n",
       "      <td>Индийских рупий</td>\n",
       "      <td>89.5766</td>\n",
       "      <td>0.895766</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>21</th>\n",
       "      <td>364</td>\n",
       "      <td>IRR</td>\n",
       "      <td>1000000</td>\n",
       "      <td>Иранских риалов</td>\n",
       "      <td>54.4093</td>\n",
       "      <td>0.000054</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>22</th>\n",
       "      <td>124</td>\n",
       "      <td>CAD</td>\n",
       "      <td>1</td>\n",
       "      <td>Канадский доллар</td>\n",
       "      <td>61.7565</td>\n",
       "      <td>61.756500</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>23</th>\n",
       "      <td>634</td>\n",
       "      <td>QAR</td>\n",
       "      <td>1</td>\n",
       "      <td>Катарский риал</td>\n",
       "      <td>23.5167</td>\n",
       "      <td>23.516700</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>24</th>\n",
       "      <td>192</td>\n",
       "      <td>CUP</td>\n",
       "      <td>10</td>\n",
       "      <td>Кубинских песо</td>\n",
       "      <td>35.6670</td>\n",
       "      <td>3.566700</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   code  iso    units               currency      rate  rate_per_unit\n",
       "0   036  AUD        1   Австралийский доллар   61.5983      61.598300\n",
       "1   944  AZN        1  Азербайджанский манат   50.3534      50.353400\n",
       "2   012  DZD      100      Алжирских динаров   64.3673       0.643673\n",
       "3   051  AMD      100       Армянских драмов   23.4960       0.234960\n",
       "4   764  THB       10                  Батов   25.9813       2.598130\n",
       "5   048  BHD        1      Бахрейнский динар  227.6123     227.612300\n",
       "6   933  BYN        1      Белорусский рубль   28.1618      28.161800\n",
       "7   068  BOB       10              Боливиано   72.3590       7.235900\n",
       "8   986  BRL        1       Бразильский реал   16.5766      16.576600\n",
       "9   410  KRW     1000                    Вон   62.0160       0.062016\n",
       "10  344  HKD        1     Гонконгский доллар   10.9171      10.917100\n",
       "11  980  UAH       10                 Гривен   19.2143       1.921430\n",
       "12  208  DKK        1          Датская крона   13.3359      13.335900\n",
       "13  784  AED        1             Дирхам ОАЭ   23.3086      23.308600\n",
       "14  840  USD        1             Доллар США   85.6007      85.600700\n",
       "15  704  VND    10000                 Донгов   33.4247       0.003342\n",
       "16  978  EUR        1                   Евро   99.6820      99.682000\n",
       "17  818  EGP       10      Египетских фунтов   17.0458       1.704580\n",
       "18  985  PLN        1                 Злотый   23.0054      23.005400\n",
       "19  392  JPY      100                    Иен   53.7153       0.537153\n",
       "20  356  INR      100        Индийских рупий   89.5766       0.895766\n",
       "21  364  IRR  1000000        Иранских риалов   54.4093       0.000054\n",
       "22  124  CAD        1       Канадский доллар   61.7565      61.756500\n",
       "23  634  QAR        1         Катарский риал   23.5167      23.516700\n",
       "24  192  CUP       10         Кубинских песо   35.6670       3.566700"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "rates[\"rate\"] = (rates[\"rate\"]\n",
    "                 .str.replace(\"\\xa0\", \"\", regex=False)   # неразрывный пробел — частый гость\n",
    "                 .str.replace(\",\", \".\", regex=False)     # десятичная запятая → точка\n",
    "                 .astype(float))\n",
    "rates[\"units\"] = rates[\"units\"].astype(int)\n",
    "\n",
    "# курс за одну единицу валюты\n",
    "rates[\"rate_per_unit\"] = rates[\"rate\"] / rates[\"units\"]\n",
    "rates.head(25)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "007e34f4",
   "metadata": {},
   "source": [
    "> **Про `\\xa0`.** Это неразрывный пробел (`&nbsp;` в HTML). Выглядит как обычный пробел,\n",
    "> но `float(\"1 234\")` на нём падает. Первое, что стоит проверить,\n",
    "> когда `astype(float)` ругается на строку, которая «выглядит нормально»."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7103e456",
   "metadata": {},
   "source": [
    "### Когда парсер не нужен вовсе\n",
    "\n",
    "Если данные на странице — **настоящая HTML-таблица**, `pandas` разберёт её одной строкой.\n",
    "Стоит всегда пробовать сначала это, и только потом писать цикл."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "d5ae461c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Найдено таблиц: 1\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Цифр. код</th>\n",
       "      <th>Букв. код</th>\n",
       "      <th>Единиц</th>\n",
       "      <th>Валюта</th>\n",
       "      <th>Курс</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>36</td>\n",
       "      <td>AUD</td>\n",
       "      <td>1</td>\n",
       "      <td>Австралийский доллар</td>\n",
       "      <td>615983</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>944</td>\n",
       "      <td>AZN</td>\n",
       "      <td>1</td>\n",
       "      <td>Азербайджанский манат</td>\n",
       "      <td>503534</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>12</td>\n",
       "      <td>DZD</td>\n",
       "      <td>100</td>\n",
       "      <td>Алжирских динаров</td>\n",
       "      <td>643673</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>51</td>\n",
       "      <td>AMD</td>\n",
       "      <td>100</td>\n",
       "      <td>Армянских драмов</td>\n",
       "      <td>234960</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>764</td>\n",
       "      <td>THB</td>\n",
       "      <td>10</td>\n",
       "      <td>Батов</td>\n",
       "      <td>259813</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>48</td>\n",
       "      <td>BHD</td>\n",
       "      <td>1</td>\n",
       "      <td>Бахрейнский динар</td>\n",
       "      <td>2276123</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>933</td>\n",
       "      <td>BYN</td>\n",
       "      <td>1</td>\n",
       "      <td>Белорусский рубль</td>\n",
       "      <td>281618</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>68</td>\n",
       "      <td>BOB</td>\n",
       "      <td>10</td>\n",
       "      <td>Боливиано</td>\n",
       "      <td>723590</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>986</td>\n",
       "      <td>BRL</td>\n",
       "      <td>1</td>\n",
       "      <td>Бразильский реал</td>\n",
       "      <td>165766</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>410</td>\n",
       "      <td>KRW</td>\n",
       "      <td>1000</td>\n",
       "      <td>Вон</td>\n",
       "      <td>620160</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>344</td>\n",
       "      <td>HKD</td>\n",
       "      <td>1</td>\n",
       "      <td>Гонконгский доллар</td>\n",
       "      <td>109171</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>980</td>\n",
       "      <td>UAH</td>\n",
       "      <td>10</td>\n",
       "      <td>Гривен</td>\n",
       "      <td>192143</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>208</td>\n",
       "      <td>DKK</td>\n",
       "      <td>1</td>\n",
       "      <td>Датская крона</td>\n",
       "      <td>133359</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>784</td>\n",
       "      <td>AED</td>\n",
       "      <td>1</td>\n",
       "      <td>Дирхам ОАЭ</td>\n",
       "      <td>233086</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>840</td>\n",
       "      <td>USD</td>\n",
       "      <td>1</td>\n",
       "      <td>Доллар США</td>\n",
       "      <td>856007</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>704</td>\n",
       "      <td>VND</td>\n",
       "      <td>10000</td>\n",
       "      <td>Донгов</td>\n",
       "      <td>334247</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>978</td>\n",
       "      <td>EUR</td>\n",
       "      <td>1</td>\n",
       "      <td>Евро</td>\n",
       "      <td>996820</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>818</td>\n",
       "      <td>EGP</td>\n",
       "      <td>10</td>\n",
       "      <td>Египетских фунтов</td>\n",
       "      <td>170458</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>985</td>\n",
       "      <td>PLN</td>\n",
       "      <td>1</td>\n",
       "      <td>Злотый</td>\n",
       "      <td>230054</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>19</th>\n",
       "      <td>392</td>\n",
       "      <td>JPY</td>\n",
       "      <td>100</td>\n",
       "      <td>Иен</td>\n",
       "      <td>537153</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>20</th>\n",
       "      <td>356</td>\n",
       "      <td>INR</td>\n",
       "      <td>100</td>\n",
       "      <td>Индийских рупий</td>\n",
       "      <td>895766</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>21</th>\n",
       "      <td>364</td>\n",
       "      <td>IRR</td>\n",
       "      <td>1000000</td>\n",
       "      <td>Иранских риалов</td>\n",
       "      <td>544093</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>22</th>\n",
       "      <td>124</td>\n",
       "      <td>CAD</td>\n",
       "      <td>1</td>\n",
       "      <td>Канадский доллар</td>\n",
       "      <td>617565</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>23</th>\n",
       "      <td>634</td>\n",
       "      <td>QAR</td>\n",
       "      <td>1</td>\n",
       "      <td>Катарский риал</td>\n",
       "      <td>235167</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>24</th>\n",
       "      <td>192</td>\n",
       "      <td>CUP</td>\n",
       "      <td>10</td>\n",
       "      <td>Кубинских песо</td>\n",
       "      <td>356670</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>25</th>\n",
       "      <td>104</td>\n",
       "      <td>MMK</td>\n",
       "      <td>1000</td>\n",
       "      <td>Кьятов</td>\n",
       "      <td>407622</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>26</th>\n",
       "      <td>981</td>\n",
       "      <td>GEL</td>\n",
       "      <td>1</td>\n",
       "      <td>Лари</td>\n",
       "      <td>327658</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>27</th>\n",
       "      <td>498</td>\n",
       "      <td>MDL</td>\n",
       "      <td>10</td>\n",
       "      <td>Молдавских леев</td>\n",
       "      <td>495243</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>28</th>\n",
       "      <td>566</td>\n",
       "      <td>NGN</td>\n",
       "      <td>1000</td>\n",
       "      <td>Найр</td>\n",
       "      <td>639486</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>29</th>\n",
       "      <td>554</td>\n",
       "      <td>NZD</td>\n",
       "      <td>1</td>\n",
       "      <td>Новозеландский доллар</td>\n",
       "      <td>510095</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>30</th>\n",
       "      <td>934</td>\n",
       "      <td>TMT</td>\n",
       "      <td>1</td>\n",
       "      <td>Новый туркменский манат</td>\n",
       "      <td>244573</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>31</th>\n",
       "      <td>578</td>\n",
       "      <td>NOK</td>\n",
       "      <td>10</td>\n",
       "      <td>Норвежских крон</td>\n",
       "      <td>916447</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>32</th>\n",
       "      <td>512</td>\n",
       "      <td>OMR</td>\n",
       "      <td>1</td>\n",
       "      <td>Оманский риал</td>\n",
       "      <td>2226286</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>33</th>\n",
       "      <td>946</td>\n",
       "      <td>RON</td>\n",
       "      <td>1</td>\n",
       "      <td>Румынский лей</td>\n",
       "      <td>189504</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>34</th>\n",
       "      <td>360</td>\n",
       "      <td>IDR</td>\n",
       "      <td>10000</td>\n",
       "      <td>Рупий</td>\n",
       "      <td>481932</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>35</th>\n",
       "      <td>710</td>\n",
       "      <td>ZAR</td>\n",
       "      <td>10</td>\n",
       "      <td>Рэндов</td>\n",
       "      <td>535701</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>36</th>\n",
       "      <td>682</td>\n",
       "      <td>SAR</td>\n",
       "      <td>1</td>\n",
       "      <td>Саудовский риял</td>\n",
       "      <td>228269</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>37</th>\n",
       "      <td>960</td>\n",
       "      <td>XDR</td>\n",
       "      <td>1</td>\n",
       "      <td>СДР (специальные права заимствования)</td>\n",
       "      <td>1173526</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>38</th>\n",
       "      <td>941</td>\n",
       "      <td>RSD</td>\n",
       "      <td>100</td>\n",
       "      <td>Сербских динаров</td>\n",
       "      <td>849292</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>39</th>\n",
       "      <td>702</td>\n",
       "      <td>SGD</td>\n",
       "      <td>1</td>\n",
       "      <td>Сингапурский доллар</td>\n",
       "      <td>673703</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>40</th>\n",
       "      <td>417</td>\n",
       "      <td>KGS</td>\n",
       "      <td>100</td>\n",
       "      <td>Сомов</td>\n",
       "      <td>978853</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>41</th>\n",
       "      <td>972</td>\n",
       "      <td>TJS</td>\n",
       "      <td>10</td>\n",
       "      <td>Сомони</td>\n",
       "      <td>925493</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>42</th>\n",
       "      <td>50</td>\n",
       "      <td>BDT</td>\n",
       "      <td>100</td>\n",
       "      <td>Так</td>\n",
       "      <td>695541</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>43</th>\n",
       "      <td>398</td>\n",
       "      <td>KZT</td>\n",
       "      <td>100</td>\n",
       "      <td>Тенге</td>\n",
       "      <td>185163</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>44</th>\n",
       "      <td>496</td>\n",
       "      <td>MNT</td>\n",
       "      <td>1000</td>\n",
       "      <td>Тугриков</td>\n",
       "      <td>238085</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>45</th>\n",
       "      <td>949</td>\n",
       "      <td>TRY</td>\n",
       "      <td>10</td>\n",
       "      <td>Турецких лир</td>\n",
       "      <td>178002</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>46</th>\n",
       "      <td>860</td>\n",
       "      <td>UZS</td>\n",
       "      <td>10000</td>\n",
       "      <td>Узбекских сумов</td>\n",
       "      <td>725354</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>47</th>\n",
       "      <td>348</td>\n",
       "      <td>HUF</td>\n",
       "      <td>100</td>\n",
       "      <td>Форинтов</td>\n",
       "      <td>273441</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>48</th>\n",
       "      <td>826</td>\n",
       "      <td>GBP</td>\n",
       "      <td>1</td>\n",
       "      <td>Фунт стерлингов</td>\n",
       "      <td>1162629</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>49</th>\n",
       "      <td>203</td>\n",
       "      <td>CZK</td>\n",
       "      <td>10</td>\n",
       "      <td>Чешских крон</td>\n",
       "      <td>412713</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>50</th>\n",
       "      <td>752</td>\n",
       "      <td>SEK</td>\n",
       "      <td>10</td>\n",
       "      <td>Шведских крон</td>\n",
       "      <td>899373</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>51</th>\n",
       "      <td>756</td>\n",
       "      <td>CHF</td>\n",
       "      <td>1</td>\n",
       "      <td>Швейцарский франк</td>\n",
       "      <td>1064421</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>52</th>\n",
       "      <td>230</td>\n",
       "      <td>ETB</td>\n",
       "      <td>100</td>\n",
       "      <td>Эфиопских быров</td>\n",
       "      <td>534309</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>53</th>\n",
       "      <td>156</td>\n",
       "      <td>CNY</td>\n",
       "      <td>1</td>\n",
       "      <td>Юань</td>\n",
       "      <td>127335</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "    Цифр. код Букв. код   Единиц                                 Валюта  \\\n",
       "0          36       AUD        1                   Австралийский доллар   \n",
       "1         944       AZN        1                  Азербайджанский манат   \n",
       "2          12       DZD      100                      Алжирских динаров   \n",
       "3          51       AMD      100                       Армянских драмов   \n",
       "4         764       THB       10                                  Батов   \n",
       "5          48       BHD        1                      Бахрейнский динар   \n",
       "6         933       BYN        1                      Белорусский рубль   \n",
       "7          68       BOB       10                              Боливиано   \n",
       "8         986       BRL        1                       Бразильский реал   \n",
       "9         410       KRW     1000                                    Вон   \n",
       "10        344       HKD        1                     Гонконгский доллар   \n",
       "11        980       UAH       10                                 Гривен   \n",
       "12        208       DKK        1                          Датская крона   \n",
       "13        784       AED        1                             Дирхам ОАЭ   \n",
       "14        840       USD        1                             Доллар США   \n",
       "15        704       VND    10000                                 Донгов   \n",
       "16        978       EUR        1                                   Евро   \n",
       "17        818       EGP       10                      Египетских фунтов   \n",
       "18        985       PLN        1                                 Злотый   \n",
       "19        392       JPY      100                                    Иен   \n",
       "20        356       INR      100                        Индийских рупий   \n",
       "21        364       IRR  1000000                        Иранских риалов   \n",
       "22        124       CAD        1                       Канадский доллар   \n",
       "23        634       QAR        1                         Катарский риал   \n",
       "24        192       CUP       10                         Кубинских песо   \n",
       "25        104       MMK     1000                                 Кьятов   \n",
       "26        981       GEL        1                                   Лари   \n",
       "27        498       MDL       10                        Молдавских леев   \n",
       "28        566       NGN     1000                                   Найр   \n",
       "29        554       NZD        1                  Новозеландский доллар   \n",
       "30        934       TMT        1                Новый туркменский манат   \n",
       "31        578       NOK       10                        Норвежских крон   \n",
       "32        512       OMR        1                          Оманский риал   \n",
       "33        946       RON        1                          Румынский лей   \n",
       "34        360       IDR    10000                                  Рупий   \n",
       "35        710       ZAR       10                                 Рэндов   \n",
       "36        682       SAR        1                        Саудовский риял   \n",
       "37        960       XDR        1  СДР (специальные права заимствования)   \n",
       "38        941       RSD      100                       Сербских динаров   \n",
       "39        702       SGD        1                    Сингапурский доллар   \n",
       "40        417       KGS      100                                  Сомов   \n",
       "41        972       TJS       10                                 Сомони   \n",
       "42         50       BDT      100                                    Так   \n",
       "43        398       KZT      100                                  Тенге   \n",
       "44        496       MNT     1000                               Тугриков   \n",
       "45        949       TRY       10                           Турецких лир   \n",
       "46        860       UZS    10000                        Узбекских сумов   \n",
       "47        348       HUF      100                               Форинтов   \n",
       "48        826       GBP        1                        Фунт стерлингов   \n",
       "49        203       CZK       10                           Чешских крон   \n",
       "50        752       SEK       10                          Шведских крон   \n",
       "51        756       CHF        1                      Швейцарский франк   \n",
       "52        230       ETB      100                        Эфиопских быров   \n",
       "53        156       CNY        1                                   Юань   \n",
       "\n",
       "       Курс  \n",
       "0    615983  \n",
       "1    503534  \n",
       "2    643673  \n",
       "3    234960  \n",
       "4    259813  \n",
       "5   2276123  \n",
       "6    281618  \n",
       "7    723590  \n",
       "8    165766  \n",
       "9    620160  \n",
       "10   109171  \n",
       "11   192143  \n",
       "12   133359  \n",
       "13   233086  \n",
       "14   856007  \n",
       "15   334247  \n",
       "16   996820  \n",
       "17   170458  \n",
       "18   230054  \n",
       "19   537153  \n",
       "20   895766  \n",
       "21   544093  \n",
       "22   617565  \n",
       "23   235167  \n",
       "24   356670  \n",
       "25   407622  \n",
       "26   327658  \n",
       "27   495243  \n",
       "28   639486  \n",
       "29   510095  \n",
       "30   244573  \n",
       "31   916447  \n",
       "32  2226286  \n",
       "33   189504  \n",
       "34   481932  \n",
       "35   535701  \n",
       "36   228269  \n",
       "37  1173526  \n",
       "38   849292  \n",
       "39   673703  \n",
       "40   978853  \n",
       "41   925493  \n",
       "42   695541  \n",
       "43   185163  \n",
       "44   238085  \n",
       "45   178002  \n",
       "46   725354  \n",
       "47   273441  \n",
       "48  1162629  \n",
       "49   412713  \n",
       "50   899373  \n",
       "51  1064421  \n",
       "52   534309  \n",
       "53   127335  "
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "tables = pd.read_html(raw)\n",
    "print(\"Найдено таблиц:\", len(tables))\n",
    "tables[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5c661132",
   "metadata": {},
   "source": [
    "**Стоп.** Посмотрите на колонку «Курс»: было `52,3145`, стало `523145`.\n",
    "`pandas` принял запятую за разделитель разрядов и молча умножил все курсы на 10 000.\n",
    "\n",
    "Это самая опасная ошибка во всём занятии: код не упал, таблица выглядит нормально,\n",
    "а данные испорчены. С российскими источниками — где запятая десятичная,\n",
    "а разряды разделены неразрывным пробелом — это происходит **всегда**.\n",
    "Лечится одним аргументом:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "b147b638",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Цифр. код</th>\n",
       "      <th>Букв. код</th>\n",
       "      <th>Единиц</th>\n",
       "      <th>Валюта</th>\n",
       "      <th>Курс</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>36</td>\n",
       "      <td>AUD</td>\n",
       "      <td>1</td>\n",
       "      <td>Австралийский доллар</td>\n",
       "      <td>61.5983</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>944</td>\n",
       "      <td>AZN</td>\n",
       "      <td>1</td>\n",
       "      <td>Азербайджанский манат</td>\n",
       "      <td>50.3534</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>12</td>\n",
       "      <td>DZD</td>\n",
       "      <td>100</td>\n",
       "      <td>Алжирских динаров</td>\n",
       "      <td>64.3673</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>51</td>\n",
       "      <td>AMD</td>\n",
       "      <td>100</td>\n",
       "      <td>Армянских драмов</td>\n",
       "      <td>23.4960</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>764</td>\n",
       "      <td>THB</td>\n",
       "      <td>10</td>\n",
       "      <td>Батов</td>\n",
       "      <td>25.9813</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>48</td>\n",
       "      <td>BHD</td>\n",
       "      <td>1</td>\n",
       "      <td>Бахрейнский динар</td>\n",
       "      <td>227.6123</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>933</td>\n",
       "      <td>BYN</td>\n",
       "      <td>1</td>\n",
       "      <td>Белорусский рубль</td>\n",
       "      <td>28.1618</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>68</td>\n",
       "      <td>BOB</td>\n",
       "      <td>10</td>\n",
       "      <td>Боливиано</td>\n",
       "      <td>72.3590</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>986</td>\n",
       "      <td>BRL</td>\n",
       "      <td>1</td>\n",
       "      <td>Бразильский реал</td>\n",
       "      <td>16.5766</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>410</td>\n",
       "      <td>KRW</td>\n",
       "      <td>1000</td>\n",
       "      <td>Вон</td>\n",
       "      <td>62.0160</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>344</td>\n",
       "      <td>HKD</td>\n",
       "      <td>1</td>\n",
       "      <td>Гонконгский доллар</td>\n",
       "      <td>10.9171</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>980</td>\n",
       "      <td>UAH</td>\n",
       "      <td>10</td>\n",
       "      <td>Гривен</td>\n",
       "      <td>19.2143</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>208</td>\n",
       "      <td>DKK</td>\n",
       "      <td>1</td>\n",
       "      <td>Датская крона</td>\n",
       "      <td>13.3359</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>784</td>\n",
       "      <td>AED</td>\n",
       "      <td>1</td>\n",
       "      <td>Дирхам ОАЭ</td>\n",
       "      <td>23.3086</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>840</td>\n",
       "      <td>USD</td>\n",
       "      <td>1</td>\n",
       "      <td>Доллар США</td>\n",
       "      <td>85.6007</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>704</td>\n",
       "      <td>VND</td>\n",
       "      <td>10000</td>\n",
       "      <td>Донгов</td>\n",
       "      <td>33.4247</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>978</td>\n",
       "      <td>EUR</td>\n",
       "      <td>1</td>\n",
       "      <td>Евро</td>\n",
       "      <td>99.6820</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>818</td>\n",
       "      <td>EGP</td>\n",
       "      <td>10</td>\n",
       "      <td>Египетских фунтов</td>\n",
       "      <td>17.0458</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>985</td>\n",
       "      <td>PLN</td>\n",
       "      <td>1</td>\n",
       "      <td>Злотый</td>\n",
       "      <td>23.0054</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>19</th>\n",
       "      <td>392</td>\n",
       "      <td>JPY</td>\n",
       "      <td>100</td>\n",
       "      <td>Иен</td>\n",
       "      <td>53.7153</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>20</th>\n",
       "      <td>356</td>\n",
       "      <td>INR</td>\n",
       "      <td>100</td>\n",
       "      <td>Индийских рупий</td>\n",
       "      <td>89.5766</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>21</th>\n",
       "      <td>364</td>\n",
       "      <td>IRR</td>\n",
       "      <td>1000000</td>\n",
       "      <td>Иранских риалов</td>\n",
       "      <td>54.4093</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>22</th>\n",
       "      <td>124</td>\n",
       "      <td>CAD</td>\n",
       "      <td>1</td>\n",
       "      <td>Канадский доллар</td>\n",
       "      <td>61.7565</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>23</th>\n",
       "      <td>634</td>\n",
       "      <td>QAR</td>\n",
       "      <td>1</td>\n",
       "      <td>Катарский риал</td>\n",
       "      <td>23.5167</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>24</th>\n",
       "      <td>192</td>\n",
       "      <td>CUP</td>\n",
       "      <td>10</td>\n",
       "      <td>Кубинских песо</td>\n",
       "      <td>35.6670</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>25</th>\n",
       "      <td>104</td>\n",
       "      <td>MMK</td>\n",
       "      <td>1000</td>\n",
       "      <td>Кьятов</td>\n",
       "      <td>40.7622</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>26</th>\n",
       "      <td>981</td>\n",
       "      <td>GEL</td>\n",
       "      <td>1</td>\n",
       "      <td>Лари</td>\n",
       "      <td>32.7658</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>27</th>\n",
       "      <td>498</td>\n",
       "      <td>MDL</td>\n",
       "      <td>10</td>\n",
       "      <td>Молдавских леев</td>\n",
       "      <td>49.5243</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>28</th>\n",
       "      <td>566</td>\n",
       "      <td>NGN</td>\n",
       "      <td>1000</td>\n",
       "      <td>Найр</td>\n",
       "      <td>63.9486</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>29</th>\n",
       "      <td>554</td>\n",
       "      <td>NZD</td>\n",
       "      <td>1</td>\n",
       "      <td>Новозеландский доллар</td>\n",
       "      <td>51.0095</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>30</th>\n",
       "      <td>934</td>\n",
       "      <td>TMT</td>\n",
       "      <td>1</td>\n",
       "      <td>Новый туркменский манат</td>\n",
       "      <td>24.4573</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>31</th>\n",
       "      <td>578</td>\n",
       "      <td>NOK</td>\n",
       "      <td>10</td>\n",
       "      <td>Норвежских крон</td>\n",
       "      <td>91.6447</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>32</th>\n",
       "      <td>512</td>\n",
       "      <td>OMR</td>\n",
       "      <td>1</td>\n",
       "      <td>Оманский риал</td>\n",
       "      <td>222.6286</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>33</th>\n",
       "      <td>946</td>\n",
       "      <td>RON</td>\n",
       "      <td>1</td>\n",
       "      <td>Румынский лей</td>\n",
       "      <td>18.9504</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>34</th>\n",
       "      <td>360</td>\n",
       "      <td>IDR</td>\n",
       "      <td>10000</td>\n",
       "      <td>Рупий</td>\n",
       "      <td>48.1932</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>35</th>\n",
       "      <td>710</td>\n",
       "      <td>ZAR</td>\n",
       "      <td>10</td>\n",
       "      <td>Рэндов</td>\n",
       "      <td>53.5701</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>36</th>\n",
       "      <td>682</td>\n",
       "      <td>SAR</td>\n",
       "      <td>1</td>\n",
       "      <td>Саудовский риял</td>\n",
       "      <td>22.8269</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>37</th>\n",
       "      <td>960</td>\n",
       "      <td>XDR</td>\n",
       "      <td>1</td>\n",
       "      <td>СДР (специальные права заимствования)</td>\n",
       "      <td>117.3526</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>38</th>\n",
       "      <td>941</td>\n",
       "      <td>RSD</td>\n",
       "      <td>100</td>\n",
       "      <td>Сербских динаров</td>\n",
       "      <td>84.9292</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>39</th>\n",
       "      <td>702</td>\n",
       "      <td>SGD</td>\n",
       "      <td>1</td>\n",
       "      <td>Сингапурский доллар</td>\n",
       "      <td>67.3703</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>40</th>\n",
       "      <td>417</td>\n",
       "      <td>KGS</td>\n",
       "      <td>100</td>\n",
       "      <td>Сомов</td>\n",
       "      <td>97.8853</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>41</th>\n",
       "      <td>972</td>\n",
       "      <td>TJS</td>\n",
       "      <td>10</td>\n",
       "      <td>Сомони</td>\n",
       "      <td>92.5493</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>42</th>\n",
       "      <td>50</td>\n",
       "      <td>BDT</td>\n",
       "      <td>100</td>\n",
       "      <td>Так</td>\n",
       "      <td>69.5541</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>43</th>\n",
       "      <td>398</td>\n",
       "      <td>KZT</td>\n",
       "      <td>100</td>\n",
       "      <td>Тенге</td>\n",
       "      <td>18.5163</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>44</th>\n",
       "      <td>496</td>\n",
       "      <td>MNT</td>\n",
       "      <td>1000</td>\n",
       "      <td>Тугриков</td>\n",
       "      <td>23.8085</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>45</th>\n",
       "      <td>949</td>\n",
       "      <td>TRY</td>\n",
       "      <td>10</td>\n",
       "      <td>Турецких лир</td>\n",
       "      <td>17.8002</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>46</th>\n",
       "      <td>860</td>\n",
       "      <td>UZS</td>\n",
       "      <td>10000</td>\n",
       "      <td>Узбекских сумов</td>\n",
       "      <td>72.5354</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>47</th>\n",
       "      <td>348</td>\n",
       "      <td>HUF</td>\n",
       "      <td>100</td>\n",
       "      <td>Форинтов</td>\n",
       "      <td>27.3441</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>48</th>\n",
       "      <td>826</td>\n",
       "      <td>GBP</td>\n",
       "      <td>1</td>\n",
       "      <td>Фунт стерлингов</td>\n",
       "      <td>116.2629</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>49</th>\n",
       "      <td>203</td>\n",
       "      <td>CZK</td>\n",
       "      <td>10</td>\n",
       "      <td>Чешских крон</td>\n",
       "      <td>41.2713</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>50</th>\n",
       "      <td>752</td>\n",
       "      <td>SEK</td>\n",
       "      <td>10</td>\n",
       "      <td>Шведских крон</td>\n",
       "      <td>89.9373</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>51</th>\n",
       "      <td>756</td>\n",
       "      <td>CHF</td>\n",
       "      <td>1</td>\n",
       "      <td>Швейцарский франк</td>\n",
       "      <td>106.4421</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>52</th>\n",
       "      <td>230</td>\n",
       "      <td>ETB</td>\n",
       "      <td>100</td>\n",
       "      <td>Эфиопских быров</td>\n",
       "      <td>53.4309</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>53</th>\n",
       "      <td>156</td>\n",
       "      <td>CNY</td>\n",
       "      <td>1</td>\n",
       "      <td>Юань</td>\n",
       "      <td>12.7335</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "    Цифр. код Букв. код   Единиц                                 Валюта  \\\n",
       "0          36       AUD        1                   Австралийский доллар   \n",
       "1         944       AZN        1                  Азербайджанский манат   \n",
       "2          12       DZD      100                      Алжирских динаров   \n",
       "3          51       AMD      100                       Армянских драмов   \n",
       "4         764       THB       10                                  Батов   \n",
       "5          48       BHD        1                      Бахрейнский динар   \n",
       "6         933       BYN        1                      Белорусский рубль   \n",
       "7          68       BOB       10                              Боливиано   \n",
       "8         986       BRL        1                       Бразильский реал   \n",
       "9         410       KRW     1000                                    Вон   \n",
       "10        344       HKD        1                     Гонконгский доллар   \n",
       "11        980       UAH       10                                 Гривен   \n",
       "12        208       DKK        1                          Датская крона   \n",
       "13        784       AED        1                             Дирхам ОАЭ   \n",
       "14        840       USD        1                             Доллар США   \n",
       "15        704       VND    10000                                 Донгов   \n",
       "16        978       EUR        1                                   Евро   \n",
       "17        818       EGP       10                      Египетских фунтов   \n",
       "18        985       PLN        1                                 Злотый   \n",
       "19        392       JPY      100                                    Иен   \n",
       "20        356       INR      100                        Индийских рупий   \n",
       "21        364       IRR  1000000                        Иранских риалов   \n",
       "22        124       CAD        1                       Канадский доллар   \n",
       "23        634       QAR        1                         Катарский риал   \n",
       "24        192       CUP       10                         Кубинских песо   \n",
       "25        104       MMK     1000                                 Кьятов   \n",
       "26        981       GEL        1                                   Лари   \n",
       "27        498       MDL       10                        Молдавских леев   \n",
       "28        566       NGN     1000                                   Найр   \n",
       "29        554       NZD        1                  Новозеландский доллар   \n",
       "30        934       TMT        1                Новый туркменский манат   \n",
       "31        578       NOK       10                        Норвежских крон   \n",
       "32        512       OMR        1                          Оманский риал   \n",
       "33        946       RON        1                          Румынский лей   \n",
       "34        360       IDR    10000                                  Рупий   \n",
       "35        710       ZAR       10                                 Рэндов   \n",
       "36        682       SAR        1                        Саудовский риял   \n",
       "37        960       XDR        1  СДР (специальные права заимствования)   \n",
       "38        941       RSD      100                       Сербских динаров   \n",
       "39        702       SGD        1                    Сингапурский доллар   \n",
       "40        417       KGS      100                                  Сомов   \n",
       "41        972       TJS       10                                 Сомони   \n",
       "42         50       BDT      100                                    Так   \n",
       "43        398       KZT      100                                  Тенге   \n",
       "44        496       MNT     1000                               Тугриков   \n",
       "45        949       TRY       10                           Турецких лир   \n",
       "46        860       UZS    10000                        Узбекских сумов   \n",
       "47        348       HUF      100                               Форинтов   \n",
       "48        826       GBP        1                        Фунт стерлингов   \n",
       "49        203       CZK       10                           Чешских крон   \n",
       "50        752       SEK       10                          Шведских крон   \n",
       "51        756       CHF        1                      Швейцарский франк   \n",
       "52        230       ETB      100                        Эфиопских быров   \n",
       "53        156       CNY        1                                   Юань   \n",
       "\n",
       "        Курс  \n",
       "0    61.5983  \n",
       "1    50.3534  \n",
       "2    64.3673  \n",
       "3    23.4960  \n",
       "4    25.9813  \n",
       "5   227.6123  \n",
       "6    28.1618  \n",
       "7    72.3590  \n",
       "8    16.5766  \n",
       "9    62.0160  \n",
       "10   10.9171  \n",
       "11   19.2143  \n",
       "12   13.3359  \n",
       "13   23.3086  \n",
       "14   85.6007  \n",
       "15   33.4247  \n",
       "16   99.6820  \n",
       "17   17.0458  \n",
       "18   23.0054  \n",
       "19   53.7153  \n",
       "20   89.5766  \n",
       "21   54.4093  \n",
       "22   61.7565  \n",
       "23   23.5167  \n",
       "24   35.6670  \n",
       "25   40.7622  \n",
       "26   32.7658  \n",
       "27   49.5243  \n",
       "28   63.9486  \n",
       "29   51.0095  \n",
       "30   24.4573  \n",
       "31   91.6447  \n",
       "32  222.6286  \n",
       "33   18.9504  \n",
       "34   48.1932  \n",
       "35   53.5701  \n",
       "36   22.8269  \n",
       "37  117.3526  \n",
       "38   84.9292  \n",
       "39   67.3703  \n",
       "40   97.8853  \n",
       "41   92.5493  \n",
       "42   69.5541  \n",
       "43   18.5163  \n",
       "44   23.8085  \n",
       "45   17.8002  \n",
       "46   72.5354  \n",
       "47   27.3441  \n",
       "48  116.2629  \n",
       "49   41.2713  \n",
       "50   89.9373  \n",
       "51  106.4421  \n",
       "52   53.4309  \n",
       "53   12.7335  "
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "tables = pd.read_html(raw, decimal=\",\", thousands=\"\\xa0\")\n",
    "tables[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "49228418",
   "metadata": {},
   "source": [
    "> **Мораль.** «Одна строка вместо цикла» — удобно, но вы теряете контроль над приведением типов.\n",
    "> Всегда глазами сверяйте пару значений с тем, что видно на странице.\n",
    "> Явный цикл с `to_float` (блок 6) многословнее, но не преподносит сюрпризов.\n",
    "\n",
    "Второе ограничение: `read_html` находит только теги `<table>`. Современные сайты\n",
    "часто верстают «таблицы» из `<div>`, и тогда возвращаться к BeautifulSoup всё равно придётся."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "06ea6cb1",
   "metadata": {},
   "source": [
    "---\n",
    "# Блок 5. Источники тональностей."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dd5f4586",
   "metadata": {},
   "source": [
    "### Шаг 0. Подготовка сессии"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "1b6a5b94",
   "metadata": {},
   "outputs": [],
   "source": [
    "CACHE = pathlib.Path(\"cache_live\"); CACHE.mkdir(exist_ok=True)\n",
    "SOURCES_STATUS = {}  # сюда запишем, что получилось скачать, а что взято из снимка\n",
    "\n",
    "def make_session():\n",
    "    s = requests.Session()\n",
    "    s.headers.update({\n",
    "        \"User-Agent\": \"Mozilla/5.0 (compatible; seminar-demo/1.0; +mailto:illakap1d0@gmail.com)\",\n",
    "        \"Accept-Language\": \"ru-RU,ru;q=0.9\",\n",
    "    })\n",
    "    retry = Retry(total=2, backoff_factor=1,\n",
    "                  status_forcelist=[429, 500, 502, 503, 504], allowed_methods=[\"GET\"])\n",
    "    s.mount(\"https://\", HTTPAdapter(max_retries=retry))\n",
    "    return s\n",
    "\n",
    "session = make_session()\n",
    "\n",
    "def polite_sleep(base=1.0, jitter=0.5):\n",
    "    time.sleep(base + random.uniform(0, jitter))\n",
    "\n",
    "def get_html(url, name, encoding=None):\n",
    "    \"\"\"Скачать страницу; при любой проблеме подставить сохранённый снимок.\n",
    "\n",
    "    Возвращает (байты, откуда_взято).\"\"\"\n",
    "    path = CACHE / f\"{name}.html\"\n",
    "    if path.exists():\n",
    "        return path.read_bytes(), \"кэш\"\n",
    "    try:\n",
    "        r = session.get(url, timeout=12)\n",
    "        r.raise_for_status()\n",
    "        path.write_bytes(r.content)\n",
    "        polite_sleep()\n",
    "        return r.content, \"сеть\"\n",
    "    except Exception as e:\n",
    "        print(f\"{type(e).__name__}: не удалось скачать {url}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6b4faf04",
   "metadata": {},
   "source": [
    "---\n",
    "## Шаг 1. [Лента новостей ПРАЙМ Раскрытие](https://disclosure.1prime.ru/)\n",
    "\n",
    "Самый простой случай: данные лежат в честной HTML-таблице с говорящими классами.\n",
    "`table#TableNews` → строки → в каждой `span.datetime` и `<a>` с заголовком.\n",
    "\n",
    "Обратите внимание на кодировку: сайт целиком в **windows-1251**. Передаём в\n",
    "BeautifulSoup байты, а не `resp.text`, и она определит кодировку сама."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "b3db5364",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Источник данных: кэш\n",
      "Кодировка, определённая парсером: windows-1251\n",
      "Строк в таблице: 100\n",
      "\n",
      "<tr>\n",
      " <td valign=\"top\">\n",
      "  <div class=\"news\">\n",
      "   <span class=\"datetime\">\n",
      "    26.08.2026\n",
      "   </span>\n",
      "   <br/>\n",
      "   <p>\n",
      "    <a href=\"/news/-203/{a8035bc9-2a4e-48f6-9eb7-4fc2f1fa931f}.uif\" title=\"Крупнейшие брокеры-депозитарии РФ готовы к запуску быстрых переводов ценных бумаг - ЦБ\">\n",
      "     Крупнейшие брокеры-депозитарии РФ готовы к запуску быстрых переводов ценных бумаг - ЦБ\n",
      "    </a>\n",
      "   </p>\n",
      "  </div>\n",
      " </td>\n"
     ]
    }
   ],
   "source": [
    "raw, src = get_html(\"https://disclosure.1prime.ru/\", \"prime_news\")\n",
    "SOURCES_STATUS[\"ПРАЙМ\"] = src\n",
    "print(f\"Источник данных: {src}\")\n",
    "\n",
    "soup = BeautifulSoup(raw, \"lxml\")\n",
    "print(\"Кодировка, определённая парсером:\", soup.original_encoding)\n",
    "\n",
    "table = soup.find(\"table\", id=\"TableNews\")\n",
    "print(\"Строк в таблице:\", len(table.find_all(\"tr\")))\n",
    "print()\n",
    "print(table.find(\"tr\").prettify()[:402])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "41413a51",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Новостей собрано: 100\n",
      "Период: 2026-04-21 — 2026-08-26\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>date</th>\n",
       "      <th>title</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2026-08-26</td>\n",
       "      <td>Крупнейшие брокеры-депозитарии РФ готовы к зап...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2026-08-26</td>\n",
       "      <td>ЦБ РФ предлагает усилить требования к представ...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2026-08-26</td>\n",
       "      <td>Чистая прибыль Мосбиржи по МСФО во II квартале...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2026-08-25</td>\n",
       "      <td>Число квалифицированных розничных инвесторов в...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2026-08-25</td>\n",
       "      <td>Инвесторы во II квартале внесли рекордный трил...</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "        date                                              title\n",
       "0 2026-08-26  Крупнейшие брокеры-депозитарии РФ готовы к зап...\n",
       "1 2026-08-26  ЦБ РФ предлагает усилить требования к представ...\n",
       "2 2026-08-26  Чистая прибыль Мосбиржи по МСФО во II квартале...\n",
       "3 2026-08-25  Число квалифицированных розничных инвесторов в...\n",
       "4 2026-08-25  Инвесторы во II квартале внесли рекордный трил..."
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def parse_prime_row(tr):\n",
    "    \"\"\"Одна строка ленты. None — если строка не новостная.\"\"\"\n",
    "    dt = tr.find(\"span\", class_=\"datetime\")\n",
    "    a = tr.find(\"a\")\n",
    "    if not (dt and a):\n",
    "        return None\n",
    "    return {\n",
    "        \"date\":  dt.get_text(strip=True),\n",
    "        \"title\": a.get(\"title\") or a.get_text(\" \", strip=True),\n",
    "        \"url\":   a.get(\"href\"),\n",
    "    }\n",
    "\n",
    "news = pd.DataFrame([r for r in (parse_prime_row(tr) for tr in table.find_all(\"tr\")) if r])\n",
    "news[\"date\"] = pd.to_datetime(news[\"date\"], format=\"%d.%m.%Y\", errors=\"coerce\")\n",
    "news = news.dropna(subset=[\"date\"]).reset_index(drop=True)\n",
    "\n",
    "print(\"Новостей собрано:\", len(news))\n",
    "print(\"Период:\", news[\"date\"].min().date(), \"—\", news[\"date\"].max().date())\n",
    "news[[\"date\", \"title\"]].head(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "75972f8f",
   "metadata": {},
   "source": [
    "---\n",
    "# Шаг 2. [Форум частных инвесторов Smart-lab](https://smart-lab.ru/forum/)\n",
    "\n",
    "Еще один тип текста: живая речь трейдеров.\n",
    "\n",
    "Разметка здесь семантическая и удобная — список `ol.forum_cmts`, в каждом\n",
    "`li.cm_wrap` лежат автор, время и текст."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "53a28a58",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "SBER: 25 реплик (источник: кэш)\n",
      "GAZP: 25 реплик (источник: кэш)\n",
      "\n",
      "Всего реплик: 50\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>ticker</th>\n",
       "      <th>dt</th>\n",
       "      <th>author</th>\n",
       "      <th>text</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>45</th>\n",
       "      <td>GAZP</td>\n",
       "      <td>2026-08-28 18:08:56+03:00</td>\n",
       "      <td>Alex666</td>\n",
       "      <td>Alex666, ну в отчёте посмотри нижнюю строчку T...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>46</th>\n",
       "      <td>GAZP</td>\n",
       "      <td>2026-08-28 18:01:42+03:00</td>\n",
       "      <td>Toptygin11</td>\n",
       "      <td>Toptygin11, с чего это в прошлом году больше? ...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>47</th>\n",
       "      <td>GAZP</td>\n",
       "      <td>2026-08-28 18:01:18+03:00</td>\n",
       "      <td>Илья</td>\n",
       "      <td>Чистый долг Газпрома по итогам I полугодия сох...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>48</th>\n",
       "      <td>GAZP</td>\n",
       "      <td>2026-08-28 18:02:16+03:00</td>\n",
       "      <td>Alex666</td>\n",
       "      <td>В прошлом году больше было и ничего не дали. В...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>49</th>\n",
       "      <td>GAZP</td>\n",
       "      <td>2026-08-28 17:58:26+03:00</td>\n",
       "      <td>Toptygin11</td>\n",
       "      <td>МОСКВА, 28 августа. /ТАСС/. Группа «Газпром» п...</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   ticker                        dt      author  \\\n",
       "45   GAZP 2026-08-28 18:08:56+03:00     Alex666   \n",
       "46   GAZP 2026-08-28 18:01:42+03:00  Toptygin11   \n",
       "47   GAZP 2026-08-28 18:01:18+03:00        Илья   \n",
       "48   GAZP 2026-08-28 18:02:16+03:00     Alex666   \n",
       "49   GAZP 2026-08-28 17:58:26+03:00  Toptygin11   \n",
       "\n",
       "                                                 text  \n",
       "45  Alex666, ну в отчёте посмотри нижнюю строчку T...  \n",
       "46  Toptygin11, с чего это в прошлом году больше? ...  \n",
       "47  Чистый долг Газпрома по итогам I полугодия сох...  \n",
       "48  В прошлом году больше было и ничего не дали. В...  \n",
       "49  МОСКВА, 28 августа. /ТАСС/. Группа «Газпром» п...  "
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def parse_forum(raw_bytes):\n",
    "    s = BeautifulSoup(raw_bytes, \"lxml\")\n",
    "    out = []\n",
    "    for li in s.select(\"ol.forum_cmts > li.cm_wrap\"):\n",
    "        body = li.find(\"div\", class_=\"text\")\n",
    "        if body is None:                       # первый li — форма ввода комментария\n",
    "            continue\n",
    "        author = li.find(\"a\", class_=\"a_name\")\n",
    "        tm = li.find(\"time\")\n",
    "        out.append({\n",
    "            \"author\": author.get_text(strip=True) if author else None,\n",
    "            \"dt\":     tm.get(\"datetime\") if tm else None,\n",
    "            \"text\":   body.get_text(\" \", strip=True),\n",
    "        })\n",
    "    return out\n",
    "\n",
    "forum_rows = []\n",
    "for ticker in [\"SBER\", \"GAZP\"]:\n",
    "    raw_f, src_f = get_html(f\"https://smart-lab.ru/forum/{ticker}\",\n",
    "                            f\"slab_{ticker}\")\n",
    "    SOURCES_STATUS[f\"Smart-lab {ticker}\"] = src_f\n",
    "    rows = parse_forum(raw_f)\n",
    "    for r in rows:\n",
    "        r[\"ticker\"] = ticker\n",
    "    forum_rows += rows\n",
    "    print(f\"{ticker}: {len(rows)} реплик (источник: {src_f})\")\n",
    "\n",
    "forum = pd.DataFrame(forum_rows)\n",
    "forum[\"dt\"] = pd.to_datetime(forum[\"dt\"], format=\"ISO8601\", utc=True).dt.tz_convert(\"Europe/Moscow\")\n",
    "forum[\"date\"] = forum[\"dt\"].dt.date\n",
    "print(\"\\nВсего реплик:\", len(forum))\n",
    "forum[[\"ticker\", \"dt\", \"author\", \"text\"]].tail(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "45cbdc12",
   "metadata": {},
   "source": [
    "---\n",
    "## Шаг 3. Измерение тональности\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2f311a73",
   "metadata": {},
   "source": [
    "Попробуем прикинуть, какие слова ассоциированы с позитивным тоном новостей, а какие с негативным. Список примерный и его можно редактировать по своему усмотрению."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "f54913aa",
   "metadata": {},
   "outputs": [],
   "source": [
    "WORD_RE = re.compile(r\"[а-яёА-ЯЁa-zA-Z]{3,}\")\n",
    "\n",
    "def make_tone(negative, positive):\n",
    "    \"\"\"Возвращает функцию тона для заданной пары словарей.\"\"\"\n",
    "    def tone(text):\n",
    "        words = [w.lower() for w in WORD_RE.findall(text or \"\")]\n",
    "        if not words:\n",
    "            return 0.0\n",
    "        neg = sum(w in negative for w in words)\n",
    "        pos = sum(w in positive for w in words)\n",
    "        return (pos - neg) / len(words)\n",
    "    return tone\n",
    "\n",
    "# Сами словари\n",
    "NEG = {\n",
    "    \"убыток\", \"убытки\", \"падение\", \"падения\", \"снижение\", \"снизились\", \"снизилась\",\n",
    "    \"обвал\", \"сокращение\", \"сократили\", \"сократил\", \"риск\", \"риски\", \"хуже\",\n",
    "    \"неопределённость\", \"санкции\", \"санкций\", \"понизила\", \"понизили\", \"давит\",\n",
    "    \"проблема\", \"проблемы\", \"кризис\", \"просрочка\", \"дефолт\", \"жалоб\", \"жалобы\",\n",
    "    \"отказ\", \"отказали\", \"отказано\", \"заблокировали\", \"заблокировал\", \"блокировка\",\n",
    "    \"обман\", \"обманули\", \"лжет\", \"вводит\", \"заблуждение\", \"навязали\", \"навязывание\",\n",
    "    \"незаконное\", \"незаконно\", \"нарушает\", \"нарушением\", \"списали\", \"списание\",\n",
    "    \"ошибка\", \"ошибками\", \"сбой\", \"молчит\", \"игнорировав\", \"невозможно\",\n",
    "    \"переносит\", \"затягивание\", \"худший\", \"равнодушием\",\n",
    "    \"ущемление\", \"необоснованное\", \"повышение\", \"поднял\", \"комиссия\",\n",
    "}\n",
    "POS = {\n",
    "    \"прибыль\", \"рост\", \"роста\", \"выросла\", \"вырос\", \"выросли\", \"рекордный\",\n",
    "    \"рекордных\", \"улучшились\", \"улучшить\", \"оптимистично\", \"превысила\", \"превысило\",\n",
    "    \"повысил\", \"дивиденды\", \"дивидендных\", \"поддержки\", \"стабильный\", \"успешно\",\n",
    "    \"лучший\", \"выгодно\", \"прибыли\", \"спасибо\", \"благодарность\", \"благодарна\", \"благодарю\", \"поблагодарить\",\n",
    "    \"помогли\", \"помогла\", \"помочь\", \"помощь\", \"решил\", \"решили\", \"решение\",\n",
    "    \"вежливые\", \"внимательное\", \"профессионализм\", \"удобное\", \"оперативно\",\n",
    "    \"качественно\", \"отличный\", \"рекомендую\", \"довольна\", \"рада\", \"полезно\",\n",
    "    \"продуктивно\", \"вернули\", \"восстановили\", \"мгновенно\", \"надёжная\",\n",
    "}\n",
    "\n",
    "tone_v2 = make_tone(NEG, POS)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fc95f7db",
   "metadata": {},
   "source": [
    "Один и тот же словарь применим ко обоим корпусам. Посмотрим,\n",
    "где он вообще что-то различает, а где молчит.\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "11570243",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>источник</th>\n",
       "      <th>n</th>\n",
       "      <th>слов в тексте (медиана)</th>\n",
       "      <th>средний тон</th>\n",
       "      <th>стд. откл.</th>\n",
       "      <th>нулевой тон, %</th>\n",
       "      <th>уникальных значений</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>A. Новости ПРАЙМ</td>\n",
       "      <td>100</td>\n",
       "      <td>11</td>\n",
       "      <td>0.0095</td>\n",
       "      <td>0.0622</td>\n",
       "      <td>81.0</td>\n",
       "      <td>14</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>C. Форум Smart-lab</td>\n",
       "      <td>50</td>\n",
       "      <td>36</td>\n",
       "      <td>0.0137</td>\n",
       "      <td>0.0388</td>\n",
       "      <td>56.0</td>\n",
       "      <td>21</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "             источник    n  слов в тексте (медиана)  средний тон  стд. откл.  \\\n",
       "0    A. Новости ПРАЙМ  100                       11       0.0095      0.0622   \n",
       "1  C. Форум Smart-lab   50                       36       0.0137      0.0388   \n",
       "\n",
       "   нулевой тон, %  уникальных значений  \n",
       "0            81.0                   14  \n",
       "1            56.0                   21  "
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "news[\"tone\"] = news[\"title\"].apply(tone_v2)\n",
    "forum[\"tone\"] = forum[\"text\"].apply(tone_v2)\n",
    "\n",
    "def profile(name, series, texts):\n",
    "    lens = texts.str.split().str.len()\n",
    "    return {\n",
    "        \"источник\": name,\n",
    "        \"n\": len(series),\n",
    "        \"слов в тексте (медиана)\": int(lens.median()),\n",
    "        \"средний тон\": round(series.mean(), 4),\n",
    "        \"стд. откл.\": round(series.std(), 4),\n",
    "        \"нулевой тон, %\": round(100 * (series == 0).mean(), 1),\n",
    "        \"уникальных значений\": series.nunique(),\n",
    "    }\n",
    "\n",
    "summary = pd.DataFrame([\n",
    "    profile(\"A. Новости ПРАЙМ\", news[\"tone\"], news[\"title\"]),\n",
    "    profile(\"C. Форум Smart-lab\", forum[\"tone\"], forum[\"text\"]),\n",
    "])\n",
    "summary"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "be5f949f",
   "metadata": {},
   "source": [
    "### Тон по эмитентам на форуме\n",
    "\n",
    "Реплики собраны за один день, поэтому временного ряда здесь не построить —\n",
    "зато отлично видно разницу настроений между двумя тикерами."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "7900c774",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "        реплик  средний тон  стд. откл.\n",
      "ticker                                 \n",
      "GAZP        25       0.0221      0.0371\n",
      "SBER        25       0.0053      0.0394\n",
      "\n",
      "Разница средних: -0.0168   t = -1.56, p = 0.126\n",
      "Значима на 5%: нет\n"
     ]
    }
   ],
   "source": [
    "from scipy import stats\n",
    "\n",
    "by_ticker = forum.groupby(\"ticker\")[\"tone\"].agg([\"size\", \"mean\", \"std\"]).round(4)\n",
    "by_ticker.columns = [\"реплик\", \"средний тон\", \"стд. откл.\"]\n",
    "print(by_ticker.to_string())\n",
    "print()\n",
    "\n",
    "a = forum.loc[forum[\"ticker\"] == \"SBER\", \"tone\"]\n",
    "b = forum.loc[forum[\"ticker\"] == \"GAZP\", \"tone\"]\n",
    "t, p = stats.ttest_ind(a, b, equal_var=False)\n",
    "\n",
    "print(f\"Разница средних: {a.mean() - b.mean():+.4f}   t = {t:.2f}, p = {p:.3f}\")\n",
    "print(\"Значима на 5%:\", \"да\" if p < 0.05 else \"нет\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "ee19b311",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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EREREckEAzwiIiIiIiKZYIBHREREREQkEwzwiIiIiIiIZIIBHhERERERkUwwwCMiIiIiIpIJBnhEREREREQywQCPiIiIiIhIJhjgERERERERyQQDPCIiIiIiIplggEdERERERCQTDPCIiIiIiIhkggEeERERERGRTDDAIyIiIiIikgkGeERERERERDLBAI+IiIiIiEgmGOARERERERHJBAM8IiIiIiIimWCAR0REREREJBMM8IiIiIiIiGSCAR4REREREZFMMMAjIiIiIiKSCQZ4REREREREMsEAj4iIiIiISCYY4BEREREREckEAzwiIiIiIiKZYIBHREREREQkEwzwiIiIiIiIZIIBHhERERERkUwwwCMiIiIiIpIJBnhEREREREQywQCPiIiIiIhIJhjgERERERERyQQDPCIiIiIiIplggEdERERERCQTDPCIiMigpaSk6PxLREREJTOp6QoQERHpk5SUhMjISBw7dgwA8M033yA4OBj+/v5wcHAo8/25mXfw4Npe6W9brx5QWjpXWX2JiIgMAQO8GpKSkoK8vDyYmprCzs6upqtDRGRQkpKSsGnTJjx8+BBeXl64fPkyvLy8cPLkSZw9exZDhw4tM8jLe3gX9859K/1t6dySAR4REckeA7xqpr0iffz4caSnp8Pa2hpBQUHlviJNRFQbREdHo2HDhrh9+zb27dsHT09P7Nu3D8HBwahXrx7Onj2L0NDQmq4mPUV4YZWIagsGeNVIe0U6NjZWmpaRkYFt27aV+4o0EZHcpaSkQKPRYO/evYiJiYGVlRUAIC0tDVu3bkXjxo3x4osvIiUlhT/UqUy8sEpEtQ2TrFSjyMhIneCusNjYWERFRVVzjYiIDI8QAtevX0diYiKUSiXu378PALh//z6USiUSExNx48YNCCFquKZk6LQXVrdt24aEhARkZGQgISEB27Ztw6ZNm5CUlFTTVSQiqnQM8KpJSkoKjh8/XmqZ48ePM0scEdV6ubm5OH/+PO7du4fbt28jKysLAJCVlYXbt2/j3r17OH/+PHJzc2u4pmToeGGViGojBnjVJC8vD+np6aWWSUtLQ15eXjXViIjIMBkZGSEpKQlpaWl656elpeH+/fswMuIpjErGC6tEVFvx7FhNTE1NYW1tXWoZGxsbmJqaVlONiIgMk0ajgYmJCYyNjfXONzY2hqmpKTQa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EwzwiIiIiIiIZIIBHhERERERkUwwwCMiIiIiIpIJBnhEREREREQywQCPiIiIiIhIJhjgERERERERyQQDPCIiIiIiIplggEdERERERCQTDPCIiIiIiIhkggEeERERERGRTDDAIyIiIiIikgkGeERERERERDLBAI+IiIiIiEgmGOARERERERHJBAM8IiIiIiIimWCAR0REREREJBMM8IiIiIiIiGSCAR4REREREZFMMMAjIiIiIiKSCQZ4REREREREMsEAj4iIiIiISCYY4BEREREREckEAzwiIiIiIiKZYIBHREREREQkEwzwiIiIiIiIZIIBHhERERERkUwwwCMiIiIiIpIJBnhEREREREQywQCPiIiIiIhIJhjgERERERERyQQDPCIiIiIiIplggEdERERERCQTDPCIiIiIiIhkggEeERERERGRTDDAIyIiIiIikgkGeERERERERDJhUtMVICIiepplJScjPzcXJkolzO3ta7o6BoXbhoio+jHAIyIiegyZ9+4hPjwcNw4fRk5aGsxsbODRvj1cW7eGpZNTTVevRnHbEBHVHAZ4REREFZR57x5Of/MN7l+8KE3LSU9HdFgYEiIj0Wr06FobyHDbEBHVLN6DR0REVEHx4eE6AUxh9y9eRPzp09VcI8PBbUNEVLMY4BEREVVAVnIybhw+XGqZG4cPIys5uZpqZDi4bYiIah6HaBIRUTFMjlGy/Nxc5KSllVomJzUV+bm5T7yu3Mw7eHBtr/S3rVcPKC2dn3i5VaU6tw0REenHAI+IiCRMjlEybdArCgpgZmODnPT0EsuaqdUwUSqfeJ15D+/i3rlvpb8tnVsadIBnolRW27YhIiL9GOAREREAJscoSdGg16FJE9Tx9UXKtWswLiFQ8Wjfvlb2fJrb28OjfXtEh4WVWKa2bhsiourCe/CIiAgAk2Poow16o8PCkHb7NnLS0xEfHg4TlQrmdnYo0DPU0NHHB66tWtVAbQ2Da+vWcPTx0Tuvtm8bIqLqwB48IiIqd3IMtzZtqr33JSslRfrXzMWlWtddUtB7+bff0LBbN+Tn5ODuuXPISU2FmVr9aDhrq1a1sqdTy9LJCa1Gj0b86dOPej25bYiIqhUDPCIiMsjkGJn37uH+pUt4kJAA4FGAmVq3LhybNKmWIKG0oDc/OxsXd+yAa+vWaPPmm1AYGxtMQhpDSJBj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zL4oD5U55JOIGOARyQJ/5BDVDg/v3UNOuhkv4KDqHrhNNYNJiogqDx+TQPSUY4ppotpLewGnNmL2RXkyt7eHtYsLgzuiJ8AAj+gpxx85RPKVm1ZyZkGt2noBR5t9sTTMvkhEtREDPKKnHH/kEMmYsQ0e3HDA/Vhr3I+1Rl6WcbEitfUCTmU/cJuISC4Y4BE95fgjh0i+VDauyHrQAEmxNkiKtUF+dvFb52vzBRzX1q3h6OOjdx6zLxJRbcUAj0gG+COHSJ54Aad02uyLzQcNgo2bG8ysrWHj5obmgwbV2uQzRETMokkkA0wxTSRffEZY6Zh9kYhIFwM8IpngjxwieeIFnPLh8Y6I6BEGeEQywx85RPLDCzhERFReDPCIiIieEgzqiIioLEyyQkREREREJBMM8IiIiIiIiGSCAR4REREREZFMMMAjIiIiIiKSCQZ4REREREREMsEAj4iIiIiISCYY4BEREREREckEAzwiIiIiIiKZYIBHREREREQkEwzwiIiIiIiIZIIBHhERERERkUwwwCMiIiIiIpIJBnhEREREREQywQCPiIiIiIhIJhjgERERERERyQQDPCIiIiIiIplggEdERERERCQTDPCIiIiIiIhkggEeERERERGRTDDAIyIiIiIikgkGeERERERERDLBAI+IiIiIiEgmGOARERERERHJBAM8IiIiIiIimWCAR0REREREJBMM8IiIiIiIiGSCAR4REREREZFMMMAjIiIiIiKSCQZ4REREREREMsEAj4iIiIiISCYY4BEREREREckEAzwiIiIiIiKZYIBHREREREQkEwzwiIiIiIiIZIIBHhERERERkUwwwCMiIiIiIpIJBnhEREREREQywQCPiIiIiIhIJhjgERERERERyYRJTVegOgghAABpaWk1XBMiIqqonJwcZGdnIy0tDWZmZjVdHSIiohqhjWW0sU1JFKKsEjIQFxcHd3f3mq4GERERERHRE7l16xbc3NxKnF8rAjyNRoP4+HhYW1tDoVDUdHWeKmlpaXB3d8etW7dgY2NT09WhpxDbED0ptiF6UmxD9KTYhuhJVUYbEkIgPT0drq6uMDIq+U67WjFE08jIqNQol8pmY2PDAxo9EbYhelJsQ/Sk2IboSbEN0ZN60jakVqvLLMMkK0RERERERDLBAI+IiIiIiEgmGOBRqczMzDBnzhxmrqPH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",
      "text/plain": [
       "<Figure size 900x360 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "rng = np.random.default_rng(7)\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 3.6))\n",
    "for i, (tk, g) in enumerate(forum.groupby(\"ticker\")):\n",
    "    ax.scatter(g[\"tone\"], i + rng.uniform(-0.22, 0.22, len(g)),\n",
    "               alpha=0.7, s=48, label=tk, color=[\"#8B1A1A\", \"#2C2C2C\"][i],\n",
    "               edgecolor=\"white\", linewidth=0.6)\n",
    "    ax.scatter([g[\"tone\"].mean()], [i], marker=\"|\", s=1400, linewidth=2.5,\n",
    "               color=\"#D4A843\", zorder=5)\n",
    "ax.axvline(0, color=\"grey\", lw=0.8)\n",
    "ax.set_yticks([0, 1]); ax.set_yticklabels(sorted(forum[\"ticker\"].unique()))\n",
    "ax.set_ylim(-0.6, 1.6)\n",
    "ax.set_xlabel(\"Тон реплики (по словарю)\")\n",
    "ax.set_title(\"Настроения на форуме: каждая точка — реплика, золотая черта — среднее\")\n",
    "plt.tight_layout(); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c3a05197",
   "metadata": {},
   "source": [
    "---\n",
    "## Блок 6. Регулярные выражения — дочистка результата\n",
    "\n",
    "Регулярные выражения в парсинге нужны **не для разбора HTML** (для этого есть BeautifulSoup),\n",
    "а для того, чтобы вытащить структуру из уже добытого текста: суммы, проценты, даты, тикеры.\n",
    "Это последние 10% работы, но без них данные в модель не положишь."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "a7a33568",
   "metadata": {},
   "outputs": [],
   "source": [
    "release = \"\"\"\n",
    "Чистая прибыль ПАО «Сбербанк» по МСФО за первое полугодие 2026 года составила\n",
    "1 234,5 млрд руб., что на 12,4% выше показателя годом ранее.\n",
    "Рентабельность капитала достигла 24,8%. Совет директоров рекомендовал дивиденды\n",
    "в размере 34,84 руб. на акцию. Отчётность опубликована 24.08.2026.\n",
    "Бумаги SBER и SBERP отреагировали ростом.\n",
    "\"\"\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eda94e7e",
   "metadata": {},
   "source": [
    "**Четыре шаблона, которые покрывают почти все ваши задачи.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "c4adaa32",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "проценты: ['12,4%', '24,8%']\n",
      "суммы   : ['1 234,5 млрд руб', '34,84 руб']\n",
      "даты    : ['24.08.2026']\n",
      "тикеры  : ['SBER', 'SBERP']\n"
     ]
    }
   ],
   "source": [
    "# 1) Проценты\n",
    "print(\"проценты:\", re.findall(r\"\\d+[.,]?\\d*\\s*%\", release))\n",
    "\n",
    "# 2) Денежные суммы (с неразрывными пробелами внутри числа)\n",
    "print(\"суммы   :\", re.findall(r\"\\d[\\d\\s\\xa0]*[.,]?\\d*\\s*(?:млрд|млн|тыс\\.?)?\\s*руб\", release))\n",
    "\n",
    "# 3) Даты в формате ДД.ММ.ГГГГ\n",
    "print(\"даты    :\", re.findall(r\"\\b\\d{2}\\.\\d{2}\\.\\d{4}\\b\", release))\n",
    "\n",
    "# 4) Тикеры: 4-5 заглавных латинских букв\n",
    "print(\"тикеры  :\", re.findall(r\"\\b[A-Z]{4,5}\\b\", release))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b3db000a",
   "metadata": {},
   "source": [
    "### Группы: достаём не всё совпадение, а его часть\n",
    "\n",
    "Круглые скобки `( )` помечают, какую часть совпадения вернуть.\n",
    "Это превращает regex из поиска в **извлечение**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "89ac0fbf",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "   '1 234,5' млрд           → 1,234,500,000,000.00 руб.\n",
      "     '34,84' (без единицы)  →              34.84 руб.\n"
     ]
    }
   ],
   "source": [
    "# число и единица измерения — по отдельности; единица необязательна\n",
    "MULT = {\"млрд\": 1e9, \"млн\": 1e6, \"тыс\": 1e3, \"тыс.\": 1e3, \"\": 1}\n",
    "\n",
    "for value, unit in re.findall(r\"([\\d\\s\\xa0]*\\d[.,]\\d+)\\s*(млрд|млн|тыс\\.?)?\\s*руб\", release):\n",
    "    clean = float(value.replace(\"\\xa0\", \"\").replace(\" \", \"\").replace(\",\", \".\"))\n",
    "    print(f\"{value.strip()!r:>12} {unit or '(без единицы)':<14} → {clean * MULT[unit]:>18,.2f} руб.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dd63fd50",
   "metadata": {},
   "source": [
    "### Нормализация числа: функция, которая вам понадобится всегда\n",
    "\n",
    "Всё, что вы достанете со страницы, — строка вида `\"1 234,5\"`.\n",
    "Эта функция превращает её в `float` и переживает неразрывные пробелы, знак `%` и валюту.\n",
    "\n",
    "Обратите внимание на приём: мы **ищем число**, а не вычищаем из строки всё лишнее.\n",
    "Второй подход кажется проще, но ломается на `\"80,4419 руб.\"` — точка от «руб.» остаётся\n",
    "в конце и `float()` падает."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "0e54cc02",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "      '1 234,5' → 1234.5\n",
      "       '24,8 %' → 24.8\n",
      " '80,4419 руб.' → 80.4419\n",
      "  '1\\xa0240,50' → 1240.5\n",
      "         '-3,7' → -3.7\n",
      "            '—' → None\n",
      "          'н/д' → None\n",
      "           None → None\n"
     ]
    }
   ],
   "source": [
    "NUM_RE = re.compile(r\"-?\\d[\\d\\s]*(?:[.,]\\d+)?\")\n",
    "\n",
    "def to_float(s):\n",
    "    \"\"\"'1 234,5 %' → 1234.5 ; '80,4419 руб.' → 80.4419 ; '—' → None\"\"\"\n",
    "    if s is None:\n",
    "        return None\n",
    "    m = NUM_RE.search(str(s).replace(\"\\xa0\", \" \"))   # ищем число, а не вычищаем всё остальное\n",
    "    if not m:\n",
    "        return None\n",
    "    try:\n",
    "        return float(m.group(0).replace(\" \", \"\").replace(\",\", \".\"))\n",
    "    except ValueError:\n",
    "        return None\n",
    "\n",
    "for raw in [\"1 234,5\", \"24,8 %\", \"80,4419 руб.\", \"1\\xa0240,50\", \"-3,7\", \"—\", \"н/д\", None]:\n",
    "    print(f\"{raw!r:>15} → {to_float(raw)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adeb3a2e",
   "metadata": {},
   "source": [
    "### Полезные приёмы очистки"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "e85fcae7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "'Прибыль Сбербанка выросла на 12%'\n",
      "ПАО Сбербанк\n",
      "['Отчетность', 'ПАО', 'Сбербанк', 'по', 'МСФО']\n"
     ]
    }
   ],
   "source": [
    "dirty = \"  Прибыль\\xa0Сбербанка\\n\\n   выросла   на 12% \"\n",
    "\n",
    "print(repr(re.sub(r\"\\s+\", \" \", dirty).strip()))          # схлопнуть все пробелы\n",
    "print(re.sub(r\"[«»\\\"']\", \"\", \"ПАО «Сбербанк»\"))          # убрать кавычки\n",
    "print(re.split(r\"\\W+\", \"Отчетность ПАО Сбербанк по МСФО\"))  # разбить на слова"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7ff6380a",
   "metadata": {},
   "source": [
    "> **Правило.** Если хочется распарсить HTML регуляркой — остановитесь.\n",
    "> HTML не является регулярным языком: вложенность, необязательные закрывающие теги\n",
    "> и атрибуты в произвольном порядке ломают любой шаблон. Regex — для текста внутри тега,\n",
    "> BeautifulSoup — для самих тегов."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "69d9c932",
   "metadata": {},
   "source": [
    "---\n",
    "## Блок 7. Вежливый парсинг: как не получить бан по IP\n",
    "\n",
    "Сайты не любят, когда их парсят, и у них есть на то основания: ваш скрипт создаёт нагрузку,\n",
    "за которую платит владелец сервера. Хорошая новость — **исследовательский парсинг почти никогда\n",
    "не конфликтует с сайтом**, если соблюдать несколько правил. Плохая — нарушив их,\n",
    "вы теряете доступ ровно тогда, когда данные нужны для дедлайна.\n",
    "\n",
    "### Что именно вас выдаёт\n",
    "\n",
    "| Признак | Как выглядит для сервера | Что делать |\n",
    "|---|---|---|\n",
    "| **Частота** | 50 запросов в секунду с одного IP | Пауза 1–3 сек между запросами |\n",
    "| **`User-Agent`** | `python-requests/2.31.0` | Подставить настоящий UA и контакт |\n",
    "| **Отсутствие сессии** | Каждый запрос без cookie, как первый визит | `requests.Session()` |\n",
    "| **Идеальная регулярность** | Запрос ровно каждые 1000 мс | Случайная добавка к паузе |\n",
    "| **Игнор `robots.txt`** | Заход в запрещённые разделы | Проверять перед сбором |\n",
    "| **Реакция на `429`** | Продолжает долбить после отказа | Экспоненциальный backoff |\n",
    "\n",
    "### Шкала последствий\n",
    "\n",
    "`429 Too Many Requests` → временное замедление → `403 Forbidden` для вашего UA →\n",
    "бан IP на часы или сутки → бан подсети (а это уже вся сеть университета).\n",
    "Последнее — реальный риск: заблокируют не вас, а всех коллег."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "682b3992",
   "metadata": {},
   "source": [
    "### Правило 1. Читать `robots.txt`\n",
    "\n",
    "Это файл в корне сайта, где владелец указывает, что можно обходить роботам.\n",
    "В Python есть готовый разборщик в стандартной библиотеке."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "afba7cb2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Разрешено: True | Crawl-delay: None\n"
     ]
    }
   ],
   "source": [
    "from urllib.robotparser import RobotFileParser\n",
    "\n",
    "def may_fetch(base_url, path=\"/\", agent=\"research-scraper\"):\n",
    "    rp = RobotFileParser()\n",
    "    rp.set_url(base_url.rstrip(\"/\") + \"/robots.txt\")\n",
    "    try:\n",
    "        rp.read()\n",
    "    except Exception as e:\n",
    "        print(\"robots.txt недоступен:\", type(e).__name__)\n",
    "        return None, None\n",
    "    return rp.can_fetch(agent, base_url.rstrip(\"/\") + path), rp.crawl_delay(agent)\n",
    "\n",
    "allowed, delay = may_fetch(\"https://httpbin.org\", \"/html\")\n",
    "print(\"Разрешено:\", allowed, \"| Crawl-delay:\", delay)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "51dfe71b",
   "metadata": {},
   "source": [
    "`Crawl-delay` — прямое указание сайта, сколько секунд ждать между запросами.\n",
    "Если он задан, его нужно соблюдать; если нет — 1–2 секунды по умолчанию.\n",
    "\n",
    "> **Юридический статус.** `robots.txt` — не закон, а соглашение. Но нарушение\n",
    "> вместе с Terms of Service — стандартный аргумент в спорах о неправомерном доступе.\n",
    "> Для персональных данных в РФ действует ФЗ-152, в ЕС — GDPR: то, что данные лежат\n",
    "> в открытом доступе, **не** делает их сбор автоматически законным.\n",
    "> Для публикации в академическом журнале это уже вопрос research ethics, а не только техники."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "810bfea6",
   "metadata": {},
   "source": [
    "### Правило 2. Сессия + честные заголовки + backoff\n",
    "\n",
    "Каркас ниже — то, что стоит копировать в каждый свой проект.\n",
    "`Retry` из `urllib3` сам обрабатывает `429` и `503`: ждёт, повторяет, увеличивает паузу."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "b81ddb15",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "200 {\n",
      "  \"user-agent\": \"Mozilla/5.0 (compatible; academic-research/1.0; +mailto:illakap1d0@gmail.com)\"\n",
      "}\n"
     ]
    }
   ],
   "source": [
    "from requests.adapters import HTTPAdapter\n",
    "from urllib3.util.retry import Retry\n",
    "\n",
    "def make_session(contact=\"illakap1d0@gmail.com\"):\n",
    "    s = requests.Session()\n",
    "    s.headers.update({\n",
    "        \"User-Agent\": f\"Mozilla/5.0 (compatible; academic-research/1.0; +mailto:{contact})\",\n",
    "        \"Accept-Language\": \"ru-RU,ru;q=0.9\",\n",
    "    })\n",
    "    retry = Retry(\n",
    "        total=3,                                # в реальном сборе имеет смысл 5\n",
    "        backoff_factor=1,                       # паузы 1, 2, 4 сек (при total=5 и factor=2 → до 32)\n",
    "        status_forcelist=[429, 500, 502, 503, 504],\n",
    "        allowed_methods=[\"GET\"],\n",
    "        respect_retry_after_header=True,        # если сервер сказал, сколько ждать, — ждём столько\n",
    "    )\n",
    "    s.mount(\"https://\", HTTPAdapter(max_retries=retry))\n",
    "    s.mount(\"http://\",  HTTPAdapter(max_retries=retry))\n",
    "    return s\n",
    "\n",
    "session = make_session()\n",
    "try:\n",
    "    r = session.get(\"https://httpbin.org/user-agent\", timeout=10)\n",
    "    print(r.status_code, r.text.strip())\n",
    "except requests.RequestException as e:\n",
    "    print(\"Сеть недоступна:\", type(e).__name__, \"— сессия всё равно создана и готова к работе.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3e3ba098",
   "metadata": {},
   "source": [
    "**Про `User-Agent` без лицемерия.** Указывать контакт в UA — не формальность.\n",
    "Администратор, увидевший в логах нагрузку, скорее напишет вам письмо, чем забанит подсеть. А может и получится договориться и получить исходные данные, минуя парсинг"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "40df6a91",
   "metadata": {},
   "source": [
    "### Правило 3. Пауза со случайной добавкой\n",
    "\n",
    "Ровно одинаковые интервалы — характерный признак бота: люди так не кликают."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "08b3c8e6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "3 итерации заняли 1.33 сек\n"
     ]
    }
   ],
   "source": [
    "import random\n",
    "\n",
    "def polite_sleep(base=1.0, jitter=0.5):\n",
    "    time.sleep(base + random.uniform(0, jitter))\n",
    "\n",
    "t0 = time.time()\n",
    "for i in range(3):\n",
    "    polite_sleep(0.3, 0.3)\n",
    "print(f\"3 итерации заняли {time.time() - t0:.2f} сек\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a9c05cbf",
   "metadata": {},
   "source": [
    "### Правило 4. Кэшировать. Каждую страницу качать один раз\n",
    "\n",
    "Если верстка страницы поменяется, то парсер перестанет работать. Сохраняйте сырой HTML на диск при первом скачивании."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "ec27ff18",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "из кэша: https://httpbin.org/html\n",
      "из кэша: https://httpbin.org/html\n"
     ]
    }
   ],
   "source": [
    "CACHE = pathlib.Path(\"cache\")\n",
    "CACHE.mkdir(exist_ok=True)\n",
    "\n",
    "def cached_get(url, session=None, base=1.0):\n",
    "    \"\"\"Скачивает страницу один раз; при повторном вызове читает с диска.\"\"\"\n",
    "    import hashlib\n",
    "    key = hashlib.md5(url.encode()).hexdigest()[:16]\n",
    "    path = CACHE / f\"{key}.html\"\n",
    "\n",
    "    if path.exists():\n",
    "        print(f\"из кэша: {url}\")\n",
    "        return path.read_bytes()\n",
    "\n",
    "    sess = session or requests\n",
    "    r = sess.get(url, timeout=10)\n",
    "    r.raise_for_status()\n",
    "    path.write_bytes(r.content)\n",
    "    polite_sleep(base)\n",
    "    print(f\"скачано : {url}\")\n",
    "    return r.content\n",
    "\n",
    "try:\n",
    "    _ = cached_get(\"https://httpbin.org/html\", session=session)\n",
    "    _ = cached_get(\"https://httpbin.org/html\", session=session)   # второй раз — уже с диска\n",
    "except requests.RequestException as e:\n",
    "    print(\"Сеть недоступна:\", type(e).__name__)\n",
    "    # покажем работу кэша на локальном файле\n",
    "    (CACHE / \"demo.html\").write_bytes(b\"<html>demo</html>\")\n",
    "    print(\"в кэше лежит:\", [p.name for p in CACHE.glob(\"*.html\")])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9e940053",
   "metadata": {},
   "source": [
    "> Побочная выгода кэша — воспроизводимость. Рецензент через год попросит пересчитать\n",
    "> результат, а сайт к тому моменту сменит вёрстку или удалит архив.\n",
    "> **Сырой HTML — это ваши первичные данные, храните их вместе с кодом.**\n",
    "\n",
    "**Чего мы сознательно не делаем.** Прокси-ротация, обход CAPTCHA, подмена отпечатка браузера —\n",
    "это инструменты коммерческого скрейпинга. В академической работе они создают\n",
    "и этическую, и юридическую проблему, и почти всегда означают, что вы парсите то,\n",
    "что парсить не следует. Если сайт настойчиво не хочет отдавать данные —\n",
    "пишите письмо владельцу, а не обходите защиту."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "base",
   "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.13.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
