um comando shell para encontrar cada n-grama que funciona no texto

um comando shell para encontrar cada n-grama que funciona no texto

Eu tenho um fluxo de texto ou arquivo que contém palavras separadas por espaços em branco. Como:

I have a toy. you may not like it.

Cada palavra separada por espaços em branco pode ser composta por duas ou mais palavras pequenas, talvez organizadas por caixa de camelo (separadas por caixa diferente), caixa de cobra (separada por sublinhado) ou separada por ponto, como:

I_amAManTest you_haveAHouse FOO_BAR_test.model

por exemplo:

I_amAManTest

pode ser dividido em:

I
am
A
Man
Test

mas quero imprimir todosnpalavras (cada subconjunto de pequenas palavras contíguas) na palavra composta, como:

I_amAManTest

saída:

// from first word on
I
I_am
I_amA
I_amAMan
I_amAManTest
// from second word on 
am
amA
amAMan
amAManTest
// from third word on 
A
AMan
AManTest
// from fourth word on
Man
ManTest
// from fifth word on
Test

Então, em conclusão, para entradas como

I_amAManTest you_haveAHouse FOO_BAR_test

a saída deve ser

I
I_am
I_amA
I_amAMan
I_amAManTest
am
amA
amAMan
amAManTest
A
AMan
AManTest
Man
ManTest
Test
you
you_have
you_haveA
you_haveAHouse
have
haveA
haveAHouse
A
AHouse
House
FOO
FOO_BAR
FOO_BAR_test
BAR
BAR_test
test

Responder1

Uma solução (principalmente) sed:

cat "$@" |
    tr -cs -- '._[:alpha:]' '[\n*]' |
    sed -n  -e 'h; :ms' \
            -e 'p; :ss' \
                -e 's/\([[:lower:]]\)[[:upper:]][[:lower:]]*$/\1/p; t ss' \
                -e 's/\([[:lower:]]\)[[:upper:]][[:upper:]]*$/\1/p; t ss' \
                -e 's/\([[:upper:]]\)[[:upper:]][[:lower:]]\+$/\1/p; t ss' \
                -e 's/[._][[:alpha:]][[:lower:]]*$//p; t ss' \
                -e 's/[._][[:upper:]]\+$//p; t ss' \
            -e 'g' \
            -e 's/^[[:upper:]]\?[[:lower:]]\+\([[:upper:]]\)/\1/; t mw' \
            -e 's/^[[:upper:]]\+\([[:upper:]][[:lower:]]\)/\1/; t mw' \
            -e 's/^[[:alpha:]][[:lower:]]*[._]//; t mw' \
            -e 's/^[[:upper:]]\+[._]//; t mw' \
            -e 'b' \
            -e ':mw; h; b ms'

O algoritmo é

for each compound word (e.g., “FOO_BAR_test”) in the input
do
    repeat
        print what you’ve got
        repeat
            remove a small word from the end (e.g., “FOO_BAR_test” → “FOO_BAR”) and print what’s left
        until you’re down to the last one (e.g., “FOO_BAR_test” → “FOO”)
        go back to what you had at the beginning of the above loop
          and remove a small word from the beginning
          (e.g., “FOO_BAR_test” → “BAR_test”) ... but don’t print anything
    until you’re down to the last one (e.g., “FOO_BAR_test” → “test”)
end for loop

Detalhes:

  • cat "$@"é um UUOC. Geralmente evito isso; você pode fazer , mas não pode passar vários arquivos  diretamente.tr args <filetr
  • tr -cs -- '._[:alpha:]' '[\n*]'quebra uma linha de muitas palavras compostas em linhas separadas; por exemplo,
    I_amAManTest you_haveAHouse FOO_BAR_test
    
    torna-se
    I_amAManTest
    you_haveAHouse
    FOO_BAR_test
    
    então sed pode processar uma palavra composta por vez.
  • sed -n— não imprima nada automaticamente; imprimir apenas quando solicitado.
  • -eespecifica que o seguinteexpression faz parte do script sed.
  • h— copie o espaço padrão para o espaço de espera.
  • :ms— um rótulo (Main loop Start)
  • p- imprimir
  • :ss— um rótulo (início do loop secundário)
  • Os comandos a seguir removem uma palavra pequena do final de uma palavra composta e, se forem bem-sucedidos, imprimem o resultado e voltam ao início do loop Secundário.
    • s/\([[:lower:]]\)[[:upper:]][[:lower:]]*$/\1/p; t ss— altera “nTest” para “n”.
    • s/\([[:lower:]]\)[[:upper:]][[:upper:]]*$/\1/p; t ss— altera “mOK” para “m”.
    • s/\([[:upper:]]\)[[:upper:]][[:lower:]]\+$/\1/p; t ss— muda “AMan” para “A”.
    • s/[._][[:alpha:]][[:lower:]]*$//p; t ss— exclui “_am” (substitui por nada).
    • s/[._][[:upper:]]\+$//p; t ss— exclui “_BAR” (substitui por nada).
  • Este é o fim do loop secundário.
  • g— copie o espaço de espera para o espaço do padrão (volte ao que você tinha no início do loop acima).
  • Os comandos a seguir removem uma palavra pequena do início de uma palavra composta e, se bem-sucedidos, saltam para o final do loop principal (mw = encerramento do loop principal).
  • s/^[[:upper:]]\?[[:lower:]]\+\([[:upper:]]\)/\1/; t mw— muda “amA” para “A” e “ManT” para “T”.
  • s/^[[:upper:]]\+\([[:upper:]][[:lower:]]\)/\1/; t mw— muda “AMa” para “Ma”.
  • s/^[[:alpha:]][[:lower:]]*[._]//; t mw— exclui “I_” e “you_” (substitui-os por nada).
  • s/^[[:upper:]]\+[._]//; t mw— exclui “FOO_” (substitui por nada).
  • Cada um dos comandos substitutos acima salta para o encerramento do loop principal (abaixo) se for bem-sucedido (se encontrar/corresponder a algo). Se chegarmos aqui, o espaço padrão contém apenas uma pequena palavra, então terminamos.
  • b— ramifica (salta) para o final do script sed; ou seja, saia do script sed.
  • :mw— rótulo para encerramento do loop principal.
  • h- copie o espaço padrão para o espaço de espera, para nos prepararmos para a próxima iteração do loop principal.
  • b ms— salte para o início do loop principal.

Ele produz a saída solicitada. Infelizmente, isso coloca tudo em uma ordem diferente. Provavelmente posso consertar isso se for importante.

$ echo "I_amAManTest you_haveAHouse FOO_BAR_test" | ./myscript
I_amAManTest
I_amAMan
I_amA
I_am
I
amAManTest
amAMan
amA
am
AManTest
AMan
A
ManTest
Man
Test
you_haveAHouse
you_haveA
you_have
you
haveAHouse
haveA
have
AHouse
A
House
FOO_BAR_test
FOO_BAR
FOO
BAR_test
BAR
Test

Responder2

Sua melhor aposta provavelmente será encontrar um módulo tokenizador para Perl. O Grep não pode fazer isso sem várias execuções, provavelmente precisando de -P(PCRE).

Aqui está uma solução parcial sem nenhum módulo Perl:

while (<>) {
  my $n = 1;
  while (/(\S+)/g) {
    printf "// outputting whitespace-separated word %d\n", $n++;
    my $whole = $1;
    while ($whole =~ /([a-zA-Z0-9][a-z]*+)/g) {
      print "$1\n";
    }
    print "$whole\n";    # whole space-delimited tokens
  }
}

Lê entradas de entradas ou arquivos padrão, uma linha por vez. $né um contador de palavras para o comentário impresso, então iteramos através das palavras (conforme delineado por espaço em branco, portanto, a regex /(\S+)/gcorresponde globalmente a caracteres consecutivos que não sejam de espaço em branco). Dentro de cada palavra, iteramos nas partes do token usando([a-zA-Z0-9][a-z]*+), cujas correspondências começam com números ou letras e são seguidas por zero ou mais letras minúsculas ( *+é como *se o retrocesso fosse desativado para proteção contra umReDoS). Depois de imprimir todos os tokens correspondentes na palavra, imprimimos a palavra inteira.

Você executa isso como perl solution.pl intput.txtou então inline como:

$ echo "I_amAManTest you_haveAHouse FOO_BAR_test.model" |perl solution.pl
// outputting whitespace-separated word 1
I
am
A
Man
Test
I_amAManTest
// outputting whitespace-separated word 2
you
have
A
House
you_haveAHouse
// outputting whitespace-separated word 3
F
O
O
B
A
R
test
model
FOO_BAR_test.model

Observe que faltam os subtokens de palavras com várias partes.

Observe também que sua solicitação para I_AmAMananalisar como I, Am, A, Manentra em conflito com sua solicitação para FOO_BARanalisar FOO, BARem vez de F, O, O, B... como faz o código acima. (Talvez um exemplo melhor seria: o que deveria I_AmOKser? Três unigramas ou quatro?)

Responder3

Aqui está um começo, você só terá que massageá-lo depois de descobrir seus requisitos para strings que contêm misturas de letras maiúsculas e minúsculas e imprimir a saída na ordem que você está mostrando em sua pergunta:

$ cat tst.awk
{
    for (wordNr=1; wordNr<=NF; wordNr++) {
        delete ngrams
        word = $wordNr
        ngrams[word]
        print "word", word
        numUndSeps = split(word,undSeps,/_/)
        for (undSepNr=1; undSepNr<=numUndSeps; undSepNr++) {
            undSep = undSeps[undSepNr]
            ngrams[undSep]
            print "undSep", undSep
            numDotSeps = split(undSep,dotSeps,/[.]/)
            for (dotSepNr=1; dotSepNr<=numDotSeps; dotSepNr++) {
                dotSep = dotSeps[dotSepNr]
                ngrams[dotSep]
                print "dotSep", dotSep
                while ( match(dotSep,/[[:upper:]]+[^[:upper:]]+/) ) {
                    camel = substr(dotSep,RSTART,RLENGTH)
                    dotSep = substr(dotSep,RSTART+RLENGTH)
                    ngrams[camel]
                    print "camel", camel
                }
            }
        }
        print "-----------"
        for (ngram in ngrams) {
            print ngram
        }
        print "###########"
    }
}

.

$ awk -f tst.awk file
word I_amAManTest
undSep I
dotSep I
undSep amAManTest
dotSep amAManTest
camel AMan
camel Test
-----------
Test
amAManTest
I_amAManTest
I
AMan
###########
word you_haveAHouse
undSep you
dotSep you
undSep haveAHouse
dotSep haveAHouse
camel AHouse
-----------
you
you_haveAHouse
haveAHouse
AHouse
###########
word FOO_BAR_test.model
undSep FOO
dotSep FOO
undSep BAR
dotSep BAR
undSep test.model
dotSep test
dotSep model
-----------
model
FOO
FOO_BAR_test.model
test.model
BAR
test
###########

informação relacionada