Alat baris perintah untuk "cat" ekspansi berpasangan semua baris dalam file

13

Misalkan saya memiliki file (sebut saja sample.txt) yang terlihat seperti ini:

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

Saya ingin dapat bekerja pada aliran dari file ini yang pada dasarnya adalah kombinasi berpasangan dari keempat baris (jadi kita harus berakhir dengan total 16). Sebagai contoh, saya mencari perintah streaming (yaitu efisien) di mana outputnya adalah:

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

Kasus penggunaan saya adalah bahwa saya ingin mengalirkan output ini ke perintah lain (seperti awk) untuk menghitung beberapa metrik tentang kombinasi berpasangan ini.

Saya memiliki cara untuk melakukan ini dalam awk tetapi kekhawatiran saya adalah bahwa saya menggunakan blok END {} berarti bahwa saya pada dasarnya menyimpan seluruh file dalam memori sebelum saya output. Kode contoh:

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

Apakah ada cara streaming yang efisien untuk melakukan ini tanpa harus dasarnya menyimpan file dalam memori dan kemudian output di blok END?

Tom Hayden
sumber
1
Anda harus selalu membaca satu file sampai selesai sebelum dapat mulai menghasilkan output untuk baris kedua dari file lainnya. File lain yang dapat Anda streaming.
reinierpost

Jawaban:

12

Berikut cara melakukannya dalam awk sehingga tidak perlu menyimpan seluruh file dalam sebuah array. Ini pada dasarnya adalah algoritma yang sama dengan terdon.

Jika Anda suka, Anda bahkan dapat memberikan beberapa nama file pada baris perintah dan itu akan memproses setiap file secara independen, menyatukan hasilnya bersama-sama.

#!/usr/bin/awk -f

#Cartesian product of records

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

Pada sistem saya, ini berjalan sekitar 2/3 waktu solusi perl terdon.

PM 2Ring
sumber
1
Terima kasih! Semua solusi untuk masalah ini sangat fantastis tetapi saya akhirnya memilih yang ini karena 1) kesederhanaan dan 2) tinggal di awk. Terima kasih!
Tom Hayden
1
Senang Anda menyukainya, Tom. Saya cenderung memprogram sebagian besar dalam Python hari ini, tapi saya masih suka awk untuk pemrosesan teks baris demi baris karena built-in loop atas baris dan file. Dan seringkali lebih cepat dari Python.
PM 2Ring
7

Saya tidak yakin ini lebih baik daripada melakukannya di memori, tetapi dengan sedyang rkeluar infile untuk setiap baris di infile dan yang lain di sisi lain dari pipa bergantian Hruang lama dengan jalur input ...

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'

KELUARAN

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

Saya melakukan ini dengan cara lain. Itu menyimpan beberapa dalam memori - menyimpan string seperti:

"$1" -

... untuk setiap baris dalam file.

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

Ini sangat cepat. Ini catfile sebanyak yang ada baris dalam file ke file |pipe. Di sisi lain pipa input yang digabungkan dengan file itu sendiri sebanyak ada garis dalam file.

The casehal ini hanya untuk portabilitas - yashdan zshkedua add satu elemen untuk perpecahan, sementara mkshdan poshkedua satu kalah. ksh, dash, busybox, Dan bashsemua perpecahan untuk persis seperti berbagai bidang karena ada nol seperti yang dicetak oleh printf. Seperti yang ditulis di atas memberikan hasil yang sama untuk setiap shell yang disebutkan di atas pada mesin saya.

Jika file tersebut sangat panjang, mungkin ada $ARGMAXmasalah dengan terlalu banyak argumen yang perlu Anda perkenalkan xargsatau serupa.

Diberikan input yang sama yang saya gunakan sebelum output identik. Tapi, jika saya menjadi lebih besar ...

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

Itu menghasilkan file yang hampir identik dengan apa yang saya gunakan sebelumnya (tanpa 'Baris') - tetapi pada 1000 baris. Anda dapat melihat sendiri seberapa cepat:

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

Pada 1000 baris ada beberapa variasi kecil dalam kinerja antara shell - bashselalu yang paling lambat - tetapi karena satu-satunya pekerjaan yang mereka lakukan adalah menghasilkan string arg (1000 salinan filename -) efeknya minimal. Perbedaan kinerja antara zsh- seperti di atas - dan bash100 detik di sini.

Ini versi lain yang bisa digunakan untuk file dengan panjang berapa pun:

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

Ini membuat soft-link ke arg pertama /tmpdengan nama semi-acak sehingga tidak akan terpaku pada nama file yang aneh. Itu penting karena catargumen diberikan ke pipa melalui xargs. catOutput disimpan <&3sementara sed pmeretas setiap baris dalam argumen pertama sebanyak ada baris dalam file itu - dan skripnya juga dimasukkan ke dalamnya melalui pipa. Sekali lagi pastemenggabungkan inputnya, tetapi kali ini hanya diperlukan dua argumen -lagi untuk input standar dan nama tautannya /dev/fd/3.

Yang terakhir - /dev/fd/[num]tautan - harus bekerja pada sistem linux dan banyak lagi selain itu, tetapi jika itu tidak membuat pipa bernama dengan mkfifodan menggunakan itu malah harus bekerja juga.

Hal terakhir yang dilakukannya adalah rmtautan lunak yang dibuatnya sebelum keluar.

Versi ini sebenarnya masih lebih cepat di sistem saya. Saya kira itu karena meskipun ia mengeksekusi lebih banyak aplikasi, ia mulai menyerahkan argumen mereka segera - padahal sebelum menumpuk semuanya terlebih dahulu.

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
sumber
Apakah fungsi pasangan seharusnya ada dalam file, jika tidak, bagaimana Anda menyatakannya?
@Jidder - bagaimana saya menyatakan apa? Anda bisa menyalin + menempelkannya ke terminal, bukan?
mikeserv
1
Deklarasikan fungsinya. Jadi Anda bisa! Saya pikir Anda akan lolos dari baris baru, saya waspada hanya menempelkan kode, terima kasih :) Juga itu sangat cepat, jawaban yang bagus!
@Jidder - Saya biasanya menulis ini di live shell hanya menggunakan ctrl+v; ctrl+juntuk mendapatkan baris baru seperti yang saya lakukan.
mikeserv
@Jidder - terima kasih banyak. Dan bijaksana untuk waspada - baik untuk Anda. Mereka akan bekerja dengan baik dalam file - Anda dapat menyalinnya dan . ./file; fn_namedalam kasus itu.
mikeserv
5

Nah, Anda selalu bisa melakukannya di shell Anda:

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

Ini jauh lebih lambat daripada awksolusi Anda (pada komputer saya, butuh ~ 11 detik untuk 1000 baris, dibandingkan ~ 0,3 detik awk), tetapi setidaknya tidak pernah menyimpan lebih dari beberapa baris dalam memori.

Loop di atas berfungsi untuk data yang sangat sederhana yang Anda miliki dalam contoh Anda. Ini akan tersedak backslash dan akan memakan ruang trailing dan memimpin. Versi yang lebih kuat dari hal yang sama adalah:

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

Pilihan lain adalah menggunakan perl:

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

Script di atas akan membaca setiap baris file input ( -ln), menyimpannya sebagai $l, buka sample.txtlagi, dan cetak setiap baris bersama $l. Hasilnya adalah semua kombinasi berpasangan sementara hanya 2 baris yang pernah disimpan dalam memori. Di sistem saya, hanya butuh sekitar 0.6detik pada 1000 baris.

terdon
sumber
Wow terima kasih! Saya bertanya-tanya mengapa solusi perl jauh lebih cepat daripada pernyataan bash sementara
Tom Hayden
@ TomHayden pada dasarnya karena perl, seperti awk, jauh lebih cepat daripada bash.
terdon
1
Harus downvote untuk loop sementara Anda. 4 praktik buruk yang berbeda di sana. Anda lebih tahu.
Stéphane Chazelas
1
@ StéphaneChazelas, berdasarkan jawaban Anda di sini , saya tidak bisa memikirkan kasus mana pun yang echomungkin menjadi masalah. Apa yang saya tulis (saya tambahkan printfsekarang) harus bekerja dengan mereka semua kan? Adapun whileloop, mengapa? Ada apa dengan ini while read f; do ..; done < file? Tentunya Anda tidak menyarankan forloop! Apa alternatif lain?
terdon
2
@cuonglm, itu hanya mengisyaratkan satu alasan yang memungkinkan mengapa seseorang harus menghindarinya. Di luar aspek konseptual , keandalan , keterbacaan , kinerja , dan keamanan , yang hanya mencakup keandalan .
Stéphane Chazelas
4

Dengan zsh:

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

$^apada array mengaktifkan ekspansi brace-like (seperti dalam {elt1,elt2}) untuk array.

Stéphane Chazelas
sumber
4

Anda dapat mengkompilasi kode untuk hasil yang cukup cepat.
Ini selesai dalam sekitar 0,19 - 0,27 detik pada file 1000 baris.

Saat ini membaca 10000baris ke memori (untuk mempercepat pencetakan ke layar) yang jika Anda memiliki 1000karakter per baris akan menggunakan kurang dari 10mbmemori yang saya tidak akan berpikir akan menjadi masalah. Anda dapat menghapus bagian itu sepenuhnya dan hanya mencetak langsung ke layar jika hal itu menyebabkan masalah.

Anda dapat mengkompilasi menggunakan g++ -o "NAME" "NAME.cpp"
Di mana NAMEnama File untuk menyimpannya dan NAME.cppmerupakan file tempat kode ini disimpan

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

Demonstrasi

$ 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

sumber
3
join -j 2 file.txt file.txt | cut -c 2-
  • bergabunglah dengan bidang yang tidak ada dan hapus spasi pertama

Field 2 kosong dan sama untuk semua elemen dalam file.txt sehingga joinakan menggabungkan setiap elemen dengan yang lainnya: sebenarnya menghitung produk Cartesian.

Joao
sumber
2

Salah satu opsi dengan Python adalah memetakan memori file dan mengambil keuntungan dari fakta bahwa pustaka ekspresi reguler Python dapat bekerja secara langsung dengan file yang dipetakan memori. Meskipun ini memiliki tampilan menjalankan loop bersarang di atas file, pemetaan memori memastikan bahwa OS menghadirkan RAM fisik yang tersedia secara optimal

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)

Alternatif solusi cepat dengan Python, meskipun efisiensi memori mungkin masih menjadi perhatian

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
sumber
Bukankah itu, menurut definisi, menyimpan seluruh file dalam memori? Saya tidak tahu Python, tetapi bahasa Anda jelas menunjukkan bahwa itu akan.
terdon
1
@terdon, jika Anda merujuk pada solusi pemetaan memori, OS akan secara transparan menyimpan hanya sebanyak mungkin file dalam memori karena mampu berdasarkan pada RAM fisik yang tersedia. RAM fisik yang tersedia tidak harus melebihi ukuran file (walaupun memiliki RAM fisik tambahan jelas akan menjadi situasi yang menguntungkan). Dalam kasus terburuk ini bisa menurunkan kecepatan perulangan melalui file pada disk atau lebih buruk. Keuntungan utama dengan pendekatan ini adalah penggunaan transparan dari RAM fisik yang tersedia karena ini adalah sesuatu yang dapat berfluktuasi dari waktu ke waktu
iruvar
1

Dalam bash, ksh juga bisa digunakan, hanya menggunakan shell bawaan:

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

Perhatikan bahwa sementara ini menyimpan seluruh file dalam memori dalam variabel shell, itu hanya membutuhkan akses baca tunggal untuk itu.

Franki
sumber
1
Saya pikir inti dari OP adalah untuk tidak menyimpan file dalam memori. Jika tidak, pendekatan gawk mereka saat ini lebih sederhana dan lebih cepat. Saya menduga ini perlu bekerja dengan file teks yang beberapa gigabytes.
terdon
Ya, itu benar sekali - Saya punya beberapa file data BESAR yang harus saya lakukan dengan ini dan tidak ingin disimpan dalam memori
Tom Hayden
Jika Anda dibatasi oleh memori, saya akan merekomendasikan menggunakan salah satu solusi dari @terdon
Franki
0

sed larutan.

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

Penjelasan:

  • sed 'r file2' file1 - Baca semua isi file file2 untuk setiap baris file1.
  • Konstruksi 1~i berarti garis ke-1, kemudian garis 1 + i, 1 + 2 * i, 1 + 3 * i, dll. Oleh karena itu, 1~$((line_num + 1)){h;d}berarti hgaris runcing lama ke buffer, dhapus ruang pola, dan mulai siklus baru.
  • 'G;s/(.*)\n(.*)/\2 \1/' - untuk semua baris, kecuali diambil pada langkah sebelumnya, lakukan selanjutnya: G et line dari hold buffer dan tambahkan ke baris saat ini. Kemudian bertukar tempat garis. Apakah current_line\nbuffer_line\n, menjadibuffer_line\ncurrent_line\n

Keluaran

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
sumber