Сбор данных в таблицу: CSV и JSON

Финальная цель скрейпинга — превратить хаос HTML в аккуратную таблицу.
Извлечённые данные складывают в список словарей, а затем сохраняют в CSV или JSON. Каждая строка — одна сущность (товар, вакансия), каждый ключ — одно поле.

Парсинг отдельных элементов — это половина дела. Скрейпер должен собрать их в структуру: пройти по всем карточкам, из каждой вытащить набор полей и сложить в список словарей. Такой список легко записать в файл или загрузить в pandas. Разберём типовой паттерн «цикл по карточкам».

from bs4 import BeautifulSoup

soup = BeautifulSoup(html, 'lxml')
rows = []

for card in soup.select('div.product'):
    title_el = card.select_one('.title')
    price_el = card.select_one('.price')
    rows.append({
        'title': title_el.text.strip() if title_el else None,
        'price': price_el.text.strip() if price_el else None,
        'url':   card.find('a').get('href'),
    })

Сохранение в CSV и JSON

Для сохранения хватает стандартных модулей csv и json — никаких внешних зависимостей.

import csv, json

with open('products.csv', 'w', newline='', encoding='utf-8') as f:
    writer = csv.DictWriter(f, fieldnames=['title', 'price', 'url'])
    writer.writeheader()
    writer.writerows(rows)

with open('products.json', 'w', encoding='utf-8') as f:
    json.dump(rows, f, ensure_ascii=False, indent=2)

Как работает под капотом

Паттерн «список словарей» — это, по сути, таблица в памяти: список = строки, ключи словаря = столбцы. csv.DictWriter по списку имён полей формирует заголовок и сериализует каждую строку. json.dump с ensure_ascii=False сохраняет кириллицу читаемой, а не в виде \uXXXX. Очистка .strip() убирает лишние пробелы и переводы строк, которыми обычно «загрязнён» текст из HTML. Покажем чистку и сборку строки таблицы прямо в браузере:

Попробуй сам ▶

raw_cards = [
    ('  Ноутбук Pro\n', '79 990 ₽'),
    ('Мышь Lite ', ' 1290 руб '),
]

import re
rows = []
for title, price in raw_cards:
    clean_title = title.strip()
    # оставить только цифры в цене
    digits = re.sub(r'[^0-9]', '', price)
    rows.append({'title': clean_title, 'price': int(digits)})

for r in rows:
    print(r)
print('Самый дорогой:', max(rows, key=lambda x: x['price'])['title'])

От разрозненных полей к чистому датасету

Сбор данных редко заканчивается на «вытащил текст». Реальный датасет требует согласованности: цена везде число, а не строка «79 990 ₽»; дата в одном формате (лучше ISO ГГГГ-ММ-ДД); отсутствующие поля помечены явным None, а не пустой строкой. Эту работу удобно вынести в отдельную функцию-нормализатор, через которую прогоняется каждая собранная запись. Тогда логика извлечения и логика очистки не перемешиваются, и код проще править.

Полезная привычка — сохранять сырой HTML страниц на диск во время разработки. Парсер почти всегда приходится дорабатывать: нашёлся товар без цены, появилась акционная плашка, изменилась вёрстка одной категории. Если каждый перезапуск парсера снова бьёт по живому сайту, ты создаёшь лишнюю нагрузку и рискуешь быть заблокированным за частоту. Скачав страницы один раз и отлаживая разбор локально, ты и работаешь быстрее, и ведёшь себя вежливо по отношению к источнику. Для масштабных проектов ту же идею реализует HTTP-кэш Scrapy.

Частые ошибки

  • Не чистить текст. «\n 79 990 ₽ » превратится в мусор без .strip() и нормализации.
  • Падать на отсутствующем поле. Не у всех карточек есть все поля — используй защиту if el else None.
  • Сохранять кириллицу как \uXXXX. Указывай ensure_ascii=False и encoding='utf-8'.

Best practices

  • Складывай данные в единообразные словари с фиксированным набором ключей.
  • Нормализуй значения сразу: strip(), приведение цены к числу, дат к ISO-формату.
  • Сохраняй сырой HTML на диск при отладке — чтобы не перезапрашивать сайт ради переразбора.

Для табличных данных удобно сразу думать в терминах будущего анализа. Список словарей легко превращается в pandas.DataFrame одной строкой, а оттуда — в Excel, базу или график. Поэтому ещё на этапе сбора стоит давать полям понятные, единообразные имена и приводить значения к правильным типам. Чем чище данные на выходе скрейпера, тем меньше мучений на этапе анализа — а ведь именно ради анализа данные обычно и собирают.

Итог: цель — список словарей, превращённый в CSV/JSON стандартными модулями. Главное на этом этапе — аккуратная чистка и нормализация значений, иначе таблица будет грязной.

Проверьте себя
1. Зачем при сохранении JSON указывать ensure_ascii=False?
AЧтобы файл стал меньше
BЧтобы кириллица сохранялась читаемой, а не как escape-последовательности \uXXXX
CЧтобы ускорить запись
DЭто обязательный параметр
2. Почему при сборке строк используют конструкцию вида `el.text if el else None`?
AДля красоты кода
BПотому что не у всех карточек есть все поля — защита от падения на None
CЭто требование CSV
DЧтобы ускорить парсинг