Метка: beautiful soup

Парсим Википедию с Beautiful Soup 4

Для одного из моих проектов понадобилось раздобыть список субъектов РФ с их гербами. Я решил автоматизировать этот процесс, написав скрипт на языке Python. Поделюсь с вами процессом разработки, трудностями, с которыми столкнулся и их решениями.

Парсить будем эту страницу – статья «Субъекты Российской Федерации»: https://ru.wikipedia.org/wiki/%D0%A1%D1%83%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D1%8B_%D0%A0%D0%BE%D1%81%D1%81%D0%B8%D0%B9%D1%81%D0%BA%D0%BE%D0%B9_%D0%A4%D0%B5%D0%B4%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%B8

Нам понадобятся библиотеки (requests – для HTTP запросов, BeautifulSoup4 – для разбор HTML документа и lxml – нужна для BeautifulSoup4):

pip install requests lxml BeautifulSoup4

BeautifulSoup4 сам по себе не ставит парсер lxml, его мы поставили вручную.

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

import requests
from bs4 import BeautifulSoup
import os

URL = 'https://ru.wikipedia.org/wiki/%D0%A1%D1%83%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D1%8B_%D0%A0%D0%BE%D1%81%D1%81%D0%B8%D0%B9%D1%81%D0%BA%D0%BE%D0%B9_%D0%A4%D0%B5%D0%B4%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%B8'
FILE = 'wikipage.html'


def load_url_contents_cached(url, local_file):
    # если есть на диске, то считаем с диска
    if os.path.exists(local_file):
        with open(local_file, 'r') as f:
            contents = f.read()
    else:
        # нет на диске - скачаем
        contents = requests.get(url).text
        # сохраним в файл
        with open(local_file, 'w') as f:
            f.write(contents)
    return contents

В переменной contents лежит целиком HTML код в виде текста. Превратим его в суп, т. е. в объект BeautifulSoup, который даст возможность работать с ним в форме объектной модели: искать элементы по их именам, классам и атрибутам, и извлекать их контент и свойства.

soup = BeautifulSoup(contents, 'lxml')

print(soup.prettify())  # посмотреть код страницы – много текста

Далее, нам нужно понять, что искать на странице. Мне нужна таблица с регионами и скачать их гербы. Листаем до таблицы, выбираем название первого субъекта, правая кнопка, посмотреть код. Да, сразу скажу, что использую браузер Chrome.

Ищем элемент.

Находим в открывшейся консоли разработчика нужный элемент с названием региона, снова правой кнопкой – Copy – Copy selector. Это копирует селектор (строку, которая позволяет однозначно выбрать именно этот элемент страницы) в буфер обмена.

Копирование селектора

Теперь по селектору BS4 может найти нам элемент:

elem = soup.select('#mw-content-text > div > table.standard.sortable.jquery-tablesorter > tbody > tr:nth-child(3) > td:nth-child(2) > a')

print(elem)  # []

Увы, пусто. Давайте откроем код страницы, чтобы посмотреть, что не так. Оказывается при загрузке у таблицы нет класса jquery-tablesorter. Он добавляется уже во время исполнения кода JS:

Классы таблицы.

А мы парсим просто HTML пришедший с сервера и не запускаем никакие скрипты на страницы. Поэтому удаляем этот лишний класс из селектора:

elem = soup.select('#mw-content-text > div > table.standard.sortable > tbody > tr:nth-child(3) > td:nth-child(2) > a')

print(elem)
# [<a href="/wiki/%D0%90%D0%B4%D1%8B%D0%B3%D0%B5%D1%8F" title="Адыгея">Республика Адыгея</a>]

Теперь элемент успешно обнаруживается. Но мы разобьем этот запрос на части, чтобы сначала извлечь все строки таблицы, а затем из каждой строки достать название и ссылку. Изучим, как нам достать путь к картинге герба из каждой строки таблицы tr:

Путь к картинке герба

Можно было бы взять атрибут src от img, но в атрибуте srcset есть варианты побольше размером, однако его придется тоже разобрать на части – разбить сначала по запятым, потом по пробелам.

Обратите внимание, что селектор всегда возвращает нам массив элементов, даже если элемент внутри всего один такой, поэтому после select всегда обращаемся по индексу. Атрибут у элемента тоже берут через квадратные скобки.

def extract_names_and_image_urls(contents):
    soup = BeautifulSoup(contents, 'lxml')

    # строки таблицы
    rows = soup.select('#mw-content-text > div > table.standard.sortable > tbody > tr')

    for row in rows:
        # все ячейки этой строки
        columns = row.select('td')
        try:
            # название региона - это содержание нулевого по счету тэга "a" в ячейке номер 1
            name = columns[1].select('a')[0].text.strip()

            # а картинка в 3 ячейке в тэге img возьмем атрибут srcset
            image_page_url: str = columns[3].select('img')[0]['srcset']
            
            # разбить атрибут по запятым, взять последний вариант, потом забить по пробелам, взять адрес
            # и добавить протокол к нему, чтобы иметь полный URL
            large_image = 'https:' + image_page_url.split(',')[-1].strip().split(' ')[0]

            yield name, large_image

        except IndexError:
            continue

Если не понимаете, что именно происходит, то запустите этот код и увидите по шагам, как мы достаем данные из тэгов:

def separator(double=False):
    """Разделительная линия"""
    print('-' * 100)
    if double:
        print()
        separator()
        

contents = load_url_contents_cached(URL, FILE)
soup = BeautifulSoup(contents, 'lxml')

# строки таблицы
rows = soup.select('#mw-content-text > div > table.standard.sortable > tbody > tr')

# 42 строка
row = rows[42]

print(row)

separator()

columns = row.select('td')
print(columns)

separator()

print(columns[1])

separator()

print(columns[1].select('a'))

separator()

print(columns[1].select('a')[0])

separator()

print(columns[1].select('a')[0].text)

separator(double=True)

print(columns[3])

separator()

print(columns[3].select('img'))

separator()

print(columns[3].select('img')[0])

separator()

print(columns[3].select('img')[0]['srcset'])

separator()

image_page_url = columns[3].select('img')[0]['srcset']

print(image_page_url.split(','))

separator()

print(image_page_url.split(',')[-1])

separator()

print(image_page_url.split(',')[-1].strip().split(' '))

separator()

print(image_page_url.split(',')[-1].strip().split(' ')[0])

separator()

print('https:' + image_page_url.split(',')[-1].strip().split(' ')[0])

Полученные картинки нужно только скачать:

def download_file(url, save_to):
    r = requests.get(url, allow_redirects=True)
    with open(save_to, 'wb') as f:
        f.write(r.content)

Собираем все вместе. Качаем страницу, читаем список имен и адресов, создаем папку для картинок, качаем каждый герб и сохраняем его под именем субъекта:

data = extract_names_and_image_urls(load_url_contents_cached(URL, FILE))

IMAGE_PATH = 'out_img'
os.makedirs(IMAGE_PATH, exist_ok=True)  # создать папку, если ее нет 

for name, image_url in data:
    print(f'Downloading {name}')
    download_file(image_url, os.path.join(IMAGE_PATH, name + '.png'))

Вот что у меня сохранилось в итоге:

Результаты загрузки

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈