Как я могу разбить несколько соединенных слов?

У меня есть массив из 1000 или около того записей с примерами ниже:

wickedweather
liquidweather
driveourtrucks
gocompact
slimprojector

Я хотел бы иметь возможность разбить их на соответствующие слова, например:

wicked weather
liquid weather
drive our trucks
go compact
slim projector

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

Я полагаю, это можно сделать вручную, но зачем - когда это можно сделать с помощью кода! =) Но это меня поставило в тупик. Есть идеи?

Обратите внимание, что наивная реализация вернет "злую погоду"

Brad Gilbert 15.10.2008 01:20

Привет, оптимальные решения, я видел ваш ответ на вопрос EMR, и мне было интересно, могу ли я связаться с вами с некоторыми вопросами, касающимися ИТ в области здравоохранения?

Alex Gordon 27.09.2009 20:42

Также см. Реализации Python и Ruby на stackoverflow.com/questions/11447859/…

Matthias Winkelmann 27.12.2019 22:37
В чем разница между методом "==" и equals()
В чем разница между методом "==" и equals()
Это один из наиболее часто задаваемых вопросов новичкам на собеседовании. Давайте обсудим его на примере.
Замена символа по определенному индексу в JavaScript
Замена символа по определенному индексу в JavaScript
В JavaScript существует несколько способов заменить символ в строке по определенному индексу.
51
3
24 066
13
Перейти к ответу Данный вопрос помечен как решенный

Ответы 13

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

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

Надо найти файл словаря, чтобы ударить по нему.

Taptronic 12.10.2008 07:08

Спасибо! Я собираюсь собрать тот и тот Perl вместе, посмотреть, что получится.

Taptronic 12.10.2008 07:27

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

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

Для этого я могу понизить мод, но пусть секретарь сделает это.

Вы потратите больше времени на словарное решение, чем на обработку вручную. Кроме того, у вас не будет 100% уверенности в решении, поэтому вам все равно придется уделить ему внимание вручную.

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

SquareCog 12.10.2008 06:55
Ответ принят как подходящий

Может ли это сделать человек?

farsidebag
far sidebag
farside bag
far side bag

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

Чтобы узнать, как сделать статистику, которая может быть полезной, я обращаюсь к доктору Питеру Норвигу, который решает другую, но связанную проблему проверки орфографии в 21 строке кода: http://norvig.com/spell-correct.html

(он немного обманывает, сворачивая каждый цикл for в одну строку ... но все же).

Обновлять Это застряло у меня в голове, поэтому мне пришлось родить его сегодня. Этот код выполняет такое же разбиение, что и описанный Робертом Гэмблом, но затем он упорядочивает результаты в зависимости от частоты слов в предоставленном файле словаря (который теперь, как ожидается, будет некоторым текстовым представителем вашего домена или английского языка в целом. Я использовал большой .txt из Norvig, ссылка на который приведена выше, и добавила к нему словарь, чтобы скрыть пропущенные слова).

Комбинация двух слов в большинстве случаев превосходит комбинацию из 3 слов, если разница в частоте не очень велика.


Я разместил этот код с небольшими изменениями в своем блоге

http://squarecog.wordpress.com/2008/10/19/splitting-words-joined-into-a-single-string/ а также немного написал об ошибке недополнения в этом коде ... Я был склонен просто тихо ее исправить, но решил, что это может помочь некоторым людям, которые раньше не видели трюка с журналом: http://squarecog.wordpress.com/2009/01/10/dealing-with-underflow-in-joint-probability-calculations/


Выведите свои слова, а также несколько моих собственных - обратите внимание, что происходит с "orcore":

perl splitwords.pl big.txt words
answerveal: 2 possibilities
 -  answer veal
 -  answer ve al

wickedweather: 4 possibilities
 -  wicked weather
 -  wicked we at her
 -  wick ed weather
 -  wick ed we at her

liquidweather: 6 possibilities
 -  liquid weather
 -  liquid we at her
 -  li quid weather
 -  li quid we at her
 -  li qu id weather
 -  li qu id we at her

driveourtrucks: 1 possibilities
 -  drive our trucks

gocompact: 1 possibilities
 -  go compact

slimprojector: 2 possibilities
 -  slim projector
 -  slim project or

orcore: 3 possibilities
 -  or core
 -  or co re
 -  orc ore

Код:

#!/usr/bin/env perl

use strict;
use warnings;

sub find_matches($);
sub find_matches_rec($\@\@);
sub find_word_seq_score(@);
sub get_word_stats($);
sub print_results($@);
sub Usage();

our(%DICT,$TOTAL);
{
  my( $dict_file, $word_file ) = @ARGV;
  ($dict_file && $word_file) or die(Usage);

  {
    my $DICT;
    ($DICT, $TOTAL) = get_word_stats($dict_file);
    %DICT = %$DICT;
  }

  {
    open( my $WORDS, '<', $word_file ) or die "unable to open $word_file\n";

    foreach my $word (<$WORDS>) {
      chomp $word;
      my $arr = find_matches($word);


      local $_;
      # Schwartzian Transform
      my @sorted_arr =
        map  { $_->[0] }
        sort { $b->[1] <=> $a->[1] }
        map  {
          [ $_, find_word_seq_score(@$_) ]
        }
        @$arr;


      print_results( $word, @sorted_arr );
    }

    close $WORDS;
  }
}


sub find_matches($){
    my( $string ) = @_;

    my @found_parses;
    my @words;
    find_matches_rec( $string, @words, @found_parses );

    return  @found_parses if wantarray;
    return \@found_parses;
}

sub find_matches_rec($\@\@){
    my( $string, $words_sofar, $found_parses ) = @_;
    my $length = length $string;

    unless( $length ){
      push @$found_parses, $words_sofar;

      return @$found_parses if wantarray;
      return  $found_parses;
    }

    foreach my $i ( 2..$length ){
      my $prefix = substr($string, 0, $i);
      my $suffix = substr($string, $i, $length-$i);

      if ( exists $DICT{$prefix} ){
        my @words = ( @$words_sofar, $prefix );
        find_matches_rec( $suffix, @words, @$found_parses );
      }
    }

    return @$found_parses if wantarray;
    return  $found_parses;
}


## Just a simple joint probability
## assumes independence between words, which is obviously untrue
## that's why this is broken out -- feel free to add better brains
sub find_word_seq_score(@){
    my( @words ) = @_;
    local $_;

    my $score = 1;
    foreach ( @words ){
        $score = $score * $DICT{$_} / $TOTAL;
    }

    return $score;
}

sub get_word_stats($){
    my ($filename) = @_;

    open(my $DICT, '<', $filename) or die "unable to open $filename\n";

    local $/= undef;
    local $_;
    my %dict;
    my $total = 0;

    while ( <$DICT> ){
      foreach ( split(/\b/, $_) ) {
        $dict{$_} += 1;
        $total++;
      }
    }

    close $DICT;

    return (\%dict, $total);
}

sub print_results($@){
    #( 'word', [qw'test one'], [qw'test two'], ... )
    my ($word,  @combos) = @_;
    local $_;
    my $possible = scalar @combos;

    print "$word: $possible possibilities\n";
    foreach (@combos) {
      print ' -  ', join(' ', @$_), "\n";
    }
    print "\n";
}

sub Usage(){
    return "$0 /path/to/dictionary /path/to/your_words";
}

Можно ли это запустить в Windows XP? Как мне загрузить Perl. Очевидно, мне нужно больше узнавать (в плане других языков)! :)

Taptronic 14.10.2008 19:00

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

SquareCog 14.10.2008 21:15

+1 - Я не знаю Perl, но я дал вам +1 за то, что вы выходите за рамки служебного долга. Хороший!

Mark Brittingham 27.01.2009 02:30

Я изменил код, чтобы попытаться сделать его более удобным в обслуживании. Хотя для начала это было довольно прилично.

Brad Gilbert 19.03.2009 00:34

Я бы не изменил его, если бы это еще не было постом сообщества вики.

Brad Gilbert 19.03.2009 00:35

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

SquareCog 19.03.2009 02:01

В этом коде, очевидно, есть недостаток, он предлагает только «экспертную смену пола» как второй наиболее вероятный результат.

Rob Van Dam 03.05.2011 03:28

У меня такая же проблема, но мне нужно решение на python :(

Imran Ahmad Ghazali 29.08.2018 10:12

Лучшим инструментом для работы здесь является рекурсия, а не регулярные выражения. Основная идея состоит в том, чтобы начать поиск слова с начала строки, затем взять оставшуюся часть строки и найти другое слово и так далее, пока не будет достигнут конец строки. Рекурсивное решение является естественным, поскольку возврат с возвратом должен происходить, когда данный остаток строки не может быть разбит на набор слов. В приведенном ниже решении используется словарь для определения того, что является словом, и распечатываются решения по мере их нахождения (некоторые строки могут быть разбиты на несколько возможных наборов слов, например, wickedweather может быть проанализирован как «мы злые на нее»). Если вам нужен только один набор слов, вам нужно будет определить правила выбора наилучшего набора, возможно, выбрав решение с наименьшим количеством слов или установив минимальную длину слова.

#!/usr/bin/perl

use strict;

my $WORD_FILE = '/usr/share/dict/words'; #Change as needed
my %words; # Hash of words in dictionary

# Open dictionary, load words into hash
open(WORDS, $WORD_FILE) or die "Failed to open dictionary: $!\n";
while (<WORDS>) {
  chomp;
  $words{lc($_)} = 1;
}
close(WORDS);

# Read one line at a time from stdin, break into words
while (<>) {
  chomp;
  my @words;
  find_words(lc($_));
}

sub find_words {
  # Print every way $string can be parsed into whole words
  my $string = shift;
  my @words = @_;
  my $length = length $string;

  foreach my $i ( 1 .. $length ) {
    my $word = substr $string, 0, $i;
    my $remainder = substr $string, $i, $length - $i;
    # Some dictionaries contain each letter as a word
    next if ($i == 1 && ($word ne "a" && $word ne "i"));

    if (defined($words{$word})) {
      push @words, $word;
      if ($remainder eq "") {
        print join(' ', @words), "\n";
        return;
      } else {
        find_words($remainder, @words);
      }
      pop @words;
    }
  }

  return;
}

не запускал его, но он выглядит как лучшее решение, чем BKB, поскольку дает все возможности.

SquareCog 12.10.2008 09:38

Это работает как по волшебству. Именно то, что я искал, большое вам спасибо. Пытаюсь перевести на PHP. Если есть версия PHP, поделитесь ею здесь.

Mani 23.02.2016 12:27

Алгоритм Витерби намного быстрее. Он вычисляет те же оценки, что и рекурсивный поиск в ответе Дмитрия выше, но за время O (n). (Поиск Дмитрия занимает экспоненциальное время; Витерби делает это с помощью динамического программирования.)

import re
from collections import Counter

def viterbi_segment(text):
    probs, lasts = [1.0], [0]
    for i in range(1, len(text) + 1):
        prob_k, k = max((probs[j] * word_prob(text[j:i]), j)
                        for j in range(max(0, i - max_word_length), i))
        probs.append(prob_k)
        lasts.append(k)
    words = []
    i = len(text)
    while 0 < i:
        words.append(text[lasts[i]:i])
        i = lasts[i]
    words.reverse()
    return words, probs[-1]

def word_prob(word): return dictionary[word] / total
def words(text): return re.findall('[a-z]+', text.lower()) 
dictionary = Counter(words(open('big.txt').read()))
max_word_length = max(map(len, dictionary))
total = float(sum(dictionary.values()))

Тестирование:

>>> viterbi_segment('wickedweather')
(['wicked', 'weather'], 5.1518198982768158e-10)
>>> ' '.join(viterbi_segment('itseasyformetosplitlongruntogetherblocks')[0])
'its easy for me to split long run together blocks'

Чтобы быть практичным, вам, вероятно, понадобится пара уточнений:

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

Разве это не алгоритм, используемый для сортировки последовательностей ДНК?

wisty 05.01.2010 08:31

Я не знаю, но общая идея Витерби (нахождение наиболее вероятной последовательности скрытых состояний с учетом последовательности наблюдений) - тоже должна иметь применение с ДНК.

Darius Bacon 06.01.2010 00:49
en.wikipedia.org/wiki/… says they sometimes use hidden Markov models for sequence alignment, and sequence alignment is the basic task in shotgun sequencing: en.wikipedia.org/wiki/Bioinformatics#Sequence_analysis -- so I guess you're right, at least sort of!
Darius Bacon 06.01.2010 00:53

У меня вопрос по этому поводу. Мне нравится это решение, однако, пытаясь воспроизвести его на C#, я столкнулся с проблемой. Поскольку первый цикл for находится в диапазоне от 1 до len + 1, а внутренний цикл находится в диапазоне от max (i-maxWordLen) до i, это означает, что первое слово, которое он просматривает, имеет значение от 0 до 1 (длина 2). Если вставить туда предложение вроде «Я думаю ясно», то «Я» не будет распознано как отдельное слово, а скорее поймает «Оно». Я прав в своей логике? Кроме того, на каком языке это написано?

mj_ 15.06.2012 17:24

@mj_, это в Python, а диапазон (low, high) означает [low, low + 1, ..., high-1] - то есть верхняя граница не включается. Цикл для j в диапазоне (0, 1) смотрит только на j = 0.

Darius Bacon 16.06.2012 11:11

Дарий, почему тогда слово «я» не выбрано в отличие от «его». «Я» имеет большую частоту. Насколько я могу судить, при таком подходе не происходит возврата в случае невозможности сопоставления последующих букв. Я прав?

mj_ 17.06.2012 06:31

Вы правы, что не отступает. Он действительно производит сегментацию с наивысшим общим баллом (произведение вероятностей отдельных слов). Мне непонятно, с какой конкретной проблемой вы столкнулись - в вашем словаре есть слово «задуматься», а вы получаете «понятно»? Результаты с частотами, состоящими из одного слова, можно улучшить за счет значительно большего объема вычислений, перейдя к модели второго порядка, в которой вы рассматриваете условные вероятности: P (слово | предшествующее_слово). См. norvig.com/ngrams (ответ на который я должен обновить ссылкой).

Darius Bacon 18.06.2012 01:01

Для идеального результата вам понадобится еще пара уточнений: 1) вам нужно вернуть очень маленькую вероятность для не-слов вместо нуля, иначе, если какая-то часть фразы никогда не появится в словаре, это испортит результаты для всего фраза. Я использовал (log (1 / total) -max_word_len-1) * (j-i) для логарифма вероятности отсутствия слов (это также штрафует более длинные неслова, чтобы предотвратить поглощение правильных слов). 2) Вам необходимо сохранить кортежи (-non_words_len, -non_words_count, prob) в проблемах, чтобы минимизировать нераспознанные последовательности и объединить соседние блоки, не состоящие из слов.

Ilya Semenov 22.05.2015 14:48

Этот ответ похож на «лучшее» для StackOverflow.

jchook 12.09.2016 03:49

Что такое big.txt?

dawg 05.09.2017 19:28

@dawg - просто любой большой текстовый файл, полный английских слов, из которого можно построить словарь. Скажем, вы можете использовать «Войну и мир» из проекта Гутенберг.

Darius Bacon 12.09.2017 22:08

Привет, @IlyaSemenov, не могли бы вы помочь мне понять формулы сглаживания, которые вы использовали? Большое спасибо

Roxana 04.07.2018 16:57

Список английских слов здесь. raw.githubusercontent.com/dwyl/english-words/master/words.tx‌ t

Aakash Kag 15.07.2019 13:34

Это связано с проблемой, известной как разделение идентификатора или токенизация имени идентификатора. В случае OP входные данные кажутся конкатенациями обычных слов; при разделении идентификаторов входными данными являются имена классов, имена функций или другие идентификаторы из исходного кода, и проблема усложняется. Я понимаю, что это старый вопрос, и OP либо решил свою проблему, либо перешел, но в случае, если кто-то еще столкнется с этим вопросом при поиске разделителей идентификаторов (как я был не так давно), я хотел бы предложить Спираль ( «SPlitters для идентификаторов: библиотека»). Он написан на Python, но поставляется с утилитой командной строки, которая может читать файл идентификаторов (по одному на строку) и разбивать каждый из них.

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

Спираль реализует множество алгоритмов разделения идентификаторов, включая новый алгоритм под названием Ronin. Он использует различные эвристические правила, английские словари и таблицы частот токенов, полученные из репозиториев исходного кода майнинга. Ronin может разделять идентификаторы, в которых не используется верблюжий регистр или другие соглашения об именах, включая такие случаи, как разделение J2SEProjectTypeProfiler на [J2SE, Project, Type, Profiler], что требует, чтобы считыватель распознал J2SE как единое целое. Вот еще несколько примеров того, что может разделить Ронин:

# spiral mStartCData nonnegativedecimaltype getUtf8Octets GPSmodule savefileas nbrOfbugs
mStartCData: ['m', 'Start', 'C', 'Data']
nonnegativedecimaltype: ['nonnegative', 'decimal', 'type']
getUtf8Octets: ['get', 'Utf8', 'Octets']
GPSmodule: ['GPS', 'module']
savefileas: ['save', 'file', 'as']
nbrOfbugs: ['nbr', 'Of', 'bugs']

Используя примеры из вопроса OP:

# spiral wickedweather liquidweather  driveourtrucks gocompact slimprojector
wickedweather: ['wicked', 'weather']
liquidweather: ['liquid', 'weather']
driveourtrucks: ['driveourtrucks']
gocompact: ['go', 'compact']
slimprojector: ['slim', 'projector']

Как видите, он не идеален. Стоит отметить, что Ronin имеет ряд параметров, и их настройка позволяет разделить driveourtrucks, но за счет ухудшения производительности идентификаторов программ.

Более подробную информацию можно найти в Репозиторий на GitHub для Spiral.

Выпущен пакет Python Santhosh thottingal под названием mlmorph, который можно использовать для морфологического анализа.

https://pypi.org/project/mlmorph/

Примеры:

from mlmorph import Analyser
analyser = Analyser()
analyser.analyse("കേരളത്തിന്റെ")

Дает

[('കേരളം<np><genitive>', 179)]

Также он вел блог на тему https://thottingal.in/blog/2017/11/26/towards-a-malayalam-morphology-analyser/

Простое решение с Python: установите пакет отрывок слова: pip install wordsegment.

$ echo thisisatest | python -m wordsegment
this is a test

pip install wordninja

>>> import wordninja
>>> wordninja.split('bettergood')
['better', 'good']

Лучше упомянуть ответ, который породил упомянутый пакет wordninja: stackoverflow.com/a/11642687/8291949

wp78de 15.09.2020 16:43

Это будет работать, если это camelCase. JavaScript !!!

function spinalCase(str) {
  let lowercase = str.trim()
  let regEx = /\W+|(?=[A-Z])|_/g
  let result = lowercase.split(regEx).join("-").toLowerCase()

  return result;
}

spinalCase("AllThe-small Things");

Хорошо, сначала мы создаем функцию, которая принимает значение. let lowercase = str.trim(), по сути, .trim() не проверяет пробелы в или между, строку, которая проверяет и удаляет пробелы из либо начало, либо конец, либо обе стороны, если они есть, let regEx = /\W+|(?=[A-Z])|_/g - это регулярное выражение, где \ W + не соответствует буквам и цифрам, поэтому, оставляя _ и пробелы для соответствия, | означает ИЛИ ЖЕ, (?=[A-Z]) означает положительный просмотр вперед, при котором выполняется поиск всех Capletters, затем result запускает выражение, соединяет его с дефисом и преобразует все в нижний регистр.

Naphtali Duniya 12.04.2020 15:55

Одно из решений может быть с рекурсией (то же самое можно преобразовать в динамическое программирование):

static List<String> wordBreak(
    String input,
    Set<String> dictionary
) {

  List<List<String>> result = new ArrayList<>();
  List<String> r = new ArrayList<>();

  helper(input, dictionary, result, "", 0, new Stack<>());

  for (List<String> strings : result) {
    String s = String.join(" ", strings);
    r.add(s);
  }

  return r;
}

static void helper(
    final String input,
    final Set<String> dictionary,
    final List<List<String>> result,
    String state,
    int index,
    Stack<String> stack
) {

  if (index == input.length()) {

    // add the last word
    stack.push(state);

    for (String s : stack) {
      if (!dictionary.contains(s)) {
        return;
      }
    }

    result.add((List<String>) stack.clone());

    return;
  }

  if (dictionary.contains(state)) {
    // bifurcate
    stack.push(state);
    helper(input, dictionary, result, "" + input.charAt(index),
           index + 1, stack);

    String pop = stack.pop();
    String s = stack.pop();

    helper(input, dictionary, result, s + pop.charAt(0),
           index + 1, stack);

  }
  else {
    helper(input, dictionary, result, state + input.charAt(index),
           index + 1, stack);
  }

  return;
}

Другое возможное решение - использование структуры данных Tries.

Другие вопросы по теме