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

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

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

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

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

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

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

Что делаем

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

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

Текст:

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

Редактор:

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

Художник:

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

Корректор:

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

Вёрстка:

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

Соцсети:

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

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