Công cụ dòng lệnh để cải tiến theo chiều ngang của tất cả các hàng trong một tệp


13

Giả sử tôi có một tệp (gọi nó là sample.txt) trông như thế này:

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

Tôi muốn có thể làm việc trên một luồng từ tệp này về cơ bản là sự kết hợp theo cặp của cả bốn hàng (vì vậy chúng tôi sẽ kết thúc với tổng cộng 16). Chẳng hạn, tôi đang tìm kiếm một lệnh streaming (tức là hiệu quả) trong đó đầu ra là:

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

Trường hợp sử dụng của tôi là tôi muốn truyền đầu ra này vào một lệnh khác (như awk) để tính toán một số liệu về sự kết hợp theo cặp này.

Tôi có một cách để làm điều này trong awk nhưng mối quan tâm của tôi là việc tôi sử dụng khối END {} có nghĩa là về cơ bản tôi đang lưu trữ toàn bộ tệp trong bộ nhớ trước khi tôi xuất. Mã ví dụ:

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

Có một cách truyền phát hiệu quả để làm điều này mà không cần phải lưu trữ tệp trong bộ nhớ và sau đó xuất ra trong khối END?


1
Bạn sẽ luôn cần đọc một tệp đến cuối trước khi có thể bắt đầu tạo đầu ra cho dòng thứ hai của tệp khác. Các tập tin khác bạn có thể truyền phát.
rebierpost

Câu trả lời:


12

Đây là cách thực hiện trong awk để không phải lưu toàn bộ tệp trong một mảng. Về cơ bản, đây là thuật toán tương tự như terdon.

Nếu bạn thích, bạn thậm chí có thể cung cấp cho nó nhiều tên tệp trên dòng lệnh và nó sẽ xử lý từng tệp một cách độc lập, nối các kết quả lại với nhau.

#!/usr/bin/awk -f

#Cartesian product of records

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

Trên hệ thống của tôi, điều này chạy trong khoảng 2/3 thời gian giải pháp perl của terdon.


1
Cảm ơn! Tất cả các giải pháp cho vấn đề này đều tuyệt vời nhưng cuối cùng tôi đã đi đến vấn đề này do 1) đơn giản và 2) ở lại awk. Cảm ơn!
Tom Hayden

1
Vui mừng bạn thích nó, Tom. Tôi có xu hướng lập trình chủ yếu bằng Python ngày nay, nhưng tôi vẫn thích awk cho xử lý văn bản từng dòng vì các vòng lặp tích hợp của nó trên các dòng và tệp. Và nó thường nhanh hơn Python.
PM 2Ring

7

Tôi không chắc điều này tốt hơn việc thực hiện nó trong bộ nhớ, nhưng với sedđiều đó dẫn đến rsự bất ổn của nó cho mọi dòng trong infile của nó và một dòng khác ở phía bên kia của một đường ống xen kẽ Hkhông gian cũ với các dòng đầu vào ...

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'

ĐẦU RA

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

Tôi đã làm điều này theo cách khác. Nó lưu trữ một số trong bộ nhớ - nó lưu trữ một chuỗi như:

"$1" -

... cho mỗi dòng trong tệp.

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

Nó rất nhanh. Đó catlà tệp nhiều lần như có các dòng trong tệp tới a |pipe. Ở phía bên kia của đường ống mà đầu vào được hợp nhất với chính tệp đó nhiều lần như có các dòng trong tệp.

Các casecông cụ chỉ dành cho tính di động - yashzshcả hai thêm một yếu tố để phân chia, trong khi mkshposhcả hai đều mất một. ksh, dash, busybox, Và bashtất cả chia ra chính xác như nhiều lĩnh vực như có zero như in bằng printf. Như đã viết ở trên, kết quả tương tự cho mỗi một trong các shell được đề cập ở trên trên máy của tôi.

Nếu tệp quá dài, có thể có $ARGMAXvấn đề với quá nhiều đối số trong trường hợp bạn cần giới thiệu xargshoặc tương tự.

Cho cùng một đầu vào tôi đã sử dụng trước khi đầu ra giống hệt nhau. Nhưng, nếu tôi định đi lớn hơn ...

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

Điều đó tạo ra một tệp gần giống với những gì tôi đã sử dụng trước đây (sans 'Row') - nhưng ở 1000 dòng. Bạn có thể tự mình thấy nó nhanh như thế nào:

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

Tại 1000 dòng có một số thay đổi nhỏ về hiệu suất giữa các shell - bashluôn là chậm nhất - nhưng vì công việc duy nhất họ làm dù sao là tạo ra chuỗi arg (1000 bản sao filename -) nên hiệu quả là tối thiểu. Sự khác biệt về hiệu suất giữa zsh- như trên - và bashlà 100 giây trong đây.

Đây là một phiên bản khác nên hoạt động cho một tệp có độ dài bất kỳ:

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

Nó tạo ra một liên kết mềm với đối số đầu tiên của nó /tmpbằng một tên bán ngẫu nhiên để nó không bị treo trên các tên tệp lạ. Điều đó quan trọng bởi vì catcác đối số được đưa đến nó qua một đường ống thông qua xargs. catĐầu ra của được lưu vào <&3trong khi sed pgợi ý mọi dòng trong đối số đầu tiên nhiều lần như có các dòng trong tệp đó - và tập lệnh của nó cũng được đưa đến nó thông qua một đường ống. Một lần nữa pastehợp nhất đầu vào của nó, nhưng lần này chỉ cần hai đối số -một lần nữa cho đầu vào tiêu chuẩn và tên liên kết /dev/fd/3.

Cuối cùng - /dev/fd/[num]liên kết - sẽ hoạt động trên bất kỳ hệ thống linux nào và nhiều hệ thống khác bên cạnh đó, nhưng nếu nó không tạo ra một ống có tên mkfifovà sử dụng thay thế thì nó cũng hoạt động tốt.

Điều cuối cùng nó làm là rmliên kết mềm mà nó tạo ra trước khi thoát.

Phiên bản này thực sự nhanh hơn vẫn còn trên hệ thống của tôi. Tôi đoán đó là bởi vì mặc dù nó thực thi nhiều ứng dụng hơn, nhưng nó bắt đầu xử lý chúng ngay lập tức - trong khi trước khi nó xếp chồng chúng lên trước.

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

Là các cặp chức năng giả sử là trong một tập tin, nếu không làm thế nào bạn sẽ khai báo nó?

@Jidder - Làm thế nào để tôi tuyên bố những gì? Bạn chỉ có thể sao chép + dán nó vào một thiết bị đầu cuối, không?
mikeerv

1
Khai báo hàm. Vì vậy, bạn có thể! Tôi nghĩ rằng bạn sẽ thoát khỏi dòng mới, tôi cảnh giác chỉ cần dán mã vào, cảm ơn mặc dù :) Ngoài ra đó là cực kỳ nhanh chóng, câu trả lời tốt đẹp!

@Jidder - Tôi thường viết những thứ này trong một vỏ trực tiếp chỉ bằng cách sử dụng ctrl+v; ctrl+jđể có được dòng mới như tôi làm.
mikeerv

@Jidder - Cảm ơn bạn rất nhiều. Và thật khôn ngoan khi cảnh giác - tốt cho bạn. Chúng sẽ hoạt động tốt trong một tệp - bạn có thể sao chép nó trong và . ./file; fn_nametrong trường hợp đó.
mikeerv

5

Vâng, bạn luôn có thể làm điều đó trong vỏ của bạn:

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

Đó là một awkgiải pháp chậm hơn so với giải pháp của bạn (trên máy của tôi, mất ~ 11 giây cho 1000 dòng, so với ~ 0,3 giây awk) nhưng ít nhất nó không bao giờ chứa nhiều hơn một vài dòng trong bộ nhớ.

Vòng lặp ở trên hoạt động với dữ liệu rất đơn giản mà bạn có trong ví dụ của mình. Nó sẽ nghẹt thở vì dấu gạch chéo ngược và nó sẽ ăn dấu vết và dấu cách. Một phiên bản mạnh mẽ hơn của điều tương tự là:

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

Một lựa chọn khác là sử dụng perlthay thế:

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

Kịch bản trên sẽ đọc từng dòng của tệp đầu vào ( -ln), lưu nó dưới dạng $l, mở sample.txtlại và in từng dòng cùng với $l. Kết quả là tất cả các kết hợp theo cặp trong khi chỉ có 2 dòng được lưu trong bộ nhớ. Trên hệ thống của tôi, điều đó chỉ mất khoảng 0.6vài giây trên 1000 dòng.


Ồ cảm ơn nhé! Tôi tự hỏi tại sao giải pháp perl lại nhanh hơn nhiều so với bash while statement
Tom Hayden

@TomHayden về cơ bản vì perl, như awk, nhanh hơn bash rất nhiều .
terdon

1
Phải downvote cho vòng lặp while của bạn. 4 thực hành xấu khác nhau trong đó. Bạn biết rõ hơn.
Stéphane Chazelas

1
@ StéphaneChazelas, dựa trên câu trả lời của bạn ở đây , tôi không thể nghĩ ra bất kỳ trường hợp nào echocó thể là một vấn đề. Những gì tôi đã viết (tôi đã thêm printfbây giờ) nên làm việc với tất cả chúng phải không? Đối với whilevòng lặp, tại sao? Có chuyện gì với bạn while read f; do ..; done < filevậy? Chắc chắn bạn không đề xuất một forvòng lặp! Cái gì khác thay thế?
terdon

2
@cuonglm, người ta chỉ gợi ý một lý do có thể tại sao người ta nên tránh nó. Trong số các khía cạnh khái niệm , độ tin cậy , mức độ dễ đọc , hiệu suấtbảo mật , chỉ bao gồm độ tin cậy .
Stéphane Chazelas

4

Với zsh:

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

$^atrên một mảng bật mở rộng giống như dấu ngoặc kép (như trong {elt1,elt2}) cho mảng.


4

Bạn có thể biên dịch mã để có kết quả khá nhanh.
Nó hoàn thành trong khoảng 0,19 - 0,27 giây trên tệp 1000 dòng.

Hiện tại nó đọc 10000các dòng vào bộ nhớ (để tăng tốc độ in ra màn hình) mà nếu bạn có các 1000ký tự trên mỗi dòng sẽ sử dụng ít hơn 10mbbộ nhớ mà tôi không nghĩ sẽ gặp vấn đề. Bạn có thể loại bỏ hoàn toàn phần đó và chỉ in trực tiếp ra màn hình nếu nó gây ra sự cố.

Bạn có thể biên dịch sử dụng g++ -o "NAME" "NAME.cpp"
ở đâu NAMElà tên của File để lưu nó vào và NAME.cpplà file mã này sẽ được lưu vào

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

Trình diễn

$ 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-
  • tham gia bởi một trường không tồn tại và xóa không gian đầu tiên

Trường 2 trống và bằng nhau cho tất cả các phần tử trong file.txt, do đó joinsẽ ghép từng phần tử với tất cả các phần tử khác: thực tế nó đang tính toán sản phẩm của Cartesian.


2

Một tùy chọn với Python là ánh xạ bộ nhớ tệp và tận dụng thực tế là thư viện biểu thức chính quy Python có thể hoạt động trực tiếp với các tệp ánh xạ bộ nhớ. Mặc dù điều này có sự xuất hiện của các vòng lặp lồng nhau trên tệp, ánh xạ bộ nhớ đảm bảo rằng HĐH mang tối ưu RAM vật lý có sẵn để chơi

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)

Thay thế một giải pháp nhanh chóng trong Python, mặc dù hiệu quả bộ nhớ vẫn có thể là một mối quan tâm

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

Theo định nghĩa, sẽ không giữ toàn bộ tập tin trong bộ nhớ? Tôi không biết Python nhưng ngôn ngữ của bạn chắc chắn gợi ý rằng nó sẽ như vậy.
terdon

1
@terdon, nếu bạn đang đề cập đến giải pháp ánh xạ bộ nhớ, HĐH sẽ chỉ giữ lại một cách rõ ràng nhiều tệp trong bộ nhớ vì nó có thể đủ khả năng dựa trên RAM vật lý có sẵn. RAM vật lý khả dụng không phải vượt quá kích thước tệp (mặc dù có thêm RAM vật lý rõ ràng sẽ là một tình huống thuận lợi). Trong trường hợp xấu nhất, điều này có thể làm giảm tốc độ lặp qua tệp trên đĩa hoặc tệ hơn. Ưu điểm chính của phương pháp này là việc sử dụng RAM vật lý có sẵn trong suốt vì đây là thứ có thể dao động theo thời gian
iruvar

1

Trong bash, ksh cũng hoạt động tốt, chỉ sử dụng shell dựng sẵn:

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

Lưu ý rằng mặc dù điều này giữ toàn bộ tệp trong bộ nhớ trong một biến shell, nhưng nó chỉ cần một lần truy cập đọc vào nó.


1
Tôi nghĩ rằng toàn bộ quan điểm của OP là không giữ tệp trong bộ nhớ. Mặt khác, cách tiếp cận gawk hiện tại của họ vừa đơn giản vừa nhanh hơn nhiều . Tôi đoán điều này cần để làm việc với các tệp văn bản có kích thước vài gigabyte.
terdon

Vâng, điều đó hoàn toàn chính xác - Tôi có một vài tệp dữ liệu HUGE mà tôi cần thực hiện việc này và không muốn giữ trong bộ nhớ
Tom Hayden

Trong trường hợp bạn bị hạn chế bởi bộ nhớ, tôi khuyên bạn nên sử dụng một trong các giải pháp từ @terdon
Franki

0

sed giải pháp.

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

Giải trình:

  • sed 'r file2' file1 - đọc tất cả nội dung tệp của tệp2 cho mỗi dòng của tệp1.
  • Xây dựng 1~icó nghĩa là dòng 1, sau đó là dòng 1 + i, 1 + 2 * i, 1 + 3 * i, v.v. Do đó, 1~$((line_num + 1)){h;d}có nghĩa là hdòng nhọn cũ vào bộ đệm, dkhông gian mẫu elete và bắt đầu chu kỳ mới.
  • 'G;s/(.*)\n(.*)/\2 \1/'- đối với tất cả các dòng, ngoại trừ được chọn trong bước trước, hãy thực hiện tiếp theo: Get line từ bộ đệm giữ và nối nó với dòng hiện tại. Sau đó trao đổi địa điểm của các dòng. Đã current_line\nbuffer_line\n, trở thànhbuffer_line\ncurrent_line\n

Đầu ra

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.