Інструмент командного рядка для "кішки" попарно розширення всіх рядків у файлі


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), щоб обчислити деякий показник щодо цієї попарної комбінації.

У мене є спосіб зробити це дивним чином, але я занепокоєний тим, що моє використання блоку 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

Ось як це зробити дивним чином, щоб не потрібно було зберігати весь файл у масиві. Це в основному той же алгоритм, що і тердон.

Якщо вам подобається, ви можете навіть надати йому декілька імен файлів у командному рядку, і він обробить кожен файл самостійно, об'єднавши результати разом.

#!/usr/bin/awk -f

#Cartesian product of records

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

У моїй системі це працює приблизно за 2/3 часу рішення Perl тердона.


1
Спасибі! Всі рішення цієї проблеми були фантастичними, але я закінчив цю проблему завдяки 1) простоті та 2) перебуванню в див. Спасибі!
Том Хейден

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файл стільки разів, скільки є файлів у файлі a |pipe. З іншого боку труби, що вводиться, об'єднується з самим файлом стільки разів, скільки є файли у файлі.

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

Якщо файл дуже довгий, можуть виникнути $ARGMAXпроблеми із занадто великою кількістю аргументів, і в цьому випадку вам також потрібно буде ввести xargsабо подібні.

З огляду на той же вхід, який я використовував до виходу ідентичний. Але, якби я став більшим ...

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

Це генерує файл, майже ідентичний тому, що я використовував раніше (sans '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це незмінно найповільніше - але тому, що єдиною роботою, яку вони роблять, все-таки є генерування рядка аргументу (1000 копій filename -), ефект мінімальний. Різниця в продуктивності між zsh- як вище - і bashстановить 100-ту секунду.

Ось ще одна версія, яка повинна працювати для файлу будь-якої довжини:

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 pзаписує кожен рядок у першому аргументі стільки разів, скільки є рядки у цьому файлі - і його сценарій також подається на нього через трубу. Знову 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

Чи функція пар припускає бути у файлі, якщо ні, як би ви це оголосили?

@Jidder - як я декларував би що? Ви можете просто скопіювати + вставити його в термінал, ні?
mikeserv

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

@ Jidder - Зазвичай я записую їх у живу оболонку, просто використовуючи ctrl+v; ctrl+jдля отримання нових рядків, як я.
mikeserv

@Jidder - дуже дякую І розумно бути обережними - добре тобі. Вони також працюватимуть у файлі - ви можете скопіювати його і . ./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 настільки швидше, ніж баш, тоді як заява
Том Хейден,

@TomHayden в основному тому, що perl, як awk, набагато швидше, ніж bash.
terdon

1
Довелося подати заявку на цикл while. 4 різні погані практики. Ти краще знаєш.
Стефан Шазелас

1
@ StéphaneChazelas добре, виходячи з вашої відповіді тут , я не міг придумати жодних випадків, де це echoможе бути проблемою. Те, що я написав (я додав printfзараз), має працювати з усіма ними правильно? Щодо whileпетлі, чому? Що не так while read f; do ..; done < file? Звичайно, ви не пропонуєте forциклу! Яка інша альтернатива?
terdon

2
@cuonglm, це лише натякає на одну можливу причину, чому слід уникати цього. З точки зору концептуальної , надійності , розбірливості , продуктивності та безпеки , це покриває лише надійність .
Stéphane Chazelas

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, якщо ви посилаєтесь на рішення для картографування пам'яті, ОС прозоро збереже лише стільки файлу в пам'яті, скільки може дозволити на основі наявної фізичної оперативної пам'яті. Наявна фізична ОЗУ не повинна перевищувати розмір файлу (хоча наявність додаткової фізичної ОЗУ, очевидно, буде вигідною ситуацією). У гіршому випадку це може погіршитись до швидкості прокручування файлу на диску чи гірше. Ключовою перевагою такого підходу є прозоре використання доступної фізичної оперативної пам’яті, оскільки це могло коливатися з часом
iruvar

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
Я думаю, що вся справа в ОП - це не тримати файл в пам'яті. В іншому випадку їх поточний підхід є простішим та набагато швидшим. Я здогадуюсь, що для цього потрібно працювати з текстовими файлами розміром кілька гігабайт.
terdon

Так, це абсолютно правильно - у мене є кілька ВЕЛИЧЕЗНИХ файлів даних, з якими мені потрібно це зробити і не хочу зберігати в пам’яті
Том Хейден

У разі , якщо ви що по пам'яті зі зв'язками, я рекомендував би використовувати один з розчинів від @terdon
Франки

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 для кожного рядка файлу1.
  • Побудова 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 рядок із буфера утримування та додайте його до поточного рядка. Потім поміняйте місцями рядки. Був 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.