Saya memiliki masalah aneh dengan file besar dan bash
. Inilah konteksnya:
- Saya memiliki file besar: 75G dan 400.000.000 + baris (ini adalah file log, salah saya, saya biarkan tumbuh).
- 10 karakter pertama dari setiap baris adalah stempel waktu dalam format YYYY-MM-DD.
- Saya ingin membagi file itu: satu file per hari.
Saya mencoba dengan skrip berikut yang tidak berfungsi. Pertanyaan saya adalah tentang skrip ini tidak berfungsi, bukan solusi alternatif .
while read line; do
new_file=${line:0:10}_file.log
echo "$line" >> $new_file
done < file.log
Setelah debugging, saya menemukan masalah dalam new_file
variabel. Skrip ini:
while read line; do
new_file=${line:0:10}_file.log
echo $new_file
done < file.log | uniq -c
memberikan hasilnya di bawah (saya meletakkan x
es untuk menjaga data rahasia, karakter lain adalah yang asli). Perhatikan dh
string dan lebih pendek:
...
27402 2011-xx-x4
27262 2011-xx-x5
22514 2011-xx-x6
17908 2011-xx-x7
...
3227382 2011-xx-x9
4474604 2011-xx-x0
1557680 2011-xx-x1
1 2011-xx-x2
3 2011-xx-x1
...
12 2011-xx-x1
1 2011-xx-dh
1 2011-xx-x1
1 208--
1 2011-xx-x1
1 2011-xx-dh
1 2011-xx-x1
...
Ini bukan masalah dalam format file saya . Script cut -c 1-10 file.log | uniq -c
hanya memberikan perangko waktu yang valid. Menariknya, bagian dari output di atas menjadi dengan cut ... | uniq -c
:
3227382 2011-xx-x9
4474604 2011-xx-x0
5722027 2011-xx-x1
Kita dapat melihat bahwa setelah hitungan uniq 4474604
, skrip awal saya gagal.
Apakah saya mencapai batas dalam bash yang saya tidak tahu, apakah saya menemukan bug di bash (sepertinya tidak mungkin), atau apakah saya melakukan sesuatu yang salah?
Perbarui :
Masalahnya terjadi setelah membaca 2G file. Jahitan read
dan pengalihan tidak suka file yang lebih besar dari 2G. Namun masih mencari penjelasan yang lebih tepat.
Pembaruan2 :
Ini pasti terlihat seperti bug. Itu dapat direproduksi dengan:
yes "0123456789abcdefghijklmnopqrs" | head -n 100000000 > file
while read line; do file=${line:0:10}; echo $file; done < file | uniq -c
tetapi ini berfungsi dengan baik sebagai solusi (sepertinya saya menemukan penggunaan yang bermanfaat cat
):
cat file | while read line; do file=${line:0:10}; echo $file; done | uniq -c
Bug telah diajukan ke GNU dan Debian. Versi yang terpengaruh adalah bash
4.1.5 pada Debian Squeeze 6.0.2 dan 6.0.4.
echo ${BASH_VERSINFO[@]}
4 1 5 1 release x86_64-pc-linux-gnu
Pembaruan3:
Terima kasih kepada Andreas Schwab yang bereaksi cepat terhadap laporan bug saya, ini adalah tambalan yang merupakan solusi untuk perilaku buruk ini. File yang terpengaruh adalah lib/sh/zread.c
seperti yang Gilles tunjukkan lebih cepat:
diff --git a/lib/sh/zread.c b/lib/sh/zread.c index 0fd1199..3731a41 100644
--- a/lib/sh/zread.c
+++ b/lib/sh/zread.c @@ -161,7 +161,7 @@ zsyncfd (fd)
int fd; { off_t off;
- int r;
+ off_t r;
off = lused - lind; r = 0;
The r
variabel digunakan untuk menyimpan nilai kembali dari lseek
. Ketika lseek
mengembalikan offset dari awal file, ketika lebih dari 2GB, int
nilainya negatif, yang menyebabkan tes if (r >= 0)
gagal di tempat yang seharusnya berhasil.
read
pernyataan dalam bash.Jawaban:
Anda menemukan bug di bash, atau sejenisnya. Ini adalah bug yang dikenal dengan perbaikan yang dikenal.
Program mewakili offset dalam file sebagai variabel dalam beberapa tipe integer dengan ukuran terbatas. Di masa lalu, semua orang menggunakan
int
hampir semuanya, danint
jenisnya terbatas pada 32 bit, termasuk bit tanda, sehingga dapat menyimpan nilai dari -2147483648 hingga 2147483647. Saat ini ada berbagai jenis nama untuk berbagai hal , termasukoff_t
untuk offset dalam suatu file.Secara default,
off_t
adalah tipe 32-bit pada platform 32-bit (memungkinkan hingga 2GB), dan tipe 64-bit pada platform 64-bit (memungkinkan hingga 8EB). Namun, itu umum untuk mengkompilasi program dengan opsi LARGEFILE, yang mengubah jenisoff_t
menjadi 64 bit lebar dan membuat panggilan program implementasi yang sesuai dari fungsi sepertilseek
.Tampaknya Anda menjalankan bash pada platform 32-bit dan biner bash Anda tidak dikompilasi dengan dukungan file besar. Sekarang, ketika Anda membaca baris dari file biasa, bash menggunakan buffer internal untuk membaca karakter dalam batch untuk kinerja (untuk lebih jelasnya, lihat sumber di
builtins/read.def
). Ketika saluran selesai, bash panggilanlseek
untuk memundurkan file mengimbangi kembali ke posisi akhir baris, jika ada program lain yang peduli tentang posisi dalam file itu. Panggilan untuklseek
terjadi dalamzsyncfc
fungsi dilib/sh/zread.c
.Saya belum membaca sumbernya secara mendetail, tetapi saya menduga bahwa sesuatu tidak terjadi dengan lancar pada titik transisi ketika offset absolut negatif. Jadi bash akhirnya membaca di offset yang salah saat mengisi ulang buffernya, setelah melewati tanda 2GB.
Jika kesimpulan saya salah dan bash Anda sebenarnya berjalan pada platform 64-bit atau dikompilasi dengan dukungan bigfile, itu pasti bug. Silakan laporkan ke distribusi Anda atau ke hulu .
Shell bukanlah alat yang tepat untuk memproses file besar seperti itu. Ini akan lambat. Gunakan sed jika mungkin, jika tidak awk.
sumber
Saya tidak tahu tentang yang salah, tapi itu jelas berbelit-belit. Jika jalur input Anda terlihat seperti ini:
Maka benar-benar tidak ada alasan untuk ini:
Anda melakukan banyak pekerjaan substring untuk menghasilkan sesuatu yang terlihat ... persis seperti yang terlihat dalam file. Bagaimana dengan ini?
Itu hanya mengambil 10 karakter pertama dari garis. Anda juga bisa membuang
bash
sepenuhnya dan cukup gunakanawk
:Ini mengambil tanggal di
$1
(kolom spasi-dibatasi pertama di setiap baris) dan menggunakannya untuk menghasilkan nama file.Perhatikan bahwa ada kemungkinan bahwa ada beberapa garis log palsu di file Anda. Artinya, masalahnya mungkin dengan input, bukan skrip Anda. Anda dapat memperluas
awk
skrip untuk menandai garis palsu seperti ini:Ini menulis baris yang cocok
YYYY-MM-DD
dengan file log Anda, dan menandai baris yang tidak dimulai dengan stempel waktu di stdout.sumber
cut -c 1-10 file.log | uniq -c
memberi saya hasil yang diharapkan. Saya menggunakan${line:0:4}-${line:5:2}-${line:8:2}
karena saya akan meletakkan file dalam direktori${line:0:4}/${line:5:2}/${line:8:2}
, dan saya menyederhanakan masalah (saya akan memperbarui pernyataan masalah). Saya tahuawk
dapat membantu saya di sini, tetapi saya mengalami masalah lain dalam menggunakannya. Yang saya inginkan adalah memahami masalahnyabash
, bukan mencari solusi alternatif.cut
pernyataan yang berfungsi. Karena saya ingin membandingkan apel dengan apel, bukan dengan jeruk, saya harus membuat hal-hal yang sama mungkin.Kedengarannya seperti apa yang ingin Anda lakukan adalah:
Itu
close
membuat tabel file terbuka dari mengisi.sumber