Простая работа с исключениями

Простая работа с исключениями

Чтобы программа не падала из-за разных ошибок

Сегодня говорим про обработку ошибок и отказоустойчивость программ. Это продолжение статьи про исключения, но применительно к нашему проекту под кодовым названием «Воруй, убивай, цепи Маркова». Мы учимся забирать текст с чужих сайтов и генерировать на основе этого текста собственные. 

В предыдущих версиях у нас были идеальные условия: заготовленный список веб-страниц одинакового формата, с одинаковой кодировкой и одинаковой разметкой заголовков. 

В реальном парсинге условия неидеальные: чаще всего нужно парсить не свой сайт, а чужой. И на этом чужом сайте может быть что угодно: не открываются адреса, нет заголовка на странице, разные кодировки на разных страницах. Для компьютера это непреодолимые трудности. 

Чтобы такого не происходило, нам нужно научить наш алгоритм обрабатывать нештатные ситуации — то есть исключения. 

Что делаем

Идея для сегодняшнего проекта — спарсить часть текста и заголовков с сайта «Коммерсанта» для учебных целей. Потом мы их отдадим нашему алгоритму на цепях Маркова и получим новые тексты в духе «Коммерсанта».

Мы выбрали «Коммерсант» из-за его удобной структуры URL-адреса. Вот как выглядят типичные адреса новостей оттуда:

https://www.kommersant.ru/doc/4815427

https://www.kommersant.ru/doc/4803922

Видно, что каждая новость или статья просто опубликована под каким-то своим номером и есть ощущение, что эти номера идут по порядку. Поэтому сделаем так:

  1. Выберем стартовый номер у новости.
  2. Будем отнимать от этого номера единичку, подставлять его в адрес и смотреть на результат.
  3. Если страница откроется, сохраним заголовок и текст новости, а если нет — пойдём дальше.
  4. Повторим это 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")

Теперь мы найдём на странице только те абзацы, у которых будет нужный нам класс, а остальные проигнорируем.

Добавляем исключение для обработки заголовков

👉 В этом проекте мы варварски отнеслись к исключениям и не проверяли тип ошибки, зато быстро получили рабочий код. В следующий раз мы исправимся, а пока будем работать на скорость.

Мы уже рассказывали про то, что такое исключения и как они помогают программистам. Если коротко, то исключения позволяют обработать заранее известную ошибку так, чтобы программа не прекращала работу, а продолжала делать что-то своё.

Мы будем использовать самый простой вариант обработки исключений: когда исключение обрабатывается в общем виде, без уточнения ошибки. Вот как это будет работать:

  1. Мы добавляем обработчик исключений к команде нахождения заголовка.
  2. Если всё нашлось нормально и ошибки нет, то обработчик будет сидеть тихо и ничего не делать.
  3. Если после команды поиска заголовка случилась ошибка, то мы сразу прекращаем дальнейшие команды и переходим к следующему адресу.

Плюсы такого решения — простота и скорость внедрения. Нам не нужно задумываться о том, какая именно ошибка случилась: при любой ошибке мы бросаем этот адрес и переходим к следующему.

Минус у этого способа тоже есть: мы не знаем, что именно произошло, и реагируем на всё одинаково. В простом учебном проекте это можно сделать, а в настоящем коммерческом коде — нет. Там нужно чётко всегда знать, что за ошибка случилась, чтобы проект более гибко и правильно реагировал на происходящее.

# включаем обработчик исключений для команды поиска
    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 заголовков и столько же новостей — можно собрать новости в стиле «Коммерсанта». Если не знаете, как это сделать, — почитайте нашу статью про генератор на цепях Маркова

Текст:

Михаил Полянин

Редактор:

Максим Ильяхов

Художник:

Даня Берковский

Корректор:

Ирина Михеева

Вёрстка:

Никита Кучеров

Соцсети:

Олег Вешкурцев

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства и компенсация, если вы пойдёте работать в «Яндекс».
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
Делаем своё расширение для браузера за 10 минут
Делаем своё расширение для браузера за 10 минут

Cнова запускаем снежинки.

Как подключить фотогалерею к сайту
Как подключить фотогалерею к сайту

Рецепт на 6 минут.

Переводим аудио в текст. Часть 2

Python, выручай!

глитч-эффект
Проглючило: делаем глитч-эффект на SASS

На это можно смотреть бесконечно.

Uncaught SyntaxError: Unexpected identifier — что это означает?
Uncaught SyntaxError: Unexpected identifier — что это означает?

Вредная ошибка, которую легко исправить.

Делаем страницу «О себе» на Бутстрапе

Если ты можешь сделать страницу о себе, ты можешь сделать всё.

Uncaught SyntaxError: Unexpected end of input — что это значит?
Uncaught SyntaxError: Unexpected end of input — что это значит?

Скорее всего, вы забыли закрыть скобки при объявлении функции.

Делаем эффектную фотогалерею на сайте
Делаем эффектную фотогалерею на сайте

Красивый трёхмерный виджет с несложным кодом

Вжух, вжух! Добавляем звук меча на любой странице

Время дурацких проектов.

Адаптируем статью под время суток
Адаптируем статью под время суток

Простая игрушка на JS.

Как утащить что угодно с любого сайта
Как утащить что угодно с любого сайта

Например, заголовки или содержимое статьи.

Как сделать колесо фортуны на сайте
Как сделать колесо фортуны на сайте

Достаточно одного скрипта и немного CSS

Как добавить кнопки «Поделиться» на страницу
Как добавить кнопки «Поделиться» на страницу

Простой рецепт от Яндекса.

easy