Befehlszeilen-Tool zum paarweisen Erweitern aller Zeilen in einer Datei


13

Angenommen, ich habe eine Datei (nenne sie sample.txt), die so aussieht:

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

Ich möchte in der Lage sein, an einem Stream aus dieser Datei zu arbeiten, der im Wesentlichen die paarweise Kombination aller vier Zeilen ist (also sollten wir insgesamt 16 erhalten). Ich suche zum Beispiel nach einem Streaming-Befehl (dh einem effizienten Befehl), bei dem die Ausgabe wie folgt lautet:

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

Mein Anwendungsfall ist, dass ich diese Ausgabe in einen anderen Befehl (wie awk) streamen möchte, um eine Metrik für diese paarweise Kombination zu berechnen.

Ich habe eine Möglichkeit, dies in awk zu tun, aber meine Sorge ist, dass meine Verwendung des END {} -Blocks bedeutet, dass ich die gesamte Datei vor der Ausgabe im Speicher speichere. Beispielcode:

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

Gibt es eine effiziente Möglichkeit zum Streamen, ohne dass die Datei im Wesentlichen im Speicher gespeichert und dann im END-Block ausgegeben werden muss?


1
Sie müssen immer eine Datei bis zum Ende lesen, bevor Sie mit der Ausgabe für die zweite Zeile der anderen Datei beginnen können. Die andere Datei können Sie streamen.
Reinierpost

Antworten:


12

Gehen Sie wie folgt vor, damit nicht die gesamte Datei in einem Array gespeichert werden muss. Dies ist im Grunde der gleiche Algorithmus wie bei Terdon.

Wenn Sie möchten, können Sie ihm sogar mehrere Dateinamen in der Befehlszeile geben, und es verarbeitet jede Datei unabhängig und verknüpft die Ergebnisse miteinander.

#!/usr/bin/awk -f

#Cartesian product of records

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

Auf meinem System läuft dies in ungefähr 2/3 der Zeit von Terdons Perl-Lösung.


1
Vielen Dank! Alle Lösungen für dieses Problem waren fantastisch, aber ich habe mich letztendlich für diese entschieden, weil ich 1) einfach war und 2) in awk blieb. Vielen Dank!
Tom Hayden

1
Schön, dass es dir gefällt, Tom. Ich programmiere heutzutage meistens in Python, aber ich mag awk immer noch für die zeilenweise Textverarbeitung, weil es eingebaute Schleifen über Zeilen und Dateien gibt. Und es ist oft schneller als Python.
PM 2Ring

7

Ich bin mir nicht sicher, ob dies besser ist, als es im Speicher zu tun, aber mit einem sed, rdas seine Infile für jede Zeile in seiner Infile ausliest, und einem anderen auf der anderen Seite einer Pipe, die Halte Leerzeichen mit Eingabezeilen abwechselt ...

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'

AUSGABE

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

Ich habe das anders gemacht. Es speichert einige im Speicher - es speichert eine Zeichenfolge wie:

"$1" -

... für jede Zeile in der Datei.

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

Es ist sehr schnell. Es ist catdie Datei so oft wie es Zeilen in der Datei zu a gibt |pipe. Auf der anderen Seite der Pipe wird diese Eingabe so oft mit der Datei selbst zusammengeführt, wie sich Zeilen in der Datei befinden.

Das caseZeug ist nur für die Portabilität - yashund zshbeide fügen ein Element zum Split hinzu, während mkshund poshbeide eines verlieren. ksh, dash, busybox, Und bashalle Split, um genau so viele Felder wie es Nullen sind als gedruckte durchprintf . Wie oben beschrieben, werden auf meinem Computer für alle oben genannten Shells die gleichen Ergebnisse erzielt.

Wenn die Datei sehr lang ist, kann es zu $ARGMAXProblemen mit zu vielen Argumenten kommen, die Sie in diesem Fall einführen müsstenxargs oder Ähnliches.

Bei der gleichen Eingabe, die ich vor der Ausgabe verwendet habe, ist sie identisch. Aber wenn ich größer würde ...

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

Das erzeugt eine Datei, die fast mit der zuvor verwendeten identisch ist (ohne Zeile) - aber mit 1000 Zeilen. Sie können selbst sehen, wie schnell es ist:

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

Bei 1000 Zeilen gibt es einige geringfügige Leistungsunterschiede zwischen den Shells - dies bashist ausnahmslos die langsamste -, aber da sie sowieso nur die Arg-Zeichenfolge (1000 Kopien filename -) generieren, ist der Effekt minimal. Der Leistungsunterschied zwischen zsh- wie oben - undbash beträgt hier 100stel Sekunden.

Hier ist eine andere Version, die für eine Datei beliebiger Länge funktionieren sollte:

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

Es erstellt einen Softlink zu seinem ersten Argument /tmpmit einem halb-zufälligen Namen, damit es nicht an seltsamen Dateinamen hängen bleibt. Das ist wichtig, weil catdie Argumente über eine Leitung zugeführt werden xargs. cat‚s Ausgabe wird gespeichert , <&3während sed pRints jede Zeile in dem ersten arg so oft wie es Zeilen in dieser Datei ist - und sein Skript wird auch über ein Rohr , um es zugeführt. Wieder pasteverschmilzt seine Eingabe, aber dieses Mal ist es dauert nur zwei Argumente -noch einmal für seine Standard - Eingabe und der Linknamen /dev/fd/3.

Das Letzte - der /dev/fd/[num]Link - sollte auf jedem Linux-System und vielen anderen funktionieren, aber wenn es keine Named Pipe erstellt mkfifound stattdessen verwendet, sollte es auch funktionieren.

Als letztes wird rmder Softlink erstellt, der vor dem Beenden erstellt wird.

Diese Version ist eigentlich noch schneller auf meinem System. Ich vermute, es liegt daran, dass es, obwohl es mehr Anwendungen ausführt, beginnt, ihnen ihre Argumente sofort zu übergeben - wohingegen, bevor es sie alle zuerst stapelte.

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

Soll sich die Funktion pairs in einer Datei befinden, wenn nicht, wie würden Sie sie deklarieren?

@Jidder - wie würde ich was deklarieren? Sie können es einfach kopieren und in ein Terminal einfügen, nicht wahr?
mikeserv

1
Deklarieren Sie die Funktion. Ich dachte, du hättest Flucht-Zeilenumbrüche, ich bin jedoch vorsichtig, wenn ich nur Code einfüge, danke :) Auch das ist eine extrem schnelle, nette Antwort!

@Jidder - Normalerweise schreibe ich diese in einer Live-Shell, nur ctrl+v; ctrl+jum wie ich Zeilenumbrüche zu erhalten.
mikeserv

@Jidder - vielen Dank. Und es ist ratsam, vorsichtig zu sein - gut für Sie. Sie funktionieren auch in einer Datei - Sie können sie in und . ./file; fn_namein diesem Fall kopieren .
mikeserv

5

Nun, Sie könnten es immer in Ihrer Shell tun:

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

Es ist viel langsamer als Ihre awkLösung (auf meinem Computer dauerte es ~ 11 Sekunden für 1000 Zeilen, verglichen mit ~ 0,3 Sekunden in awk), aber es speichert immerhin nie mehr als ein paar Zeilen im Speicher.

Die obige Schleife funktioniert für die sehr einfachen Daten, die Sie in Ihrem Beispiel haben. Es verschluckt sich an Backslashes und frisst nachgestellte und führende Leerzeichen. Eine robustere Version desselben ist:

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

Eine andere Möglichkeit ist, perlstattdessen Folgendes zu verwenden :

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

Das obige Skript liest jede Zeile der Eingabedatei ( -ln), speichert sie als $l, öffnet sie sample.txterneut und druckt jede Zeile zusammen mit $l. Das Ergebnis sind alle paarweisen Kombinationen, während immer nur 2 Zeilen gespeichert sind. Auf meinem System dauerte das 0.6auf 1000 Zeilen nur ungefähr Sekunden.


Wow, danke! Ich frage mich, warum die Perl-Lösung so viel schneller ist als die Bash-While-Anweisung
Tom Hayden,

@TomHayden im Grunde, weil Perl, wie awk, viel schneller ist als bash.
Terdon

1
Musste für deine while-Schleife abstimmen. 4 verschiedene schlechte Praktiken drin. Du weißt es besser.
Stéphane Chazelas

1
@StéphaneChazelas gut, basierend auf deiner Antwort hier , konnte ich mir keine Fälle vorstellen, in denen das echoein Problem sein könnte. Was ich geschrieben hatte (ich fügte printfjetzt hinzu ), sollte mit allen richtig funktionieren? Was die whileSchleife betrifft, warum? Was ist los mit while read f; do ..; done < file? Sicherlich schlagen Sie keine forSchleife vor! Was ist die andere Alternative?
Terdon

2
@cuonglm, dass man nur auf einen möglichen Grund hinweist, warum man es vermeiden sollte. Aus konzeptionellen , Zuverlässigkeits- , Lesbarkeits- , Leistungs- und Sicherheitsaspekten , die nur die Zuverlässigkeit abdecken .
Stéphane Chazelas

4

Mit zsh:

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

$^aon a Array aktiviert die geschweifte Erweiterung (like in {elt1,elt2}) für das Array.


4

Sie können diesen Code für recht schnelle Ergebnisse kompilieren .
Bei einer 1000-Zeilen-Datei dauert es ungefähr 0,19 bis 0,27 Sekunden.

Derzeit werden 10000Zeilen in den Speicher eingelesen (um das Drucken auf dem Bildschirm zu beschleunigen). Wenn Sie 1000Zeichen pro Zeile hätten, würden Sie weniger als benötigen10mb Speicher ich für ein Problem halten würde. Sie können diesen Abschnitt jedoch vollständig entfernen und ihn direkt auf dem Bildschirm ausdrucken, wenn dies dennoch zu Problemen führt.

Sie können kompilieren, indem Sie g++ -o "NAME" "NAME.cpp"
Wo NAMEist der Name der Datei, in NAME.cppder sie gespeichert werden soll , und in der Datei, in der dieser Code gespeichert wird

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

Demonstration

$ 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-
  • Verbinden Sie durch ein nicht vorhandenes Feld und entfernen Sie das erste Leerzeichen

Feld 2 ist leer und für alle Elemente in file.txt gleich, sodass joinjedes Element mit allen anderen verknüpft wird: Es berechnet das kartesische Produkt.


2

Eine Option mit Python ist die Speicherzuordnung der Datei und die Ausnutzung der Tatsache, dass die Python-Bibliothek für reguläre Ausdrücke direkt mit Dateien mit Speicherzuordnung arbeiten kann. Obwohl dies den Anschein hat, als würden verschachtelte Schleifen über die Datei laufen, stellt die Speicherzuordnung sicher, dass das Betriebssystem den verfügbaren physischen RAM optimal zur Geltung bringt

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)

Alternativ eine schnelle Lösung in Python, obwohl die Speichereffizienz immer noch ein Problem sein könnte

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

Wird das nicht per Definition die gesamte Datei im Speicher behalten? Ich kenne Python nicht, aber Ihre Sprache legt es mit Sicherheit nahe.
Terdon

1
@terdon, wenn Sie sich auf die Speicherzuordnungslösung beziehen, behält das Betriebssystem transparent nur so viel von der Datei im Speicher, wie es sich leisten kann, basierend auf dem verfügbaren physischen RAM. Der verfügbare physische RAM muss die Dateigröße nicht überschreiten (obwohl ein zusätzlicher physischer RAM offensichtlich eine vorteilhafte Situation wäre). Im schlimmsten Fall kann dies die Geschwindigkeit des Durchschleifens von Dateien auf der Festplatte beeinträchtigen oder sogar verschlechtern. Der Hauptvorteil bei diesem Ansatz ist die transparente Nutzung des verfügbaren physischen Arbeitsspeichers, da dies im Laufe der Zeit schwanken kann
iruvar

1

In bash sollte ksh ebenfalls funktionieren und nur Shell-Built-Ins verwenden:

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

Beachten Sie, dass hierdurch zwar die gesamte Datei in einer Shell-Variablen gespeichert wird, jedoch nur ein einziger Lesezugriff erforderlich ist.


1
Ich denke, der springende Punkt für das OP ist, die Datei nicht im Speicher zu halten. Ansonsten ist ihr gegenwärtiger Gawk-Ansatz sowohl einfacher als auch viel schneller. Ich vermute, dies muss mit Textdateien funktionieren, die mehrere Gigabyte groß sind.
Terdon

Ja, das ist genau richtig - ich habe ein paar RIESIGE Datendateien, mit denen ich das machen muss und die ich nicht im Gedächtnis behalten möchte
Tom Hayden

Für den Fall, dass Sie durch den Speicher eingeschränkt sind, würde ich empfehlen, eine der Lösungen von @terdon
Franki

0

sed Lösung.

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

Erläuterung:

  • sed 'r file2' file1 - Den gesamten Dateiinhalt von Datei2 für jede Zeile von Datei1 lesen.
  • Konstruktion 1~ibedeutet 1-te Zeile, dann 1 + i Zeile, 1 + 2 * i, 1 + 3 * i usw. Daher 1~$((line_num + 1)){h;d}bedeutet halte spitze Zeile zum Puffer,d lösche Musterraum und beginne neuen Zyklus.
  • 'G;s/(.*)\n(.*)/\2 \1/'- GFühren Sie für alle Zeilen, mit Ausnahme der im vorherigen Schritt ausgewählten, Folgendes aus: et-Zeile aus dem Haltepuffer und fügen Sie sie an die aktuelle Zeile an. Tauschen Sie dann die Linienstellen aus. Wurde current_line\nbuffer_line\n, wurdebuffer_line\ncurrent_line\n

Ausgabe

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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.