У меня есть этот текстовый файл:
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 (потому что это быстро)
Если ваш реальный ввод может содержать символы Юникода, включите символы Юникода в образец ввода/вывода. Вам необходимо предоставить минимальный воспроизводимый пример, на котором мы можем протестировать, чтобы знать, работает ли данное решение или нет, и правильная обработка юникода может быть отличительной чертой.
Прямой подход будет следующим:
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,,
Кстати, вы могли бы немного ускорить это, не вызывая length(s)
на каждой итерации цикла и вместо этого устанавливая/используя переменную, и вы могли бы сделать его более переносимым, добавив пробел после каждого -v
(без него некоторые awks не сработают, это может быть только gawk, с этим все в порядке, я не знаю).
Вы также можете ускорить его еще больше, зацикливая только количество символов в строке, а затем один раз используя $n=$n
для заполнения всех конечных пустых полей, т. е. awk -v n=7 -v OFS=, '{s=$0; len=length(s); for (i=1;i<=len;i++) $i=substr(s,1,i); $n=$n}1'
. В тесте я выполнил 100 000 копий ввода OP, что было примерно на 15% быстрее, чем исходный сценарий, так что это не так уж и важно — разница, вероятно, больше при более различной длине ввода.
Если вы заранее не знаете максимальную длину строки, вы можете использовать:
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,,
Придирка - print ""
лучше, чем printf "\n"
, поскольку он использует значение, которое имеет ORS
, а не жестко запрограммирует значение "\n"
, которое, как вы думаете/надеетесь, будет иметь ORS
, и поэтому оно будет работать, например, даже на платформах, которые используют "\r\n"
для ORS
.
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,,
Учитывая вышесказанное, я
length()
только один раз перед началом цикла, чтобы awk не вызывал его на каждой итерации цикла.$0
только вне цикла, чтобы избежать повторного разделения и/или повторной конструкции awk $0
на каждой итерации цикла.$maxLen = $maxLen
после цикла итак что он будет портативным, быстрым и будет работать с входным файлом любого размера.
@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 и пытается обработать входные данные в соответствии с текущей локалью (см. «Где вы находитесь»). Имеет значение). Часто это может включать преобразование многобайтовых символы в широкие символы (внутренне), что может привести к проблемам. или путаница, если входные данные не содержат действительных многобайтовых данных персонажи. Этот вариант — простой способ сказать наблюдателю: «Руки прочь от меня». данные!"
Второй скрипт ничего не печатает на MacOS при вводе со стандартного ввода. Первый предсказуемо ведет себя плохо с такими данными, как резюме или зоология.
@tripleee вы не можете использовать двухпроходный подход с использованием стандартного ввода, вам нужно будет сохранить ввод в памяти, чтобы вычислить максимальную длину перед обработкой ввода, если вы хотите использовать стандартный ввод. Вторая проблема похожа на проблему с вашей локалью.
Ой, извините, я пропустил там двухпроходную конструкцию. Моя локаль — en_US.UTF-8
, и мой Perl отлично работает с этими строками Unicode.
Если ОП скажет, что у них проблемы с обработкой строк Юникода, я посмотрю, но у меня нет Mac для тестирования. FWIW, используя gawk --posix
на cygwin с той же локалью, что и у вас, я получаю тот же результат, что и ваш Perl-скрипт. Стандартный awk в MacOS, как известно, содержит ошибки, так что, возможно, это всего лишь одна из таких ошибок.
К сожалению, я получаю такой же искаженный результат с LC_ALL=C
С помощью brew install gawk
я получаю правильный вывод для них, если входные данные нормализованы до одной кодовой точки для каждого глифа. С iconv -t utf-8-mac file | gawk '...'
я получаю r,re,ré,rés,résu,résum,résume,résumé
, т. е. он (как и ожидалось) рассматривает сочетающийся акцент как отдельный символ.
В ОП упоминается: «Он может содержать символы Юникода», хотя, возможно, они означают только простые составленные символы Latin-1.
А, так и есть, я это пропустил. Тогда для них имело бы смысл включить некоторые из них в пример!
Абсолютно согласен!
Я надеялся, что пример вьетнамского языка заставит акценты быть разделены, но, увы, все акценты в моем образце имеют простые составные представления.
Я обновил свой ответ, упомянув ваш комментарий и показав gawk, работающий на предоставленном вами образце, спасибо.
Попробуйте также printf '%s\n' $'re\u0301sume\u0301'
@tripleee закончил, и я добавил вывод с опцией -b
и без нее в конец моего ответа. Что-то из этого похоже на то, что вы ожидали?
Оба они демонстрируют проблему, которую я надеюсь подчеркнуть, а именно то, что Awk по своей сути ничего не знает о кластерах графем Юникода.
Предполагая, что Perl это делает, можете ли вы добавить этот случай в свой ответ, чтобы я мог видеть, каким должен быть ожидаемый результат? Не знаю, что касается awk, но я, конечно, ничего не знаю о «кластерах графем Юникода», поэтому объяснение этой проблемы тоже было бы полезно, если вы не возражаете.
Спасибо за подсказку, я добавил к своему ответу простой пример.
А, теперь понял, спасибо.
Похоже, достойное обсуждение графем и т. д. на stackoverflow.com/q/27331819/1745001.
Это не биграммы и триграммы. Вы имеете в виду, что хотите, чтобы все префиксы имели фиксированную длину, как предполагает ваш вывод; или это пример того, чего вы не хотите?