Разделите слово, используя биграмму, триграмму

У меня есть этот текстовый файл:

worked
working
works
tested
tests
find
found

Он содержит миллион слов без пробелов. Он может содержать символы Юникода.

Самое длинное слово — «работает»:

awk '{print length, $0}' test.txt | sort -nr | head -1
7 working

Мне нужно создать биграмму, триграмму (максимум 7 столбцов)

w,wo,wor,work,worke,worked,
w,wo,wor,work,worki,workin,working
w,wo,wor,work,works,,
t,te,tes,test,teste,tested,
t,te,tes,test,tests,,
f,fi,fin,find,,,,
f,fo,fou,foun,found,,

желательно в awk (потому что это быстро)


2
55
4

Ответы:

Решено

Прямой подход будет следующим:

awk -vn=7 -vOFS=, \
  '{s=$0; for (i=1;i<=n;i++) $i=i<=length(s)? substr(s,1,i): ""}1'
# or
# '{s=$0; for (i=1;i<=n;i++) $i=substr(s, i<=length(s)?1:n, i)}1'
w,wo,wor,work,worke,worked,
w,wo,wor,work,worki,workin,working
w,wo,wor,work,works,,
t,te,tes,test,teste,tested,
t,te,tes,test,tests,,
f,fi,fin,find,,,
f,fo,fou,foun,found,,

Если вы заранее не знаете максимальную длину строки, вы можете использовать:

awk '
 BEGIN {max_length=1}; { a[NR]=$1; m=length($1); if (m > max_length) {max_length=m} }
 END { for(i=1;i<=NR;i++) {for (j=1; j<=max_length;j++) {if (j<=length(a[i])){printf "%s", substr(a[i],1,j)}; if (j<max_length){ printf "%s", "," } }; printf "\n"} }
' file
w,wo,wor,work,worke,worked,
w,wo,wor,work,worki,workin,working
w,wo,wor,work,works,,
t,te,tes,test,teste,tested,
t,te,tes,test,tests,,
f,fi,fin,find,,,
f,fo,fou,foun,found,,

Awk не очень хорошо справляется с последовательностями компоновки Unicode. Возможно, стоит обратиться к Perl за удобной заменой.

Вот краткий тест со случайной строкой на вьетнамском языке, в которой используется множество акцентов.

bash$ printf '%s\n' "người" "đặt" | xxd
00000000: 6e67 c6b0 e1bb 9d69 0ac4 91e1 bab7 740a  ng.....i......t.

Вот демонстрация того, как nawk ведет себя с этим вводом:

bash$ printf '%s\n' "người" "đặt" |
> awk -vn=7 -vOFS=, '{s=$0; for (i=1;i<=n;i++) $i=i<=length(s)? substr(s,1,i): ""}1'
n,ng,ng?,ngư,ngư?,ngư?,ngườ
?,đ,đ?,đ?,đặ,đặt,

Вот быстрая и грязная реализация Perl:

bash$ printf '%s\n' "người" "đặt" |
> perl -CSD -ne 'BEGIN { $n = 7 }
> chomp; $sep = ""; for my $i (1..$n) {
>   print ($_ =~ "\\X{$i}" ? "$sep$&" : "$sep"); $sep = ","; }
> print "\n";'
n,ng,ngư,ngườ,người,,
đ,đặ,đặt,,,,

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

bash$ printf '%s\n' $'re\u0301sume\u0301' |
> perl -CSD -ne 'BEGIN { $n = 7 }
> chomp; $sep = ""; for my $i (1..$n) {
>   print ($_ =~ "\\X{$i}" ? "$sep$&" : "$sep"); $sep = ","; }
> print "\n";'
r,ré,rés,résu,résum,résumé,

Если это не очевидно, Perl прекрасно знает, что U+0301 является комбинирующим символом, который должен быть графически соединен с предыдущим базовым символом, и поэтому рассматривает полученный кластер как один символ (или, точнее, графему), насколько касается регулярного выражения \X. Поскольку у Awk нет этих знаний, он не может этого сделать. (Возможно, см. также Эквивалентность Юникода в Википедии.)

Если вам действительно нужны биграммы и триграммы, а не префиксы определенной длины, это тоже легко.

bash$ printf '%s\n' "người" "đặt" |
> perl -CSD -ne 'chomp; $sep = "";
> for my $i (0..length($_)) {
>     break unless ($_ =~ "\\X{$i}\\K\\X{2}");
>     print "$sep$&";
>     $sep = ",";
>     print ($_ =~ "\\X{$i}\\K\\X{3}" ? "$sep$&" : "");
> } print "\n";'
ng,ngư,gư,gườ,ườ,ười,ời
đặ,đặt,ặt

Использование любого awk (по крайней мере, для ввода символов английского языка), если вы уже знаете максимальную длину входной строки:

$ cat tst.awk
BEGIN { FS=OFS = ","; maxLen=7 }
{
    curLen = length($0)
    out = substr($0,1,1)
    for ( i=2; i<=curLen; i++ ) {
        out = out OFS substr($0,1,i)
    }
    $0 = out
    $maxLen = $maxLen
    print
}

$ awk -f tst.awk file
w,wo,wor,work,worke,worked,
w,wo,wor,work,worki,workin,working
w,wo,wor,work,works,,
t,te,tes,test,teste,tested,
t,te,tes,test,tests,,
f,fi,fin,find,,,
f,fo,fou,foun,found,,

в противном случае с помощью двухпроходного подхода сначала вычислите максимальную длину:

$ cat tst.awk
BEGIN { FS=OFS = "," }
{ curLen = length($0) }
NR == FNR {
    if ( curLen > maxLen ) {
        maxLen = curLen
    }
    next
}
{
    out = substr($0,1,1)
    for ( i=2; i<=curLen; i++ ) {
        out = out OFS substr($0,1,i)
    }
    $0 = out
    $maxLen = $maxLen
    print
}

$ awk -f tst.awk file file
w,wo,wor,work,worke,worked,
w,wo,wor,work,worki,workin,working
w,wo,wor,work,works,,
t,te,tes,test,teste,tested,
t,te,tes,test,tests,,
f,fi,fin,find,,,
f,fo,fou,foun,found,,

Учитывая вышесказанное, я

  1. Вызывайте length() только один раз перед началом цикла, чтобы awk не вызывал его на каждой итерации цикла.
  2. Заполняйте $0 только вне цикла, чтобы избежать повторного разделения и/или повторной конструкции awk $0 на каждой итерации цикла.
  3. Цикл только до длины текущей строки, а не максимальной длины всех строк,
  4. Создавайте пустые поля только до максимального количества полей 1 раз в строке ввода, используя $maxLen = $maxLen после цикла и
  5. Храните в памяти только одну строку за раз.

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

@tripleee отмечает в комментарии, что они получили неправильный вывод на MacOS из приведенного выше сценария при использовании символов Юникода. awk по умолчанию в MacOS, как известно, содержит ошибки, и у меня нет Mac для тестирования, поэтому я не собираюсь это исследовать, но, кстати, вот что я вижу из второго сценария выше, учитывая ввод @tripleee и использование gawk 5.3.0 на Cygwin с LC_ALL='en_US.UTF-8':

$ printf '%s\n' "người" "đặt" > file

$ awk -f tst.awk file file
n,ng,ngư,ngườ,người
đ,đặ,đặt,,

$ awk -b -f tst.awk file file
n,ng,ng▒,ngư,ngư▒,ngư▒,ngườ,người
▒,đ,đ▒,đ▒,đặ,đặt,,

$ printf '%s\n' 'résumé' 'zoölogy' > file

$ awk -f tst.awk file file
r,ré,rés,résu,résum,résumé,
z,zo,zoö,zoöl,zoölo,zoölog,zoölogy

$ awk -b -f tst.awk file file
r,r▒,ré,rés,résu,résum,résum▒,résumé
z,zo,zo▒,zoö,zoöl,zoölo,zoölog,zoölogy

$ printf '%s\n' $'re\u0301sume\u0301' > file

$ awk -f tst.awk file file
r,re,ré,rés,résu,résum,résume,résumé

$ awk -b -f tst.awk file file
r,re,re▒,ré,rés,résu,résum,résume,résume▒,résumé

Вот что делает -b (из руководства):

-б --символы как байты

Заставьте gawk обрабатывать все входные данные как однобайтовые символы. Кроме того, весь вывод, записанный с помощью print или printf, рассматривается как однобайтовые символы.

Обычно gawk следует стандарту POSIX и пытается обработать входные данные в соответствии с текущей локалью (см. «Где вы находитесь»). Имеет значение). Часто это может включать преобразование многобайтовых символы в широкие символы (внутренне), что может привести к проблемам. или путаница, если входные данные не содержат действительных многобайтовых данных персонажи. Этот вариант — простой способ сказать наблюдателю: «Руки прочь от меня». данные!"