Очистка «грязных» данных при скрейпинге

До сих пор, мы игнорировали проблему плохо форматированных данных, используя в основном хорошо отформатированные источники данных и отбрасывая данные полностью, если они отличались от того, что вы ожидали. Однако часто при веб-скрапинге вам необходимо брать данные откуда угодно и в любом виде.

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

Очистка в коде

Как и в случае с обработкой явных исключений, важно практиковать обработку непредвиденных ситуаций.

В лингвистике n-грамма представляет собой последовательность из n слов, используемых в тексте или речи. При анализе естественного языка часто бывает полезно разбить текст на часто используемые n-граммы, или повторяющиеся наборы слов, которые часто используются вместе.

Этот раздел сосредоточен на получении правильно форматированных n-грамм, а не на их использовании для анализа. Позже, вы сможете увидеть n-граммы в действии для суммирования текста и анализа.

Вот пример получения списка 2-грамм из статьи на Википедии о языке программирования Python:

from urllib.request import urlopen
from bs4 import BeautifulSoup

def getNgrams(content, n):
    content = content.split(' ')
    output = []
    for i in range(len(content)-n+1):
        output.append(content[i:i+n])
    return output

html = urlopen('http://en.wikipedia.org/wiki/Python_(programming_language)')
bs = BeautifulSoup(html, 'html.parser')
content = bs.find('div', {'id':'mw-content-text'}).get_text()
ngrams = getNgrams(content, 2)

print(ngrams)
print('2-grams count is: '+str(len(ngrams)))

Функция getNgrams принимает входную строку, разбивает ее на последовательность слов (предполагая, что все слова разделены пробелами), и добавляет в массив каждую n-грамму (в данном случае, 2-грамму), с которой начинается каждое слово.

Код возвращает несколько действительно интересных и полезных 2-грамм из текста. Но также возвращает много мусора. Кроме того, поскольку 2-грамма создается для каждого слова (кроме последнего), на момент написания этого текста в статье содержится 7 411 2-грамм. Не очень управляемый набор данных!

Используя регулярные выражения для удаления управляющих символов (например, \n) и фильтрации для удаления любых символов Unicode, можно немного очистить вывод:

import re

def getNgrams(content, n):
    content = re.sub('\n|[[\d+\]]', ' ', content)
    content = bytes(content, 'UTF-8')
    content = content.decode('ascii', 'ignore')
    content = content.split(' ')
    content = [word for word in content if word != '']
    output = []
    for i in range(len(content)-n+1):
        output.append(content[i:i+n])
    return output

Код заменяет все вхождения символа новой строки пробелом, удаляет ссылки вида [123] и фильтрует все пустые строки, вызванные несколькими пробелами подряд. Затем управляющие символы удаляются путем кодирования содержимого с использованием UTF-8.

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

['years', 'ago('], ['ago(', '-'], ['-', '-'], ['-', ')'], [')', 'Stable']

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

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

Например, учитывая текст

«Python features a dynamic type system and automatic memory management. It supports multiple programming paradigms…»

2-грамма [‘memory’, ‘management’] будет допустимой, но 2-грамма [‘management’, ‘It’] — нет.

Теперь, когда у вас есть более длинный список «задач по очистке», вы вводите концепцию «предложений», и ваша программа стала более сложной, лучше разделить их на четыре разные функции:

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
import string

def cleanSentence(sentence):
    sentence = sentence.split(' ')
    sentence = [word.strip(string.punctuation + string.whitespace) for word in sentence]
    sentence = [word for word in sentence if len(word) > 1 or (word.lower() == 'a' or word.lower() == 'i')]
    return sentence

def cleanInput(content):
    content = re.sub('\n|[[\d+\]]', ' ', content)
    content = bytes(content, "UTF-8")
    content = content.decode("ascii", "ignore")
    sentences = content.split('. ')
    return [cleanSentence(sentence) for sentence in sentences]

def getNgramsFromSentence(content, n):
    output = []
    for i in range(len(content)-n+1):
        output.append(content[i:i+n])
    return output

def getNgrams(content, n):
    content = cleanInput(content)
    ngrams = []
    for sentence in content:
        ngrams.extend(getNgramsFromSentence(sentence, n))
    return(ngrams)

Функция getNgrams остается основной точкой входа в программу. cleanInput удаляет символы новой строки и цитаты, как и раньше, но также разделяет текст на «предложения» на основе расположения точек, за которыми следует пробел. Она также вызывает cleanSentence, которая разделяет предложение на слова, удаляет знаки препинания и пробелы, а также удаляет односимвольные слова, кроме «I» и «a».

Основные строки, создающие n-граммы, перенесены в getNgramsFromSentence, который вызывается для каждого предложения getNgrams. Это гарантирует, что n-граммы не создаются, пересекаясь с несколькими предложениями.

Обратите внимание на использование string.punctuation и string.whitespace для получения списка всех знаков препинания в Python. Вы можете просмотреть вывод string.punctuation из терминала Python:

>>> import string
>>> print(string.punctuation)
!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~

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

Используя item.strip(string.punctuation + string.whitespace) внутри цикла, перебирающего все слова в содержимом, любые знаки препинания по обе стороны слова будут удалены, хотя слова с дефисами (где знак препинания ограничен буквами с обеих сторон) останутся нетронутыми.

Вывод этого усилия приводит к более чистым 2-граммам:

[['Python', 'Paradigm'], ['Paradigm', 'Object-oriented'], ['Object-oriented', 'imperative'], ['imperative', 'functional'], ['functional', 'procedural'], ['procedural', 'reflective'],...

Нормализация данных

Каждый сталкивался с плохо спроектированной веб-формой: «Введите свой номер телефона. Ваш номер телефона должен иметь вид ‘xxx-xxx-xxxx’.»

Хороший программист, скорее всего, подумает: «Почему бы им просто не удалить ненужные символы, которые я туда ввел, и сделать это самим?» Нормализация данных — это процесс обеспечения того, чтобы строки, которые лингвистически или логически эквивалентны друг другу, такие как номера телефонов (555) 123-4567 и 555.123.4567, отображались, или по крайней мере сравнивались, как эквивалентные.

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

Одной из очевидных проблем этого кода является наличие множества дублирующихся 2-грамм. Каждый встреченный 2-грамм добавляется в список, без учета его частоты. Интересно не только записать частоту этих 2-граммов, а не только их существование, но это также может быть полезно для отслеживания эффектов изменений алгоритмов очистки и нормализации данных. Если данные нормализованы успешно, общее количество уникальных n-грамм будет уменьшено, в то время как общее количество обнаруженных n-грамм (т.е. количество уникальных или неповторяющихся элементов, идентифицированных как n-граммы) не будет уменьшено. Другими словами, будет меньше «ведер» для того же количества n-грамм.

Вы можете сделать это, изменив код, который собирает n-граммы, чтобы добавить их в объект Counter, а не в список:

from collections import Counter

def getNgrams(content, n):
    content = cleanInput(content)
    ngrams = Counter()
    for sentence in content:
        newNgrams = [' '.join(ngram) for ngram in getNgramsFromSentence(sentence, 2)]
        ngrams.update(newNgrams)
    return(ngrams)

Существует много других способов сделать это, таких как добавление n-грамм в словарный объект, в котором значение списка указывает на количество раз, когда он был виден. Это имеет недостаток в том, что требуется немного больше управления и делает сортировку сложной. Однако использование объекта Counter также имеет недостаток: он не может хранить списки (списки нехешируемы), поэтому сначала их нужно преобразовать в строки, используя ‘ ‘.join(ngram) внутри генератора списка для каждого n-грамма.

Вот результаты:

Counter({'Python Software': 37, 'Software Foundation': 37, 'of the': 34,
'of Python': 28, 'in Python': 24, 'in the': 23, 'van Rossum': 20, 'to the':
20, 'such as': 19, 'Retrieved February': 19, 'is a': 16, 'from the': 16,
'Python Enhancement': 15,...

На момент написания этого текста общее количество 2-грамм составляет 7 275, а уникальных 2-грамм — 5 628, при этом самым популярным 2-граммом является «Software Foundation», за которым следует «Python Software». Однако анализ результатов показывает, что «Python Software» встречается еще два раза в форме «Python software». Аналогично, как «van Rossum», так и «Van Rossum» появляются в списке отдельно.

Добавление строки content = content.upper() в функцию cleanInput сохраняет общее количество найденных 2-грамм на уровне 7 275, при этом уменьшая количество уникальных 2-грамм до 5 479.

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

Например, «Python 1st» и «Python first» оба встречаются в списке 2-грамм. Однако создание общего правила, которое гласит: «Все first, second, third и т.д. будут преобразованы в 1st, 2nd, 3rd и т.д. (и наоборот)», приведет к добавлению еще около 10 проверок на каждое слово.

Аналогично, несогласованное использование дефисов («co-ordinated» против «coordinated»), орфографические ошибки и другие несоответствия естественного языка повлияют на группировку n-грамм и могут замутнить результаты вывода, если несоответствия встречаются достаточно часто.

Одно из решений в случае дефисированных слов может состоять в удалении дефисов полностью и рассмотрении слова как одной строки, что потребует только одной операции. Однако это также означает, что дефисированные фразы (слишком частое явление) будут рассматриваться как одно слово. Другой подход — рассмотрение дефисов как пробелов, что может быть более хорошим вариантом. Просто будьте готовы к тому, что иногда «co ordinated» и «ordinated attack» могут проскользнуть в вашу систему!

Очистка «постфактум»

Есть только определённые вещи, которые вы можете (или хотите) сделать в коде. Кроме того, вы можете столкнуться с набором данных, который вы не создали, или с набором данных, который было бы сложно даже знать, как очистить, не видя его сначала.

Многие программисты в такой ситуации реагируют импульсивно: «написать скрипт», что может быть отличным решением. Однако сторонние инструменты, такие как OpenRefine, также способны не только быстро и легко очищать данные, но и позволяют вашим данным быть легко просмотренными и использованными непрограммистами.

OpenRefine

OpenRefine — это проект с открытым исходным кодом, начатый компанией Metaweb в 2009 году. Google приобрела Metaweb в 2010 году, изменив название проекта с Freebase Gridworks на Google Refine. В 2012 году Google прекратила поддержку Refine и снова изменила название на OpenRefine, и каждый может вносить свой вклад в развитие проекта.

Установка OpenRefine необычна тем, что, хотя его интерфейс работает в браузере, технически это приложение для настольных компьютеров, которое нужно скачать и установить. Вы можете скачать приложение для Linux, Windows и macOS с веб-сайта.

Если у вас возникли проблемы с открытием файла на Mac, перейдите в «System Preferences» → «Security & Privacy» → «General». В разделе «Allow apps downloaded from» выберите «Anywhere». К сожалению, во время перехода от проекта Google к проекту с открытым исходным кодом OpenRefine, он, кажется, потерял свою легитимность в глазах Apple.

Для использования OpenRefine вам необходимо сохранить ваши данные в формате CSV. Кроме того, если ваши данные хранятся в базе данных, возможно, вы сможете экспортировать их в файл CSV.

Использование OpenRefine

В следующих примерах вы будете использовать данные, собранные со страницы «Сравнение текстовых редакторов» на Википедии; см. Рисунок. Хотя эта таблица относительно хорошо форматирована, в ней содержится много правок от разных людей за долгое время, поэтому есть несколько мелких несоответствий форматирования. Кроме того, поскольку данные предназначены для чтения людьми, а не машинами, некоторые выборы форматирования (например, использование «Бесплатно» вместо «$0.00») не подходят для программных входных данныхopenrefine1

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

Фильтрация. Фильтрацию данных можно выполнять двумя методами: фильтры и фасеты. Фильтры хороши для использования регулярных выражений для фильтрации данных; например, «Показать мне только данные, которые содержат три или более разделенных запятой языка программирования в столбце Язык программирования», как показано на рисунке 8-2.

Фильтры могут быть комбинированы, редактированы и легко добавляются путем манипулирования блоками в правой колонке. Они также могут быть комбинированы с фасетами.openrefine filtering

Фасеты отлично подходят для включения или исключения данных на основе всего содержимого столбца. (например, «Показать все строки, использующие лицензию GPL или MIT и были выпущены впервые после 2005 года»). В них встроены инструменты фильтрации. Например, при фильтрации числового значения вам предоставляются ползунки для выбора диапазона значений, которые вы хотите включить.

openrefine2

Однако, как бы вы не фильтровали ваши данные, их можно экспортировать в любой момент в один из нескольких форматов, поддерживаемых OpenRefine. Сюда входят CSV, HTML (HTML-таблица), Excel и несколько других форматов.

Очистка данных. Фильтрация данных может быть успешно выполнена только в том случае, если данные изначально относительно чистые. Например, в примере с фасетом в предыдущем разделе текстовый редактор, который имел дату выпуска 01-01-2006, не был бы выбран в фасете «Первый общедоступный выпуск», который искал значение 2006 и игнорировал значения, которые не выглядели так.

Преобразование данных выполняется в OpenRefine с использованием языка выражений OpenRefine, называемого GREL (G осталась от предыдущего названия OpenRefine — Google Refine). Этот язык используется для создания коротких лямбда-функций, которые преобразуют значения в ячейках на основе простых правил. Например:

if(value.length() != 4, "invalid", value)

Когда эта функция применяется к столбцу «Первый стабильный релиз», она сохраняет значения ячеек, где дата представлена в формате YYYY, и помечает все остальные столбцы как недопустимые.

openrefine3

Произвольные выражения GREL можно применять, нажав стрелку вниз рядом с любой меткой столбца и выбрав «Изменить ячейки → Преобразовать».

Однако пометка всех менее идеальных значений как недопустимых, хотя и делает их легко заметными, не слишком полезна. Было бы лучше попытаться спасти информацию из плохо отформатированных значений, если это возможно. Это можно сделать с помощью функции match в GREL:

value.match(".*([0-9]{4}).*").get(0)

Эта функция пытается сопоставить строковое значение с заданным регулярным выражением. Если регулярное выражение соответствует строке, возвращается массив. Любые подстроки, соответствующие «группе захвата» в регулярном выражении (ограниченные скобками в выражении, в данном случае [0-9]{4}), возвращаются в качестве значений массива.

Этот код, по существу, находит все вхождения четырех цифр подряд и возвращает первое из них. Обычно этого достаточно для извлечения годов из текста или плохо отформатированных дат. Кроме того, он имеет преимущество возвращения null для отсутствующих дат. (GREL не вызывает исключение нулевого указателя при выполнении операций с нулевой переменной).

Множество других преобразований данных возможно с редактированием ячеек и GREL. Полное руководство по языку можно найти на странице GitHub OpenRefine.

Индивидуальное и групповое обучение «Аналитик данных»
Если вы хотите стать экспертом в аналитике, могу помочь. Запишитесь на мой курс «Аналитик данных» и начните свой путь в мир ИТ уже сегодня!

Контакты
Для получения дополнительной информации и записи на курсы свяжитесь со мной:

Телеграм: https://t.me/Vvkomlev
Email: victor.komlev@mail.ru

Объясняю сложное простыми словами. Даже если вы никогда не работали с ИТ и далеки от программирования, теперь у вас точно все получится! Проверено десятками примеров моих учеников.

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

Практическая направленность. 80%: практики, 20% теории. У меня множество авторских заданий, которые фокусируются на практике. Вы не просто изучаете теорию, а сразу применяете знания в реальных проектах и задачах.

Разнообразие учебных материалов: Теория представлена в виде текстовых уроков с примерами и видео, что делает обучение максимально эффективным и удобным.

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

Понравилась статья? Поделиться с друзьями:
Школа Виктора Комлева
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.