Предположим, вы хотите создать набор данных, состоящий из кратких определений и фактов, взятых из большого массива текстов, такого как набор рефератов всех статей, найденных в англоязычной версии Википедии. Вы также хотели бы, чтобы рабочий процесс, который вы собираете, был многоразовым и гибким, позволяя вам быстро обновлять набор данных и/или расширять его возможности. Какие функции и библиотеки Python, как встроенные, так и предоставленные более широким сообществом, вы можете использовать для достижения этой цели?
Несмотря на то, что встроенные строковые методы обладают ограниченной гибкостью, а регулярные выражения — ограниченной выразительной силой, и то, и другое можно творчески использовать для реализации масштабируемых рабочих процессов, обрабатывающих и анализирующих текстовые данные. В этой статье рассматриваются эти инструменты и представлены несколько полезных периферийных пакетов и методов в контексте описанного выше варианта использования.
Получение и чтение необработанных данных
Архивы, содержащие все рефераты из англоязычной версии Википедии, регулярно обновляются и доступны по адресу https://dumps.wikimedia.org/enwiki/latest/ (ссылку можно найти на этой целевой странице). Вы решаете использовать объединенный файл архива тезисов, содержащий все тезисы. Как вы можете получить его в автоматическом режиме? Один из способов получить большой файл, доступный в Интернете, а также получить удобную обратную связь о ходе выполнения — использовать библиотеку requests вместе с библиотекой tqdm.
Библиотека tqdm позволяет отображать анимированный индикатор выполнения, в то время как экземпляр итерируемой структуры данных испускает каждый из своих элементов. Пример ниже иллюстрирует вывод. Параметры position
и leave
гарантируют, что индикатор выполнения не прерывается символами новой строки, если вывод отображается в Jupyter Notebook.
>>> import tqdm >>> import time >>> for i in tqdm.tqdm(range(3), position=0, leave=True): ... time.sleep(1) ... 100%|███████████████████████| 3/3 [00:03<00:00, 1.01s/it]
Библиотека requests позволяет создать итерируемый поток данных из файла, расположенного по указанному URL-адресу, установив для параметра stream
значение True
. Затем вы можете получать данные порциями и записывать их в файл, отображая прогресс для пользователя. В приведенном ниже примере параметр total
, предоставленный tqdm.tqdm
, представляет собой количество фрагментов данных размером 1 МБ (т. е.) в файле; определяется путем деления размера файла, указанного в заголовке ответа HTTP, на выбранное chunk_size
в 1 МБ.
>>> import requests >>> >>> url = "https://dumps.wikimedia.org/" >>> url += "strings-regular-expressions-and-text-data-analysis/" >>> url += "enwiki-20191120-abstract-sample.xml.gz" >>> >>> with requests.get(url, stream=True) as response: ... file_size = int(response.headers['Content-length']) ... chunk_size = 1024*1024 ... with open("abstracts.xml.gz", "wb") as file: ... chunks = response.iter_content(chunk_size=chunk_size) ... for chunk in tqdm.tqdm( ... chunks, total=ceil(file_size/chunk_size), ... position=0, leave=True ... ): ... file.write(chunk) ... 100%|████████████████████████| 10/10 [00:06<00:00, 1.89it/s]
Когда у вас есть сжатый файл abstracts.xml.gz
на вашем диске, вы можете открыть его и просмотреть данные, распаковывая их по одной строке за раз, используя встроенную библиотеку gzip. Ниже общее количество строк в файле подсчитывается с помощью понимания и встроенной функции sum
.
>>> import gzip >>> with gzip.open("abstracts.xml.gz") as file: ... print(sum(1 for line in file)) ... 238564
Обработка необработанных строковых данных с помощью строковых функций
Учитывая, что набор данных находится в формате XML, наиболее общий подход к его анализу заключается в использовании анализатора XML, такого как lxml. Однако в этом случае текстовое содержимое в файле имеет согласованный формат: текст для каждого реферата полностью отображается на отдельной строке, окруженный двумя разделителями элементов <abstract>
и </abstract>
. Таким образом, вы можете просмотреть каждую строку в файле и определить, хотите ли вы ее извлечь. Вы делите этот рабочий процесс на два этапа: извлечение строк, а затем извлечение самих тезисов.
Во-первых, вы решаете реализовать генератор, который перебирает все строки в файле и выдает их по одной за раз. В случае, если текстовые данные на определенной строке имеют неизвестную кодировку, для проверки каждой строки используется библиотека chardet. Это вряд ли будет проблемой в этом конкретном наборе данных, но разрешение только тех строк, которые являются допустимыми ASCII или UTF-8, может помочь вам избежать любых непредвиденных проблем позже в процессе. Прежде чем получить его, вы преобразуете каждую строку из двоичных данных в строку, используя метод decode
для двоичных данных типа bytes
, а затем удаляете все пробелы в начале или конце строки, используя strip
.
from chardet import detect def lines(): with gzip.open("abstracts.xml.gz") as file: for line in file: if detect(line)["encoding"] in ['ascii', 'utf-8']: yield line.decode().strip()
Как вы извлекаете фактический текст аннотации из каждой строки формы <abstract>...</abstract>
? Вы можете воспользоваться встроенными строковыми методами startswith
и endswith
для идентификации этих строк, а затем использовать строковые индексы срезов для извлечения необработанного текста каждой аннотации. Приведенная ниже функция определяет генератор, который выдает каждую аннотацию, которая появляется в любой строке сжатого файла.
def abstracts(): for line in lines(): if line.startswith("<abstract>") and\ line.endswith("</abstract>"): line = line[len("<abstract>"):-len("</abstract>")] yield line
Вы можете использовать функцию islice
для итерируемых экземпляров структуры данных, чтобы перебирать только часть тезисов. Строка ниже вычисляет максимальную длину первых 100 тезисов.
>>> from itertools import islice >>> max(len(a) for a in islice(abstracts(), 0, 100)) 553
Извлечение строк с определенной структурой
Регулярные выражения — это математические конструкции, которые можно использовать для описания наборов строк, соответствующих определенному шаблону или формату. История и концептуальные детали регулярных выражений выходят далеко за рамки этой статьи, но вам нужно только понять несколько основных строительных блоков, чтобы начать использовать встроенную библиотеку re, чтобы определить, когда строка соответствует определенному шаблону.
В Python регулярные выражения представлены в виде строк. В приведенном ниже примере регулярному выражению "abc"
удовлетворяют только те строки, которые начинаются с точной последовательности символов abc
. Чтобы проверить, удовлетворяет ли строка регулярному выражению, достаточно скомпилировать строку регулярного выражения с помощью compile
, а затем использовать метод match
для выполнения проверки.
>>> import re >>> re.compile("abc").match("abcd") <re.Match object; span=(0, 3), match='abc'>
Чтобы регулярному выражению соответствовала только вся строка (а не только ее часть), можно добавить ^
в начало регулярного выражения и $
в конец.
>>> re.compile("^abc$").match("abc") <re.Match object; span=(0, 3), match='abc'> >>> re.compile("^abc$").match("abcd") None
Синтаксис строк регулярных выражений состоит из большого набора специальных символов и последовательностей символов. Для целей мотивирующего примера достаточно следующих синтаксических конструкций:
[a-z]
соответствует любой строчной одиночной букве;\s
удовлетворяется любым одиночным символом пробела, а\.
удовлетворяется точкой;- для любого регулярного выражения
r
набор строк, удовлетворяющихr
, также удовлетворяет(r)
и наоборот; - для любого регулярного выражения
r
набор строк, удовлетворяющихr*
, состоит из всех строк, являющихся конкатенацией нуля или более отдельных строк, каждая из которых независимо удовлетворяетr
(например,,"abc"
,"aaaa"
и""
все удовлетворяют[a-z]*
); - для любых двух регулярных выражений
r
иs
регулярному выражениюr|s
соответствует любая строка, удовлетворяющая либоr
, либоs
, либо обоим; - для любых двух регулярных выражений
r
иs
регулярному выражениюrs
соответствует любая строка, состоящая из части, удовлетворяющейr
, за которой сразу следует часть, удовлетворяющаяs
(например,,"xy"
удовлетворяет[a-z][a-z]
, а"x"
и"xyz"
нет).
Чтобы упростить быстрое экспериментирование с более крупными и сложными регулярными выражениями при повторном использовании стандартных стандартных блоков, вы можете определить функции, которые принимают меньшие выражения и создают более крупные. Поскольку регулярные выражения представлены с помощью строк Python, для этого можно использовать строковые методы, такие как join
. Приведенные ниже функции также используют синтаксис распаковки аргументов, чтобы сделать вызывающий их код более кратким.
def atom(s): # Wrap an expression in parenthesis. return "(" + s + ")" def options(*rs): # Given expressions r1, r2, ..., rN, build # the expression (r1)|(r2)|...|(rN). return atom("|".join([atom(r) for r in rs])) def adjacent(*rs): # Given expressions r1, r2, ..., rN, build # the expression (r1)(r2)...(rN). return atom("".join([atom(r) for r in rs])) def spaced(*rs): # Given expressions r1, r2, ..., rN, build # the expression (r1)\s(r2)\s...\s(rN). return atom("\s".join([atom(r) for r in rs])) def repeated(r): # Given the expression r0, build ((r0)*). return atom(atom(r) + "*") def repeated_spaced(r): # Given the expression r0, build (((r0)(\s))*(r0)). return adjacent(repeated(adjacent(r, "\s")), r)
Приведенные ниже определения говорят сами за себя и используют приведенные выше функции для сборки более сложных регулярных выражений. Приведенные ниже регулярные выражения, в свою очередь, полезны в качестве строительных блоков для построения регулярного выражения, которое соответствует предложению формы, которую вы пытаетесь идентифицировать в наборе данных тезисов.
article = options("A", "a", "An", "an", "The", "the") word = atom("[a-z]+") verb = options("are", "is") period = atom("\.")
Приведенное ниже определение предназначено для регулярного выражения, которому удовлетворяет любая строка, которая (1) начинается с определенного или неопределенного артикля, (2) имеет любое ненулевое количество слов после артикля, (3) имеет глагол-связку, за которым следует одно или несколько дополнительных слов, а (4) заканчивается точкой.
sentence = adjacent( spaced( article, word, repeated_spaced(word), verb, repeated_spaced(word) ), period )
Вы можете увидеть фактическую строку, которую вызывает функция, описанная выше. Должно быть очевидно, что сборка (или изменение) строки, такой как приведенная ниже, вручную в необработанном виде, скорее всего, будет процессом, подверженным ошибкам. Абстракции, подобные приведенным выше, являются эффективным способом управления сложностью регулярных выражений.
>>> sentence '((((((A)|(a)|(An)|(an)|(The)|(the)))\\s(([a-z]+))\\s(((((((([a-z]+))(\\s+)))*))(([a-z]+))))\\s(((are)|(is)))\\s(((((((([a-z]+))(\\s+)))*))(([a-z]+))))))((\\.)))'
Вы решаете использовать это регулярное выражение для идентификации фактов и определений в рефератах, сохраняя его повторно используемую скомпилированную версию как sentence_re
для повышения производительности. Прежде чем проверять, соответствует ли реферат регулярному выражению sentence
, вы используете строковый метод split
, чтобы разделить строку на список подстрок (т.е., тех подстрок, которые находятся между экземплярами .
символ в строке). Затем вы используете нотацию индекса списка, чтобы сохранить только первую запись. Поскольку метод split
не включает символ, используемый для разделения строки, вам необходимо объединить его обратно в первую подстроку. В приведенном ниже примере этот рабочий процесс применяется к части тезисов.
>>> sentence_re = re.compile("^" + sentence + "$") >>> for a in itertools.islice(abstracts(), 0, 400): ... a = a.split(".")[0] + '.' # Keep only first sentence. ... if sentence_re.match(a) is not None: ... print(a) ... An audio file format is a file format for storing digital audio data on a computer system. The bell curve is typical of the normal distribution. The immune system is a host defense system comprising many biological structures and processes within an organism that protects against disease. A monolithic kernel is an operating system architecture where the entire operating system is working in kernel space.
Пример базового анализа
Вам интересно узнать больше о конкретных терминах или фразах, описанных в первом предложении каждого реферата. В частности, вы хотели бы знать распределение количества слов перед глаголом. Один из подходов заключается в использовании метода search
для поиска первого экземпляра глагола, а затем для проверки содержимого Match object для определения места совпадения. Затем можно использовать строковый метод count
для подстроки (до совпадающего глагола включительно), чтобы определить количество пробелов (и, следовательно, слов) перед первым глаголом.
>>> counts = [] >>> verb_re = re.compile(verb) >>> >>> for abstract in tqdm.tqdm( ... abstracts(), position=0, leave=True, total=210777 ... ): ... abstract = abstract.split(".")[0] + '.' ... if sentence_re.match(abstract) is not None: ... if (m := verb_re.search(abstract)) is not None: ... counts.append(abstract[:m.end()].count(" ")) ... 100%|███████████████████| 210777/210777 [04:44<00:00, 739.92it/s]
Вы можете заметить в приведенном выше примере использование выражения присваивания, включающего оператор :=
. Сценарии, подобные этому, играют важную роль в обосновании этой новой функции (представленной в Python 3.8). Альтернативный синтаксис потребовал бы отдельной строки для оператора m = verb_re.search(abstract)
.
Теперь, когда количество слов перед глаголом в каждой аннотации находится в списке целых чисел counts
, сгенерировать гистограмму с помощью Matplotlib несложно. Результаты показывают, что наиболее распространенным случаем является случай с тремя словами перед глаголом (что, учитывая, что исходный определенный или неопределенный артикль также учитывается, предполагает, что большинство рефератов относятся к понятию, описываемому фразой из двух слов). .
import matplotlib.pyplot as plt plt.hist( counts, bins=range(max(counts)), density=False, histtype="bar", color="#DD7600", edgecolor="k", alpha=0.8 ) plt.ylabel("number of abstracts") plt.xlabel("number of words before verb") plt.xticks(range(0, max(counts))) plt.show()
Дальнейшее чтение
В этой статье рассматривается вариант использования при ссылке или использовании методов, взятых из широкого круга дисциплин и тематических областей: наука о данных (в частности, очистка данных), ETL, синтаксический анализ и поиск и обход структуры данных, анализ текстовых данных. и обработка естественного языка, регулярные выражения и грамматика обычного языка, просмотр веб-страниц и некоторые другие. Предоставление более подробных указаний по дальнейшему изучению каждого из них выходит за рамки этой статьи, но стоит отметить несколько моментов, относящихся конкретно к реализации таких рабочих процессов в Python.
Пакет regex обратно совместим со встроенной библиотекой re, но предлагает дополнительные функции, такие как нечеткое сопоставление. Библиотека-долгожитель Beautiful Soup описывается как инструмент для анализа документов HTML и XML (обычно в сценариях, включающих веб-скрапинг), но на самом деле она сама по себе является мощным инструментом обработки текстовых данных. Набор сложных инструментов и методов обработки естественного языка доступен в Natural Language Toolkit, включая возможность токенизации, тегирования и анализа текста. Наконец, в конце спектра машинного обучения находятся такие библиотеки, как Gensim, которые предоставляют такие возможности, как тематическое моделирование.
Эта статья также доступна в формате Jupyter Notebook и в формате HTML на GitHub.