Как часто нужные книги попадают в руки когда они уже не так актуальны! Вот первое, что я подумал, открыв "Practical Text Mining With Perl" (Roger Bilisoly, Wiley & Sons, 2008). А не повод ли это вернуться к старым увлечениям?
Одна из первых глав посвящена проблеме разбиения текста на слова. Задачка не такая уж и простая. Разбить строку по пробелам и убрать знаки препинания несложно:
@tokens = split(/\s+/, $line);
foreach $tk (@tokens) {
if ( $x =- /(\w+)/ ) {
# $1 содержит слово
}
}
foreach $tk (@tokens) {
if ( $x =- /(\w+)/ ) {
# $1 содержит слово
}
}
Но результат будет немного неожиданным.
Внутренняя пунктуация
От слова "инженер-программист" останется лишь "инженер". От английского "o'clock" -- "o". Издатель O'Reilly не узнает своей фамилии... Шаблон \w+ набирает буквы, цифры и нижнее подчеркивание (_). Как только в тексте встречается другой символ, дверца (круглая скобка) закрывается. Слово закончилось.Чтобы не отсекать правые части составных слов, автор модифицирует регулярное выражение следующим образом:
$x =~ /(([a-zA-Z']+-)*[a-zA-Z']+)/
Включение апострофа в квадратные скобки -- не лучшее решение, потому что теперь словом может считаться, например: '-'-'-'-'-'-'-'-'-'-'-'-'-'-' . И разве бывают слова, где за апострофом следует дефис? Не лучше ли принять, что разделителем составных слов может быть либо дефис либо апостроф?
$x =~ /(([a-zA-Z]+[-']))*[a-zA-Z]+'?)/
$x =~ /(([[:alpha:]]+['-])*[[:alpha:]]+'?)/
***
Всегда ли имеет смысл считать составное слово чем-то отдельным? Вот ряд примеров:
- бледно-оранжевый
- красно-коричневый
- давным-давно
- когда-нибудь
- высоко-высоко
- северо-западный
- мяу-мяу
- иван-да-марья
- человек-амфибия
- самолет-разведчик
- советско-американский
- авто-мастерская
Автор "Practical Text Mining With Perl" считает, что проще все же не разбивать составные слова, и я с ним согласен. Некоторое количество лишних словообразований в лексиконе не так вредно с точки зрения анализа смысла, как дробление. Что останется от понятия "mother-in-law" (свекровь) -- пишет Bilisoly -- если разбить его на "mother", "in" и "law" ("мать", "в", "закон")?
Тире
Тире -- еще один камень преткновения. Оно может быть обозначено разными символами, отделяться или не отделяться от слов пробелами. Тут частично помогает предварительная нормализация. Например, если в тексте встретились два идущих подряд дефиса, возле которых нет пробела, обеспечим пробелы с двух сторон:s/--/ -- /g
Скрипт
Ниже код программы, адаптированный к русскому языку. В качестве аргументов задается имя исходного файла и файла с результатом (списком слов). Предполагается, что исходный файл в Windows-кодировке. При другой кодировке следует отредактировать строчку:use open ':encoding(cp1251)';
###############################################
# split_words.pl
# Usage: perl split_words.pl FROM_FILE TO_FILE
###############################################
use strict;
use warnings;
use open ':encoding(cp1251)';
open (my $I, '<', $ARGV[0]) or die $!;
open (my $O, '>', $ARGV[1]) or die $!;
while (<$I>) {
chomp;
s/--/ -- /g;
my @words = split /\s+/;
foreach my $w (@words) {
print $O "$1\n"
if $w =~ /(([[:alpha:]]+['-])*[[:alpha:]]+'?)/ )
}
}
close $O;
close $I;
# split_words.pl
# Usage: perl split_words.pl FROM_FILE TO_FILE
###############################################
use strict;
use warnings;
use open ':encoding(cp1251)';
open (my $I, '<', $ARGV[0]) or die $!;
open (my $O, '>', $ARGV[1]) or die $!;
while (<$I>) {
chomp;
s/--/ -- /g;
my @words = split /\s+/;
foreach my $w (@words) {
print $O "$1\n"
if $w =~ /(([[:alpha:]]+['-])*[[:alpha:]]+'?)/ )
}
}
close $O;
close $I;
Python-версия
А вот версия для Питона:#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# from __future__ import with_statement # Python2.5
import sys
import re
pattern = re.compile("(([\w]+[-'])*[\w']+'?)", re.U)
with open(sys.argv[1]) as f:
for line in f:
line = unicode(line, 'cp1251')
line = line.replace('--', ' -- ')
for token in line.split():
m = pattern.match(token)
if m:
print m.group()
Везде свои тонкости
Локализованная версия испытывалась на русском переводе рассказа Эдгара По "Сердце-обличитель". В книге "Text Mining With Perl" с этой же целью используется оригинал: "Tell-Tale Heart". А что если мы обрабатываем текст, по которому не прошелся корректор? Авторы блог-постов, например, часто забывают поставить пробел после точки в конце предложения или после запятой. Предположение, что слова всегда разделены пробелами, может оказаться неверным.Вот почему на практике часто используют несколько иной подход. Исходная строка разбивается на слова не только пробелами, а всем, что не входит в слово. Простейший пример:
@tokens = split(/\W+/, $line);
На Питоне:
splitter = re.compile(r'\W*', re.U)
words = [ s for s in splitter.split(text) if s ]
words = [ s for s in splitter.split(text) if s ]
А вот более интересный пример из книги "An Introduction to Language Processing with Perl and Prolog" (Pierre M. Nugues, Springer, 2006):
$text = <>;
while ($line = <>) {
$text .= $line;
}
$text =~ tr/a-zåàâäæçéèêëîïôöoeùûüßAZÅÀÂÄÆÇÉÈÊËÎÏÔÖOEÙÛÜ’()\-,.?!:;/\n/cs;
$text =~ s/([,.?!:;()’\-])/\n$1\n/g;
$text =~ s/\n+/\n/g;
print $text;
while ($line = <>) {
$text .= $line;
}
$text =~ tr/a-zåàâäæçéèêëîïôöoeùûüßAZÅÀÂÄÆÇÉÈÊËÎÏÔÖOEÙÛÜ’()\-,.?!:;/\n/cs;
$text =~ s/([,.?!:;()’\-])/\n$1\n/g;
$text =~ s/\n+/\n/g;
print $text;
- Если символ не является ни буквой ни знаком пунктуации, он заменяется символом новой строки
- Каждый знаки пунктуации помещается на отдельную строку
- Повторяющиеся символы новой строки заменяются одним
Первую программу можно тоже научить работать с неправильными текстами, не меняя алгоритма. Для этого можно расширить оператор разбиения слов. Или предварительно обрабатывать входящие строки -- примерно так же, как мы поступали с "тире".
Ясно одно: трудно написать универсальную процедуру на все случаи жизни. Надо учитывать, с каким материалом придется иметь дело: с литературными текстами, блогами, юридическими документами... везде свои тонкости.
4 комментария:
У вас, похоже, закралась ошибка в регулярку:
(([[:alpha:]]+['-]?)*.
Ведь не обязательно первая буквенная группа должна оканчиваться апострофом или дефисом.
И даже более того, часть регулярки после первых групппирующих скобок тоже необязательна. Получится что-то типа ([[:alpha:]+]'?)?
Я рассуждал так. Cлово -- это серия букв: [[:alpha:]]+
Перед ними может идти любое количество таких же серий, заканчивающихся разделителем: ([[:alpha:]]+['-])*
Наконец, слово может заканчиваться апострофом: '?
...Где тут ошибка?
Да, все правильно. Ошибся я.
Отправить комментарий