Какой самый быстрый способ подсчитать количество каждого символа в файле?


121

Я хочу посчитать буквы A, T, C, G и N в файле или каждую букву, если необходимо, есть ли быстрая команда Unix, чтобы сделать это?


56
Подсчет основ в нитях ДНК?
Индрек

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

10
Хех, это пограничный код-гольф
Earlz 10.10.12

13
если Сомоне интересует версия Windows PowerShell:[System.IO.File]::ReadAllText("C:\yourfile.txt").ToCharArray() | Group-Object $_ | Sort Count -Descending
Guillaume86

4
Хорошо, я думаю, что нашел чистый путь PS:Get-Content "C:\eula.3082.txt" | % { $_.ToCharArray() } | Group-Object | Sort Count -Descending
Guillaume86

Ответы:


136

Если вам нужна реальная скорость:

echo 'int cache[256],x,y;char buf[4096],letters[]="tacgn-"; int main(){while((x=read(0,buf,sizeof buf))>0)for(y=0;y<x;y++)cache[(unsigned char)buf[y]]++;for(x=0;x<sizeof letters-1;x++)printf("%c: %d\n",letters[x],cache[letters[x]]);}' | gcc -w -xc -; ./a.out < file; rm a.out;

Это невероятно быстрый псевдо-один лайнер.

Простой тест показывает, что на моем процессоре Core i7 870 @ 2,93 ГГц он составляет чуть более 600 МБ / с:

$ du -h bigdna 
1.1G    bigdna

time ./a.out < bigdna 
t: 178977308
a: 178958411
c: 178958823
g: 178947772
n: 178959673
-: 178939837

real    0m1.718s
user    0m1.539s
sys     0m0.171s

В отличие от решений, связанных с сортировкой, этот работает в постоянной (4 КБ) памяти, что очень полезно, если ваш файл намного больше, чем ОЗУ.

И, конечно же, немного смазав локоть, мы можем сбрить 0,7 секунды:

echo 'int cache[256],x,buf[4096],*bp,*ep;char letters[]="tacgn-"; int main(){while((ep=buf+(read(0,buf,sizeof buf)/sizeof(int)))>buf)for(bp=buf;bp<ep;bp++){cache[(*bp)&0xff]++;cache[(*bp>>8)&0xff]++;cache[(*bp>>16)&0xff]++;cache[(*bp>>24)&0xff]++;}for(x=0;x<sizeof letters-1;x++)printf("%c: %d\n",letters[x],cache[letters[x]]);}' | gcc -O2 -xc -; ./a.out < file; rm a.out;

Сети чуть более 1,1 ГБ / с, заканчивающиеся в:

real    0m0.943s
user    0m0.798s
sys     0m0.134s

Для сравнения я проверил некоторые другие решения на этой странице, которые, казалось, имели какое-то обещание скорости.

Решение sed/ awkсделал доблестное усилие, но умер через 30 секунд. С таким простым регулярным выражением, я ожидаю, что это будет ошибка в sed (GNU sed версия 4.2.1):

$ time sed 's/./&\n/g' bigdna | awk '!/^$/{a[$0]++}END{for (i in a)print i,a[i];}' 
sed: couldn't re-allocate memory

real    0m31.326s
user    0m21.696s
sys     0m2.111s

Метод perl тоже казался многообещающим, но я сдался после запуска в течение 7 минут

time perl -e 'while (<>) {$c{$&}++ while /./g} print "$c{$_} $_\n" for keys %c' < bigdna 
^C

real    7m44.161s
user    4m53.941s
sys     2m35.593s

1
+1 Для разумного решения, когда много данных, а не просто несколько байтов. Файлы находятся в кеше диска, не так ли?
Даниэль Бек

2
Важно то, что он имеет сложность O (N) в обработке и O (1) в памяти. Каналы обычно имеют O (N log N) при обработке (или даже O (N ^ 2)) и O (N) в памяти.
Мартин Уединг

73
Вы немного растягиваете определение «командной строки».
Gerrit

11
Эпический изгиб требований вопроса - я одобряю; с. superuser.com/a/486037/10165 <- кто - то побежали тесты, и это является самым быстрым вариантом.
Подмастерье Компьютерщик

2
+1 Я высоко ценю использование C в нужных местах.
Джефф Ферланд

119

grep -o foo.text -e A -e T -e C -e G -e N -e -|sort|uniq -c

Сделаем трюк как один лайнер. Небольшое объяснение необходимо, хотя.

grep -o foo.text -e A -e T -e C -e G -e N -e -greps файл foo.text для букв a и g и символа -для каждого символа, который вы хотите найти. Он также печатает один символ в строке.

sortсортирует это по порядку. Это создает основу для следующего инструмента

uniq -cсчитает повторяющиеся повторения любой строки. В этом случае, поскольку у нас есть отсортированный список символов, мы получаем точное количество символов, которые мы выделили на первом шаге.

Если бы foo.txt содержал строку, GATTACA-это то, что я получил бы из этого набора команд.

[geek@atremis ~]$ grep -o foo.text -e A -e T -e C -e G -e N -e -|sort|uniq -c
      1 -
      3 A
      1 C
      1 G
      2 T

8
Кровавая юникс магия! : D
Питто

27
если в ваших файлах есть только символы CTAG, само регулярное выражение становится бессмысленным, верно? grep -o. | сортировать | uniq -c будет работать одинаково хорошо, афаик.
sylvainulg

7
+1 Я использую grep 25 лет и не знаю, о чем -o.
LarsH

9
@JourneymanGeek: проблема в том, что он генерирует много данных, которые затем направляются для сортировки. Было бы дешевле позволить программе разбирать каждый символ. См. Ответ Дэйва для ответа O (1) вместо O (N) сложности памяти.
Мартин Уединг

2
@Pitto Собственные сборки Windows для coreutils широко доступны - просто спросите Google или еще что-нибудь
OrangeDog

46

Попробуйте, вдохновленный ответом @ Journeyman.

grep -o -E 'A|T|C|G|N|-' foo.txt | sort | uniq -c

Ключ знает о опции -o для grep . Это разделяет совпадение, так что каждая выходная строка соответствует одному экземпляру шаблона, а не всей строке для любой совпадающей строки. Учитывая эти знания, нам нужен только шаблон и способ подсчета строк. Используя регулярное выражение, мы можем создать дизъюнктивный шаблон, который будет соответствовать любому из указанных вами символов:

A|T|C|G|N|-

Это означает «соответствует A или T или C или G или N или -». Руководство описывает различные синтаксисы регулярных выражений, которые вы можете использовать .

Теперь у нас есть вывод, который выглядит примерно так:

$ grep -o -E 'A|T|C|G|N|-' foo.txt 
A
T
C
G
N
-
-
A
A
N
N
N

Нашим последним шагом является объединение и подсчет всех похожих строк, что можно просто выполнить с помощью a sort | uniq -c, как в ответе @ Journeyman. Сортировка дает нам вывод, как это:

$ grep -o -E 'A|T|C|G|N|-' foo.txt | sort
-
-
A
A
A
C
G
N
N
N
N
T

Который, когда по каналу uniq -c, в конце концов напоминает то, что мы хотим:

$ grep -o -E 'A|T|C|G|N|-' foo.txt | sort | uniq -c
      2 -
      3 A
      1 C
      1 G
      4 N
      1 T

Приложение: Если вы хотите подсчитать количество символов A, C, G, N, T и - в файле, вы можете wc -lвместо этого передать по конвейеру вывод grep sort | uniq -c. Есть много разных вещей, которые вы можете посчитать с небольшими изменениями в этом подходе.


Мне действительно нужно углубиться в кроличьи норы, такие как coreutils и regex. Это несколько более элегантно, чем у меня для него; р
подмастерье Компьютерщик

2
@JourneymanGeek: изучение регулярных выражений стоит того, так как это полезно для многих вещей. Просто поймите, что это ограничения, и не злоупотребляйте властью, пытаясь сделать что-то, выходящее за рамки возможностей регулярных выражений, например, пытаясь разобрать XHTML .
crazy2be

20
grep -o '[ATCGN-]' может быть немного более читабельным здесь.
sylvainulg

14

Один лайнер, подсчитывающий все буквы с использованием Python:

$ python -c "import collections, pprint; pprint.pprint(dict(collections.Counter(open('FILENAME_HERE', 'r').read())))"

... создавая дружественный YAML вывод, как это:

{'\n': 202,
 ' ': 2153,
 '!': 4,
 '"': 62,
 '#': 12,
 '%': 9,
 "'": 10,
 '(': 84,
 ')': 84,
 '*': 1,
 ',': 39,
 '-': 5,
 '.': 121,
 '/': 12,
 '0': 5,
 '1': 7,
 '2': 1,
 '3': 1,
 ':': 65,
 ';': 3,
 '<': 1,
 '=': 41,
 '>': 12,
 '@': 6,
 'A': 3,
 'B': 2,
 'C': 1,
 'D': 3,
 'E': 25}

Интересно посмотреть, как в большинстве случаев Python может легко превзойти даже bash с точки зрения ясности кода.


11

Похож на awkметод Гуру :

perl -e 'while (<>) {$c{$&}++ while /./g} print "$c{$_} $_\n" for keys %c'

10

После использования UNIX в течение нескольких лет вы очень хорошо разбираетесь в нескольких небольших операциях для выполнения различных задач фильтрации и подсчета. У каждого свой стиль - некоторым нравится, awkа другим sedнравится cutи tr. Вот как я бы это сделал:

Чтобы обработать определенное имя файла:

 od -a FILENAME_HERE | cut -b 9- | tr " " \\n | egrep -v "^$" | sort | uniq -c

или в качестве фильтра:

 od -a | cut -b 9- | tr " " \\n | egrep -v "^$" | sort | uniq -c

Это работает так:

  1. od -a разделяет файл на символы ASCII.
  2. cut -b 9-исключает префикс odставит.
  3. tr " " \\n преобразует пробелы между символами в новые строки, так что в каждой строке по одному символу.
  4. egrep -v "^$" избавляется от всех лишних пустых строк, которые это создает.
  5. sort собирает экземпляры каждого персонажа вместе.
  6. uniq -c считает количество повторов каждой строки.

Я кормил его "Привет, мир!" с последующим переводом строки и получил это:

  1 ,
  1 !
  1 d
  1 e
  1 H
  3 l
  1 nl
  2 o
  1 r
  1 sp
  1 w

9

sedЧасть базируясь на ответ @ Гуру , вот еще один подход с использованием uniq, аналогично решению Дэвида Шварца.

$ cat foo
aix
linux
bsd
foo
$ sed 's/\(.\)/\1\n/g' foo | sort | uniq -c
4 
1 a
1 b
1 d
1 f
2 i
1 l
1 n
2 o
1 s
1 u
2 x

1
Используйте, [[:alpha:]]а не .в, sedчтобы соответствовать только символы, а не переводы строк.
Клавдий

1
[[:alpha:]]потерпит неудачу, если вы также попытаетесь сопоставить такие вещи, как -, что было упомянуто в вопросе
Izkata

Верный. Это может быть лучше , чтобы добавить второе выражение СЭД на первый отфильтровывать все остальное , а затем явно совпадают с требуемыми символами: sed -e 's/[^ATCGN-]//g' -e 's/\([ATCGN-]\)/\1\n/g' foo | sort | uniq -c. Тем не менее, я не знаю, как избавиться от новой строки там: \
Клавдий

7

Вы можете комбинировать grepи wcделать это:

grep -o 'character' file.txt | wc -w

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

wcпечатает количество байтов, слов и строк для каждого файла или, в этом случае, вывод grepкоманды. -wОпция говорит его подсчет количества слов, причем каждое слово является вхождение поискового характера. Конечно, -lопция (которая считает количество строк) также будет работать, так как grepкаждое вхождение вашего символа поиска выводится на отдельной строке.

Чтобы сделать это для нескольких символов одновременно, поместите символы в массив и зациклите его:

chars=(A T C G N -)
for c in "${chars[@]}"; do echo -n $c ' ' && grep -o $c file.txt | wc -w; done

Пример: для файла, содержащего строку TGC-GTCCNATGCGNNTCACANN-, вывод будет:

A  3
T  4
C  6
G  4
N  5
-  2

Для получения дополнительной информации см man grepи man wc.


Недостатком этого подхода, как замечает пользователь Journeyman Geek ниже в комментарии, является то, что grepего нужно запускать один раз для каждого персонажа. В зависимости от размера ваших файлов это может привести к заметному снижению производительности. С другой стороны, когда это делается таким образом, становится немного проще быстро увидеть, какие символы ищут, и добавить / удалить их, так как они находятся на отдельной строке от остальной части кода.


3
им нужно будет повторять это в зависимости от того, что они хотят ... Я бы добавил. Я мог бы поклясться, что есть более элегантное решение, но оно требует больше сований; p
Journeyman Geek

@JourneymanGeek Хороший вопрос. Один подход, который приходит на ум, - это помещать символы в массив и проходить по нему. Я обновил свой пост.
Индрек

Слишком сложное ИМО. Просто используйте grep -ea -et и так далее. Если вы поместите его в массив и пройдете по нему, вам не придется проходить цикл grep один раз для каждого символа?
Подмастерье Компьютерщик

@JourneymanGeek Вы, вероятно, правы. uniq -cтакже кажется лучшим способом получить красиво отформатированный вывод. Я не гуру * nix, вышеизложенное - это то, что мне удалось собрать из моих ограниченных знаний и нескольких страниц руководства :)
Indrek

Как и я; p, и одно из моих последних заданий заключалось в сортировке около 5000 записей в адресной книге, и uniq сделал это намного проще.
Подмастерье Компьютерщик

7

Используя строки последовательности из 22hgp10a.txt, разница во времени между grep и awk в моей системе делает использование awk способом ...

[Редактировать]: После просмотра скомпилированного решения Дейва, забудьте также awk, так как его выполнение заняло ~ 0,1 секунды в этом файле для полного учета регистра с учетом регистра.

# A nice large sample file.
wget http://gutenberg.readingroo.ms/etext02/22hgp10a.txt

# Omit the regular text up to the start `>chr22` indicator.
sed -ie '1,/^>chr22/d' 22hgp10a.txt

sudo test # Just get sudo setup to not ask for password...

# ghostdog74 answered a question <linked below> about character frequency which
# gave me all case sensitive [ACGNTacgnt] counts in ~10 seconds.
sudo chrt -f 99 /usr/bin/time -f "%E elapsed, %c context switches" \
awk -vFS="" '{for(i=1;i<=NF;i++)w[$i]++}END{for(i in w) print i,w[i]}' 22hgp10a.txt

# The grep version given by Journeyman Geek took a whopping 3:41.47 minutes
# and yielded the case sensitive [ACGNT] counts.
sudo chrt -f 99 /usr/bin/time -f "%E elapsed, %c context switches" \
grep -o foo.text -e A -e T -e C -e G -e N -e -|sort|uniq -c

Версия ghostdog без учета регистра завершается за ~ 14 секунд.

Сед объяснен в принятом ответе на этот вопрос .
Бенчмаркинг как в принятом ответе на этот вопрос .
Принятый ответ от ghostdog74 был на этот вопрос .


1
Вы можете s/cache[letters[x]]/cache[letters[x]]+cache[toupper(letters[x])]использовать его, чтобы сделать его нечувствительным к регистру, не влияя на его скорость.
Дейв

6

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

time cat /dev/random | tr -d -C 'AGCTN\-' | head -c16M >dna.txt
real    0m5.797s
user    0m6.816s
sys     0m1.371s

$ time tr -d -C 'AGCTN\-' <dna.txt | tee >(wc -c >tmp0.txt) | tr -d 'A' | 
tee >(wc -c >tmp1.txt) | tr -d 'G' | tee >(wc -c >tmp2.txt) | tr -d 'C' | 
tee >(wc -c >tmp3.txt) | tr -d 'T' | tee >(wc -c >tmp4.txt) | tr -d 'N' | 
tee >(wc -c >tmp5.txt) | tr -d '\-' | wc -c >tmp6.txt && cat tmp[0-6].txt

real    0m0.742s
user    0m0.883s
sys     0m0.866s

16777216
13983005
11184107
8387205
5591177
2795114
0

Тогда накопленные суммы находятся в tmp [0-6] .txt .., поэтому работа еще продолжается

В этом подходе всего 13 каналов, что позволяет преобразовать менее 1 МБ памяти.
Конечно, мое любимое решение:

time cat >f.c && gcc -O6 f.c && ./a.out
# then type your favourite c-program
real    0m42.130s

Это очень хорошее применение tr.
adavid

4

Я не знал uniqни о grep -o, ни о , но, поскольку мои комментарии к @JourneymanGeek и @ crazy2be имели такую ​​поддержку, возможно, мне стоит превратить его в своего собственного ответчика:

Если вы знаете, что в вашем файле есть только «хорошие» символы (те, которые вы хотите посчитать), вы можете перейти к

grep . -o YourFile | sort | uniq -c

Если только некоторые символы должны учитываться, а другие нет (т.е. разделители)

grep '[ACTGN-]' YourFile | sort | uniq -c

Первый использует подстановочный знак регулярного выражения ., который соответствует любому отдельному символу. Второй использует «набор принятых символов», без определенного порядка, за исключением того, что -должен стоять последним ( A-Cинтерпретируется как «любой символ между Aи C). В этом случае требуются кавычки, чтобы ваша оболочка не пыталась расширить ее, чтобы проверить односимвольные файлы, если они есть (и выдает ошибку «нет совпадения», если ее нет).

Обратите внимание, что «sort» также имеет -uфлаг «nique», так что он сообщает об этом только один раз, но не имеет сопутствующего флага для подсчета дубликатов, поэтому uniqдействительно является обязательным.


-не должен идти последним, если вы избежите его с обратной косой чертой: '[A\-CTGN]'должно работать просто отлично.
Индрек

2

Глупый

tr -cd ATCGN- | iconv -f ascii -t ucs2 | tr '\0' '\n' | sort | uniq -c
  • trудалить ( -d) все символы, кроме ( -c) ATCGN-
  • iconv преобразовать в ucs2 (UTF16 ограничен 2 байтами), чтобы добавить 0 байт после каждого байта,
  • другой trдля перевода этих символов NUL в NL. Теперь каждый персонаж находится на своей линии
  • sort | uniq -cсчитать каждую уникальную строку

Это альтернатива нестандартной (GNU) -oопции grep.


Не могли бы вы дать краткое объяснение команд и логики здесь?
Эндрю Ламберт

2
time $( { tr -cd ACGTD- < dna.txt | dd | tr -d A | dd | tr -d C | dd | tr -d G |
dd | tr -d T | dd | tr -d D | dd | tr -d - | dd >/dev/null; } 2>tmp ) &&
grep byte < tmp | sort -r -g | awk '{ if ((s-$0)>=0) { print s-$0} s=$0 }'

Формат вывода не самый лучший ...

real    0m0.176s
user    0m0.200s
sys     0m0.160s
2069046
2070218
2061086
2057418
2070062
2052266

Теория Операции:

  • $ ({command | command} 2> tmp) перенаправляет stderr потока во временный файл.
  • dd выводит стандартный вывод в стандартный вывод и выводит количество байтов, переданных в стандартный поток ошибок
  • tr -d отфильтровывает по одному символу за раз
  • grep и sort фильтруют вывод dd в порядке убывания
  • awk вычисляет разницу
  • sort используется только на этапе постобработки для обработки неопределенности порядка выхода экземпляров dd

Скорость, кажется, составляет 60 Мбит / с +


Улучшения: избавиться от ТМП? использовать «вставить», чтобы напечатать письмо?
Аки Суйхконен

1

Образец файла:

$ cat file
aix
unix
linux

Команда:

$ sed 's/./&\n/g' file | awk '!/^$/{a[$0]++}END{for (i in a)print i,a[i];}'
u 2
i 3
x 3
l 1
n 2
a 1

-1 за неясность и за размещение однострочника без объяснения причин. AFAIK, это может быть вилочная бомба
КПП

1

Объединяя несколько других

chars='abcdefghijklmnopqrstuvwxyz-'
grep -o -i "[$chars]" foo|sort | uniq -c

Добавьте, | sort -nrчтобы увидеть результаты в порядке частоты.


1

Краткий ответ:

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

Ах, но запутанные детали:

Это все персонажи Ascii. Один байт на. В файлах, конечно, есть дополнительные метаданные, предваряемые для различных вещей, используемых ОС и приложением, которое ее создало. В большинстве случаев я ожидал бы, что они будут занимать одинаковое количество места независимо от метаданных, но я постараюсь сохранить идентичные обстоятельства, когда вы сначала проверяете подход, а затем проверяете, что у вас есть постоянное смещение, прежде чем беспокоиться об этом. Другая проблема заключается в том, что разрывы строк обычно включают два символа пробела ascii, и любые табуляции или пробелы будут по одному на каждой. Если вы можете быть уверены, что они будут присутствовать, и нет никакого способа узнать, сколько их заранее, я бы прекратил читать сейчас.

Это может показаться большим количеством ограничений, но если вы можете легко их установить, это выглядит как самый простой / наиболее эффективный подход, если у вас есть тонна из них, чтобы рассмотреть (что, вероятно, если это ДНК). Проверка тонны файлов на длину и вычитание константы будет быстрее, чем выполнение grep (или аналогичного) для каждого.

Если:

  • Это простые неразрывные строки в чистых текстовых файлах
  • Они находятся в идентичных типах файлов, созданных тем же ванильным неформатирующим текстовым редактором, как Scite (вставка в порядке, если вы проверяете наличие пробелов / возвратов) или какой-то базовой программой, написанной кем-то

И две вещи, которые могут не иметь значения, но я бы попробовал сначала

  • Имена файлов имеют одинаковую длину
  • Файлы находятся в одном каталоге

Попробуйте найти смещение, выполнив следующие действия:

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


1

Haskell путь:

import Data.Ord
import Data.List
import Control.Arrow

main :: IO ()
main = interact $
  show . sortBy (comparing fst) . map (length &&& head) . group . sort

это работает так:

112123123412345
=> sort
111112222333445
=> group
11111 2222 333 44 5
=> map (length &&& head)
(5 '1') (4 '2') (3 '3') (2 '4') (1,'5')
=> sortBy (comparing fst)
(1 '5') (2 '4') (3 '3') (4 '2') (5 '1')
=> one can add some pretty-printing here
...

составление и использование:

$ ghc -O2 q.hs
[1 of 1] Compiling Main             ( q.hs, q.o )
Linking q ...
$ echo 112123123412345 | ./q
[(1,'\n'),(1,'5'),(2,'4'),(3,'3'),(4,'2'),(5,'1')]%       
$ cat path/to/file | ./q
...

не подходит для больших файлов, может быть.


1

Quick Perl Hack:

perl -nle 'while(/[ATCGN]/g){$a{$&}+=1};END{for(keys(%a)){print "$_:$a{$_}"}}'
  • -n: Перебирать входные строки, но ничего для них не печатать
  • -l: Автоматическое удаление или добавление разрывов строк
  • while: перебрать все вхождения запрошенных символов в текущей строке
  • END: В конце распечатать результаты
  • %a: Хэш, где хранятся значения

Символы, которые вообще не встречаются, не будут включены в результат.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.