Первичная обработка текста

При анализе естественного языка часто приходится сталкиваться с задачей предварительной обработки текстов. Для того, чтобы начать работу с текстовой информацией, нужно ее нужно почистить, убрать ненужное. Здесь приводится простая процедура на Python'е, которую я использую для первичной чистки данных.

Простые способы

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

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import re
from collections import defaultdict

r_alphabet = re.compile(u'[а-яА-Я0-9-]+|[.,:;?!]+')

def count(corpus):
    tokens = defaultdict(lambda: 0)
    for line in open(corpus):
        line = line.decode('utf-8').lower()
        for token in r_alphabet.findall(line):
            tokens[token] += 1
    return tokens

tokens = count('corpus.txt')

Здесь defaultdict используется для подсчета частоты встречаемых слов. Это словарь, дефолтные значение которого определяется функцией. В данном случае, дефолтное значение - 'lambda: 0'. Довольно распространенный прием для подобного рода задач. В итоге, функция count возвращает словарь токенов в виде пар (слово, частота).

Есть еще один прием, эквивалентный первому. Он использует метод split скомпилированного выражения.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import re
from collections import defaultdict

r_alphabet = re.compile(u'[а-яА-Я0-9-.,:;?!]+')
r_punct = re.compile(u'([.,:;?!]+)')

def count(corpus):
    tokens = defaultdict(lambda: 0)
    data = open(corpus)
    for line in data:
        line = line.decode('utf-8').lower()
        for chunk in r_alphabet.findall(line):
            for token in r_punct.split(chunk):
                if token:
                    tokens[token] += 1
    return tokens

tokens = count('corpus.txt')

Здесь r_punct - список знаков пунктуации. Для "очищения" слов от лишних символов используем метод split скомпилированного регулярного выражения. Значение регулярного выражения помещено в скобки затем, что нам интересно получить также и знаки препинания: '?слово...' -> ['?', 'слово', ','].

В итоге, получим очищенный набор слов. На моем корпусе, в первой десятке:

    ,: 312282
    .: 200092
    и: 89792
    в: 70790
    не: 50108
    ...
    снова: 1598
    всегда: 1593
    свою: 1584
    совсем: 1578
    

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

Способ на генераторах

Для гурманов есть еще один вариант с применением генераторов (см статьи Дэвида Бизли):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import re
from collections import defaultdict

r_alphabet = re.compile(u'[а-яА-Я0-9-]+|[.,:;?!]+')

def gen_lines(corpus):
    data = open(corpus)
    for line in data:
        yield line.decode('utf-8').lower()

def gen_tokens(lines):
    for line in lines:
        for token in r_alphabet.findall(line):
            yield token
    
def count(corpus):
    tokens = defaultdict(lambda: 0)
    lines = gen_lines(corpus)
    tokens_ = gen_tokens(lines)
    for token in tokens_:
        tokens[token] += 1
    return tokens

tokens = count('corpus.txt')

Здесь больше кода и нет выигрыша по производительности. Однако, у этого метода есть преимущество. Этот способ разделяет правила обработки строк текста, создавая своего рода конвейер. Если нам потребуется дополнительная обработка, мы просто вписываем новое правило.

Обработка с помощью re.Scanner

Модуль re имеет один полезный, хотя и недокументированный класс re.Scanner. Этот класс позволяет провести простую токенизацию текста, т.е. преобразовать входящую последовательность символов текста в последовательность токенов (т.е. тех элементов текста, которые нам важны для дальнейшей работы).

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

# -*- encoding: utf-8 -*-
import re

def en_word(scanner, token):    return 'RU_W', token
def ru_word(scanner, token):    return 'EN_W', token
def punct(scanner, token):      return 'PUNCT', token

scanner = re.Scanner([
    (u'[a-zA-Z]+', en_word),
    (u'[а-яА-Я]+', ru_word),
    (u'[.,!?:;()-]+', punct),
    (u'.', None)                # Other
    ])

tokens, remainder = scanner.scan(u'Токенизация (tokenization) - это процесс преобразования текста в последовательность токенов.')

for token, value in tokens:
    print token, '	', value

На выходе получаем следующую последовательность токенов.

EN_W 	Токенизация
PUNCT 	(
RU_W 	tokenization
PUNCT 	)
PUNCT 	-
EN_W 	это
EN_W 	процесс
EN_W 	преобразования
EN_W 	текста
EN_W 	в
EN_W 	последовательность
EN_W 	токенов
PUNCT 	.

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