[{"data":1,"prerenderedAt":600},["ShallowReactive",2],{"blog-ru-parsers-scrapy-python":3,"blog-counterpart-ru-parsers-scrapy-python":590},{"id":4,"title":5,"body":6,"cluster":576,"ctaType":577,"date":578,"description":579,"extension":580,"intent":581,"meta":582,"navigation":140,"ogImage":583,"path":584,"primaryKeyword":585,"readTime":586,"relatedCases":587,"relatedCourse":576,"relatedService":589,"relatedServices":590,"seo":591,"slug":592,"stem":593,"tags":594,"updated":590,"__hash__":599},"blogRu\u002Fru\u002Fblog\u002Fparsers-scrapy-python.md","Как написать парсер на Python: пример Beautiful Soup",{"type":7,"value":8,"toc":557},"minimark",[9,17,20,25,28,68,71,75,83,86,118,356,367,375,390,413,417,422,435,439,442,446,449,453,456,460,463,467,470,474,487,491,494,507,515,519,523,526,530,533,537,540,553],[10,11,12,16],"p",{},[13,14,15],"strong",{},"Парсер сайта на Python"," автоматически получает разрешённые данные из веб-страниц, приводит их к согласованному формату и передаёт в таблицу, API или отчёт. Это полезно для регулярного мониторинга цен и объявлений, но надёжный парсер — не просто скрипт, который однажды нашёл текст: он также проверяет результат и сообщает, когда источник изменился.",[10,18,19],{},"Если источник предлагает подходящий API или выгрузку, обычно стоит начать с них. HTML-парсер выбирают, когда нужных данных иначе получить нельзя и правила источника это допускают. Примеры ниже показывают подход к качеству данных, а не готовый парсер конкретного сайта.",[21,22,24],"h2",{"id":23},"что-должен-решить-парсер","Что должен решить парсер",[10,26,27],{},"До выбора Python-библиотеки опишите задачу как поток данных:",[29,30,31,38,44,50,56,62],"ol",{},[32,33,34,37],"li",{},[13,35,36],{},"Источник:"," страницы, API или файлы, к которым есть разрешённый доступ.",[32,39,40,43],{},[13,41,42],{},"Поля:"," какие значения нужны, например название, цена, дата и ссылка на карточку.",[32,45,46,49],{},[13,47,48],{},"Правила:"," как нормализовать валюту, даты, пробелы и пропущенные значения.",[32,51,52,55],{},[13,53,54],{},"Обновление:"," как часто проверять источник с учётом его правил и допустимой нагрузки.",[32,57,58,61],{},[13,59,60],{},"Результат:"," CSV, Google Sheets, база данных, API или уведомление.",[32,63,64,67],{},[13,65,66],{},"Сбой:"," что сохранить в лог и кому сообщить, если данные временно недоступны.",[10,69,70],{},"Так становится понятно, нужен ли небольшой скрипт, планировщик задач или отдельный сервис с хранилищем и мониторингом.",[21,72,74],{"id":73},"минимальное-извлечение-и-проверка","Минимальное извлечение и проверка",[10,76,77,78,82],{},"Для статичной HTML-страницы часто достаточно ",[79,80,81],"code",{},"BeautifulSoup",". Ниже — самодостаточный пример на тестовой разметке: он извлекает записи, приводит цену к числу и пропускает карточки без обязательных полей.",[10,84,85],{},"Установите библиотеку:",[87,88,93],"pre",{"className":89,"code":90,"language":91,"meta":92,"style":92},"language-bash shiki shiki-themes github-light github-dark","python -m pip install beautifulsoup4\n","bash","",[79,94,95],{"__ignoreMap":92},[96,97,100,104,108,112,115],"span",{"class":98,"line":99},"line",1,[96,101,103],{"class":102},"sScJk","python",[96,105,107],{"class":106},"sj4cs"," -m",[96,109,111],{"class":110},"sZZnC"," pip",[96,113,114],{"class":110}," install",[96,116,117],{"class":110}," beautifulsoup4\n",[87,119,122],{"className":120,"code":121,"language":103,"meta":92,"style":92},"language-python shiki shiki-themes github-light github-dark","from decimal import Decimal, InvalidOperation\nfrom urllib.parse import urljoin\n\nfrom bs4 import BeautifulSoup\n\nbase_url = \"https:\u002F\u002Fexample.test\u002F\"\nhtml = \"\"\"\n\u003Carticle class=\"listing\">\n  \u003Ca class=\"listing__link\" href=\"\u002Fitem\u002F42\">\n    \u003Ch2 class=\"listing__title\">Пример объявления\u003C\u002Fh2>\n  \u003C\u002Fa>\n  \u003Cspan class=\"listing__price\">1 250.50\u003C\u002Fspan>\n\u003C\u002Farticle>\n\"\"\"\n\nsoup = BeautifulSoup(html, \"html.parser\")\nitems = []\n\nfor card in soup.select(\"article.listing\"):\n    title = card.select_one(\".listing__title\")\n    link = card.select_one(\"a.listing__link\")\n    price_node = card.select_one(\".listing__price\")\n\n    href = link.get(\"href\") if link else None\n    if not title or not href or not price_node:\n        continue\n\n    raw_price = price_node.get_text(\" \", strip=True).replace(\" \", \"\")\n    try:\n        price = Decimal(raw_price)\n    except InvalidOperation:\n        continue\n\n    items.append({\n        \"title\": title.get_text(\" \", strip=True),\n        \"url\": urljoin(base_url, href),\n        \"price\": price,\n    })\n\nprint(items)\n",[79,123,124,129,135,142,148,153,159,165,171,177,183,189,195,201,207,212,218,224,229,235,241,247,253,258,264,270,276,281,287,293,299,305,310,315,321,327,333,339,345,350],{"__ignoreMap":92},[96,125,126],{"class":98,"line":99},[96,127,128],{},"from decimal import Decimal, InvalidOperation\n",[96,130,132],{"class":98,"line":131},2,[96,133,134],{},"from urllib.parse import urljoin\n",[96,136,138],{"class":98,"line":137},3,[96,139,141],{"emptyLinePlaceholder":140},true,"\n",[96,143,145],{"class":98,"line":144},4,[96,146,147],{},"from bs4 import BeautifulSoup\n",[96,149,151],{"class":98,"line":150},5,[96,152,141],{"emptyLinePlaceholder":140},[96,154,156],{"class":98,"line":155},6,[96,157,158],{},"base_url = \"https:\u002F\u002Fexample.test\u002F\"\n",[96,160,162],{"class":98,"line":161},7,[96,163,164],{},"html = \"\"\"\n",[96,166,168],{"class":98,"line":167},8,[96,169,170],{},"\u003Carticle class=\"listing\">\n",[96,172,174],{"class":98,"line":173},9,[96,175,176],{},"  \u003Ca class=\"listing__link\" href=\"\u002Fitem\u002F42\">\n",[96,178,180],{"class":98,"line":179},10,[96,181,182],{},"    \u003Ch2 class=\"listing__title\">Пример объявления\u003C\u002Fh2>\n",[96,184,186],{"class":98,"line":185},11,[96,187,188],{},"  \u003C\u002Fa>\n",[96,190,192],{"class":98,"line":191},12,[96,193,194],{},"  \u003Cspan class=\"listing__price\">1 250.50\u003C\u002Fspan>\n",[96,196,198],{"class":98,"line":197},13,[96,199,200],{},"\u003C\u002Farticle>\n",[96,202,204],{"class":98,"line":203},14,[96,205,206],{},"\"\"\"\n",[96,208,210],{"class":98,"line":209},15,[96,211,141],{"emptyLinePlaceholder":140},[96,213,215],{"class":98,"line":214},16,[96,216,217],{},"soup = BeautifulSoup(html, \"html.parser\")\n",[96,219,221],{"class":98,"line":220},17,[96,222,223],{},"items = []\n",[96,225,227],{"class":98,"line":226},18,[96,228,141],{"emptyLinePlaceholder":140},[96,230,232],{"class":98,"line":231},19,[96,233,234],{},"for card in soup.select(\"article.listing\"):\n",[96,236,238],{"class":98,"line":237},20,[96,239,240],{},"    title = card.select_one(\".listing__title\")\n",[96,242,244],{"class":98,"line":243},21,[96,245,246],{},"    link = card.select_one(\"a.listing__link\")\n",[96,248,250],{"class":98,"line":249},22,[96,251,252],{},"    price_node = card.select_one(\".listing__price\")\n",[96,254,256],{"class":98,"line":255},23,[96,257,141],{"emptyLinePlaceholder":140},[96,259,261],{"class":98,"line":260},24,[96,262,263],{},"    href = link.get(\"href\") if link else None\n",[96,265,267],{"class":98,"line":266},25,[96,268,269],{},"    if not title or not href or not price_node:\n",[96,271,273],{"class":98,"line":272},26,[96,274,275],{},"        continue\n",[96,277,279],{"class":98,"line":278},27,[96,280,141],{"emptyLinePlaceholder":140},[96,282,284],{"class":98,"line":283},28,[96,285,286],{},"    raw_price = price_node.get_text(\" \", strip=True).replace(\" \", \"\")\n",[96,288,290],{"class":98,"line":289},29,[96,291,292],{},"    try:\n",[96,294,296],{"class":98,"line":295},30,[96,297,298],{},"        price = Decimal(raw_price)\n",[96,300,302],{"class":98,"line":301},31,[96,303,304],{},"    except InvalidOperation:\n",[96,306,308],{"class":98,"line":307},32,[96,309,275],{},[96,311,313],{"class":98,"line":312},33,[96,314,141],{"emptyLinePlaceholder":140},[96,316,318],{"class":98,"line":317},34,[96,319,320],{},"    items.append({\n",[96,322,324],{"class":98,"line":323},35,[96,325,326],{},"        \"title\": title.get_text(\" \", strip=True),\n",[96,328,330],{"class":98,"line":329},36,[96,331,332],{},"        \"url\": urljoin(base_url, href),\n",[96,334,336],{"class":98,"line":335},37,[96,337,338],{},"        \"price\": price,\n",[96,340,342],{"class":98,"line":341},38,[96,343,344],{},"    })\n",[96,346,348],{"class":98,"line":347},39,[96,349,141],{"emptyLinePlaceholder":140},[96,351,353],{"class":98,"line":352},40,[96,354,355],{},"print(items)\n",[10,357,358,359,362,363,366],{},"Сохраните код в ",[79,360,361],{},"parse_listing.py"," и запустите ",[79,364,365],{},"python parse_listing.py",". Для приведённой тестовой карточки результат будет таким:",[87,368,373],{"className":369,"code":371,"language":372,"meta":92},[370],"language-text","[{'title': 'Пример объявления', 'url': 'https:\u002F\u002Fexample.test\u002Fitem\u002F42', 'price': Decimal('1250.50')}]\n","text",[79,374,371],{"__ignoreMap":92},[10,376,377,378,381,382,385,386,389],{},"Селекторы ",[79,379,380],{},".listing",", ",[79,383,384],{},".listing__title"," и остальные здесь условные: для конкретного источника их подбирают по разрешённой HTML-разметке. ",[79,387,388],{},"example.test"," и разметка выше — локальная фикстура; скрипт не отправляет сетевых запросов. Если сервер отдаёт только пустую оболочку, а данные появляются в браузере после JavaScript, нужно проверить официальный API или выбрать инструмент с браузерным рендерингом. Не следует автоматически переходить к обходу ограничений.",[10,391,392,393,400,401,406,407,412],{},"Для сетевых запросов задают конечный таймаут, проверяют HTTP-статус и обрабатывают временные ошибки. См. документацию ",[394,395,399],"a",{"href":396,"rel":397},"https:\u002F\u002Fwww.python-httpx.org\u002Fadvanced\u002Ftimeouts\u002F",[398],"nofollow","HTTPX по таймаутам"," и ",[394,402,405],{"href":403,"rel":404},"https:\u002F\u002Fdocs.scrapy.org\u002Fen\u002Flatest\u002Ftopics\u002Fsettings.html#download-delay",[398],"Scrapy по задержкам между запросами",". Для HTML-разметки есть руководство ",[394,408,411],{"href":409,"rel":410},"https:\u002F\u002Fbeautiful-soup-4.readthedocs.io\u002Fen\u002Flatest\u002F",[398],"Beautiful Soup",". Частоту выбирают по правилам и поведению конкретного источника, а не по максимальной технической скорости.",[21,414,416],{"id":415},"как-сделать-сбор-устойчивым","Как сделать сбор устойчивым",[418,419,421],"h3",{"id":420},"beautiful-soup-или-scrapy","Beautiful Soup или Scrapy",[10,423,424,426,427,430,431,434],{},[79,425,81],{}," помогает разобрать уже полученный HTML и подходит для небольшого числа страниц, когда достаточно извлечь поля. ",[79,428,429],{},"Scrapy"," удобнее, когда нужно обходить много разрешённых страниц, управлять очередью запросов, обрабатывать результаты через pipeline и ограничивать частоту для домена. В документации Scrapy параметр ",[79,432,433],{},"DOWNLOAD_DELAY"," задаёт минимальную задержку между запросами к одному домену; конкретные значения всё равно сверяют с правилами источника.",[418,436,438],{"id":437},"проверяйте-поля-до-сохранения","Проверяйте поля до сохранения",[10,440,441],{},"Пустая цена или дата могут означать изменение HTML, а не отсутствие значения в объявлении. Задайте обязательные поля и диапазоны, посчитайте пропуски, проверяйте дату обновления и не записывайте некорректный результат как успешный импорт.",[418,443,445],{"id":444},"убирайте-дубли","Убирайте дубли",[10,447,448],{},"Стабильный идентификатор — например ID записи из источника или нормализованный URL — помогает обновлять существующую запись вместо создания копии при каждом запуске. Нормализуйте пробелы, валюты и даты до сравнения.",[418,450,452],{"id":451},"разделяйте-сбор-и-уведомления","Разделяйте сбор и уведомления",[10,454,455],{},"Сначала сохраните и проверьте данные, затем определите, что действительно изменилось, и только после этого отправляйте уведомление. Такой порядок не создаёт повторные сообщения после перезапуска задачи.",[418,457,459],{"id":458},"записывайте-полезные-логи","Записывайте полезные логи",[10,461,462],{},"Логируйте время запуска, число просмотренных страниц, число валидных записей и тип ошибки. Не записывайте в логи секреты, cookies или персональные данные, если они не нужны для задачи.",[418,464,466],{"id":465},"планируйте-сопровождение","Планируйте сопровождение",[10,468,469],{},"Страницы меняются. Парсер должен обнаруживать отсутствие ожидаемых полей, сохранять диагностический контекст и сообщать о сбое. Кто будет проверять источник и обновлять селекторы после изменения сайта, нужно определить до запуска.",[21,471,473],{"id":472},"доступ-и-ограничения-источника","Доступ и ограничения источника",[10,475,476,477,480,481,486],{},"Перед сбором проверьте условия использования и разрешённый способ доступа. Файл ",[79,478,479],{},"robots.txt"," описывает инструкции для автоматических клиентов, но сам по себе не является разрешением на использование данных. Не обходите вход в аккаунт, CAPTCHA, paywall, rate limits или другие технические меры; при сомнениях согласуйте доступ с владельцем источника. ",[394,482,485],{"href":483,"rel":484},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc9309.html",[398],"Стандарт Robots Exclusion Protocol"," отдельно описывает правила для автоматических клиентов.",[21,488,490],{"id":489},"когда-нужен-не-скрипт-а-сервис","Когда нужен не скрипт, а сервис",[10,492,493],{},"Одноразовую таблицу можно обработать локальным скриптом. Регулярный мониторинг с историей изменений, расписанием, несколькими источниками, уведомлениями и административным контролем уже требует эксплуатации: хранения, повторных попыток, логов и проверки качества.",[10,495,496,497,501,502,506],{},"В ",[394,498,500],{"href":499},"\u002Fru\u002Fportfolio\u002Fflatrentby\u002F","кейсе FlatRentBy"," регулярный сбор объявлений связан с фильтрами и Telegram-уведомлениями. Если нужно автоматизировать похожую задачу, сначала зафиксируем источник, поля и формат результата в ",[394,503,505],{"href":504},"\u002Fru\u002Fservices\u002Fautomation\u002F","кратком брифе на разработку парсера",". Готовое ТЗ не обязательно.",[10,508,509,510,514],{},"Для самостоятельной практики можно пройти ",[394,511,513],{"href":512},"\u002Fru\u002Fcourses\u002Fpython-automation\u002F","бесплатный курс Python для автоматизации",": в нём сбор файлов и обработка данных доводятся до запускаемого проекта.",[21,516,518],{"id":517},"частые-вопросы","Частые вопросы",[418,520,522],{"id":521},"можно-ли-собирать-данные-с-любого-сайта","Можно ли собирать данные с любого сайта?",[10,524,525],{},"Нет. Сначала проверяются доступность, условия использования, ограничения запросов и наличие официального API. Источники с закрытым доступом или запретом на автоматизированный сбор не подходят.",[418,527,529],{"id":528},"чем-парсер-отличается-от-обычного-скрипта","Чем парсер отличается от обычного скрипта?",[10,531,532],{},"Парсер извлекает поля из структурированного текста или разметки. Скрипт автоматизации может дополнительно объединять файлы, вызывать API, преобразовывать данные, формировать отчёты и отправлять уведомления.",[418,534,536],{"id":535},"можно-ли-получать-данные-почти-в-реальном-времени","Можно ли получать данные почти в реальном времени?",[10,538,539],{},"Это зависит от источника, его правил и нагрузки. Реалистичный интервал определяют после проверки доступа и объёма данных; обещать частоту заранее без этих сведений нельзя.",[10,541,542,543,547,548,552],{},"После получения данных часто нужно собрать несколько выгрузок и очистить повторы. Для этого есть отдельный пример, как ",[394,544,546],{"href":545},"\u002Fru\u002Fblog\u002Fobedinit-csv-faily-python\u002F","объединить CSV-файлы на Python и проверить результат",", и инструкция по ",[394,549,551],{"href":550},"\u002Fru\u002Fblog\u002Fzapusk-python-po-raspisaniyu-linux\u002F","регулярному запуску скрипта на Linux",".",[554,555,556],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":92,"searchDepth":131,"depth":131,"links":558},[559,560,561,569,570,571],{"id":23,"depth":131,"text":24},{"id":73,"depth":131,"text":74},{"id":415,"depth":131,"text":416,"children":562},[563,564,565,566,567,568],{"id":420,"depth":137,"text":421},{"id":437,"depth":137,"text":438},{"id":444,"depth":137,"text":445},{"id":451,"depth":137,"text":452},{"id":458,"depth":137,"text":459},{"id":465,"depth":137,"text":466},{"id":472,"depth":131,"text":473},{"id":489,"depth":131,"text":490},{"id":517,"depth":131,"text":518,"children":572},[573,574,575],{"id":521,"depth":137,"text":522},{"id":528,"depth":137,"text":529},{"id":535,"depth":137,"text":536},"python-automation","service","2026-10-12","Практика Beautiful Soup на тестовой HTML-разметке: извлечение полей, проверка значений, дубли и безопасная работа с источником.","md","educational",{},"\u002Fimages\u002Fog\u002Fblog\u002Fru\u002Fparsers-scrapy-python.png","\u002Fru\u002Fblog\u002Fparsers-scrapy-python","как написать парсер на Python","8 мин",[588],"flatrentby","automation",null,{"title":5,"description":579},"parsers-scrapy-python","ru\u002Fblog\u002Fparsers-scrapy-python",[595,596,429,597,598],"Python","Парсер","Автоматизация","Данные","E5fN5YOSZoQMxUdXzDz_SYGgMZeJq8XZw4-JKBB-6Nk",1791825743735]