Narzędzie wiersza poleceń do „cat” parowego rozwijania wszystkich wierszy w pliku

13

Załóżmy, że mam plik (nazwij go sample.txt), który wygląda następująco:

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

Chcę mieć możliwość pracy ze strumieniem z tego pliku, który jest w zasadzie parą kombinacji wszystkich czterech wierszy (więc powinniśmy mieć w sumie 16). Na przykład szukam polecenia przesyłania strumieniowego (tzn. Wydajnego), którego wynikiem jest:

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

Mój przypadek użycia polega na tym, że chcę przesłać strumień danych wyjściowych do innego polecenia (takiego jak awk), aby obliczyć niektóre dane dotyczące tej kombinacji par.

Mam na to sposób w awk, ale martwię się, że użycie bloku END {} oznacza, że ​​zasadniczo przechowuję cały plik w pamięci przed wyjściem. Przykładowy kod:

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

Czy istnieje skuteczny sposób przesyłania strumieniowego, aby to zrobić bez konieczności przechowywania pliku w pamięci, a następnie wyprowadzania go do bloku END?

Tom Hayden
źródło
1
Zawsze będziesz musiał przeczytać jeden plik do końca, zanim będziesz mógł zacząć generować dane wyjściowe dla drugiego wiersza drugiego pliku. Drugi plik, który możesz przesyłać strumieniowo.
reinierpost

Odpowiedzi:

12

Oto jak to zrobić w awk, aby nie musiał przechowywać całego pliku w tablicy. Jest to zasadniczo ten sam algorytm co terdon.

Jeśli chcesz, możesz nawet nadać mu wiele nazw plików w wierszu poleceń i będzie przetwarzał każdy plik niezależnie, łącząc wyniki razem.

#!/usr/bin/awk -f

#Cartesian product of records

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

W moim systemie działa to około 2/3 czasu rozwiązania perla Terdona.

PM 2 Ring
źródło
1
Dzięki! Wszystkie rozwiązania tego problemu były fantastyczne, ale ostatecznie wybrałem ten z powodu 1) prostoty i 2) pozostawania w awk. Dzięki!
Tom Hayden
1
Cieszę się, że ci się podoba, Tom. Obecnie programuję głównie w języku Python, ale nadal lubię awk do przetwarzania tekstu wiersz po wierszu, ponieważ ma wbudowane pętle nad wierszami i plikami. I często jest szybszy niż Python.
PM 2, pierścień
7

Nie jestem pewien, czy jest to lepsze niż robienie tego w pamięci, ale z tym, sedktóry roddziela swój infile dla każdej linii w swoim infile i innym po drugiej stronie potoku na przemian ze Hstarą przestrzenią z liniami wejściowymi ...

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'

WYNIK

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

Zrobiłem to w inny sposób. Przechowuje niektóre elementy w pamięci - przechowuje ciąg taki jak:

"$1" -

... dla każdej linii w pliku.

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"
}

To jest bardzo szybkie. Jest catto plik tyle razy, ile jest linii w pliku do |pipe. Po drugiej stronie potoku dane wejściowe są scalane z samym plikiem tyle razy, ile jest linii w pliku.

caseRzeczy jest właśnie dla przenośności - yashi zshzarówno dodatek jeden element do rozłamu, podczas mkshi poshzarówno stracić. ksh, dash, busybox, I bashwszystko podzielonego się dokładnie tak, jak wielu dziedzinach jak istnieją zera podany przez printf. Jak napisano powyżej, renderuje takie same wyniki dla każdej z wyżej wymienionych powłok na moim komputerze.

Jeśli plik jest bardzo długi, mogą występować $ARGMAXproblemy ze zbyt dużą liczbą argumentów, w którym to przypadku należy wprowadzić xargslub podobne.

Biorąc pod uwagę to samo wejście, którego użyłem przed wyjściem jest identyczne. Ale gdybym miał zwiększyć ...

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

To generuje plik prawie identyczny z tym, którego użyłem wcześniej (bez wiersza) - ale w 1000 linii. Możesz sam przekonać się, jak szybko to jest:

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

Przy 1000 liniach występuje niewielka różnica w wydajności między powłokami - bashjest niezmiennie najwolniejsza - ale ponieważ jedyną pracą, którą wykonują, jest generowanie ciągu arg (1000 kopii filename -), efekt jest minimalny. Różnica w wydajności między zsh- jak wyżej - i bashwynosi tutaj setną sekundy.

Oto kolejna wersja, która powinna działać dla pliku o dowolnej długości:

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"
)

Tworzy miękkie łącze do pierwszego argumentu /tmpz pół losową nazwą, aby nie rozłączać się z dziwnymi nazwami plików. To ważne, ponieważ catargony są podawane do niego za pośrednictwem rury za pośrednictwem xargs. cat„s wyjście jest zapisywany <&3podczas sed prints każdy wiersz w pierwszej arg tyle razy, ile jest linii w tym pliku - a jej scenariusz jest także podawany do niego rurą. Ponownie pastełączy dane wejściowe, ale tym razem wymaga tylko dwóch argumentów -dla standardowego wejścia i nazwy łącza /dev/fd/3.

To ostatnie - /dev/fd/[num]link - powinno działać na każdym systemie linux i wielu innych oprócz, ale jeśli nie tworzy nazwanego potoku mkfifoi używanie go zamiast tego powinno również działać.

Ostatnią rzeczą, jaką robi, jest rmmiękkie łącze, które tworzy przed wyjściem.

Ta wersja jest jeszcze szybsza w moim systemie. Wydaje mi się, że dzieje się tak, ponieważ chociaż uruchamia więcej aplikacji, natychmiast przekazuje im swoje argumenty - a zanim najpierw ułożył je wszystkie w stos.

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
źródło
Czy funkcja par powinna znajdować się w pliku, jeśli nie, to jak byś ją zadeklarował?
@Jidder - jak mam zadeklarować co? Możesz po prostu skopiować i wkleić go do terminala, nie?
mikeserv
1
Zadeklaruj funkcję. Więc możesz! Myślałem, że będziesz mieć nowe znaki ucieczki, boję się po prostu wkleić kod, dziękuję jednak :) To też jest bardzo szybka, ładna odpowiedź!
@Jidder - Zazwyczaj piszę je w żywej powłoce, ctrl+v; ctrl+jaby uzyskać nowe wiersze.
mikeserv
@Jidder - dziękuję bardzo. I mądrze jest być ostrożnym - dobrze dla ciebie. Będą również działać w pliku - możesz go skopiować i . ./file; fn_namew takim przypadku.
mikeserv
5

Cóż, zawsze możesz to zrobić w swojej powłoce:

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

Jest znacznie wolniejszy niż twoje awkrozwiązanie (na moim komputerze zajęło to około 11 sekund na 1000 linii, w porównaniu do około 0,3 sekundy awk), ale przynajmniej nie ma więcej niż kilku linii w pamięci.

Pętla powyżej działa dla bardzo prostych danych, które masz w swoim przykładzie. Dusi się na odwrotnych ukośnikach i zjada spacje końcowe i wiodące. Bardziej niezawodna wersja tego samego jest:

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

Innym wyborem jest użycie perlzamiast tego:

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

Powyższy skrypt odczyta każdy wiersz pliku wejściowego ( -ln), zapisze go jako $l, sample.txtponownie otworzy i wydrukuje każdy wiersz wraz z $l. Wynikiem są wszystkie kombinacje par, podczas gdy tylko 2 linie są zawsze przechowywane w pamięci. W moim systemie zajęło to tylko około 0.6sekund na 1000 linii.

terdon
źródło
Wow, dzięki! Zastanawiam się, dlaczego perlowe rozwiązanie jest o wiele szybsze niż bash podczas oświadczenia
Tom Hayden
@TomHayden po prostu dlatego, że perl, podobnie jak awk, jest znacznie szybszy niż bash.
terdon
1
Musiałem głosować za pętlą while. 4 różne złe praktyki. Wiesz lepiej.
Stéphane Chazelas
1
@ StéphaneChazelas dobrze, w oparciu o twoją odpowiedź tutaj , nie mogłem wymyślić żadnych przypadków, w których echomoże to być problem. To, co napisałem (dodałem printfteraz), powinno działać z nimi wszystkimi, prawda? Co do whilepętli, dlaczego? Co jest nie tak z while read f; do ..; done < file? Na pewno nie sugerujesz forpętli! Jaka jest inna alternatywa?
terdon
2
@cuonglm, ten wskazuje tylko na jeden możliwy powód, dla którego należy go unikać. Z aspektów koncepcyjnych , niezawodności , czytelności , wydajności i bezpieczeństwa , które obejmują tylko niezawodność .
Stéphane Chazelas
4

Z zsh:

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

$^ana tablicy włącza interpretację nawiasów klamrowych (np. in {elt1,elt2}) dla tablicy.

Stéphane Chazelas
źródło
4

Możesz skompilować ten kod aby uzyskać dość szybkie wyniki.
Wykonuje się w około 0,19 - 0,27 sekundy na pliku linii 1000.

Obecnie odczytuje 10000wiersze do pamięci (aby przyspieszyć drukowanie do ekranu), co gdybyś miał 1000znaki w wierszu, zużyłoby mniej niż 10mbpamięć, co nie sądzę, że stanowiłoby problem. Możesz jednak całkowicie usunąć tę sekcję i po prostu wydrukować bezpośrednio na ekranie, jeśli spowoduje to problem.

Możesz skompilować za pomocą g++ -o "NAME" "NAME.cpp"
Where gdzie NAMEjest nazwa pliku do zapisania i NAME.cppjest plikiem, w którym zapisany jest ten kod

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());
}

Demonstracja

$ 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

źródło
3
join -j 2 file.txt file.txt | cut -c 2-
  • połącz nieistniejącym polem i usuń pierwszą spację

Pole 2 jest puste i równe dla wszystkich elementów w pliku.txt, więc joinpołączy każdy element ze wszystkimi innymi: w rzeczywistości oblicza iloczyn kartezjański.

JJoao
źródło
2

Jedną z opcji w Pythonie jest mapowanie pamięci pliku i skorzystanie z faktu, że biblioteka wyrażeń regularnych Python może pracować bezpośrednio z plikami mapowanymi w pamięci. Chociaż wygląda to na uruchamianie zagnieżdżonych pętli nad plikiem, mapowanie pamięci zapewnia, że ​​system operacyjny optymalnie wykorzystuje dostępną fizyczną pamięć RAM

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)

Alternatywnie szybkie rozwiązanie w Pythonie, chociaż wydajność pamięci może nadal stanowić problem

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
iruvar
źródło
Czy z definicji nie utrzyma to całego pliku w pamięci? Nie znam Pythona, ale twój język na pewno to sugeruje.
terdon
1
@terdon, jeśli masz na myśli rozwiązanie mapowania pamięci, system operacyjny przejrzyście przetrzymuje tylko tyle plików w pamięci, ile może sobie pozwolić na podstawie dostępnej fizycznej pamięci RAM. Dostępna fizyczna pamięć RAM nie musi przekraczać rozmiaru pliku (chociaż posiadanie dodatkowej fizycznej pamięci RAM byłoby oczywiście korzystną sytuacją). W najgorszym przypadku może to obniżyć się do prędkości zapętlania pliku na dysku lub gorzej. Kluczową zaletą tego podejścia jest przejrzyste wykorzystanie dostępnej fizycznej pamięci RAM, ponieważ może się ona zmieniać w czasie
iruvar,
1

W bash ksh powinien również działać, używając tylko wbudowanych powłok:

#!/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"

Zauważ, że chociaż przechowuje on cały plik w pamięci w zmiennej powłoki, potrzebuje tylko jednego dostępu do odczytu.

Franki
źródło
1
Myślę, że celem OP jest to, aby nie przechowywać pliku w pamięci. W przeciwnym razie ich obecne podejście do gawk jest zarówno prostsze, jak i znacznie szybsze. Zgaduję, że to musi działać z plikami tekstowymi o rozmiarze kilku gigabajtów.
terdon
Tak, to jest dokładnie poprawne - mam kilka OGROMNYCH plików danych, z którymi muszę to zrobić i nie chcę przechowywać w pamięci
Tom Hayden
Jeśli jesteś ograniczony przez pamięć, polecam użycie jednego z rozwiązań @terdon
Franki,
0

sed rozwiązanie.

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/'

Wyjaśnienie:

  • sed 'r file2' file1 - przeczytaj całą zawartość pliku file2 dla każdego wiersza pliku1.
  • Konstrukcja 1~ioznacza linię 1, następnie linię 1 + i, 1 + 2 * i, 1 + 3 * i itd. Dlatego 1~$((line_num + 1)){h;d}oznacza hstarą szpiczastą linię do bufora, dusuwając przestrzeń wzoru i rozpoczynając nowy cykl.
  • 'G;s/(.*)\n(.*)/\2 \1/'- dla wszystkich linii, z wyjątkiem wybranych w poprzednim kroku, wykonaj next: Get linia z bufora wstrzymania i dołącz ją do bieżącej linii. Następnie zamień miejsca linii. Był current_line\nbuffer_line\n, stał siębuffer_line\ncurrent_line\n

Wynik

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
MiniMax
źródło