¿Cómo puedo dividir varias palabras juntas?

Tengo un conjunto de 1000 o más entradas, con ejemplos a continuación:

wickedweather liquidweather driveourtrucks gocompact slimprojector 

Me gustaría poder dividir estos en sus respectivas palabras, como:

 wicked weather liquid weather drive our trucks go compact slim projector 

Esperaba que una expresión regular hiciera el truco. Pero, dado que no hay límite para detenerse, ni hay ningún tipo de capitalización que pueda activar, estoy pensando que podría ser necesario algún tipo de referencia a un diccionario.

Supongo que podría hacerse a mano, pero ¿por qué? ¡Cuando se puede hacer con código! =) Pero esto me ha dejado perplejo. ¿Algunas ideas?

¿Puede un humano hacerlo?

 farsidebag
 lejos sidebag
 bolsa farside
 bolsa de lado lejano

No solo tiene que usar un diccionario, puede que tenga que usar un enfoque estadístico para descubrir qué es lo más probable (o, Dios no lo quiera, un HMM real para su lenguaje humano preferido …)

Para saber cómo hacer estadísticas que podrían ser útiles, lo dirijo al Dr. Peter Norvig, quien aborda un problema diferente, pero relacionado, de la corrección ortográfica en 21 líneas de código : http://norvig.com/spell-correct.html

(Él hace trampa un poco doblando cada bucle en una sola línea … pero aún así).

Actualización Esto se quedó en mi cabeza, así que tuve que nacer hoy. Este código realiza una división similar a la descrita por Robert Gamble, pero luego ordena los resultados en función de la frecuencia de las palabras en el archivo de diccionario proporcionado (que ahora se espera que sea un texto representativo de su dominio o inglés en general. .txt de Norvig, vinculado anteriormente, y le echó un diccionario, para cubrir palabras faltantes).

Una combinación de dos palabras la mayoría de las veces superará una combinación de 3 palabras, a menos que la diferencia de frecuencia sea enorme.


Publiqué este código con algunos cambios menores en mi blog

http://squarecog.wordpress.com/2008/10/19/splitter-words-joined-into-a-single-string/ y también escribió un poco sobre la falla de subdesbordamiento en este código … Estuve tentado de solo en silencio arréglelo, pero cree que esto puede ayudar a algunas personas que no han visto el truco de registro antes: http://squarecog.wordpress.com/2009/01/10/dealing-with-underflow-in-joint-probability-calculations/


Dé salida a sus palabras, más algunas propias: observe lo que sucede con “orcore”:

 palabras de perl splitwords.pl big.txt
 answerveal: 2 posibilidades
  - respuesta ternera
  - respuesta ve al

 wickedweather: 4 posibilidades
  - mal tiempo
  - malvados nosotros en ella
  - tiempo wick ed
  - nos wick en ella

 liquidweather: 6 posibilidades
  - clima líquido
  - Líquido nosotros en ella
  - li quid weather
  - li quid nosotros en ella
  - li qu id weather
  - li qu id nosotros en ella

 DriveUrtrucks: 1 posibilidades
  - conduce nuestros camiones

 gocompact: 1 posibilidades
  - ir compacto

 slimprojector: 2 posibilidades
  - proyector delgado
  - proyecto delgado o

 orcore: 3 posibilidades
  - o núcleo
  - o co re
  - mineral de orco

Código:

 #!/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"; } 

El algoritmo de Viterbi es mucho más rápido. Calcula los mismos puntajes que la búsqueda recursiva en la respuesta de Dmitry anterior, pero en O (n) tiempo. (La búsqueda de Dmitry toma tiempo exponencial, Viterbi lo hace mediante progtwigción dinámica).

 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('[az]+', text.lower()) dictionary = Counter(words(open('big.txt').read())) max_word_length = max(map(len, dictionary)) total = float(sum(dictionary.values())) 

Probándolo:

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

Para ser práctico, es probable que desee un par de mejoras:

  • Agregue registros de probabilidades, no multiplique las probabilidades. Esto evita el underflow del punto flotante.
  • Sus entradas en general usarán palabras que no están en su corpus. A estas subcadenas se les debe asignar una probabilidad distinta de cero como palabras, o terminas sin solución o una mala solución. (Esto es igualmente cierto para el algoritmo de búsqueda exponencial anterior). Esta probabilidad tiene que desviarse de las probabilidades de las palabras del corpus y distribuirse de manera plausible entre todas las demás palabras candidatas: el tema general se conoce como suavizado en modelos de lenguaje estadístico. (Sin embargo, puedes salirte con algunos trucos bastante toscos). Aquí es donde el algoritmo de O (n) Viterbi elimina el algoritmo de búsqueda, porque considerar las palabras que no son del corpus hace explotar el factor de ramificación.

La mejor herramienta para el trabajo aquí es la recursión, no las expresiones regulares. La idea básica es comenzar desde el principio de la cuerda buscando una palabra, luego tomar el rest de la cuerda y buscar otra palabra, y así sucesivamente hasta que se llegue al final de la cuerda. Una solución recursiva es natural, ya que el retroceso debe suceder cuando un rest determinado de la cadena no se puede dividir en un conjunto de palabras. La siguiente solución usa un diccionario para determinar qué es una palabra e imprime las soluciones a medida que las encuentra (algunas cadenas se pueden dividir en múltiples conjuntos de palabras posibles, por ejemplo, wickedweather podría analizarse como “malvados en ella”). Si solo desea un conjunto de palabras, necesitará determinar las reglas para seleccionar el mejor conjunto, tal vez seleccionando la solución con el menor número de palabras o estableciendo una longitud de palabra mínima.

 #!/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 () { 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; } 

Creo que tienes razón al pensar que no es realmente un trabajo para una expresión regular. Me acercaría a esto usando la idea del diccionario: busque el prefijo más largo que es una palabra en el diccionario. Cuando lo encuentres, córtalo y haz lo mismo con el rest de la cadena.

El método anterior está sujeto a ambigüedad, por ejemplo, “drivereallyfast” encontraría primero “driver” y luego tendría problemas con “eallyfast”. Así que también tendrías que hacer un poco de retroceso si te topas con esta situación. O bien, dado que no tiene tantas cadenas para dividir, simplemente haga a mano las que fallan en la división automática.

Bueno, el problema en sí mismo no se puede resolver con solo una expresión regular. Una solución (probablemente no la mejor) sería obtener un diccionario y hacer una coincidencia de expresión regular para cada trabajo en el diccionario con cada palabra en la lista, agregando el espacio cada vez que sea exitoso. Ciertamente, esto no sería terriblemente rápido, pero sería fácil de progtwigr y más rápido que hacerlo manualmente.

Se requeriría una solución basada en diccionario. Esto podría simplificarse un poco si tiene un diccionario limitado de palabras que puede ocurrir, de lo contrario las palabras que forman el prefijo de otras palabras van a ser un problema.

Esto está relacionado con un problema conocido como división de identificador o tokenización de nombre de identificador . En el caso del OP, las entradas parecen ser concatenaciones de palabras ordinarias; en la división de identificadores, las entradas son nombres de clase, nombres de funciones u otros identificadores del código fuente, y el problema es más difícil. Me doy cuenta de que esta es una vieja pregunta y el OP ha resuelto su problema o seguido, pero en caso de que alguien más se encuentre con esta pregunta mientras busca divisores de identificador (como hace poco), me gustaría ofrecerle Spiral ( ” SPLITTERS PARA IDENTIFICADORES: UNA BIBLIOTECA “). Está escrito en Python pero viene con una utilidad de línea de comandos que puede leer un archivo de identificadores (uno por línea) y dividir cada uno.

Dividir identificadores es engañosamente difícil. Los progtwigdores comúnmente usan abreviaturas, acrónimos y fragmentos de palabras al nombrar cosas, y no siempre usan convenciones consistentes. Incluso cuando los identificadores siguen alguna convención como el caso camel, pueden surgir ambigüedades.

Spiral implementa numerosos algoritmos de división de identificador, incluido un nuevo algoritmo llamado Ronin. Utiliza una variedad de reglas heurísticas, diccionarios de inglés y tablas de frecuencias de tokens obtenidas de los repositorys de código fuente de minería de datos. Ronin puede dividir identificadores que no usan camel case u otras convenciones de nomenclatura, incluidos casos como la división de J2SEProjectTypeProfiler en [ J2SE , Project , Type , Profiler ], que requiere que el lector reconozca J2SE como una unidad. Aquí hay algunos ejemplos más de lo que Ronin puede dividir:

 # 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'] 

Usando los ejemplos de la pregunta del OP:

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

Como puede ver, no es perfecto. Vale la pena señalar que Ronin tiene una serie de parámetros y ajustarlos hace posible dividir driveourtrucks también, pero a costa de empeorar el rendimiento en los identificadores de progtwig.

Se puede encontrar más información en el repository de GitHub para Spiral .

Me pueden descifrar por esto, pero que la secretaria lo haga .

Pasará más tiempo en una solución de diccionario de lo que le llevaría procesar manualmente. Además, posiblemente no tendrá una confianza del 100% en la solución, por lo que igual tendrá que darle atención manual.