Beautiful Soup и вложенные таблицы: как эффективно извлечь данные?

Краткий обзор Beautiful Soup: парсинг HTML и XML

Beautiful Soup — это мощная библиотека для парсинга HTML и XML документов. Она создает дерево разбора, которое позволяет легко извлекать данные из веб-страниц, даже если разметка не идеальна. В отличие от регулярных выражений, Beautiful Soup абстрагируется от строкового представления документа и работает с его структурой, что делает код более читаемым и устойчивым к незначительным изменениям в разметке.

Основы работы с HTML-таблицами: структура и теги

HTML-таблицы являются одним из старейших и наиболее распространенных способов структурирования данных на веб-страницах. Они строятся на основе тегов <table>, <tr> (строка таблицы) и <td> или <th> (ячейка таблицы). Тег <thead>, <tbody>, <tfoot>, <caption>, <colgroup>, <col> используются для семантического разделения и стилизации, но основные данные содержатся в ячейках внутри строк.

Сложность вложенных таблиц и задачи извлечения данных

Парсинг простых таблиц сводится к поиску <table> и последующей итерации по <tr> и <td>/<th>. Однако, когда одна таблица вложена в ячейку другой таблицы, стандартные методы обхода могут стать неэффективными или приводить к некорректному извлечению данных. Вложенные структуры требуют более продуманных стратегий для правильной идентификации и извлечения данных из каждого уровня вложенности, особенно если требуется сохранить иерархию или связать данные из внешней таблицы с данными из вложенной.

Извлечение данных из простых таблиц с помощью Beautiful Soup

Поиск таблицы по тегу <table> и атрибутам

Первый шаг при парсинге таблицы — это ее идентификация в HTML-документе. Часто таблицы имеют уникальные атрибуты, такие как id или class, которые упрощают поиск. Если таких атрибутов нет, приходится использовать позиционный поиск или атрибуты родительских элементов.

from bs4 import BeautifulSoup
import requests

def get_soup(url: str) -> BeautifulSoup:
    """Загружает HTML-документ и возвращает объект Beautiful Soup."""
    response = requests.get(url)
    response.raise_for_status() # Проверка на ошибки HTTP
    return BeautifulSoup(response.text, 'html.parser')

def find_main_table(soup: BeautifulSoup) -> BeautifulSoup | None:
    """Ищет основную таблицу по CSS-селектору или тегу."""
    # Пример поиска по классу CSS
    table = soup.find('table', class_='data-table')
    # Если класса нет, можно искать просто первую таблицу или по другим атрибутам
    # table = soup.find('table')
    return table

# Пример использования (предполагается наличие URL)
# url = 'https://example.com/page_with_table'
# soup = get_soup(url)
# main_table = find_main_table(soup)
# if main_table:
#     print("Таблица найдена!")
# else:
#     print("Таблица не найдена.")

Итерация по строкам (<tr>) и ячейкам (<td>, <th>)

Получив объект таблицы, можно итерироваться по ее строкам (<tr>) и затем по ячейкам (<td> или <th>) в каждой строке. Методы find_all или select (с CSS-селекторами) очень полезны для этого.

from bs4 import BeautifulSoup
from typing import List, Dict, Any

def parse_simple_table(table_soup: BeautifulSoup) -> List[List[str]]:
    """Парсит простую таблицу и возвращает список списков строк."""
    data: List[List[str]] = []
    # Итерируемся по всем строкам таблицы
    for row in table_soup.find_all('tr'):
        row_data: List[str] = []
        # Итерируемся по всем ячейкам (td или th) в строке
        # Используем select для более гибкого выбора td/th
        for cell in row.select('td, th'):
            # Получаем текст из ячейки, очищая от лишних пробелов
            row_data.append(cell.get_text(strip=True))
        data.append(row_data)
    return data

# Пример использования:
# Assuming 'main_table' is a BeautifulSoup object of a table
# table_data = parse_simple_table(main_table)
# for row in table_data:
#     print(row)

Получение текста из ячеек таблицы: методы .text и .get_text()

Методы .text и .get_text() используются для извлечения текстового содержимого элемента. .text просто объединяет текст всех дочерних элементов, а .get_text() предлагает больше опций, таких как указание разделителя между текстовыми узлами и удаление ведущих/завершающих пробелов (strip=True), что часто предпочтительно для парсинга табличных данных.

Работа с вложенными таблицами: стратегии и методы

Определение вложенных таблиц в HTML-структуре

Вложенные таблицы находятся внутри ячеек (<td>) других таблиц. При обходе ячеек внешней таблицы, необходимо проверить, содержат ли они другие теги <table>. Это можно сделать, выполнив поиск <table> внутри объекта ячейки.

from bs4 import BeautifulSoup

def has_nested_table(cell_soup: BeautifulSoup) -> bool:
    """Проверяет, содержит ли ячейка вложенную таблицу."""
    # Ищем тег table непосредственно внутри ячейки
    return cell_soup.find('table') is not None

# Пример использования:
# Assuming 'cell' is a BeautifulSoup object of a td/th element
# if has_nested_table(cell):
#     print("Ячейка содержит вложенную таблицу.")

Рекурсивный подход к извлечению данных из вложенных таблиц

Рекурсия — элегантное решение для обработки структур с переменным уровнем вложенности. Функция, парсящая таблицу, может вызывать саму себя при обнаружении вложенной таблицы внутри ячейки. Это позволяет обрабатывать любое количество уровней вложенности единообразно.

Реклама
from bs4 import BeautifulSoup
from typing import List, Dict, Any

def parse_table_recursive(element_soup: BeautifulSoup) -> List[List[Any]]:
    """Рекурсивно парсит таблицу или элемент, который может содержать таблицу."""
    data: List[List[Any]] = []

    # Ищем все строки в текущем элементе (предполагается, что это table или tbody)
    rows = element_soup.find_all('tr')
    if not rows: # Если строк нет, возможно, это не таблица или пустая таблица
        return []

    for row in rows:
        row_data: List[Any] = []
        cells = row.select('td, th')
        if not cells: # Пропускаем пустые строки (например, с th без td)
            continue

        for cell in cells:
            nested_table = cell.find('table')
            if nested_table:
                # Если найдена вложенная таблица, рекурсивно парсим ее
                nested_data = parse_table_recursive(nested_table)
                # Добавляем данные вложенной таблицы как вложенный список
                row_data.append(nested_data)
            else:\n                # Иначе, добавляем текст ячейки
                row_data.append(cell.get_text(strip=True))
        data.append(row_data)

    return data

# Пример использования:
# Assuming 'main_table' is a BeautifulSoup object of the outermost table
# all_table_data = parse_table_recursive(main_table)
# print(all_table_data) # Output will show nested lists for nested tables

Использование .find_all() для поиска всех таблиц внутри родительской

Метод find_all('table') на объекте ячейки или строки позволяет найти все вложенные таблицы внутри этого элемента, включая таблицы на разных уровнях вложенности в этой конкретной ячейке. Это может быть полезно, если нужно собрать все табличные данные из определенной части документа, не обязательно сохраняя точную иерархию внешней таблицы.

Обработка случаев, когда вложенные таблицы имеют разные структуры

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

Практические примеры извлечения данных из вложенных таблиц

Пример 1: Извлечение данных из таблицы с вложенной таблицей информации о продукте

Представим таблицу со списком заказов, где в одной из ячеек для каждого заказа находится вложенная таблица с деталями позиций этого заказа (название товара, количество, цена).

from bs4 import BeautifulSoup
from typing import List, Dict, Any

# Пример "HTML" строки (упрощено для демонстрации)
html_content = """
<table>
  <tr>
    <th>ID Заказа</th>
    <th>Дата</th>
    <th>Детали</th>
    <th>Статус</th>
  </tr>
  <tr>
    <td>101</td>
    <td>2023-10-26</td>
    <td>
      <table>
        <tr><th>Товар</th><th>Кол-во</th><th>Цена</th></tr>
        <tr><td>Ноутбук</td><td>1</td><td>1200</td></tr>
        <tr><td>Мышь</td><td>2</td><td>25</td></tr>
      </table>
    </td>
    <td>Выполнен</td>
  </tr>
  <tr>
    <td>102</td>
    <td>2023-10-26</td>
    <td>
      <table>
        <tr><th>Товар</th><th>Кол-во</th><th>Цена</th></tr>
        <tr><td>Клавиатура</td><td>1</td><td>75</td></tr>
      </table>
    </td>
    <td>В обработке</td>
  </tr>
</table>
"""

def parse_order_table(soup: BeautifulSoup) -> List[Dict[str, Any]]:
    """Парсит таблицу заказов с вложенными деталями."""
    orders: List[Dict[str, Any]] = []
    main_table = soup.find('table') # Находим основную таблицу

    if not main_table:
        return orders

    rows = main_table.find_all('tr')[1:] # Пропускаем заголовок

    for row in rows:
        cells = row.find_all('td')
        if len(cells) < 4: # Проверяем ожидаемое количество столбцов
            continue

        order_id = cells[0].get_text(strip=True)
        order_date = cells[1].get_text(strip=True)
        status = cells[3].get_text(strip=True)

        details_cell = cells[2]
        nested_table = details_cell.find('table') # Ищем вложенную таблицу деталей

        items: List[Dict[str, Any]] = []
        if nested_table:
            # Парсим строки вложенной таблицы (пропускаем заголовок)
            item_rows = nested_table.find_all('tr')[1:]
            for item_row in item_rows:
                item_cells = item_row.find_all('td')
                if len(item_cells) == 3:
                    item_name = item_cells[0].get_text(strip=True)
                    quantity = int(item_cells[1].get_text(strip=True))
                    price = float(item_cells[2].get_text(strip=True))
                    items.append({
                        'item': item_name,
                        'quantity': quantity,
                        'price': price
                    })

        orders.append({
            'order_id': order_id,
            'date': order_date,
            'items': items, # Вложенные данные
            'status': status
        })

    return orders

# Запуск парсинга
# soup = BeautifulSoup(html_content, 'html.parser')
# parsed_orders = parse_order_table(soup)
# import json
# print(json.dumps(parsed_orders, indent=2, ensure_ascii=False))

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

Пример 2: Разбор многоуровневой таблицы с характеристиками устройства

Более сложный случай: таблица с характеристиками устройства, где некоторые ячейки могут содержать вложенные таблицы с под-характеристиками или описаниями. Рекурсивный подход, показанный ранее, хорошо подходит для такой структуры, но может потребоваться дополнительная логика для правильной интерпретации


Добавить комментарий