Temukan file yang berisi beberapa kata kunci di mana saja di dalam file

16

Saya sedang mencari cara untuk membuat daftar semua file dalam direktori yang berisi set lengkap kata kunci yang saya cari, di mana saja dalam file tersebut.

Jadi, kata kunci tidak perlu muncul di baris yang sama.

Salah satu cara untuk melakukan ini adalah:

grep -l one $(grep -l two $(grep -l three *))

Tiga kata kunci hanyalah sebuah contoh, bisa juga dua, atau empat, dan seterusnya.

Cara kedua yang bisa saya pikirkan adalah:

grep -l one * | xargs grep -l two | xargs grep -l three

Metode ketiga, yang muncul dalam pertanyaan lain , adalah:

find . -type f \
  -exec grep -q one {} \; -a \
  -exec grep -q two {} \; -a \
  -exec grep -q three {} \; -a -print

Tapi itu jelas bukan arah yang saya tuju di sini. Aku ingin sesuatu yang memerlukan lebih sedikit mengetik, dan mungkin hanya satu panggilan untuk grep, awk, perlatau serupa.

Misalnya, saya suka cara awkAnda mencocokkan baris yang berisi semua kata kunci , seperti:

awk '/one/ && /two/ && /three/' *

Atau, cetak hanya nama file:

awk '/one/ && /two/ && /three/ { print FILENAME ; nextfile }' *

Tetapi saya ingin mencari file di mana kata kunci mungkin berada di mana saja dalam file, tidak harus pada baris yang sama.


Solusi yang disukai akan lebih ramah gzip, misalnya grepmemiliki zgrepvarian yang berfungsi pada file terkompresi. Mengapa saya menyebutkan ini, adalah bahwa beberapa solusi mungkin tidak berfungsi dengan baik mengingat kendala ini. Misalnya, dalam awkcontoh mencetak file yang cocok, Anda tidak bisa begitu saja melakukan:

zcat * | awk '/pattern/ {print FILENAME; nextfile}'

Anda perlu mengubah perintah secara signifikan, menjadi sesuatu seperti:

for f in *; do zcat $f | awk -v F=$f '/pattern/ { print F; nextfile }'; done

Jadi, karena kendala, Anda perlu menelepon awkberkali-kali, meskipun Anda hanya dapat melakukannya sekali dengan file yang tidak dikompresi. Dan tentu saja, akan lebih baik untuk hanya melakukan zawk '/pattern/ {print FILENAME; nextfile}' *dan mendapatkan efek yang sama, jadi saya lebih suka solusi yang memungkinkan ini.

arekolek
sumber
1
Anda tidak perlu mereka gzipramah, cukup zcatfile terlebih dahulu.
terdon
@terdon Saya sudah mengedit posting, menjelaskan mengapa saya menyebutkan bahwa file dikompresi.
arekolek
Tidak ada banyak perbedaan antara meluncurkan awk sekali atau berkali-kali. Maksudku, OK, beberapa overhead kecil tapi aku ragu kamu bahkan akan melihat perbedaannya. Tentu saja dimungkinkan untuk membuat awk / perl skrip apa pun yang melakukan ini sendiri tetapi mulai menjadi program yang penuh sesak nafas dan bukan satu-liner cepat. Itukah yang kamu inginkan?
terdon
@terdon Secara pribadi, aspek yang lebih penting bagi saya adalah betapa rumitnya perintah itu (saya kira edit kedua saya datang ketika Anda berkomentar). Misalnya, grepsolusinya mudah diadaptasi hanya dengan awalan greppanggilan dengan z, tidak perlu bagi saya untuk juga menangani nama file.
arekolek
Ya, tapi itu grep. AFAIK, hanya grepdan catmemiliki "z-varian" standar. Saya tidak berpikir Anda akan mendapatkan sesuatu yang lebih sederhana daripada menggunakan for f in *; do zcat -f $f ...solusi. Apa pun yang lain harus menjadi program lengkap yang memeriksa format file sebelum membuka atau menggunakan perpustakaan untuk melakukan hal yang sama.
terdon

Jawaban:

13
awk 'FNR == 1 { f1=f2=f3=0; };

     /one/   { f1++ };
     /two/   { f2++ };
     /three/ { f3++ };

     f1 && f2 && f3 {
       print FILENAME;
       nextfile;
     }' *

Jika Anda ingin secara otomatis menangani file gzip, jalankan ini dalam satu lingkaran dengan zcat(lambat dan tidak efisien karena Anda akan forking awkberkali-kali dalam satu lingkaran, sekali untuk setiap nama file) atau menulis ulang algoritma yang sama perldan menggunakan IO::Uncompress::AnyUncompressmodul perpustakaan yang dapat dekompresi beberapa jenis file terkompresi (gzip, zip, bzip2, lzop). atau dalam python, yang juga memiliki modul untuk menangani file terkompresi.


Berikut adalah perlversi yang digunakan IO::Uncompress::AnyUncompressuntuk memungkinkan sejumlah pola dan sejumlah nama file (mengandung teks biasa atau teks terkompresi).

Semua argumen sebelumnya --diperlakukan sebagai pola pencarian. Semua argumen setelah --diperlakukan sebagai nama file. Opsi penanganan yang primitif tetapi efektif untuk pekerjaan ini. Penanganan opsi yang lebih baik (misalnya untuk mendukung -iopsi untuk pencarian case-sensitive) dapat dicapai dengan modul Getopt::Stdatau Getopt::Long.

Jalankan seperti ini:

$ ./arekolek.pl one two three -- *.gz *.txt
1.txt.gz
4.txt.gz
5.txt.gz
1.txt
4.txt
5.txt

(Saya tidak akan membuat daftar file {1..6}.txt.gzdan di {1..6}.txtsini ... mereka hanya berisi beberapa atau semua kata "satu" "dua" "tiga" "empat" "lima" dan "enam" untuk pengujian. File-file yang tercantum dalam output di atas LAKUKAN mengandung ketiga pola pencarian. Uji sendiri dengan data Anda sendiri)

#! /usr/bin/perl

use strict;
use warnings;
use IO::Uncompress::AnyUncompress qw(anyuncompress $AnyUncompressError) ;

my %patterns=();
my @filenames=();
my $fileargs=0;

# all args before '--' are search patterns, all args after '--' are
# filenames
foreach (@ARGV) {
  if ($_ eq '--') { $fileargs++ ; next };

  if ($fileargs) {
    push @filenames, $_;
  } else {
    $patterns{$_}=1;
  };
};

my $pattern=join('|',keys %patterns);
$pattern=qr($pattern);
my $p_string=join('',sort keys %patterns);

foreach my $f (@filenames) {
  #my $lc=0;
  my %s = ();
  my $z = new IO::Uncompress::AnyUncompress($f)
    or die "IO::Uncompress::AnyUncompress failed: $AnyUncompressError\n";

  while ($_ = $z->getline) {
    #last if ($lc++ > 100);
    my @matches=( m/($pattern)/og);
    next unless (@matches);

    map { $s{$_}=1 } @matches;
    my $m_string=join('',sort keys %s);

    if ($m_string eq $p_string) {
      print "$f\n" ;
      last;
    }
  }
}

Hash %patternsadalah berisi kumpulan pola lengkap yang file harus mengandung setidaknya satu dari setiap anggota $_pstringadalah string yang berisi kunci yang diurutkan dari hash itu. String $patternberisi ekspresi reguler pra-dikompilasi juga dibangun dari %patternshash.

$patterndibandingkan dengan setiap baris dari setiap file input (menggunakan /opengubah untuk mengkompilasi $patternhanya sekali seperti yang kita tahu itu tidak akan pernah berubah selama menjalankan), dan map()digunakan untuk membangun hash (% s) yang berisi kecocokan untuk setiap file.

Setiap kali semua pola telah terlihat di file saat ini (dengan membandingkan jika $m_string(kunci yang diurutkan dalam %s) sama dengan $p_string), cetak nama file dan lewati ke file berikutnya.

Ini bukan solusi yang sangat cepat, tetapi tidak terlalu lambat. Versi pertama mengambil 4m58s untuk mencari tiga kata dalam 74MB senilai file log terkompresi (total 937MB terkompresi). Versi saat ini membutuhkan 1m13s. Mungkin ada optimisasi lebih lanjut yang bisa dilakukan.

Salah satu optimasi jelas adalah dengan menggunakan ini dalam hubungannya dengan xargs's -Palias --max-procsuntuk menjalankan beberapa pencarian pada himpunan bagian dari file secara paralel. Untuk melakukan itu, Anda perlu menghitung jumlah file dan membaginya dengan jumlah core / cpus / threads yang dimiliki sistem Anda (dan akhiri dengan menambahkan 1). misalnya ada 269 file yang sedang dicari dalam set sampel saya, dan sistem saya memiliki 6 core (AMD 1090T), jadi:

patterns=(one two three)
searchpath='/var/log/apache2/'
cores=6
filecount=$(find "$searchpath" -type f -name 'access.*' | wc -l)
filespercore=$((filecount / cores + 1))

find "$searchpath" -type f -print0 | 
  xargs -0r -n "$filespercore" -P "$cores" ./arekolek.pl "${patterns[@]}" --

Dengan optimasi itu, hanya butuh 23 detik untuk menemukan semua 18 file yang cocok. Tentu saja, hal yang sama dapat dilakukan dengan solusi lainnya. CATATAN: Urutan nama file yang tercantum dalam output akan berbeda, jadi mungkin perlu disortir sesudahnya jika itu penting.

Seperti dicatat oleh @arekolek, banyak zgreps dengan find -execatau xargsdapat melakukannya secara signifikan lebih cepat, tetapi skrip ini memiliki keuntungan mendukung sejumlah pola untuk mencari, dan mampu menangani beberapa jenis kompresi yang berbeda.

Jika skrip terbatas untuk memeriksa hanya 100 baris pertama dari setiap file, skrip tersebut menjalankan semuanya (dalam 74MB sampel 269 file saya) dalam 0,6 detik. Jika ini berguna dalam beberapa kasus, ini dapat dibuat menjadi opsi baris perintah (misalnya -l 100) tetapi berisiko tidak menemukan semua file yang cocok.


BTW, menurut halaman manual untuk IO::Uncompress::AnyUncompress, format kompresi yang didukung adalah:


Optimasi terakhir (saya harap). Dengan menggunakan PerlIO::gzipmodul (dikemas dalam bahasa debian sebagai libperlio-gzip-perl) alih-alih IO::Uncompress::AnyUncompresssaya punya waktu sekitar 3,1 detik untuk memproses 74MB file log saya. Ada juga beberapa perbaikan kecil dengan menggunakan hash sederhana daripada Set::Scalar(yang juga menghemat beberapa detik dengan IO::Uncompress::AnyUncompressversi).

PerlIO::gzipdirekomendasikan sebagai perl gunzip tercepat di /programming//a/1539271/137158 (ditemukan dengan pencarian google perl fast gzip decompress)

Menggunakan xargs -Pdengan ini tidak meningkatkan sama sekali. Bahkan itu bahkan tampaknya memperlambatnya mulai dari 0,1 hingga 0,7 detik. (Saya mencoba empat kali dan sistem saya melakukan hal-hal lain di latar belakang yang akan mengubah waktunya)

Harganya adalah versi skrip ini hanya dapat menangani file yang di-gzip dan tidak dikompresi. Kecepatan vs fleksibilitas: 3,1 detik untuk versi ini vs 23 detik untuk IO::Uncompress::AnyUncompressversi dengan xargs -Ppembungkus (atau tanpa 1m13s xargs -P).

#! /usr/bin/perl

use strict;
use warnings;
use PerlIO::gzip;

my %patterns=();
my @filenames=();
my $fileargs=0;

# all args before '--' are search patterns, all args after '--' are
# filenames
foreach (@ARGV) {
  if ($_ eq '--') { $fileargs++ ; next };

  if ($fileargs) {
    push @filenames, $_;
  } else {
    $patterns{$_}=1;
  };
};

my $pattern=join('|',keys %patterns);
$pattern=qr($pattern);
my $p_string=join('',sort keys %patterns);

foreach my $f (@filenames) {
  open(F, "<:gzip(autopop)", $f) or die "couldn't open $f: $!\n";
  #my $lc=0;
  my %s = ();
  while (<F>) {
    #last if ($lc++ > 100);
    my @matches=(m/($pattern)/ogi);
    next unless (@matches);

    map { $s{$_}=1 } @matches;
    my $m_string=join('',sort keys %s);

    if ($m_string eq $p_string) {
      print "$f\n" ;
      close(F);
      last;
    }
  }
}
cas
sumber
for f in *; do zcat $f | awk -v F=$f '/one/ {a++}; /two/ {b++}; /three/ {c++}; a&&b&&c { print F; nextfile }'; doneberfungsi dengan baik, tetapi memang, membutuhkan 3 kali selama grepsolusi saya , dan sebenarnya lebih rumit.
arekolek
1
OTOH, untuk file teks biasa akan lebih cepat. dan algoritma yang sama diimplementasikan dalam bahasa dengan dukungan untuk membaca file terkompresi (seperti perl atau python) seperti yang saya sarankan akan lebih cepat daripada multiple greps. "komplikasi" sebagian subjektif - secara pribadi, saya pikir satu awk atau perl atau skrip python kurang rumit daripada beberapa greps dengan atau tanpa menemukan .... @ jawaban terdon baik, dan apakah itu tanpa memerlukan modul yang saya sebutkan (tetapi dengan biaya forking zcat untuk setiap file terkompresi)
cas
Saya harus apt-get install libset-scalar-perlmenggunakan skrip. Tetapi tampaknya tidak berakhir dalam waktu yang wajar.
arekolek
berapa banyak dan ukuran apa (terkompresi dan tidak terkompresi) file yang Anda cari? puluhan atau ratusan file ukuran kecil-menengah atau ribuan yang besar?
cas
Berikut adalah histogram ukuran file terkompresi (20 hingga 100 file, hingga 50MB tetapi kebanyakan di bawah 5MB). Terlihat tidak terkompresi sama, tetapi dengan ukuran dikalikan 10.
arekolek
11

Setel pemisah rekaman .sehingga awkakan memperlakukan seluruh file sebagai satu baris:

awk -v RS='.' '/one/&&/two/&&/three/{print FILENAME}' *

Demikian pula dengan perl:

perl -ln00e '/one/&&/two/&&/three/ && print $ARGV' *
jimmij
sumber
3
Rapi. Perhatikan bahwa ini akan memuat seluruh file ke dalam memori dan itu mungkin menjadi masalah untuk file besar.
terdon
Saya awalnya mengunggulkan ini, karena terlihat menjanjikan. Tapi saya tidak bisa membuatnya bekerja dengan file gzip. for f in *; do zcat $f | awk -v RS='.' -v F=$f '/one/ && /two/ && /three/ { print F }'; donetidak menghasilkan apa-apa.
arekolek
@arekolek Loop itu bekerja untuk saya. Apakah file Anda di-gzip dengan benar?
jimmij
@arekolek Anda butuhkan zcat -f "$f"jika beberapa file tidak dikompresi.
terdon
Saya telah mengujinya juga pada file yang tidak dikompresi dan awk -v RS='.' '/bfs/&&/none/&&/rgg/{print FILENAME}' greptest/*.txtmasih tidak mengembalikan hasil, sementara grep -l rgg $(grep -l none $(grep -l bfs greptest/*.txt))mengembalikan hasil yang diharapkan.
arekolek
3

Untuk file terkompresi, Anda bisa mengulang setiap file dan mendekompres terlebih dahulu. Kemudian, dengan versi jawaban yang sedikit dimodifikasi, Anda dapat melakukan:

for f in *; do 
    zcat -f "$f" | perl -ln00e '/one/&&/two/&&/three/ && exit(0); }{ exit(1)' && 
        printf '%s\n' "$f"
done

Script Perl akan keluar dengan 0status (sukses) jika ketiga string ditemukan. The }{adalah Perl singkatan untuk END{}. Apa pun yang mengikuti itu akan dieksekusi setelah semua input telah diproses. Jadi skrip akan keluar dengan status keluar non-0 jika tidak semua string ditemukan. Oleh karena itu, && printf '%s\n' "$f"akan mencetak nama file hanya jika ketiganya ditemukan.

Atau, untuk menghindari memuat file ke dalam memori:

for f in *; do 
    zcat -f "$f" 2>/dev/null | 
        perl -lne '$k++ if /one/; $l++ if /two/; $m++ if /three/;  
                   exit(0) if $k && $l && $m; }{ exit(1)' && 
    printf '%s\n' "$f"
done

Akhirnya, jika Anda benar-benar ingin melakukan semuanya dalam sebuah skrip, Anda dapat melakukan:

#!/usr/bin/env perl

use strict;
use warnings;

## Get the target strings and file names. The first three
## arguments are assumed to be the strings, the rest are
## taken as target files.
my ($str1, $str2, $str3, @files) = @ARGV;

FILE:foreach my $file (@files) {
    my $fh;
    my ($k,$l,$m)=(0,0,0);
    ## only process regular files
    next unless -f $file ;
    ## Open the file in the right mode
    $file=~/.gz$/ ? open($fh,"-|", "zcat $file") : open($fh, $file);
    ## Read through each line
    while (<$fh>) {
        $k++ if /$str1/;
        $l++ if /$str2/;
        $m++ if /$str3/;
        ## If all 3 have been found
        if ($k && $l && $m){
            ## Print the file name
            print "$file\n";
            ## Move to the net file
            next FILE;
        }
    }
    close($fh);
}

Simpan skrip di atas sebagai foo.pltempat Anda $PATH, buat itu dapat dieksekusi dan jalankan seperti ini:

foo.pl one two three *
terdon
sumber
2

Dari semua solusi yang diusulkan sejauh ini, solusi asli saya menggunakan grep adalah yang tercepat, selesai dalam 25 detik. Kelemahannya adalah membosankan untuk menambahkan dan menghapus kata kunci. Jadi saya datang dengan skrip (dijuluki multi) yang mensimulasikan perilaku, tetapi memungkinkan untuk mengubah sintaks:

#!/bin/bash

# Usage: multi [z]grep PATTERNS -- FILES

command=$1

# first two arguments constitute the first command
command_head="$1 -le '$2'"
shift 2

# arguments before double-dash are keywords to be piped with xargs
while (("$#")) && [ "$1" != -- ] ; do
  command_tail+="| xargs $command -le '$1' "
  shift
done
shift

# remaining arguments are files
eval "$command_head $@ $command_tail"

Jadi sekarang, menulis multi grep one two three -- *setara dengan proposal asli saya dan berjalan dalam waktu yang bersamaan. Saya juga dapat dengan mudah menggunakannya pada file terkompresi dengan menggunakan zgrepargumen pertama sebagai gantinya.

Solusi lain

Saya juga bereksperimen dengan skrip Python menggunakan dua strategi: mencari semua kata kunci baris demi baris, dan mencari di seluruh file kata kunci berdasarkan kata kunci. Strategi kedua lebih cepat dalam kasus saya. Tapi itu lebih lambat daripada hanya menggunakan grep, finishing dalam 33 detik. Pencocokan kata kunci baris demi baris selesai dalam 60 detik.

#!/usr/bin/python3

import gzip, sys

i = sys.argv.index('--')
patterns = sys.argv[1:i]
files = sys.argv[i+1:]

for f in files:
  with (gzip.open if f.endswith('.gz') else open)(f, 'rt') as s:
    txt = s.read()
    if all(p in txt for p in patterns):
      print(f)

The Script yang diberikan oleh Terdon selesai dalam 54 detik. Sebenarnya butuh waktu dinding 39 detik, karena prosesor saya adalah dual core. Yang menarik, karena skrip Python saya mengambil 49 detik waktu dinding (dan grep29 detik).

The Script oleh cas gagal untuk mengakhiri dalam waktu yang wajar, bahkan pada sejumlah kecil file yang diproses dengan grepdi bawah 4 detik, jadi aku harus membunuhnya.

Tetapi awkproposal aslinya , meskipun lebih lambat dari grepapa adanya, memiliki potensi keuntungan. Dalam beberapa kasus, setidaknya dalam pengalaman saya, mungkin untuk mengharapkan bahwa semua kata kunci harus muncul di suatu tempat di kepala file jika ada di file sama sekali. Ini memberikan solusi ini dorongan dramatis dalam kinerja:

for f in *; do
  zcat $f | awk -v F=$f \
    'NR>100 {exit} /one/ {a++} /two/ {b++} /three/ {c++} a&&b&&c {print F; exit}'
done

Selesai dalam seperempat detik, dibandingkan dengan 25 detik.

Tentu saja, kami mungkin tidak memiliki keunggulan dalam mencari kata kunci yang diketahui terjadi di dekat awal file. Dalam kasus seperti itu, solusi tanpa NR>100 {exit}membutuhkan 63 detik (50-an waktu dinding).

File tidak terkompresi

Tidak ada perbedaan yang signifikan dalam menjalankan waktu antara grepsolusi saya dan awkproposal cas , keduanya membutuhkan sepersekian detik untuk dieksekusi.

Perhatikan bahwa inisialisasi variabel FNR == 1 { f1=f2=f3=0; }wajib dalam kasus tersebut untuk mengatur ulang penghitung untuk setiap file yang diproses berikutnya. Dengan demikian, solusi ini memerlukan pengeditan perintah di tiga tempat jika Anda ingin mengubah kata kunci atau menambahkan yang baru. Di sisi lain, grepAnda hanya dapat menambahkan | xargs grep -l fouratau mengedit kata kunci yang Anda inginkan.

Kelemahan dari grepsolusi yang menggunakan substitusi perintah, adalah bahwa itu akan hang jika di manapun dalam rantai, sebelum langkah terakhir, tidak ada file yang cocok. Ini tidak mempengaruhi xargsvarian karena pipa akan dibatalkan setelah grepmengembalikan status tidak nol. Saya telah memperbarui skrip saya untuk digunakan xargssehingga saya tidak harus menangani ini sendiri, membuat skrip lebih sederhana.

arekolek
sumber
Solusi Python Anda dapat mengambil manfaat dari mendorong loop ke lapisan C dengannot all(p in text for p in patterns)
iruvar
@iruvar Terima kasih atas sarannya. Saya sudah mencobanya (sans not) dan selesai dalam 32 detik, jadi tidak banyak perbaikan, tapi tentu saja lebih mudah dibaca.
arekolek
Anda bisa menggunakan array asosiatif daripada f1, f2, f3 di awk, dengan kunci = pola pencarian, val = count
cas
@arekolek lihat versi terbaru saya menggunakan PerlIO::gzipbukan IO::Uncompress::AnyUncompress. sekarang hanya membutuhkan 3,1 detik, bukannya 1m13s untuk memproses 74MB file log saya.
cas
BTW, jika sebelumnya Anda telah menjalankan eval $(lesspipe)(misalnya di Anda .profile, dll), Anda dapat menggunakan lesssebagai ganti zcat -fdan forpembungkus lingkaran Anda awkakan dapat memproses segala jenis file yang lessdapat (gzip, bzip2, xz, dan banyak lagi) .... kurang bisa mendeteksi jika stdout adalah pipa dan hanya akan mengeluarkan aliran ke stdout jika itu.
cas
0

Opsi lain - mengumpankan kata satu per satu xargsuntuk menjalankannya grepterhadap file. xargsitu sendiri dapat dibuat untuk keluar segera setelah doa grepkegagalan pengembalian dengan kembali 255ke sana (periksa xargsdokumentasi). Tentu saja pemijahan cangkang dan forking yang terlibat dalam solusi ini kemungkinan akan memperlambatnya secara signifikan

printf '%s\n' one two three | xargs -n 1 sh -c 'grep -q $2 $1 || exit 255' _ file

dan untuk mengulanginya

for f in *; do
    if printf '%s\n' one two three | xargs -n 1 sh -c 'grep -q $2 $1 || exit 255' _ "$f"
    then
         printf '%s\n' "$f"
    fi
done
iruvar
sumber
Ini terlihat bagus, tapi saya tidak yakin bagaimana menggunakannya. Apa _dan file? Akankah pencarian ini dalam banyak file diteruskan sebagai argumen dan mengembalikan file yang berisi semua kata kunci?
arekolek
@arekolek, menambahkan versi loop. Dan untuk _, itu dilewatkan sebagai $0ke shell menelurkan - ini akan muncul sebagai nama perintah dalam output ps- saya akan tunduk pada master di sini
iruvar