Выберите строки из текстового файла, идентификаторы которых указаны в другом файле


13

Я использую много сортировки grep awk в моей оболочке Unix для работы с текстовыми файлами столбцов, разделенных табуляцией среднего размера (около 10–100 млн строк). В этом отношении Unix Shell - моя электронная таблица.

Но у меня есть одна огромная проблема - выбор записей по списку идентификаторов.

Имея table.csvфайл с форматом id\tfoo\tbar...и ids.csvфайл со списком идентификаторов, выберите только записи table.csvс идентификатором, присутствующим в ids.csv.

вид /programming/13732295/extract-all-lines-from-text-file-based-on-a-given-list-of-ids, но с оболочкой, а не perl.

grep -Fочевидно, дает ложные срабатывания, если идентификаторы имеют переменную ширину. joinэто утилита, которую я никогда не мог понять. Прежде всего, это требует алфавитной сортировки (мои файлы обычно сортируются по номерам), но даже тогда я не могу заставить его работать, не жалуясь на неправильный порядок и пропуская некоторые записи. Так что мне это не нравится. grep -f для файла с ^id\t-s очень медленный, когда количество идентификаторов велико. awkэто громоздко.

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

UPD: исправлено sort->join


Если grep -fэто слишком медленно, поддержание этой стратегии кажется большим количеством проблем, чем оно того стоит - вариации, вероятно, станут жертвами тех же проблем производительности O (N * M). Может быть, ваше время было бы лучше потратить на изучение того, как использовать нормализованную базу данных SQL ...
goldilocks

1
Почему бы не использовать Perl-скрипт из вопроса, который вы связали? Кроме того, должно быть возможно написать аналогичный скрипт в awk.
CJM

Bash 4 имеет ассоциативные массивы, которые вам нужны, чтобы обойти вложенные циклы в качестве примера perl.
Златовласка

1
sortможно делать все виды сортировки, числовые, алфавитные и другие. См man sort.
Terdon

У меня есть вопрос здесь, как мы можем сделать то же самое, если исходный файл, из которого мы хотим извлечь данные, является не разделенным файлом

Ответы:


19

Я думаю, вы grep -fне имели в виду, grep -Fно на самом деле вам нужно сочетание обоих и -w:

grep -Fwf ids.csv table.csv

Причина, по которой вы получили ложные срабатывания (я думаю, вы не объяснили), потому что если идентификатор может содержаться в другом, то оба будут напечатаны. -wустраняет эту проблему и -Fгарантирует, что ваши шаблоны обрабатываются как строки, а не как регулярные выражения. От man grep:

   -F, --fixed-strings
          Interpret PATTERN as a  list  of  fixed  strings,  separated  by
          newlines,  any  of  which is to be matched.  (-F is specified by
          POSIX.)
   -w, --word-regexp
          Select  only  those  lines  containing  matches  that form whole
          words.  The test is that the matching substring must  either  be
          at  the  beginning  of  the  line,  or  preceded  by  a non-word
          constituent character.  Similarly, it must be either at the  end
          of  the  line  or  followed by a non-word constituent character.
          Word-constituent  characters  are  letters,  digits,   and   the
          underscore.

   -f FILE, --file=FILE
          Obtain  patterns  from  FILE,  one  per  line.   The  empty file
          contains zero patterns, and therefore matches nothing.   (-f  is
          specified by POSIX.)

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

while read pat; do grep -w "^$pat" table.csv; done < ids.csv

или быстрее:

xargs -I {} grep "^{}" table.csv < ids.csv

Лично я бы сделал это, perlхотя:

perl -lane 'BEGIN{open(A,"ids.csv"); while(<A>){chomp; $k{$_}++}} 
            print $_ if defined($k{$F[0]}); ' table.csv

1
+1 Но: что, если есть потенциальные ложные срабатывания, которые точно соответствуют идентификатору по слову, только не в столбце идентификатора? Если вы не можете использовать ^с -F, вы не можете нацелиться на первый столбец.
Златовласка

@goldilocks, если они точно совпадают, это не ложные срабатывания. Я понял, что вы имеете в виду, но в этом случае ОП должен показать свои входные файлы.
Terdon

^id\tБит из OP означает , idможет произойти в другой колонке. Если нет, это не имеет значения.
Златовласка

@goldilocks справедливая точка, ответ отредактирован.
Terdon

Мы использовали это для создания временных файлов (используя awk или sed), которые добавляли уникальный символ (скажем, control-A), ограничивающий поле, которое мы хотели найти, а затем использовали grep -F -f temppatternfile temptargetfile | tr -d '\ 001'
Марк Плотник

7

joinУтилита, что вы хотите. Требуется лексическая сортировка входных файлов.

Предполагая, что ваша оболочка bash или ksh:

join -t $'\t' <(sort ids.csv) <(sort table.csv)

Без необходимости сортировки, обычное решение awk

awk -F '\t' 'NR==FNR {id[$1]; next} $1 in id' ids.csv table.csv

Как я пытался, но в итоге не смог передать, join - это клудж. Не работает для меня так хорошо
Alamar

1
joinэто не клудж: твои слова были, ты не мог понять это. Открой свой разум и учись. Какой результат вы получили, и как это отличается от того, что вы ожидаете?
Гленн Джекман

+1, это работа для join.
don_crissti

awkРешение здесь очень быстро и эффективно для моих целей (я извлечение подмножеств нескольких сот из файлов с ~ 100M линий)
Лука

2

Ответы на этот ТАКОЙ вопрос помогли мне обойти ниггеры с помощью join. По сути, когда вы сортируете файл в процессе подготовки к отправке для присоединения, вам нужно убедиться, что вы сортируете по столбцу, к которому вы присоединяетесь. Так что, если это первый, вам нужно указать ему, какой символ разделителя находится в файле и что вы хотите, чтобы он сортировался в первом поле (и только в первом поле). В противном случае, если первое поле имеет переменную ширину (например), ваши разделители и, возможно, другие поля могут начать влиять на порядок сортировки.

Итак, используйте опцию -t sort, чтобы указать разделяющий символ, и опцию -k, чтобы указать поле (помня, что вам нужно поле начала и конца - даже если оно одинаковое - или оно будет отсортировано по этому символу до конца строки).

Поэтому для файла, разделенного табуляцией, как в этом вопросе, должно работать следующее (благодаря ответу Гленна за структуру):

join -t$'\t' <(sort -d ids.csv) <(sort -d -t$'\t' -k1,1 table.csv) > output.csv

(Для справки, флаг -d означает сортировку по словарю. Вы также можете использовать флаг -b, чтобы игнорировать начальные пробелы, смотрите man sortи man join).

В качестве более общего примера, предположим, что вы объединяете два файла, разделенных запятыми - input1.csvв третьем столбце и input2.csvв четвертом. Вы могли бы использовать

join -t, -1 3 -2 4 <(sort -d -t, -k3,3 input2.csv) <(sort -d -t, -k4,4 input2.csv) > output.csv

Здесь параметры -1и -2указывают, к каким полям нужно присоединиться в первом и втором входных файлах соответственно.


0

Вы также можете использовать ruby, чтобы сделать нечто подобное:

ruby -pe 'File.open("id.csv").each { |i| puts i if i =~ /\$\_/ }' table.csv
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.