Сырые данные веб-скрапинга почти никогда не попадают в состоянии, пригодном для использования моделью. Получите несколько тысяч страниц товаров, листингов или статей, и вы получите дублирующиеся строки, цены в виде строк, даты в пяти форматах, пустые ячейки и текст, пронизанный HTML-сущностями и лишними пробелами. Подайте это напрямую в обучающую задачу, и модель будет учиться на шуме так же охотно, как и на сигнале. Работа по превращению результата скрапинга в датасет, это очистка и структурирование данных, и именно здесь в действительности выигрывается или проигрывается точность пайплайна AI или ML.
Это руководство, практическое пошаговое объяснение того, как структурировать и очистить веб-данные для AI: загрузить сырой результат скрапинга, дедуплицировать его, нормализовать типы и форматы, обработать пропущенные значения, выполнить базовую очистку текста и токенизацию, спроектировать схему, на которую может опираться последующий код, и валидировать результат до того, как он попадёт к модели. Каждый шаг содержит работающий Python, который можно вставить в блокнот. В конце мы отмечаем, где Crawlbase может возвращать чистый структурированный вывод заранее, чтобы вам приходилось делать меньше вручную.
Почему очистка и структурирование определяют качество модели
Модели не рассуждают о ваших данных так, как это делаете вы. Дублирующаяся строка, это дополнительный вес на одном примере. Цена, хранящаяся как "$1,299.00", это строка, которую модель не может сравнить с 1299.0. Дата, записанная как "03/04/2025" в одних строках и "2025-04-03" в других, становится двумя несвязанными токенами. Ни один из этих случаев не вызывает ошибки, и именно поэтому они опасны: пайплайн работает, метрики выглядят правдоподобно, а модель тихо учится на искажённом представлении мира.
Очистка устраняет очевидные повреждения (дубликаты, пропущенные значения, несогласованные форматы), а структурирование навязывает контракт: каждый столбец имеет один тип, одну единицу и одно значение. Именно этот контракт позволяет одному и тому же датасету питать классификатор сегодня и другую модель в следующем квартале без сюрпризов. Та же дисциплина применяется независимо от того, занимаетесь ли вы веб-скрапингом для машинного обучения на нескольких тысячах строк или запускаете крупномасштабный веб-скрапинг в миллионы строк.
Начните с реалистичного сырого результата скрапинга
Чтобы шаги были конкретными, предположим, что вы спарсили набор листингов товаров интернет-магазина и записали их в raw_products.csv. Реальный результат скрапинга грязный, поэтому файл ниже тоже: дублирующиеся строки, символы валюты и разделители тысяч в price, смешанные форматы дат, пустые ячейки и текст отзывов с HTML-сущностями и неравномерными пробелами.
import pandas as pd df = pd.read_csv("raw_products.csv") # A first look at the damage before touching anything print(df.shape) print(df.dtypes) print(df.isna().sum()) print(df.head())
Вывод dtypes, это диагностический показатель. Если price возвращается как object вместо числового типа, pandas не смог его разобрать, а значит, в столбце есть нечисловой мусор. Ранний запуск isna().sum() показывает, в каких столбцах есть пропущенные значения и насколько всё плохо, чтобы вы могли решить, что исправить перед написанием единственной трансформации.
Сначала дедуплицируйте
Дедупликация предшествует всему остальному, потому что дубликаты раздувают каждую последующую статистику. Точные дубликаты, простой случай: идентичные строки от скрапера, который повторно посещал один и тот же URL или пагинировал по перекрывающемуся окну.
# Drop fully identical rows df = df.drop_duplicates() # Deduplicate on a business key, keeping the most recent capture df = (df.sort_values("scraped_at") .drop_duplicates(subset=["product_id"], keep="last"))
Второй паттерн на практике важнее. Два захвата одного product_id не являются идентичными строками после изменения цены, но обычно нужна одна запись на товар, а не две. Сортировка по времени захвата и сохранение "last" даёт самую свежую версию. Выбирайте тот ключ, который действительно идентифицирует сущность в вашей предметной области (ID товара, URL, SKU), а не полагайтесь на равенство полных строк.
Порядок здесь не косметический. Если сначала заполнить пропущенные значения, а потом дедуплицировать, ваши импутированные средние вычисляются на завышенных счётчиках, смещённых в сторону сущностей, которые были дублированы больше всего. Всегда удаляйте дубликаты перед любой статистикой (среднее, медиана, мода), от которой зависят последующие шаги.
Нормализуйте типы и форматы
Получив по одной строке на сущность, сделайте каждый столбец единственным предсказуемым типом. Это шаг, который превращает "$1,299.00" в 1299.0 и пять форматов дат в один. Строки с валютой нужно очистить от символов и разделителей, прежде чем они будут разобраны как числа; даты нужен единственный парсер с errors="coerce", чтобы неразбираемый мусор превращался в NaT, а не ломал запуск.
# Strip currency symbols and separators, then parse to float df["price"] = (df["price"] .astype("string") .str.replace(r"[^\d.]", "", regex=True)) df["price"] = pd.to_numeric(df["price"], errors="coerce") # Parse mixed date formats into one datetime type df["listed_on"] = pd.to_datetime( df["listed_on"], errors="coerce" ) # Normalize a categorical column: trim and lowercase df["category"] = df["category"].str.strip().str.lower()
Нормализация категориальных значений, это тихая победа. Скрапинг регулярно даёт "Electronics", "electronics " и "ELECTRONICS" как три разных значения для одной категории. Обрезка и приведение к нижнему регистру сворачивают их в одно, что означает более чистые результаты группировки и один признак вместо трёх после кодирования. Проведите такую же стандартизацию по единицам: если часть цен в долларах, а часть в центах, конвертируйте в единую единицу сейчас, пока ещё помните, что есть что.
Обрабатывайте пропущенные значения осознанно
Нет универсального правила для пропущенных данных, только компромиссы. Удаление строк безопасно, когда пробелы редки и строка бесполезна без данного поля. Импутация сохраняет строку, но изобретает значение, поэтому она оправдана только тогда, когда столбец нужен последующей обработке и существует разумная оценка. Принимайте решение по каждому столбцу отдельно, а не применяйте один общий вызов ко всему фрейму.
# Drop rows missing a field the record cannot exist without df = df.dropna(subset=["product_id", "price"]) # Impute a numeric column with its median (robust to outliers) df["rating"] = df["rating"].fillna(df["rating"].median()) # Fill a categorical with an explicit sentinel, not a guess df["brand"] = df["brand"].fillna("unknown")
Медиана лучше среднего для импутации числовых столбцов, потому что несколько экстремальных выбросов (цена 99999, полученная при ошибке скрапинга) сильно сдвигают среднее, но почти не влияют на медиану. Для категориальных значений явный сентинел "unknown" честен: он говорит модели, что значение отсутствовало, вместо того чтобы притворяться, что оно принадлежало к наиболее распространённой категории. Никогда не позволяйте последующему энкодеру молча считать NaN реальной категорией.
Очищайте текст и токенизируйте
Если ваш датасет содержит свободный текст (описания товаров, отзывы, тела статей), он требует отдельного прохода. Спарсенный текст приходит с HTML-сущностями (&, '), остаточными тегами, URL и неравномерными пробелами. Очистите его до токенизации, иначе токены будут полны мусора. Токенизация здесь означает разбиение текста на единицы, потребляемые моделью; пример ниже использует токенизацию по пробелу, которой достаточно для иллюстрации очистки, которая должна предшествовать.
import re import html def clean_text(value): if pd.isna(value): return "" text = html.unescape(str(value)) # & -> & text = re.sub(r"<[^>]+>", " ", text) # strip tags text = re.sub(r"http\S+", " ", text) # strip URLs text = re.sub(r"\s+", " ", text) # collapse whitespace return text.strip().lower() df["review_clean"] = df["review"].apply(clean_text) df["tokens"] = df["review_clean"].str.split()
Порядок внутри clean_text намеренный: сначала раскодируйте сущности, чтобы &amp; стал & до запуска регулярных выражений, затем уберите теги и URL, потом схлопните пробелы, закрывая пробелы от предыдущих удалений. Приведение к нижнему регистру в конце не раздувает счётчики токенов ("Fast" и "fast" становятся одним токеном). Для реальной работы с NLP вы заменили бы финальный split на нормальный токенизатор, но приведённая выше очистка, это то, что спарсенные данные всегда требуют.
Большая часть удаления сущностей выше существует потому, что сырые HTML-результаты скрапинга зашумлены. Crawling API может возвращать чистый структурированный вывод (включая представление страницы в markdown), так что текст приходит без тегов и шаблонных блоков, сокращая шаг очистки до нормализации типов. Направьте его на публичную страницу на бесплатном тарифе и сравните вывод с сырым запросом.
Спроектируйте схему и применяйте её
До сих пор очистка была реактивной. Схема делает её контрактом: объявленный набор столбцов, каждый с одним типом, которому должен соответствовать каждый пакет. Кодирование схемы в виде типов, к которым приводите (и утверждений, которые проверяете), означает, что следующий результат скрапинга либо соответствует, либо явно падает, вместо того чтобы тихо деградировать в тот же беспорядок, который вы только что очистили.
# A schema is just a column -> dtype contract schema = { "product_id": "string", "category": "category", "price": "float64", "rating": "float64", "brand": "string", "listed_on": "datetime64[ns]", } # Keep only schema columns, in order, and cast each one df = df[list(schema.keys())].astype(schema)
Выбор list(schema.keys()) убирает случайные столбцы, добавленные скрапером, и фиксирует порядок столбцов, так что каждый экспорт имеет одинаковую форму. Вызов astype(schema) приводит каждый столбец и вызовет ошибку, если значение нельзя привести, и это желаемое поведение: лучше явная ошибка сейчас, чем повреждённый столбец, обнаруженный после обучающего запуска. Использование типа category для полей с малым количеством уникальных значений, таких как category, также уменьшает память и ускоряет группировку на больших фреймах.
Валидируйте перед экспортом
Валидация, это ворота между «выглядит чистым» и «является чистым». Несколько утверждений ловят сбои, которые тихо отравляют модель: выжившие дубликаты, числа вне диапазона, нули в столбцах, которые должны быть заполнены. Запускайте их для каждого пакета и останавливайте пайплайн при сбое.
def validate(frame): assert frame["product_id"].is_unique, "duplicate product_id" assert frame["price"].between(0, 100000).all(), "price out of range" assert frame["rating"].between(0, 5).all(), "rating out of range" assert frame[["product_id", "price"]].notna().all().all(), "unexpected nulls" return frame df = validate(df)
Проверки диапазонов оправдывают себя: рейтинг 50 при шкале от 0 до 5 или отрицательная цена, это почти всегда ошибка парсинга из предыдущего шага, и утверждение выявляет её до того, как данные достигнут модели. Если вы переросли рукописные утверждения, библиотека валидации схем, такая как Pandera или Great Expectations, выражает те же правила декларативно, но утверждений выше достаточно, чтобы пайплайн был заслуживающим доверия.
Экспортируйте чистый датасет
После дедупликации, нормализации, импутации, приведения к схеме и валидации фрейма запишите его в формат, сохраняющий ваши типы. CSV переносим, но строково типизирован; Parquet сохраняет типы данных, хорошо сжимается и быстрее загружается, что важно при обучении AI-моделей на результате.
# Parquet preserves dtypes and is fast to reload df.to_parquet("clean_products.parquet", index=False) # CSV if you need maximum portability df.to_csv("clean_products.csv", index=False)
Этот файл теперь является датасетом, а не результатом скрапинга: одна строка на сущность, один тип на столбец, нет выживших дубликатов, пропущенные значения обработаны намеренно, и каждое значение в пределах объявленного диапазона. Отсюда путь к модели, привычный: проектирование признаков и разбивка на обучающую и тестовую выборки, и данные под ним не будут вас удивлять.
Получайте более чистые данные у источника
Самый быстрый шаг очистки, тот, который вы пропускаете, потому что данные пришли чистыми. Многая работа выше (раскодирование сущностей, удаление тегов, схлопывание пробелов) существует только потому, что вы скрапили сырой HTML. Crawling API может возвращать чистое представление страницы в стиле markdown, так что текст приходит без тегов и шаблонных блоков, а Crawling API автоматически парсит многие популярные сайты в структурированные JSON-поля, что убирает написание селекторов и большую часть угадывания типов до того, как pandas вообще видит данные. При необходимости в ротируемых резидентных IP без управления пулом Smart AI Proxy закрывает эту сторону.
Ничто из этого не устраняет необходимость дедуплицировать, валидировать и применять схему (это свойства вашего датасета, а не страницы), но это сокращает зашумлённую первую половину работы. О том, куда идёт этот датасет дальше, см. в статьях как работает извлечение данных AI и, для крупнообъёмного захвата, паттерны в статье веб-скрапинг электронной коммерции.
Ключевые выводы
- Сначала дедуплицируйте. Удаляйте дубликаты до любой статистики, иначе импутированные средние вычисляются на завышенных, смещённых счётчиках.
-
Нормализуйте типы и форматы. Убирайте символы валюты до числа с плавающей точкой, парсите смешанные даты с
errors="coerce"и обрезайте плюс приводите к нижнему регистру категориальные значения. -
Обрабатывайте пропущенные значения по столбцу. Удаляйте строки с отсутствующим обязательным полем, заполняйте числовые значения медианой, а категориальные, явным
"unknown". - Очищайте текст до токенизации. Раскодируйте сущности, уберите теги и URL, затем схлопните пробелы именно в таком порядке.
- Определите схему и валидируйте. Приводите каждый столбец к одному объявленному типу и проверяйте уникальность, диапазоны и ненулевость для каждого пакета, чтобы плохие данные падали явно.
- Более чистый ввод означает меньше работы. Crawlbase может возвращать чистый или markdown-вывод и автоматически разобранный JSON, сокращая шаг очистки до запуска pandas.
Часто задаваемые вопросы
Почему очистка данных так важна перед обучением AI-модели?
Потому что модели учатся на том, что есть в данных, включая ошибки. Дубликаты перевешивают одни примеры, цены в строковом виде нельзя сравнивать, смешанные форматы дат распадаются на несвязанные значения, а пропущенные ячейки неверно интерпретируются энкодерами. Ни один из этих случаев не вызывает ошибки, поэтому пайплайн работает и модель тихо обучается на искажённом представлении. Очистка устраняет это повреждение, чтобы модель учила сигнал, а не шум.
Что делать сначала: дедуплицировать или обрабатывать пропущенные значения?
Сначала дедуплицировать. Если импутировать до удаления дубликатов, каждое среднее, которым вы заполняете, вычисляется на завышенных счётчиках, смещённых в сторону наиболее дублированных сущностей. Удалите точные дубликаты и свернитесь по бизнес-ключу (сохраняя самый свежий захват), затем вычислите медианы и моды для импутации.
Как решить между удалением строк и импутацией пропущенных значений?
Решайте по столбцу. Удаляйте строку, когда пропущенное поле, это то, без чего запись не может существовать, например первичный ключ или целевое значение, и пробелы редки. Импутируйте, когда столбец нужен последующей обработке и существует обоснованная оценка: медиана для числовых значений, поскольку она устойчива к выбросам, и явный сентинел "unknown" для категориальных, чтобы отсутствие было зафиксировано, а не угадано.
Каков минимум очистки текста для спарсенных данных?
Раскодируйте HTML-сущности, уберите остаточные теги и URL, схлопните серии пробелов в одиночные, именно в таком порядке, затем приведите к нижнему регистру. Спарсенный текст регулярно содержит &, стray-разметку и неравномерные пробелы, которые иначе стали бы шумными токенами. Этого прохода достаточно перед токенизацией по пробелу; для промышленной NLP вы потом замените это на специализированный токенизатор.
Зачем применять схему, если данные уже чистые?
Потому что следующий пакет не будет чистым, если что-то не заставит его таковым быть. Схема объявляет один тип на столбец и приводит каждый пакет к нему, так что дрейфующий результат скрапинга (новый столбец, цена, которую внезапно нельзя разобрать) падает явно, а не тихо возвращает тот беспорядок, который вы только что убрали. Это превращает очистку из разовой работы в повторяемый контракт.
Может ли Crawlbase сократить объём очистки?
Да, для зашумлённой первой половины. Crawling API может возвращать чистое представление страницы в стиле markdown, так что текст приходит без тегов и шаблонных блоков, а Scraper API автоматически парсит многие популярные сайты в структурированный JSON, что убирает написание селекторов и большую часть угадывания типов. Дедупликацию, валидацию и применение схемы вы всё равно выполняете сами, потому что это свойства вашего датасета, а не исходной страницы.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.
