Инструмент командной строки для «парного» раскрытия всех строк в файле


13

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

Row1,10
Row2,20
Row3,30
Row4,40

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

Row1,10 Row1,10
Row1,10 Row2,20
Row1,10 Row3,30
Row1,10 Row4,40
Row2,20 Row1,10
Row1,20 Row2,20
...
Row4,40 Row4,40

Мой вариант использования заключается в том, что я хочу передать этот вывод в другую команду (например, awk) для вычисления некоторого показателя об этой попарной комбинации.

У меня есть способ сделать это в awk, но меня беспокоит то, что мое использование блока END {} означает, что я в основном сохраняю весь файл в памяти перед выводом. Пример кода:

awk '{arr[$1]=$1} END{for (a in arr){ for (a2 in arr) { print arr[a] " " arr[a2]}}}' samples/rows.txt 
Row3,30 Row3,30
Row3,30 Row4,40
Row3,30 Row1,10
Row3,30 Row2,20
Row4,40 Row3,30
Row4,40 Row4,40
Row4,40 Row1,10
Row4,40 Row2,20
Row1,10 Row3,30
Row1,10 Row4,40
Row1,10 Row1,10
Row1,10 Row2,20
Row2,20 Row3,30
Row2,20 Row4,40
Row2,20 Row1,10
Row2,20 Row2,20

Существует ли эффективный способ потоковой передачи данных без необходимости сохранять файл в памяти и затем выводить его в блок END?


1
Вам всегда нужно будет прочитать один файл до конца, прежде чем вы сможете начать вывод для второй строки другого файла. Другой файл вы можете транслировать.
reinierpost

Ответы:


12

Вот как это сделать в awk, чтобы не хранить весь файл в массиве. Это в основном тот же алгоритм, что и у Тердона.

При желании вы можете даже указать ему несколько имен файлов в командной строке, и он будет обрабатывать каждый файл независимо, объединяя результаты вместе.

#!/usr/bin/awk -f

#Cartesian product of records

{
    file = FILENAME
    while ((getline line <file) > 0)
        print $0, line
    close(file)
}

В моей системе это занимает примерно 2/3 времени решения perdon от Terdon.


1
Благодарность! Все решения этой проблемы были фантастическими, но я остановился на этом из-за 1) простоты и 2) пребывания в awk. Благодарность!
Том Хейден

1
Рад, что тебе нравится, Том. Сейчас я склонен программировать в основном на Python, но мне все еще нравится awk для построчной обработки текста из-за его встроенных циклов над строками и файлами. И это часто быстрее, чем Python.
PM 2Ring

7

Я не уверен, что это лучше, чем делать это в памяти, но с тем, sedчто rубирает свой инфил для каждой строки в своем инфиле, а другой - по другую сторону канала, чередуя Hстарое пространство с входными строками ...

cat <<\IN >/tmp/tmp
Row1,10
Row2,20
Row3,30
Row4,40
IN

</tmp/tmp sed -e 'i\
' -e 'r /tmp/tmp' | 
sed -n '/./!n;h;N;/\n$/D;G;s/\n/ /;P;D'

ВЫХОД

Row1,10 Row1,10
Row1,10 Row2,20
Row1,10 Row3,30
Row1,10 Row4,40
Row2,20 Row1,10
Row2,20 Row2,20
Row2,20 Row3,30
Row2,20 Row4,40
Row3,30 Row1,10
Row3,30 Row2,20
Row3,30 Row3,30
Row3,30 Row4,40
Row4,40 Row1,10
Row4,40 Row2,20
Row4,40 Row3,30
Row4,40 Row4,40

Я сделал это по-другому. Он хранит некоторые в памяти - он хранит строку вроде:

"$1" -

... для каждой строки в файле.

pairs(){ [ -e "$1" ] || return
    set -- "$1" "$(IFS=0 n=
        case "${0%sh*}" in (ya|*s) n=-1;; (mk|po) n=+1;;esac
        printf '"$1" - %s' $(printf "%.$(($(wc -l <"$1")$n))d" 0))"
    eval "cat -- $2 </dev/null | paste -d ' \n' -- $2"
}

Это очень быстро. Это catфайл столько раз, сколько строк в файле |pipe. С другой стороны канала этот вход объединяется с самим файлом столько раз, сколько строк в файле.

caseМатериал только для портативности - yashи zshкак добавить один элемент к расколу, в то время как mkshи poshоба проигрывают один. ksh, dash, busybox, И bashвсе отщепляются точно так много полей , так как есть нули , как напечатано printf. Как написано выше, результаты дают одинаковые результаты для каждой из вышеупомянутых оболочек на моей машине.

Если файл очень длинный, могут возникнуть $ARGMAXпроблемы со слишком большим количеством аргументов, и в этом случае вам нужно будет ввести xargsили аналогичный.

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

seq 10 10 10000 | nl -s, >/tmp/tmp

Это создает файл, почти идентичный тому, который я использовал ранее (без 'Row') - но в 1000 строк. Вы сами видите, как быстро это происходит:

time pairs /tmp/tmp |wc -l

1000000
pairs /tmp/tmp  0.20s user 0.07s system 110% cpu 0.239 total
wc -l  0.05s user 0.03s system 32% cpu 0.238 total

При 1000 строках есть небольшие различия в производительности между оболочками - они bashвсегда самые медленные - но поскольку единственная работа, которую они выполняют, это генерирование строки arg (1000 копий filename -), эффект минимален. Разница в производительности между zsh- как указано выше - и bashсоставляет сотую долю секунды здесь.

Вот еще одна версия, которая должна работать для файла любой длины:

pairs2()( [ -e "$1" ] || exit
    rpt() until [ "$((n+=1))" -gt "$1" ]
          do printf %s\\n "$2"
          done
    [ -n "${1##*/*}" ] || cd -P -- "${1%/*}" || exit
    : & set -- "$1" "/tmp/pairs$!.ln" "$(wc -l <"$1")"
    ln -s "$PWD/${1##*/}" "$2" || exit
    n=0 rpt "$3" "$2" | xargs cat | { exec 3<&0
    n=0 rpt "$3" p | sed -nf - "$2" | paste - /dev/fd/3
    }; rm "$2"
)

Он создает мягкую ссылку на свой первый аргумент /tmpс полуслучайным именем, чтобы не зацикливаться на странных именах файлов. Это важно, потому что catарги передаются через канал xargs. catВыходные данные сохраняются в <&3то время как sed pwhile печатает каждую строку в первом аргументе столько раз, сколько строк в этом файле - и его сценарий также передается в него через канал. Снова pasteобъединяет свои входные данные, но на этот раз он принимает только два аргумента -для стандартного ввода и имени ссылки /dev/fd/3.

Последнее - /dev/fd/[num]ссылка - должно работать в любой системе linux и многих других, но если оно не создает именованный канал с mkfifoиспользованием этого, вместо этого должно работать.

Последнее, что он делает, это rm это мягкая ссылка, которую он создает перед выходом.

Эта версия на самом деле еще быстрее в моей системе. Я полагаю, это потому, что, хотя он исполняет больше приложений, он сразу же начинает передавать им их аргументы - тогда как прежде чем он сложил их все сначала

time pairs2 /tmp/tmp | wc -l

1000000
pairs2 /tmp/tmp  0.30s user 0.09s system 178% cpu 0.218 total
wc -l  0.03s user 0.02s system 26% cpu 0.218 total

Предполагается, что функция пар находится в файле, если нет, то как бы вы объявили это?

@ Джиддер - как бы мне объявить что? Вы можете просто скопировать + вставить его в терминал, нет?
mikeserv

1
Объявите функцию. Так что вы можете! Я подумал, что вы бы избежали перевода строки, я опасаюсь просто вставить код, спасибо, хотя :) И это очень быстро, хороший ответ!

@Jidder - Я обычно пишу их в живой оболочке, просто используя ctrl+v; ctrl+jдля получения новых строк, как я.
mikeserv

@ Джиддер - большое спасибо. И разумно быть осторожным - хорошо для тебя. Они также будут работать в файле - вы можете скопировать его и . ./file; fn_nameв этом случае.
mikeserv

5

Ну, вы всегда можете сделать это в вашей оболочке:

while read i; do 
    while read k; do echo "$i $k"; done < sample.txt 
done < sample.txt 

Это намного медленнее, чем ваше awkрешение (на моей машине это заняло ~ 11 секунд для 1000 строк, по сравнению с ~ 0,3 секунды вawk ), но, по крайней мере, оно никогда не удерживает в памяти более пары строк.

Цикл выше работает для очень простых данных, которые вы имеете в своем примере. Он задохнется от обратной косой черты и съест отставание и пробелы. Более надежная версия того же:

while IFS= read -r i; do 
    while IFS= read -r k; do printf "%s %s\n" "$i" "$k"; done < sample.txt 
done < sample.txt 

Другой выбор - использовать perlвместо:

perl -lne '$line1=$_; open(A,"sample.txt"); 
           while($line2=<A>){printf "$line1 $line2"} close(A)' sample.txt

Сценарий выше будет читать каждую строку входного файла ( -ln), сохранять его как $l, открывать sample.txtснова и печатать каждую строку вместе с $l. Результатом являются все парные комбинации, в то время как только 2 строки хранятся в памяти. В моей системе это заняло всего около 0.6секунд на 1000 строк.


Вау, спасибо! Интересно, почему Perl-решение намного быстрее, чем оператор bash while
Том Хейден,

@ TomHayden в основном потому, что perl, как и awk, намного быстрее, чем bash.
Тердон

1
Пришлось понизить голос за ваш цикл. 4 различных плохих практики там. Ты знаешь лучше.
Стефан Шазелас

1
@ StéphaneChazelas хорошо, основываясь на вашем ответе здесь , я не мог придумать ни одного случая, когда это echoможет быть проблемой. То, что я написал (я добавил printfсейчас), должно работать со всеми из них правильно? Что касается whileцикла, почему? Что не так с while read f; do ..; done < file? Конечно, вы не предлагаете forпетлю! Какая другая альтернатива?
Terdon

2
@cuonglm, это только намек на одну возможную причину, почему следует избегать этого. Из концептуальных , надежность , разборчивость , производительности и безопасности аспектов, что покрывает только надежность .
Стефан Шазелас

4

С zsh:

a=(
Row1,10
Row2,20
Row3,30
Row4,40
)
printf '%s\n' $^a' '$^a

$^aв массиве включается в скобки расширение (как в {elt1,elt2}) для массива.


4

Вы можете скомпилировать этот код для довольно быстрых результатов.
Это завершается примерно за 0,19 - 0,27 секунды в файле из 1000 строк.

В настоящее время он считывает 10000строки в память (для ускорения печати на экран), который, если бы у вас было 1000символов в строке, использовал бы меньше 10mbпамяти, что, я не думаю, было бы проблемой. Вы можете полностью удалить этот раздел и просто распечатать его на экране, если это действительно вызывает проблемы.

Вы можете скомпилировать, используя g++ -o "NAME" "NAME.cpp"
Где NAMEимя файла, чтобы сохранить его и NAME.cppфайл, в котором этот код сохранен

CTEST.cpp:

#include <iostream>
#include <string>
#include <fstream>
#include <iomanip>
#include <cstdlib>
#include <sstream>
int main(int argc,char *argv[])
{

        if(argc != 2)
        {
                printf("You must provide at least one argument\n"); // Make                                                                                                                      sure only one arg
                exit(0);
   }
std::ifstream file(argv[1]),file2(argv[1]);
std::string line,line2;
std::stringstream ss;
int x=0;

while (file.good()){
    file2.clear();
    file2.seekg (0, file2.beg);
    getline(file, line);
    if(file.good()){
        while ( file2.good() ){
            getline(file2, line2);
            if(file2.good())
            ss << line <<" "<<line2 << "\n";
            x++;
            if(x==10000){
                    std::cout << ss.rdbuf();
                    ss.clear();
                    ss.str(std::string());
            }
    }
    }
}
std::cout << ss.rdbuf();
ss.clear();
ss.str(std::string());
}

демонстрация

$ g++ -o "Stream.exe" "CTEST.cpp"
$ seq 10 10 10000 | nl -s, > testfile
$ time ./Stream.exe testfile | wc -l
1000000

real    0m0.243s
user    0m0.210s
sys     0m0.033s

3
join -j 2 file.txt file.txt | cut -c 2-
  • объединить несуществующее поле и удалить первый пробел

Поле 2 является пустым и равным для всех элементов в file.txt, поэтому joinбудет объединять каждый элемент со всеми остальными: оно фактически вычисляет декартово произведение.


2

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

import mmap
import re
with open('test.file', 'rt') as f1, open('test.file') as f2:
    with mmap.mmap(f1.fileno(), 0, flags=mmap.MAP_SHARED, access=mmap.ACCESS_READ) as m1,\
        mmap.mmap(f2.fileno(), 0, flags=mmap.MAP_SHARED, access=mmap.ACCESS_READ) as m2:
        for line1 in re.finditer(b'.*?\n', m1):
            for line2 in re.finditer(b'.*?\n', m2):
                print('{} {}'.format(line1.group().decode().rstrip(),
                    line2.group().decode().rstrip()))
            m2.seek(0)

Альтернативное быстрое решение в Python, хотя эффективность памяти все еще может быть проблемой

from itertools import product
with open('test.file') as f:
    for a, b  in product(f, repeat=2):
        print('{} {}'.format(a.rstrip(), b.rstrip()))
Row1,10 Row1,10
Row1,10 Row2,20
Row1,10 Row3,30
Row1,10 Row4,40
Row2,20 Row1,10
Row2,20 Row2,20
Row2,20 Row3,30
Row2,20 Row4,40
Row3,30 Row1,10
Row3,30 Row2,20
Row3,30 Row3,30
Row3,30 Row4,40
Row4,40 Row1,10
Row4,40 Row2,20
Row4,40 Row3,30
Row4,40 Row4,40

Не будет ли это, по определению, хранить весь файл в памяти? Я не знаю Python, но ваш язык определенно предполагает, что так и будет.
Тердон

1
@terdon, если вы имеете в виду решение для отображения памяти, ОС будет прозрачно хранить в памяти только ту часть файла, которую она может себе позволить, основываясь на доступной физической памяти. Объем доступной физической памяти не должен превышать размер файла (хотя наличие дополнительной физической памяти, очевидно, было бы выгодной ситуацией). В худшем случае это может привести к снижению скорости зацикливания файла на диске или хуже. Основным преимуществом этого подхода является прозрачное использование доступной физической ОЗУ, поскольку оно может
меняться

1

В bash ksh также должен работать, используя только встроенные функции оболочки:

#!/bin/bash
# we require array support
d=( $(< sample.txt) )
# quote arguments and
# build up brace expansion string
d=$(printf -- '%q,' "${d[@]}")
d=$(printf -- '%s' "{${d%,}}' '{${d%,}}")
eval printf -- '%s\\n' "$d"

Обратите внимание, что, хотя он хранит весь файл в памяти в переменной оболочки, ему нужен только один доступ для чтения к нему.


1
Я думаю, что весь смысл для OP состоит в том, чтобы не держать файл в памяти. В противном случае их нынешний подход к gawk проще и намного быстрее. Я предполагаю, что это должно работать с текстовыми файлами размером в несколько гигабайт.
Terdon

Да, это совершенно правильно - у меня есть пара ОГРОМНЫХ файлов данных, с которыми мне нужно это сделать, и я не хочу их хранить в памяти
Том Хейден,

Если вы ограничены памятью, я бы порекомендовал использовать одно из решений @terdon
Franki

0

sed решение.

line_num=$(wc -l < input.txt)
sed 'r input.txt' input.txt | sed -re "1~$((line_num + 1)){h;d}" -e 'G;s/(.*)\n(.*)/\2 \1/'

Объяснение:

  • sed 'r file2' file1 - прочитать все содержимое файла file2 для каждой строки file1.
  • Конструкция 1~iозначает 1-ю строку, затем 1 + i строку, 1 + 2 * i, 1 + 3 * i и т. Д. Следовательно, 1~$((line_num + 1)){h;d}означает hстарую dуказанную линию для буфера, выборочное пространство шаблона и начало нового цикла.
  • 'G;s/(.*)\n(.*)/\2 \1/'- для всех строк, кроме выбранных на предыдущем шаге, выполните следующее: Get line из буфера для удержания и добавьте его к текущей строке. Затем поменяйте местами строки. Был current_line\nbuffer_line\n, сталbuffer_line\ncurrent_line\n

Выход

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