awk ile Çoklu Delimiter İçeren Ham Veriyi Normalize Etme ve Standart Formata Dönüştürme

Gerçek dünya loglarıyla, üretim ortamında oturmuş uğraşmış biri olarak şunu söyleyeyim: hayatınızda en az bir kez farklı sistemlerden gelen, birbirinden tamamen farklı biçimlendirilmiş ham veriyi tek bir standart formata sokmanız gerekecek. Belki bir ERP sisteminden CSV çıktısı alıyorsunuz, belki bir ağ cihazından pipe ile ayrılmış log geliyor, belki de eski bir uygulamanın çıktısı hem noktalı virgül hem tab hem de boşluk kullanıyor. İşte bu noktada awk gerçek değerini gösteriyor.

Sorunun Anatomisi: Çoklu Delimiter Nedir?

Tek bir delimiter ile ayrılmış veri hayatta pek nadir görülür. Özellikle farklı ekiplerin, farklı sistemlerin veri ürettiği ortamlarda kaçınılmaz olarak şöyle bir durumla karşılaşırsınız:

ad:soyad|departman;maas  gorev
ahmet:celik|yazilim;15000  senior
fatma:yilmaz|sistem;18000  lead
baris:kaya|devops;22000  principal

Bu dosyaya bakınca :, |, ; ve boşluk karakterlerinin aynı anda delimiter görevi gördüğünü görüyoruz. Standart cut komutu burada çaresiz kalır. awk ise bu durumu zarifçe çözer.

awk’ın -F ile Temel Delimiter Kullanımı

Önce basit bir hatırlatma. Tek delimiter durumunda -F bayrağı ile işi bitirirsiniz:

awk -F'|' '{print $1, $2}' veri.txt

Ama çoklu delimiter için -F parametresine bir regex geçmeniz gerekiyor. Köşeli parantez içinde birden fazla karakteri OR mantığıyla tanımlayabilirsiniz:

awk -F'[:|; ]' '{print $1, $2, $3, $4, $5}' veri.txt

Bu komut :, |, ; ve boşluk karakterlerinin herhangi birini delimiter olarak kabul eder. Çıktı şöyle gelir:

ad soyad departman maas gorev
ahmet celik yazilim 15000 senior
fatma yilmaz sistem 18000 lead
baris kaya devops 22000 principal

Başlık satırını da işlediği için onu sonradan ayıklamamız gerekebilir, buna birazdan geleceğiz.

Gerçek Senaryo: ERP Sisteminden Gelen Ham Veri

Bir müşteri projesinde SAP üzerinden çekilen ve aşağıdaki gibi görünen bir veriyle karşılaşmıştım. Binlerce satır, birden fazla uygulama tarafından üretilmiş, delimiter tutarsızlığı hat safhada:

cat erp_cikti.txt
KULLANICI_ID|AD;SOYAD:BOLUM   YETKI|TARIH
1001|ahmet;celik:bilgi_isleme   admin|2024-01-15
1002|fatma;arslan:insan_kaynaklari   user|2024-02-20
1003|mehmet;ozturk:muhasebe   readonly|2024-03-01
1004|zeynep;demir:yazilim   superadmin|2024-01-08

Hedef: Bu veriyi virgülle ayrılmış standart bir CSV’ye dönüştürmek. Header satırını normalize etmek, tüm alanları düzgün çıkarmak.

awk -F'[|;: t]+' 'NR==1 {
    print "kullanici_id,ad,soyad,bolum,yetki,tarih"
    next
}
{
    print $1","$2","$3","$4","$5","$6
}' erp_cikti.txt

Burada dikkat edilmesi gereken birkaç nokta var. + işareti, art arda gelen aynı veya farklı delimiter karakterlerini tek bir delimiter olarak değerlendirmesini sağlar. Tab karakterini de t ile ekliyoruz. NR==1 ile ilk satırı (başlık) atlayıp yerine kendi başlığımızı yazdırıyoruz, next ise o satır için diğer işlemleri atlıyor.

OFS: Çıktı Delimiter’ını Kontrol Etmek

Birçok yeni sysadmin awk‘ta alanları string birleştirme ile çıkarmaya çalışır. Daha temiz yol OFS (Output Field Separator) kullanmaktır:

awk 'BEGIN{FS="[|;: t]+"; OFS=","} 
NR>1 {print $1,$2,$3,$4,$5,$6}' erp_cikti.txt

BEGIN bloğu dosya işlenmeden önce çalışır. FS ile giriş ayırıcısını, OFS ile çıkış ayırıcısını tanımlıyoruz. print $1,$2,$3,$4 gibi virgülle ayrılmış alan listesi kullandığınızda awk aralarına otomatik olarak OFS değerini koyar. Bu yaklaşım hem daha okunabilir hem de bakımı kolay kod üretir.

Dinamik Sütun Sayısıyla Baş Etmek

Bazen her satırda farklı sayıda alan olur. Log dosyaları buna en güzel örnektir. Şöyle bir senaryoyu düşünün: Uygulama logları hem eski hem yeni format içeriyor.

2024-01-15 10:23:45|ERROR|database|Bağlantı zaman aşımı
2024-01-15 10:24:01;WARN;network;Yüksek gecikme;5000ms
2024-01-15 10:25:33:INFO:app:Servis yeniden başlatıldı
2024-01-15 10:26:00|DEBUG|cache Önbellek temizlendi|key_count=1250

Bu logu standart bir formata sokmak için NF (Number of Fields) değişkenini kullanabiliriz:

awk -F'[|;: t]+' '{
    timestamp = $1" "$2
    seviye = $3
    kaynak = $4
    
    # Kalan tüm alanları mesaj olarak birleştir
    mesaj = ""
    for(i=5; i<=NF; i++) {
        mesaj = mesaj (i>5 ? " " : "") $i
    }
    
    printf "%-20s %-8s %-10s %sn", timestamp, seviye, kaynak, mesaj
}' uygulama.log

NF değişkeni her satırda otomatik olarak güncellenir ve o satırdaki toplam alan sayısını verir. printf ile sabit genişlikte sütunlar oluşturarak çıktıyı düzenli hale getiriyoruz.

Koşullu Normalizasyon: Satır Tipine Göre Farklı İşlem

Gerçek dünyada tek tip veri pek gelmez. Aynı dosyada birden fazla format olabilir. Şöyle bir yaklaşımla her satırı içeriğine göre işleyebiliriz:

awk 'BEGIN {
    FS_PIPE="|"
    FS_SEMI=";"
    FS_COLON=":"
    OFS=","
}
{
    # Hangi delimiter baskın?
    if (index($0, "|") > 0 && gsub(/|/, "|") >= 2) {
        n = split($0, alanlar, "|")
        format = "PIPE"
    } else if (index($0, ";") > 0) {
        n = split($0, alanlar, ";")
        format = "SEMI"
    } else {
        n = split($0, alanlar, ":")
        format = "COLON"
    }
    
    # Normalize edilmiş çıktı
    printf "%s", alanlar[1]
    for(i=2; i<=n; i++) printf ",%s", alanlar[i]
    printf " [%s]n", format
}' karisik_veri.txt

Bu script her satırda önce hangi delimiter’ın baskın olduğunu tespit ediyor, sonra split() fonksiyonuyla o delimiter’a göre ayrıştırıyor. split() fonksiyonu diziye ayrıştırma yapar ve alan sayısını döndürür, çok kullanışlı bir araç.

Boşluk ve Tab Karakterlerinin Karmaşası

Özellikle Windows’tan gelen dosyalarda ya da elle oluşturulmuş yapılandırma dosyalarında hem tab hem boşluk hem de bunların kombinasyonlarını görürsünüz. awk‘ın varsayılan FS değeri olan tek boşluk aslında özel bir anlam taşır: birden fazla boşluğu ve tab’ı tek delimiter olarak değerlendirir. Ama başına başka karakterler de eklediğinizde bu davranış değişir.

# Varsayılan FS ile
awk '{print $2}' dosya.txt

# Özel regex ile - boşluk ve tab kombinasyonu artı diğerleri
awk -F'[[:space:]|;,]+' '{print $2}' dosya.txt

[[:space:]] POSIX karakter sınıfı, boşluk, tab, newline, carriage return gibi tüm whitespace karakterlerini kapsar. Windows satır sonları (r) ile uğraşıyorsanız bu tanım hayat kurtarır.

Şöyle somut bir örnek: Sistemdeki servis listesini farklı kaynaklardan çekip birleştiriyorsunuz ve her kaynak farklı format kullanıyor:

cat servisler_karisik.txt
nginx   80    tcp|aktif
mysql;3306;tcp;aktif
redis:6379:tcp:aktif
postgresql   5432   tcp   aktif
awk -F'[[:space:]|;:]+' 'BEGIN{OFS="t"; print "SERVIStPORTtPROTOKOLtDURUM"} 
{
    # Satır başı ve sonu whitespace temizle
    gsub(/^[[:space:]]+|[[:space:]]+$/, "")
    if(NF >= 4) print $1, $2, $3, $4
}' servisler_karisik.txt

Çok Satırlı Kayıtları İşlemek

Bazı durumlarda bir kayıt birden fazla satıra yayılmış olabilir. Bu en zorlu senaryolardan biridir. Mesela şöyle bir yapıyı düşünün:

---KAYIT---
ID: 1001
AD: Ahmet Çelik | Yazılım Müdürü
BOLUM: Bilgi İşlem; IT
---KAYIT---
ID: 1002
AD: Fatma Arslan | Sistem Uzmanı
BOLUM: Altyapı; Network
awk 'BEGIN {
    RS="---KAYIT---n"
    FS="n"
    OFS=","
}
NR > 1 {
    id=""; ad=""; bolum=""
    for(i=1; i<=NF; i++) {
        if($i ~ /^ID:/) {
            split($i, a, ": ")
            id = a[2]
        }
        else if($i ~ /^AD:/) {
            split($i, a, ": ")
            # Pipe ile ayrılmış ad ve unvan
            split(a[2], b, " | ")
            ad = b[1]
        }
        else if($i ~ /^BOLUM:/) {
            split($i, a, ": ")
            # Noktalı virgülü temizle
            gsub(/; /, "/", a[2])
            bolum = a[2]
        }
    }
    if(id != "") print id, ad, bolum
}' cok_satirli.txt

Burada RS (Record Separator) değişkenini değiştiriyoruz. Normalde her satır bir kayıttır, ama burada ---KAYIT--- ile ayrılan bloklar birer kayıt. FS="n" yaparak her satırı ayrı bir alan olarak işliyoruz.

Pipeline ile Büyük Veri İşleme

Üretim ortamında genellikle dosyalar gigabyte mertebesinde olabilir. awk stream tabanlı çalıştığı için belleğe bütün dosyayı yüklemez, bu büyük bir avantaj. Pipeline içinde kullanımına bakalım:

# Sıkıştırılmış log dosyasını açarak işle
zcat /var/log/app/uygulama-2024*.log.gz | 
    awk -F'[|;:t ]+' '
    $3 == "ERROR" || $3 == "CRITICAL" {
        gsub(/[[:space:]]+/, " ")
        printf "%s,%s,%s,"%s"n", $1, $2, $3, substr($0, index($0,$4))
    }' | 
    sort -t',' -k1,2 | 
    uniq > hatalar_normalize.csv

Bu pipeline şunları yapıyor: sıkıştırılmış log dosyalarını açıyor, çoklu delimiter ile parse ediyor, sadece ERROR ve CRITICAL satırlarını alıyor, mesaj kısmını tırnak içine alarak CSV uyumlu hale getiriyor, tarihe göre sıralıyor ve tekrarları temizliyor.

gsub ve sub ile Delimiter Temizliği

Bazen alanların içinde de delimiter karakterleri bulunabilir. Bu durumu gsub() ile hallederiz:

awk -F'[|;]' 'BEGIN{OFS=","} 
{
    for(i=1; i<=NF; i++) {
        # Her alanın içindeki fazla boşlukları temizle
        gsub(/^[[:space:]]+|[[:space:]]+$/, "", $i)
        # Alandaki virgülleri çift tırnak ile koru
        if($i ~ /,/) $i = """$i"""
    }
    print
}' ham_veri.txt

Bu yaklaşım CSV standartlarına uygun çıktı üretir. İçinde virgül olan alanları çift tırnak içine alır ki hedef sistem yanlış parse etmesin.

Büyük Ölçekli Normalizasyon Script’i

Tüm bu bilgileri bir araya getiren, üretimde kullanılabilir düzeyde bir script:

#!/bin/bash
# normalize_veri.sh - Çoklu delimiter içeren veriyi standart CSV'ye dönüştür

GIRIS_DOSYA="${1:-/dev/stdin}"
CIKIS_DOSYA="${2:-cikis.csv}"
DELIMITER_PATTERN="${3:-[|;:,t]}"

awk -v pattern="$DELIMITER_PATTERN" '
BEGIN {
    FS = pattern
    OFS = ","
    satir_sayisi = 0
    hata_sayisi = 0
    print "# Normalizasyon başladı: " strftime("%Y-%m-%d %H:%M:%S") > "/dev/stderr"
}

# Yorum satırlarını ve boş satırları atla
/^#/ || /^[[:space:]]*$/ { next }

NR == 1 {
    # Header satırını normalize et
    for(i=1; i<=NF; i++) {
        gsub(/[[:space:]]+/, "_", $i)
        $i = tolower($i)
    }
    print
    next
}

{
    satir_sayisi++
    
    # Alanları temizle
    for(i=1; i<=NF; i++) {
        gsub(/^[[:space:]]+|[[:space:]]+$/, "", $i)
        gsub(/r/, "", $i)  # Windows satır sonu temizle
        
        # Virgül içeren alanları tırnak içine al
        if($i ~ /,/) {
            $i = """ $i """
        }
    }
    
    # Minimum alan sayısı kontrolü (örnek: en az 3 alan)
    if(NF < 3) {
        print "UYARI: Satır " NR " az alan içeriyor: " $0 > "/dev/stderr"
        hata_sayisi++
        next
    }
    
    print
}

END {
    print "# İşlenen satır: " satir_sayisi > "/dev/stderr"
    print "# Atlanan satır: " hata_sayisi > "/dev/stderr"
    print "# Tamamlandı: " strftime("%Y-%m-%d %H:%M:%S") > "/dev/stderr"
}
' "$GIRIS_DOSYA" > "$CIKIS_DOSYA"

Bu script parametre olarak giriş dosyası, çıkış dosyası ve delimiter pattern alıyor. strftime() için bazı awk sürümlerinde gawk gerekebilir, bunu göz önünde bulundurun. İstatistikleri stderr’e yazdığı için çıktı dosyasını kirletmiyor.

Sık Yapılan Hatalar ve Kaçınma Yolları

Çoklu delimiter işlerken en çok karşılaşılan sorunları sıralayayım:

  • Regex’te özel karakterleri escape etmemek: . veya + gibi karakterleri delimiter olarak kullanmak istiyorsanız \. veya \+ yazmalısınız
  • Boş alan sorunları: Art arda iki delimiter geldiğinde awk aralarında boş bir alan görür. + quantifier bu durumu önler ama istenmeyen alanları da atlayabilir, dikkatli kullanın
  • NF değişikliği: $0‘ı değiştirirseniz awk satırı OFS ile yeniden birleştirir ve NF hesabı değişebilir. Döngü içinde alan ataması yaparken bunu aklınızda bulundurun
  • Büyük/küçük harf farklılıkları: Log seviyelerinin kimi yerde ERROR kimi yerde error yazıldığı durumlar için tolower() veya toupper() kullanın
  • Encoding sorunları: UTF-8 dışı karakterler içeren dosyalarda LANG=C awk ile çalışmak daha güvenli sonuç verir

Sonuç

awk ile çoklu delimiter normalizasyonu, sysadmin toolbox’ının en güçlü silahlarından biri. Python veya Perl ile de bu işi yapabilirsiniz, haklısınız. Ama awk‘ın sunduğu şey şu: tek satır ya da birkaç satır ile, ek paket kurmadan, herhangi bir Linux sisteminde çalışan, stream tabanlı ve son derece hızlı bir çözüm.

Özellikle gece yarısı bir üretim ortamında acil veri müdahalesi yapmanız gerektiğinde, Python virtual environment veya Perl modülü peşinde koşmak istemezsiniz. O anlarda awk hayat kurtarır.

Anlatılan her yaklaşımı kendi ortamınızdaki gerçek verilerle test edin. Her sistemin kendine özgü kirli verisi vardır ve en iyi öğrenme yolu o veriyle boğuşmaktır. -F ile regex denemeleri yapın, split() ve gsub() kombinasyonlarını keşfedin, BEGIN/END bloklarını etkin kullanmayı alışkanlık haline getirin.

Ham veri ne kadar kaotik olursa olsun, awk onu düzene sokacak gücü size verir.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir