Сегодня говорим про обработку ошибок и отказоустойчивость программ. Это продолжение статьи про исключения, но применительно к нашему проекту под кодовым названием «Воруй, убивай, цепи Маркова». Мы учимся забирать текст с чужих сайтов и генерировать на основе этого текста собственные.
В предыдущих версиях у нас были идеальные условия: заготовленный список веб-страниц одинакового формата, с одинаковой кодировкой и одинаковой разметкой заголовков.
В реальном парсинге условия неидеальные: чаще всего нужно парсить не свой сайт, а чужой. И на этом чужом сайте может быть что угодно: не открываются адреса, нет заголовка на странице, разные кодировки на разных страницах. Для компьютера это непреодолимые трудности.
Чтобы такого не происходило, нам нужно научить наш алгоритм обрабатывать нештатные ситуации — то есть исключения.
Что делаем
Идея для сегодняшнего проекта — спарсить часть текста и заголовков с сайта «Коммерсанта» для учебных целей. Потом мы их отдадим нашему алгоритму на цепях Маркова и получим новые тексты в духе «Коммерсанта».
Мы выбрали «Коммерсант» из-за его удобной структуры URL-адреса. Вот как выглядят типичные адреса новостей оттуда:
https://www.kommersant.ru/doc/4815427
https://www.kommersant.ru/doc/4803922
Видно, что каждая новость или статья просто опубликована под каким-то своим номером и есть ощущение, что эти номера идут по порядку. Поэтому сделаем так:
- Выберем стартовый номер у новости.
- Будем отнимать от этого номера единичку, подставлять его в адрес и смотреть на результат.
- Если страница откроется, сохраним заголовок и текст новости, а если нет — пойдём дальше.
- Повторим это 500 раз и посмотрим, что получится.
Адаптируем старый проект под новую задачу
Чтобы не писать всё с нуля, мы возьмём наш парсер из прошлого проекта и выкинем оттуда громадный кусок с массивом URL-адресов.
# подключаем urlopen из модуля urllib
from urllib.request import urlopen
# подключаем библиотеку BeautifulSout
from bs4 import BeautifulSoup
# открываем текстовый файл, куда будем добавлять заголовки
file = open("zag.txt", "a")
# перебираем все адреса из списка
for x in url:
# получаем исходный код очередной страницы из списка
html_code = str(urlopen(x).read(),'utf-8')
# отправляем исходный код страницы на обработку в библиотеку
soup = BeautifulSoup(html_code, "html.parser")
# находим название страницы с помощью метода find()
s = soup.find('title').text
# выводим его на экран
print(s)
# сохраняем заголовок в файле и переносим курсор на новую строку
file.write(s + '. ')
# закрываем файл
file.close()
Теперь добавим в этот код нашу логику. Для этого мы пропишем общую часть URL-адреса, запомним стартовый номер новости, а потом в цикле будем вычитать из него единичку и смотреть, что получилось.
👉 Мы вычитаем единицы из стартового числа, чтобы получить доступ к предыдущим материалам, потому что у новых статей номер в адресе «Коммерсанта» всегда больше, чем у старых. Ещё мы теперь ищем заголовок самой новости, а не всей страницы, потому что в заголовке страницы много лишнего текста.
# подключаем urlopen из модуля urllib
from urllib.request import urlopen
# подключаем библиотеку BeautifulSout
from bs4 import BeautifulSoup
# общая часть URL-адреса
url = "https://www.kommersant.ru/doc/"
# стартовый номер, с которого начинаем парсинг
start_id = 4804129
# открываем файл, куда будем добавлять заголовки
file_zag = open("komm_zag.txt", "a")
# открываем файл, куда будем добавлять текст
file_text = open('komm_text.txt','a')
# перебираем предыдущие 500 адресов
for x in range(0,500):
# формируем новый адрес из общей части и номера материала
# на каждом шаге номер уменьшается на единицу, чтобы обратиться к более старым материалам
work_url = url + str(start_id - x)
# получаем исходный код очередной страницы из списка
html_code = str(urlopen(work_url).read(),'utf-8')
# отправляем исходный код страницы на обработку в библиотеку
soup = BeautifulSoup(html_code, "html.parser")
# находим заголовок материала с помощью метода find()
s = soup.find('h1').text
# выводим его на экран
print(s)
# сохраняем заголовок в файле и переносим курсор на новую строку
file_zag.write(s + '. ')
# находим все абзацы с текстом
content = soup.find_all('p')
# перебираем все найденные абзацы
for item in content:
# сохраняем каждый абзац в другой файл
file_text.write(item.text + ' ')
print(item.text)
# закрываем файл
file.close()
После запуска мы видим две проблемы. Первая: у нас собирается много лишних абзацев с текстом, который не относится к новости. Все эти «Читать далее», «Архив» и «просмотров» нам не нужны:
Вторая проблема: оказывается, не на всех страницах наш парсер может найти заголовок <h1>. Например, такое случается, если по текущему адресу материал доступен только по подписке или там находится служебная страница:
Находим только текст новости
Чтобы не собирать со страницы все абзацы, а брать только нужный текст, давайте посмотрим на структуру любой подобной страницы в инспекторе:
В коде видно, что содержимое статьи помечается абзацем с классом "b-article__text
" , значит, нам нужно забирать со страницы только абзацы с таким классом. Поменяем нашу команду на такое:
content = soup.find_all('p', class_ = "b-article__text")
Теперь мы найдём на странице только те абзацы, у которых будет нужный нам класс, а остальные проигнорируем.
Добавляем исключение для обработки заголовков
👉 В этом проекте мы варварски отнеслись к исключениям и не проверяли тип ошибки, зато быстро получили рабочий код. В следующий раз мы исправимся, а пока будем работать на скорость.
Мы уже рассказывали про то, что такое исключения и как они помогают программистам. Если коротко, то исключения позволяют обработать заранее известную ошибку так, чтобы программа не прекращала работу, а продолжала делать что-то своё.
Мы будем использовать самый простой вариант обработки исключений: когда исключение обрабатывается в общем виде, без уточнения ошибки. Вот как это будет работать:
- Мы добавляем обработчик исключений к команде нахождения заголовка.
- Если всё нашлось нормально и ошибки нет, то обработчик будет сидеть тихо и ничего не делать.
- Если после команды поиска заголовка случилась ошибка, то мы сразу прекращаем дальнейшие команды и переходим к следующему адресу.
Плюсы такого решения — простота и скорость внедрения. Нам не нужно задумываться о том, какая именно ошибка случилась: при любой ошибке мы бросаем этот адрес и переходим к следующему.
Минус у этого способа тоже есть: мы не знаем, что именно произошло, и реагируем на всё одинаково. В простом учебном проекте это можно сделать, а в настоящем коммерческом коде — нет. Там нужно чётко всегда знать, что за ошибка случилась, чтобы проект более гибко и правильно реагировал на происходящее.
# включаем обработчик исключений для команды поиска
try:
# находим название страницы с помощью метода find()
s = soup.find('h1').text
# если случилась любая ошибка
except Exception as e:
print("Заголовок не найден")
# прерываем этот шаг цикла и переходим к следующему
continue
Обрабатываем ситуацию, когда страница не найдена
После того как мы исправили два предыдущих замечания и снова запустили программу, компьютер выдал ошибку 404 — страница с таким адресом не найдена:
Это значит, что мы отправили запрос на такую страницу, которой нет на сервере. Так бывает, когда проверяешь адреса простым перебором — часть вариантов окажется нерабочими.
Чтобы эта ошибка не мешала работать программе, снова добавим исключение с обработкой любой ошибки. Как только на этой команде встретили ошибку, то делаем как и раньше — бросаем всё и начинаем цикл с нового адреса.
# включаем обработчик исключений для запроса содержимого страницы
try:
# получаем исходный код страницы в виде байт-строки
html_code = urlopen(work_url).read()
# если случилась любая ошибка
except Exception as e:
print('Страница не найдена')
# прерываем этот шаг цикла и переходим к следующему
continue
Так, шаг за шагом, мы отлавливаем все ошибки и получаем код, который сможет обработать хоть 50 000 страниц и не упасть во время работы. В этом и есть смысл исключений — сделать так, чтобы программа продолжала работать, когда что-то пошло не по плану. Главное — предусмотреть возможные нештатные ситуации.
# подключаем urlopen из модуля urllib
from urllib.request import urlopen
# подключаем библиотеку BeautifulSout
from bs4 import BeautifulSoup
# общая часть URL-адреса
url = "https://www.kommersant.ru/doc/"
# стартовый номер, с которого начинаем парсинг
start_id = 4804129
# открываем файл, куда будем добавлять заголовки
file_zag = open("komm_zag.txt", "a")
# открываем файл, куда будем добавлять текст
file_text = open('komm_text.txt','a')
# перебираем предыдущие 500 адресов
for x in range(0,500):
# формируем новый адрес из общей части и номера материала
# на каждом шаге номер уменьшается на единицу, чтобы обратиться к более старым материалам
work_url = url + str(start_id - x)
# включаем обработчик исключений для запроса содержимого страницы
try:
# получаем исходный код страницы в виде байт-строки
html_code = urlopen(work_url).read()
# если случилась любая ошибка
except Exception as e:
print('Страница не найдена')
# прерываем этот шаг цикла и переходим к следующему
continue
# отправляем исходный код страницы на обработку в библиотеку
soup = BeautifulSoup(html_code, "html.parser")
# включаем обработчик исключений для команды поиска
try:
# находим название страницы с помощью метода find()
s = soup.find('h1').text
# если случилась любая ошибка
except Exception as e:
print("Заголовок не найден")
# прерываем этот шаг цикла и переходим к следующему
continue
# выводим его на экран
print(s)
# сохраняем заголовок в файле и переносим курсор на новую строку
file_zag.write(s + '. ')
# находим все абзацы с текстом новости
content = soup.find_all('p', class_ = "b-article__text")
# перебираем все найденные абзацы
for item in content:
# сохраняем каждый абзац в другой файл
file_text.write(item.text + ' ')
print(item.text)
# закрываем файл
file.close()
Что дальше
У нас есть 500 заголовков и столько же новостей — можно собрать новости в стиле «Коммерсанта». Если не знаете, как это сделать, — почитайте нашу статью про генератор на цепях Маркова