Bash komut dosyası oluşturma ve büyük dosyalar (hata): yeniden yönlendirme tarafından read builtin ile giriş beklenmedik sonuç verir


16

Büyük dosyalar ve ile ilgili garip bir sorunum var bash. Bağlam budur:

  • Büyük bir dosyam var: 75G ve 400.000.000+ satır (bir günlük dosyası, kötüyüm, büyümesine izin verdim).
  • Her satırın ilk 10 karakteri YYYY-AA-GG biçimindeki zaman damgalarıdır.
  • O dosyayı bölmek istiyorum: günde bir dosya.

Ben işe yaramadı aşağıdaki komut dosyası ile denedim. Benim sorum bu betik çalışmıyor, alternatif çözümler değil .

while read line; do
  new_file=${line:0:10}_file.log
  echo "$line" >> $new_file
done < file.log

Hata ayıklama sonra, new_filedeğişkende sorun buldum . Bu komut dosyası:

while read line; do
  new_file=${line:0:10}_file.log
  echo $new_file
done < file.log | uniq -c

aşağıdaki sonucu verir ( xVerileri gizli tutmak için es koymak , diğer karakter gerçek olanlar). dhVe daha kısa dizelere dikkat edin :

...
  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    
...

Benim dosya biçiminde bir sorun değil . Komut dosyası cut -c 1-10 file.log | uniq -cyalnızca geçerli zaman damgaları verir. İlginçtir, yukarıdaki çıktının bir kısmı şu şekilde olur cut ... | uniq -c:

3227382 2011-xx-x9
4474604 2011-xx-x0
5722027 2011-xx-x1

Uniq sayımından sonra 4474604ilk betiğimin başarısız olduğunu görebiliriz.

Bash'ta bilmediğim bir sınıra girdim mi, bash'da bir hata buldum mu (olası dikişler) mi, yoksa yanlış bir şey mi yaptım?

Güncelleme :

Sorun, dosyanın 2G'sini okuduktan sonra ortaya çıkar. Dikişler readve yönlendirme 2G'den daha büyük dosyaları sevmez. Ama yine de daha kesin bir açıklama arıyor.

Güncelleme2 :

Kesinlikle bir hata gibi görünüyor. Şununla çoğaltılabilir:

yes "0123456789abcdefghijklmnopqrs" | head -n 100000000 > file
while read line; do file=${line:0:10}; echo $file; done < file | uniq -c

ancak bu bir geçici çözüm olarak iyi çalışır (yararlı bir kullanım bulduğum dikişler cat):

cat file | while read line; do file=${line:0:10}; echo $file; done | uniq -c 

GNU ve Debian'a bir hata bildirildi. Etkilenen sürümler bashDebian Squeeze 6.0.2 ve 6.0.4 üzerinde 4.1.5'tir.

echo ${BASH_VERSINFO[@]}
4 1 5 1 release x86_64-pc-linux-gnu

Update3:

Hata raporuma hızlı tepki veren Andreas Schwab sayesinde, bu yanlış davranışın çözümü olan yama bu. Etkilenen dosya, lib/sh/zread.cGilles'in daha önce işaret ettiği gibi:

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;

rDeğişken dönüş değeri tutmak için kullanılır lseek. Gibi lseekdöner dosyanın başından mahsup o 2GB bittiğinde, intdeğer testi neden olan negatif if (r >= 0)bunun başarılı olması gereken yerde başarısız.


1
Sorunu daha küçük girdi verileri kümeleriyle çoğaltabilir misiniz? Bu sorunlara neden olan giriş satırları hep aynı mıdır?
larsks

@larks: iyi soru. Sorun her zaman # 13.520.918 satırında başlar (yaptığım testler için iki kez). Bu satırdan önceki dosyanın boyutu 2.147.487.726'dır. Burada 32 bitlik bir sınır var, ancak tam olarak 2 ^ 31'in (2.147.483.648) biraz üstünde olduğumuz gibi değil, tam bir 4K tampon sınırında (2 ^ 31 + 4K = 2.147.487.744). Önceki ve sonraki satırlar normal 100 ila 200 karakter arasındadır.
jfg956

2. bir dosyada test edildi (yaklaşık aynı boyutta): sorun # 13.522.712 satırında başlıyor ve dosya bu satırdan önce 2.147.498.679 bayt büyüklüğünde. Bash'ta readifadenin bir sınırı yönünde işaret eder .
jfg956

Yanıtlar:


13

Bash'ta bir çeşit hata buldunuz. Bilinen bir düzeltmeyle bilinen bir hatadır.

Programlar, bir dosyadaki ofseti sonlu boyutta bir tamsayı tipinde değişken olarak temsil eder. Eski günlerde, herkes inthemen hemen her şey için kullandı ve inttip, işaret biti de dahil olmak üzere 32 bit ile sınırlıydı, bu nedenle -2147483648 ile 2147483647 arasındaki değerleri depolayabilir. Günümüzde farklı şeyler için farklı tür adları vardır. da dahil olmak üzere off_tbir için dosyada ofset.

Varsayılan olarak, off_t32 bit bir platformda 32 bit tür (2 GB'a kadar izin verir) ve 64 bit bir platformda 64 bit tür (8EB'ye kadar izin verir). Bununla birlikte, programları off_t64 bit genişliğe geçiren ve program gibi işlevlerin uygun uygulamalarını çağıran LARGEFILE seçeneğiyle derlemek yaygındır lseek.

32-bit platformda bash çalıştırdığınız ve bash binary'nizin büyük dosya desteği ile derlenmediği anlaşılıyor. Şimdi, normal bir dosyadan bir satır okuduğunuzda, bash, performans için karakterleri toplu olarak okumak için dahili bir arabellek kullanır (daha fazla ayrıntı için kaynağa bakın builtins/read.def). Çizgi tamamlandığında, bash lseek, başka bir programın o dosyadaki konumu önemsemesi durumunda, dosya ofsetini satırın sonuna kadar geri sarmaya çağırır . Çağrısı lseekolur zsyncfcişlev lib/sh/zread.c.

Kaynağı çok ayrıntılı olarak okumadım, ancak mutlak ofset negatif olduğunda geçiş noktasında bir şeylerin sorunsuz bir şekilde gerçekleşmediğini düşünüyorum. Bu yüzden bash, 2GB işaretini geçtikten sonra arabelleğini yeniden doldururken yanlış ofsetlerde okumaya başlar.

Sonucum yanlışsa ve bashınız aslında 64 bit bir platformda çalışıyorsa veya büyük dosya desteğiyle derlenmişse, bu kesinlikle bir hatadır. Lütfen dağıtımınıza bildirin veya yukarı bildirin .

Kabuk, bu tür büyük dosyaları zaten işlemek için doğru araç değildir. Yavaş olacak. Mümkünse sed kullanın, aksi takdirde awk.


1
Merci Gilles. Büyük cevap: tamam, güçlü CS geçmişi olmayan insanlar için bile sorunu anlamak için yeterli bilgi ile (32 bit ...). (larsks da satır numarasını sorgulamada yardımcı olur ve kabul edilmelidir.) Bundan sonra, 32 bitlik bir sorun olsa da ve kaynağı indirdim, ancak henüz bu analiz düzeyine gelmedim. Merci encore, et bonne dergisi.
jfg956

4

Yanlış hakkında bilmiyorum, ama kesinlikle kıvrık. Giriş satırlarınız aşağıdaki gibi görünüyorsa:

YYYY-MM-DD some text ...

O zaman bunun için hiçbir sebep yok:

new_file=${line:0:4}-${line:5:2}-${line:8:2}_file.log

Görünen bir şeyle sonuçlanmak için çok sayıda alt dize işi yapıyorsunuz ... tam olarak dosyada zaten göründüğü gibi. Buna ne dersin?

while read line; do
  new_file="${line:0:10}_file.log"
  echo "$line" >> $new_file
done

Bu sadece satırdaki ilk 10 karakteri alır. Ayrıca bashtamamen ve sadece aşağıdakilerden yararlanabilirsiniz awk:

awk '{print > ($1 "_file.log")}' < file.log

Bu tarih $1 (her satırdaki ilk boşlukla ayrılmış sütun) alır ve dosya adını oluşturmak için kullanır.

Dosyalarınızda bazı sahte günlük satırları olabileceğini unutmayın. Yani, sorun betiğinizle değil girdiyle ilgili olabilir. awkKomut dosyasını aşağıdaki gibi sahte satırları işaretlemek için genişletebilirsiniz :

awk '
$1 ~ /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/ {
    print > ($1 "_file.log")
    next
}

{
    print "INVALID:", $0
}
'

Bu YYYY-MM-DD, günlük dosyalarınızla eşleşen satırlar yazar ve stdout'ta zaman damgası ile başlamayan satırları işaretler.


Dosyamda sahte satır yok: cut -c 1-10 file.log | uniq -cbeklenen sonucu verir. Kullanıyorum ${line:0:4}-${line:5:2}-${line:8:2}çünkü dosyayı bir dizine koyacağım ${line:0:4}/${line:5:2}/${line:8:2}ve sorunu basitleştirdim (sorun bildirimini güncelleyeceğim). awkBurada bana yardımcı olabileceğini biliyorum , ama bunu kullanırken başka problemlerle karşılaştım. İstediğim problemi anlamak, bashalternatif çözümler bulmak değil.
jfg956

Dediğiniz gibi ... sorudaki sorunu "basitleştirir", muhtemelen istediğiniz cevapları alamazsınız. Hala bunu bash ile çözmenin bu tür verileri işlemenin doğru yolu olmadığını düşünüyorum, ancak çalışmaması için hiçbir neden yok.
larsks

Basitleştirilmiş sorun, soruda sunduğum beklenmedik sonucu veriyor, bu yüzden bunun aşırı bir basitleştirme olduğunu düşünmüyorum. Dahası, basitleştirilmiş sorun cutçalışan ifadeye benzer bir sonuç verir . Elmaları portakalla değil elma ile karşılaştırmak istediğim için, mümkün olduğunca benzer şeyler yapmam gerekiyor.
jfg956

1
Sana işlerin nereye gittiğini anlamaya yardımcı olabilecek bir soru bıraktım ...
larsks

2

Kulağa yapmak istediğiniz gibi geliyor:

awk '
{  filename = substr($0, 0, 10) "_file.log";  # input format same as output format
   if (filename != lastfile) {
       close(lastfile);
       print 'finished writing to', lastfile;
   }
   print >> filename;
   lastfile=filename;
}' file.log

closeDolduruyor gelen açık dosya tablosunu tutar.


Awk çözümü için teşekkürler. Ben zaten benzer bir şeyle geldim. Benim sorum alternatif bir çözüm bulmak değil, bash sınırlamasını anlamaktı.
jfg956
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.