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

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

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

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

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

Что делаем

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

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

Текст:

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

Редак­тор:

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

Худож­ник:

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

Кор­рек­тор:

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

Вёрст­ка:

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

Соц­се­ти:

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