sed, xargs и stdbuf - как получить из файла только первые n совпадений шаблона - PullRequest
0 голосов
/ 28 июня 2018

У меня есть файл с шаблонами (1 строка = 1 шаблон), который я хочу найти в большом текстовом файле - только один (или ни один) шаблон будет найден в каждой строке файла. Найдя совпадение, я хочу получить символы непосредственно перед совпадением. Первая часть - приобрести шаблоны для sed

cat patterns.txt | xargs -I '{}' sed -n 's/{}.*$//p' bigtext.txt

Это работает нормально - недостатком является то, что потенциально у меня будут сотни тысяч матчей. Я не хочу / не нуждаюсь во всех матчах - достаточно было бы честного представления 1K хитов. И вот где я борюсь: я прочитал, что для ограничения количества попаданий sed я должен использовать stdbuf (gstdbuf в моем случае) и пропустить все это через голову. Но я не уверен, где разместить команду stdbuf:

cat patterns.txt | xargs -I '{}' gstdbuf -oL -eL sed -n 's/{}.*$//p' bigtext.txt | head -n100

Когда я попытался это сделать, процесс занимает столько времени, сколько если бы он выполнял sed для всего файла, а затем получил head этого вывода, в то время как я хотел бы прекратить поиск после 100 или 1000 совпадений. Есть идеи о том, как лучше всего это сделать?

Ответы [ 2 ]

0 голосов
/ 30 июня 2018

Вы можете указать файл с шаблонами, которые будут соответствовать команде grep, с помощью -f file. Вы также можете указать количество совпадений, которые нужно найти перед выходом -m count

Таким образом, эта команда получит первые 5 строк, которые соответствуют:

grep -f patterns.txt -m 5  bigtext.txt

Теперь обрезать совпадение до конца строки немного сложнее. Предполагая, что вы используете bash, мы можем построить регулярное выражение из файла, например так:

  while IFS='' read -r line || [[ -n "$line" ]]; do
    subRegex="s/$line.*//;"${subRegex}
  done < patterns.txt

Затем используйте это в команде sed. Полученный код становится:

  while IFS='' read -r line || [[ -n "$line" ]]; do
    subRegex="s/$line.*//;"${subRegex}
  done < patterns.txt
  grep -f patterns.txt -m 5  bigtext.txt | sed "$subRegex"

Команда sed выполняется только в тех строках, которые уже сопоставлены с grep, поэтому она должна быть достаточно производительной.

Теперь, если вы часто это вызываете, вы можете поместить это в функцию

function findMatches() {
  local matchCount=${1:-5}  # default to 5 matches
  local subRegex

  while IFS='' read -r line || [[ -n "$line" ]]; do
    subRegex="s/$line.*//;"${subRegex}
  done < patterns.txt

  grep -f patterns.txt -m ${matchCount}  bigtext.txt | sed "${subRegex}"
}

Тогда вы можете назвать это так

findMatches 5
findMatches 100

Обновление

На основании предоставленных вами примеров файлов это решение дает ожидаемый результат 1aaa 2aaabbb 3aaaccc 4aaa 5aaa

Однако, учитывая ваш комментарий, длина каждого шаблона составляет 120 символов, а каждая строка большого файла - 250 символов, размер файла 10 ГБ.

Вы не упомянули, сколько шаблонов у вас может быть. Итак, я проверил, и кажется, что команда sed, встроенная в строку, распадается где-то до 50 шаблонов.

(Конечно, если ваши сэмплы действительно выглядят как данные, то вы можете выполнить обрезку каждой строки, основываясь на не-AGCT, а не на файле шаблонов. Что будет намного быстрее)

Но исходя из исходного вопроса. Вы можете создать сценарий sed в отдельном файле на основе patterns.txt. Как это:

  sed -e "s/^/s\//g;s/$/.*\$\/\/g/g;" patterns.txt > temp.sed

затем используйте этот временный файл в команде sed.

 grep -f patterns.txt -m 5 bigtext.txt | sed -f temp.sed

grep останавливается после нахождения совпадений X, и sed обрезает их ... Новая функция запускается на моей машине через пару секунд . Для тестирования я создал файл объемом 2 Гб из комбо AGCT по 250 символов. И еще один файл с 50+ шаблонами, по 120 символов каждый с несколькими из этих шаблонов, взятых из случайных строк файла большого текста.

function findMatches() {
  sed -e "s/^/s\//g;s/$/.*\$\/\/g/g;" patterns.txt > temp.sed
  grep -f patterns.txt -m ${1:-5}   bigtext.txt | sed -f temp.sed
}
0 голосов
/ 30 июня 2018

Является ли предоставленный вами oneliner действительно тем, что вы хотели? Особенно так как вы упоминаете честный образец. Потому что, как он есть сейчас, он подает patterns.txt в xargs ..., что будет продолжаться и будет вызывать sed для каждого шаблона индивидуально, один за другим. И весь вывод xargs подается на головку, которая прерывает его после n строк. Другими словами, ваш первый шаблон уже может исчерпать все строки, которые вы хотели видеть, даже если другие шаблоны могли совпадать сколько угодно раз на линиях, встречающихся перед представленными вам совпадениями. Подробный пример между горизонтальными линейками.


Если у меня есть patterns.txt из:

_Pat1
_Pat2
_Pat3

А bigtext.txt с:

1matchx_Pat1x
2matchx_Pat2x 
2matchy_Pat2y 
2matchz_Pat2z 
3matchx_Pat3x 
3matchy_Pat3y 
3matchz_Pat3z
1matchy_Pat1y 
1matchz_Pat1z 

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

1matchx
2matchx
2matchy
2matchz
3matchx

Но (все (3) патчи для _Pat1 плюс 2 совпадения для _Pat2, после которых у меня закончились выходные строки):

1matchx
1matchy
1matchz
2matchx
2matchy

Теперь к вашей проблеме производительности, которая частично связана. Я должен признать, что я не мог воспроизвести это. Я взял ваш пример из комментария, взорвал "большой" файл размером до 1 ГБ, повторив шаблон, и запустил свой oneliner:

$ time { cat patterns.txt | xargs -I '{}' stdbuf -oL sed -n 's/{}.*$//p' bigtext.txt | head -5 ; }
1aaa
2aaabbb
3aaaccc
1aaa
2aaabbb
xargs: stdbuf: terminated by signal 13

real    0m0.012s
user    0m0.013s
sys     0m0.008s

Заметьте, я опустил -eL, stderr обычно не буферизован (это то, что вы обычно хотите) и на самом деле здесь не играет никакой роли. Также обратите внимание, что я выполнил stdbuf без префикса "g", который говорит мне, что вы, вероятно, находитесь в системе, где инструменты GNU не используются по умолчанию ... и, вероятно, причины, по которым вы получаете другое поведение. Я попытаюсь объяснить, что происходит, и рискну немного догадаться ... и в заключение предложу. Также обратите внимание, что мне действительно не нужно было использовать stdbuf (манипулирование буферизацией) вообще, или, скорее, это не оказало заметного влияния на результат, но опять же, это могло бы зависеть от платформы и инструментов (а также сценария).

Когда вы читаете строку с ее конца, head читает стандартный ввод по мере его поступления из xargs (и, как следствие, запускается sed (или stdbuf), который xargs разветвляется, они оба прикреплены к концу записи), пока не будет достигнут предел количества строк для печати, а затем head завершится. Это «разрывает» канал и xargs и sed (или stdbuf, в который он был обернут) получает сигнал SIGPIPE, и по умолчанию они также завершаются (что вы можете увидеть в выходных данных моего прогона: xargs: stdbuf: terminated by signal 13).

Что делает stdbuf -oL и почему кто-то мог это предложить. Если больше не использовать консоль для чтения / записи, которая обычно бывала буферизованной строкой, и использовать каналы, мы вместо этого обычно видим буферизованный ввод-вывод. stdbuf -oL изменяет это обратно на строку с буферизацией. Без этого задействованный процесс мог бы обмениваться данными большими блоками, и это могло бы занять head дольше, чтобы понять, что это делается и не требует дополнительного ввода, в то время как sed продолжает работать, чтобы увидеть, есть ли еще какие-либо подходящие совпадения. Как уже упоминалось, в моих системах (буфер 4K) и на этом примере (с повторяющимся шаблоном) это не имело никакого значения. Также обратите внимание, что, хотя это снижает риск того, что мы не узнаем, что мы можем это сделать, буферизация строки увеличивает накладные расходы, связанные с обменом данными между процессами.

Так почему же эти механики не дают ожидаемых для вас результатов? На ум приходит пара вариантов:

  • , так как вы запускаете и запускаете sed один раз для каждого шаблона, каждый раз целый файл. Может случиться так, что вы получите серию из нескольких прогонов без каких-либо попаданий. Я предполагаю, что это действительно так.
  • , так как вы даете sed файл для чтения, у вас может быть другая реализация sed, которая пытается прочитать намного больше, прежде чем предпринимать действия с содержимым файла (мой читает 4K за раз). Не вероятная причина, но в теории вы могли бы также кормить sed построчно, чтобы заставить меньшие куски и получить это SIGPIPE раньше.

Теперь, предполагая, что последовательное сопоставление с шаблоном на самом деле нежелательно, сводка всего вышеперечисленного будет такой: сначала обработайте ваши шаблоны в один, а затем выполните один проход по «большому» файлу (опционально ограничивая вывод курс). Возможно, стоит перейти от оболочки в основном к чему-то более удобному в использовании, или, по крайней мере, не сохранять формат oneliner, который может привести к путанице.


Не соответствует моей собственной рекомендации, скрипт awk, который называется так, печатает первые 5 попаданий и завершает работу:

awk -v patts="$(cat patterns.txt)" -v last=5 'BEGIN{patts="(" patts ; gsub(/\n/, "|", patts) ; sub(/.$/, ")", patts); cnt=1 ;} $0~patts{sub(patts ".*", ""); print; cnt++;} cnt>last{exit;}' bigtext.txt
...