Фільтруйте файл за номером рядка


17

З огляду на файл L з одним невід’ємним цілим числом на рядок та текстовий файл F, який би був швидкий спосіб зберегти лише ті рядки у F, чий рядок відображається у файлі L?

Приклад:

$ cat L.txt
1
3

$ cat F.txt
Hello World
Hallo Welt
Hola mundo

$ command-in-question -x L.txt F.txt
Hello World
Hola mundo

Я шукаю команду, яка може обробляти файл L з 500 мільйонами і більше записів; файл L відсортований за числовим числом.

Примітка. Я перебуваю на півдорозі впровадження, command-in-questionале я просто задумався, чи не вдасться тут використовувати деякі інструменти Unix.


Оновлення: Дякую за всі відповіді, сьогодні я багато чого дізнався! Я хотів би прийняти ще одну відповідь, але це неможливо.


Скільки рядків у файлі F?
Пітер.O

1
У F є як мінімум стільки рядків, скільки в Л.
miku

Відповіді:


8

При Cопускаючи змістовні повідомлення про помилки:

#include <stdio.h>
#include <stdlib.h>

int main (int argc, char *argv[]) {

    FILE *L;
    FILE *F;

    unsigned int to_print;
    unsigned int current = 0;
    char *line = NULL;
    size_t len = 0;

    if ((L = fopen(argv[1], "r")) == NULL) {
        return 1;
    } else if ((F = fopen(argv[2], "r")) == NULL) {
        fclose(L);
        return 1;
    } else {

        while (fscanf(L, "%u", &to_print) > 0) {
            while (getline(&line, &len, F) != -1 && ++current != to_print);
            if (current == to_print) {
                printf("%s", line);
            }
        }

        free(line);
        fclose(L);
        fclose(F);
        return 0;
    }
}

2
Це найвдаліша відповідь тут. Принаймні, саме так мої тести. У разі , якщо кому - то цікаво, я збирав його , як: xsel -bo | cc -xc - -o cselect. І це просто спрацювало - йому потрібні лише дві гілочки.
mikeserv

1
Дякую, це чудово! Сподіваюсь, ви не заперечуєте, але я загорнув ваш код у маленький інструмент .
міку

1
@miku Вперед, я радий, що можу допомогти. Я помітив, що ти збільшився LINE_MAXу своїй версії, тому ти, мабуть, працюєш із дуже великими рядками у своїх файлах. Я оновив A версією, яка використовує getline()для видалення обмеження розміру рядка.
FloHimself

@FloHimself, ну ще раз дякую:) Дійсно, деякі вхідні рядки можуть перевищувати LINE_MAX, так getlineздається, правильно.
miku

10

Я б хотів використовувати awk, але не зберігати весь вміст L.txtу пам'яті і робити зайві хеш-пам’яті ;-).

list=L.txt file=F.txt
LIST="$list" awk '
  function nextline() {
    if ((getline n < list) <=0) exit
  }
  BEGIN{
    list = ENVIRON["LIST"]
    nextline()
  }
  NR == n {
    print
    nextline()
  }' < "$file"

Точно я спробував хеш-карти, і вони перевищують пам'ять; бітсети куплять вам більше запасу; але, використовуючи той факт, що вхід відсортований, ви можете позбутися цієї проблеми (простору) цілком.
міку

1
@Janis; Це не просто стандартна хороша практика кодування: не жорсткі літеральні коди - використовуйте натомість змінні ... (більш гнучкі та менше схильні до помилок та простіші у обслуговуванні)
Peter.O

1
@ StéphaneChazelas: Необхідно попередньо рамкову ініціалізацію n, в іншому випадку (як є) він пропускає 1вL.txt
Peter.O

1
@ Peter.O, ой, саме це я намагався вирішити з допомогою NR> = n, але це було неправильно. Має бути краще зараз.
Стефан Шазелас

1
@Janis, ідея полягала в тому, що якщо цей код повинен бути вбудований в command-in-questionсценарій, то ви не можете вбудувати ім'я файлу в код. -v list="$opt_x"не працює ні через зворотну косу рису, яку зробив awk на ній. Тому я тут використовую ENVIRON.
Стефан Шазелас

10

grep -n | sort | sed | cut

(   export LC_ALL=C
    grep -n ''   | sort -t:  -nmk1,1 ./L - |
    sed /:/d\;n  | cut  -sd: -f2-
)   <./F

Це повинно працювати досить швидко (деякі тестові випробування включені нижче) із введенням будь-якого розміру. Деякі примітки про те, як:

  • export LC_ALL=C
    • Оскільки суть наступної операції полягає в тому, щоб отримати весь файл з ./Fнакопиченим ./Lрядком в рядку з файлом lineno, єдині символи, про які нам дійсно потрібно буде турбуватися, - це [0-9]цифри ASCII і :двокрапка.
    • З цієї причини легше турбуватися про те, щоб знайти ці 11 символів у наборі з 128 можливих, ніж це, якщо UTF-8 іншим чином залучений.
  • grep -n ''
    • Це вставляє рядок LINENO:у голову кожного рядка в stdin - або <./F.
  • sort -t: -nmk1,1 ./L -
    • sortнехтує взагалі сортування вхідних файлів, а натомість (правильно) припускає, що вони -mскасовуються та видаляє їх у -numericallyвідсортованому порядку, ігноруючи в основному що-небудь, що перевищує будь-який можливий символ, -k1,1що зустрічається -t:двокрапкою.
    • Хоча для цього може знадобитися деякий тимчасовий простір (залежно від того, наскільки далеко можуть відбуватися деякі послідовності) , це не зажадає багато в порівнянні з належним сортуванням, і це буде дуже швидко, оскільки воно включає нульове зворотнє відстеження.
    • sortвиведе єдиний потік, де будь-який лінобетон в ./Lбуде негайно передувати відповідним рядкам у ./F. ./LРядки завжди виходять першими, оскільки вони коротші.
  • sed /:/d\;n
    • Якщо поточний рядок збігається з /:/двокрапкою, dвиберіть його з виводу. Ще, автоматично роздрукуйте поточний та зовнішній nрядок.
    • І так виводиться sedчорнослив лише на послідовні пари рядків, які не відповідають двокрапці та наступному рядку - або лише рядку від, а потім наступному.sort./L
  • cut -sd: -f2-
    • cut -sпідсилює з виводу ті вхідні рядки, які не містять принаймні одного з його -d:рядків елімінатора - і таким чином ./Lрядки обрізаються повністю.
    • Для тих ліній , які роблять їх перші :двокрапки -fIELD це cutдалеко - і так йдуть всі grep«вставляють LINENO років.

невеликий вхідний тест

seq 5 | sed -ne'2,3!w /tmp/L
        s/.*/a-z &\& 0-9/p' >/tmp/F

... генерує 5 рядків зразкового введення. Потім...

(   export LC_ALL=C; </tmp/F \
    grep -n ''   | sort -t:  -nmk1,1 ./L - |
    sed /:/d\;n  | cut  -sd: -f2-
)|  head - /tmp[FL]

... відбитки ...

==> standard input <==
a-z 1& 0-9
a-z 4& 0-9
a-z 5& 0-9

==> /tmp/F <==
a-z 1& 0-9
a-z 2& 0-9
a-z 3& 0-9
a-z 4& 0-9
a-z 5& 0-9

==> /tmp/L <==
1
4
5

більш тестові випробування

Я створив пару досить великих файлів:

seq 5000000 | tee /tmp/F |
sort -R | head -n1500000 |
sort -n >/tmp/L

... які помістили 5mil рядків у /tmp/Fта 1,5mil випадковим чином вибраних ліній у /tmp/L. Я тоді:

time \
(   export LC_ALL=C
    grep -n ''   | sort -t:  -nmk1,1 ./L - |
    sed /:/d\;n  | cut  -sd: -f2-
)   <./F |wc - l

Він надрукував:

1500000
grep -n '' \
    0.82s user 0.05s system 73% cpu 1.185 total
sort -t: -nmk1,1 /tmp/L - \
    0.92s user 0.11s system 86% cpu 1.185 total
sed /:/d\;n \
    1.02s user 0.14s system 98% cpu 1.185 total
cut -sd: -f2- \
    0.79s user 0.17s system 80% cpu 1.184 total
wc -l \
    0.05s user 0.07s system 10% cpu 1.183 total

(Я додав туди косої риски)

Серед пропонованих тут рішень, це найшвидший з усіх, але один, коли він відповідає набору даних, згенерованих вище на моїй машині. З інших тільки один був близький до претендують на друге місце, і це meuh ось perl тут .

Це аж ніяк не оригінальне пропоноване рішення - воно втратило третину часу його виконання завдяки порадам / натхненню, запропонованим іншими. Дивіться історію публікацій для повільніших рішень (але чому?) .

Крім того, варто відзначити, що деякі інші відповіді можуть бути набагато кращими, якби не архітектура з декількома процесорами моєї системи та одночасне виконання кожного з процесів у цьому конвеєрі. Всі вони працюють одночасно - кожен у своєму ядрі процесора - передаючи дані та виконуючи свою невелику частину цілого. Це досить круто.

але найшвидше рішення -

Але це не найшвидше рішення. Найшвидше рішення пропонується тут, руки вниз, це програма C . Я це назвав cselect. Після копіювання його до буфера обміну X я скомпілював його так:

xsel -bo | cc -xc - -o cselect

Я тоді:

time \
    ./cselect /tmp/L /tmp/F |
wc -l

... і результати були ...

1500000
./cselect /tmp/L /tmp/F  \
    0.50s user 0.05s system 99% cpu 0.551 total
wc -l \
    0.05s user 0.05s system 19% cpu 0.551 total

1
Ви можете зробити це значно швидше (майже так само швидко, як у багатьох sed -ne'/:/!{n;p;}' | cut -d: -f2-sed -ne'/:/!N;/\n/s/[^:]*://p'
ядрах

@ StéphaneChazelas - ви можете отримати кращі результати, якщо переключите seds - sedя використовую реліквію sed- ви можете побачити aliasзначення в timeрезультатах. Мій пакет спадкоємців, до речі, статично складений проти musl libc - реалізація регулярного вираження, заснована на TRE . Коли я переключаю його на GNU sed- і запускаю його без cut- він додає повну секунду до часу завершення (2,8 сек) - з'єднує його більш ніж на третину. І це лише на .3 секунди швидше, ніж у вашій системі.
mikeserv

1
sort -mnна відміну від sort -nmk1,1може бути кращим, оскільки тут вам не потрібно робити розщеплення (не перевірено)
Stéphane Chazelas

@ StéphaneChazelas - так, я думав те саме і намагався це в будь-який спосіб. -nце білд просто зробити першу числову рядок на рядку , так що я зрозумів, добре -mnабо -nmй, з якої причини тільки раз він коли - або опускався нижче 2Са часу завершення, коли я додав всі варіанти , як є. Це дивно - і це вчора я не брався за -mперше місце - я знав, про що я йшов, але здавалося, що це просто спрацює як якась річ з автоматичною оптимізацією. Цікаво, що в реліквії sortє -zпараметр довжини струни, який стосується лише -[cm]....
mikeserv

-nне є першим числовим рядком у рядку . Він просто розглядає рядок як число, таким чином, abc 123було б 0. Тож він не може бути менш ефективним, ніж з-t: -k1,1
Stéphane Chazelas

9

Я б використовував awk:

awk 'NR==FNR {a[$1]; next}; FNR in a' L.txt F.txt

Оновлення: я зробив заходи щодо ефективності; здається, що ця версія масштабується навіть краще з дуже великими наборами даних (як це стосується заявлених вимог), оскільки порівняння дуже швидке і надмірно компенсує зусилля, необхідні для складання хеш-таблиці.


1
@miku; Так, це приємне компактне рішення. Але застереження; не всі awkможуть бути в змозі обробити такі величезні набори даних. - я використовую GNU awkі немає проблем; тест з 500 мільйонами рядків даних вимагав 7 хвилин.
Яніс

1
Це досить повільно (для порівняння) real 16m3.468s- user 15m48.447s- sys 0m10.725s. Він використовував 3,3 ГБ оперативної пам’яті для тестування 1/10-го розміру Lз 50 000 000 ліній; і Fз 500 000 000 рядків - проти часу для дивного анестера Стефана Шазеласа: real 2m11.637s- user 2m2.748s- sys 0m6.424s- Я не використовую швидку скриньку, але порівняння цікаве.
Пітер.О

@ Peter.O; Дякуємо за дані! Повільну швидкість слід було очікувати, враховуючи, що (в моєму тестовому випадку) півмільярда рядків було збережено в асоціативному масиві. (Ось чому я прокоментував "(+1)" вище пропозицію Стефана.) - Хоча я був здивований, що це коротке рішення все ще обробляє 1 мільйон рядків в секунду! Я думаю, що це робить цей шаблон коду (через його простоту!) Життєздатним варіантом, а саме у випадках з менш екстремальними розмірами даних.
Яніс

Це, безумовно, життєздатне рішення. На тестових даних, які я використав (5мл ліній / 1,5мл L), ваш завершився за трохи більше 4 секунд - лише секунду за відповіді Стефана. Код , який використовується для покоління тестового набору в моїй обороні, але це в основному просто seqвихід , а потім менше, випадковим чином вибирається підмножина ж в L .
mikeserv

1
Щойно я зробив ще кілька заходів щодо продуктивності з розміром файлу даних 500 мільйонів рядків та розміром ключового файлу в 50 мільйонів і відповідно. 500 мільйонів рядків, з помітним спостереженням. З меншим файлом ключів час становить 4 хв (Стефан) проти 8 хв (Джаніс), тоді як з більшим файлом ключів - 19 хв (Стефан) проти 12 хв (Джаніс).
Яніс

3

Просто для повноти: ми можемо об'єднати чудовий сценарій awk у відповідь Стефана Шазеласа та сценарій perl у відповіді kos, але не зберігаючи весь список у пам’яті, сподіваючись, що perl може бути швидшим, ніж awk. (Я змінив порядок аргументів, щоб відповідати початковому питанню).

#!/usr/bin/env perl
use strict;

die "Usage: $0 l f\n" if $#ARGV+1 != 2;
open(L,$ARGV[0]) or die "$ARGV[0]: $!";
open(F,$ARGV[1]) or die "$ARGV[1]: $!";

while(my $number = <L>){
    #chop $number;
    while (<F>) {
        if($. == $number){
            print;
            last;
        }
    }
}

Це швидше, ніж awk. Це приблизно так само швидко, як і моє - я тестував обидва рази лише зараз, і кожен раз, коли мій обробляв мій 5-міліловий тестовий набір за 1,8 ... секунди і ваш 1,9 ... секунди кожен раз. Якщо ви хвилюєтесь, ген-код тестового набору є моєю відповіддю, але справа в тому, що це дуже добре. Більше того, результат правильний - я все ще не можу зробити awkроботу ... Тим не менш, обидві наші відповіді соромно FloHimself .
mikeserv

@mikeserv, ми повинні мати різні awks. У вашому зразку я отримую 1,4 з gawk (4s для Janis '), 0,9s з mawk, 1,7s з цим розчином perl, 2,3s з kos', 4,5s з вашим (GNU sed), і 1,4s з вашим ( GNU sed) та запропоноване нами покращення (і 0,5 с для розчину С).
Стефан Шазелас

@mikeserv, ах! Звичайно, з вашим підходом, місцевість має значення. Знизившись з 4,5 до 2,3 секунди тут при переході з UFT-8 на C.
Stéphane Chazelas

3

Я написав простий сценарій Perl для цього:

Usage: script.pl inputfile_f inputfile_f

#!/usr/bin/env perl

$number_arguments = $#ARGV + 1;
if ($number_arguments != 2) {
    die "Usage: script.pl inputfile_f inputfile_l\n";
}

open($f, '<', $ARGV[0])
    or die "$ARGV[0]: Not found\n";
open($l, '<', $ARGV[1])
    or die "$ARGV[1]: Not found\n";

@line_numbers = <$l>;

while ($line = <$f>) {
    $count_f ++;
    if ($count_f == @line_numbers[$count_l]) {
        print $line;
        $count_l ++;
    }
}
  • Вантажі F.txt
  • Вантажі L.txt
  • Зберігає кожен рядок L.txtу масиві
  • Читає F.txtрядок за рядком, відстежуючи його поточний номер рядка та поточний індекс масиву; збільшує номер F.txtпоточного рядка; якщо номер F.txtпоточного рядка відповідає вмісту масиву в поточному індексі масиву, він друкує поточний рядок і збільшує індекс

Міркування щодо вартості та складності :

Беручи до уваги вартість виконання завдань, витрати на порівняння та витрати на друк рядків, N 1 - кількість рядків у F.txtта N 2 - кількість рядків L.txt, whileцикл працює не більше N 1 разів, що веде до призначення 2N 1 + N 2 (очевидно, припускаючи N 1 > N 2 ), до порівнянь 2N 1 і до N 2 друкує; задана як однакова вартість кожної операції, загальна вартість запуску whileциклу дорівнює 4N 1 + 2N 2 , що призводить до складності сценарію O (N).

Тест на вхідний файл на 10 мільйонів рядків :

Використовуючи файл на 10 мільйонів рядків, F.txtщо містить випадкові рядки довжиною 50 символів, і 10-мільйонний L.txtфайл, що містить числа від 1 до 10000000 (найгірший сценарій):

~/tmp$ for ((i=0; i<3; i++)); do time ./script.pl F.txt L.txt > output; done

real    0m15.628s
user    0m13.396s
sys 0m2.180s

real    0m16.001s
user    0m13.376s
sys 0m2.436s

real    0m16.153s
user    0m13.564s
sys 0m2.304s

2

Цей розчин perl швидше, ніж інші рішення awk або perl на 20%, але, очевидно, не такий швидкий, як розчин C.

perl -e '
  open L, shift or die $!;
  open F, shift or die $!;
  exit if ! ($n = <L>);
  while (1) {
    $_ = <F>;
    next if $. != $n;
    print;
    exit if ! ($n = <L>);
  }
' -- L F

0
cat <<! >L.txt
1
3
!

cat <<! >F.txt
Hello World
Hallo Welt
Hola mundo
!

cmd(){
 L=$1 F=$2
 cat -n $F |
 join $L - |
 sed 's/[^ ]* //'
}

cmd L.txt F.txt
Hello World
Hola mundo

Оскільки L.txt відсортований, ви можете використовувати приєднання. Просто нумеруйте кожен рядок у F.txt, з'єднайте два файли, а потім видаліть номер рядка. Не потрібні великі проміжні файли.

Насправді, вищезазначене буде керувати вашими рядками даних, замінивши весь пробіл одним простором. Щоб зберегти лінію недоторканою, вам потрібно вибрати як роздільник деякий символ, який не відображається у ваших даних, наприклад "|". Cmd є тоді

cmd(){
 L=$1 F=$2
 cat -n $F |
 sed 's/^ *//;s/\t/|/' |
 join -t'|' $L - |
 sed 's/[^|]*|//'
}

Перший sed видаляє провідні пробіли з виводу "cat -n" і замінює вкладку. Другий sed видаляє номер рядка та "|".


Я боюся, що це не працюватиме на великих файлах. Потрібно <10 рядків. У мене була така ж ідея і я спробував, join L.txt <(nl F.txt )але вона не працюватиме на великих файлах. Ласкаво просимо на сайт, до речі, не часто ми отримуємо такі чіткі та добре відформатовані відповіді від нових користувачів!
terdon

@terdon, Так, шкода, що join/ commне може працювати з цифровим відсортованим входом.
Стефан Шазелас

@terdon: Я продовжував перевіряти (тепер видалено) і намагався join -t' ' <(<L.txt awk '{printf("%010s\n",$0)}') <(<F.txt awk '{printf("%010s %s\n",NR,$0)}') | cut -d' ' -f2-- це було повільно! - і навіть коли я готував у підготовлені файли відповідними клавішами join -t' ' L.txt F.txt | cut -d' ' -f2- , awkнакладеними 0 , все ще був повільним (не враховуючи час підготовки) - повільніше, ніж відповідь від @Janis (де я опублікував коментар про фактичний час, прийнятий для обох його відповідь і @ @ StéphaneChazelas
Peter.O

@ Пітер.О так. Я спробував подібний підхід, який уникає одного з неприємностей, але я не зміг знайти спосіб зробити так, щоб він працював і був вартим цього.
тердон

@terdon та інші: Фактичний час для замінника процесуjoin + був проти Стефана Шазеласа, використовуючи 50 мільйонів рядків, 500 мільйонів ліній. awk printf real 20m11.663s user 19m35.093s sys 0m10.513sreal 2m11.637s user 2m2.748s sys 0m6.424sLF
Пітер.О

0

Для повноти ще одна спроба joinрішення:

sed -r 's/^/00000000000000/;s/[0-9]*([0-9]{15})/\1/' /tmp/L | join <( nl -w15 -nrz /tmp/F ) - | cut -d' ' -f2-

Це працює, відформатувавши стовпчик рядка з числом, який поєднує роботи, на фіксовану довжину з провідними нулями, так що числа завжди становлять 15 цифр. Це обходить проблему приєднання, що не сподобається нормальному численному порядку сортування, оскільки стовпець фактично змушений був сортувати словник. nlвикористовується для додавання номерів рядків у цьому форматі до F.txt. На жаль, sedпотрібно використовувати для переформатування нумерації у L.txt.

Цей підхід, здається, працює добре на тестових даних, створених методом @ mikeserv. Але це все ще дуже повільно - рішення c на 60 разів швидше на моїй машині. приблизно 2/3 часу проводиться в sedі 1/3 дюйма join. Можливо, є кращий вираз sed ...


Гаразд - але чому ми попереджаємо всі нулі? Я намагаюся відчути це. Крім того, nlце супер круто, але ви не можете надійно використовувати його на неперевірені дані. Одне з речей, яке робить його таким крутим, це його логічне -d усунення сторінок . За замовчуванням, якщо є вхідний рядок, що складається лише з рядків :\` (але без / з могили) 1, 2, 3 або три рази поспіль, ваші рахунки будуть трохи божевільними. Експериментуйте з ним - це досить акуратно. Особливо погляньте на те, що відбувається, коли nl` читає рядок з 1 розділовим рядком, а потім пізніше іншим w / 3 або 2
mikeserv

0

Оскільки прийнята відповідь є на С, я подумав, що це добре, щоб кинути тут рішення пітона:

# Read mask
with open('L.txt', 'r') as f:
    mask = [int(line_num) for line_num in f.read().splitlines()]

# Filter input file
filtered_lines = []
with open('F.txt', 'r') as f:
    for i, line in enumerate(f.read().splitlines()):
        if (i+1) in mask:
            filtered_lines.append(line)

# Write newly filtered file
with open('F_filtered.txt', 'w') as f:
    for line in filtered_lines:
        f.write('%s\n' % line)

Якщо використовувати зовнішню бібліотеку, наприклад numpy, рішення виглядає ще більш елегантним:

import numpy as np

with open('L.txt', 'r') as f:
    mask = np.array([int(line_num)-1 for line_num in f.read().splitlines()])

with open('F.txt', 'r') as f:
    lines = np.array(f.read().splitlines())
filtered_lines = lines[mask]

with open('F_filtered.txt', 'w') as f:
    for line in filtered_lines:
        f.write('%s\n' % line)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.