12 Veri Dosyaları ve Kalıcılık

12 Veri Dosyaları ve Kalıcılık

Veri dosyalarıyla uğraşırken verileri yazmak onları okumak dan çok daha kolaydır genelde. Bir dosyaya yazdığımızda olup biten hakkında tam kontrolümüz olur. Öte yandan, bir dosyayı okuduğumuzda bizi neyin beklediğini bilemeyiz. Hatasız bir dosyanın her türlü veriyi içerebileceği bir tarafa, iyi bir program da kötü dosyaları incelikle ele alabilmelidir. Bu nedenle sağlam girdi rutinleri kodlamak her zaman zordur.

Bu bölümde programlarımızdan basitçe ilgili bir formatta veri yazma ve okuma için tam kodu çıkarmada Lua'ı nasıl kullanabileceğimizi göreceğiz.

12.1 Veri Dosyaları

Bölüm 10.1 örneğinde gördüğümüz üzere tablo oluşturucuları dosya formatları için ilginç bir alternatif sunar. Veri yazımında biraz ekstra iş ile okuma önemsiz hale gelir. Teknik, veri dosyamızı Lua kodu olarak yazmaktır, çalıştırıldığında veriler programda oluşur. Tablo oluşturucularıyla bu öbekler neredeyse bildik düz veri dosyasına benzer.

Her zamanki gibi olayı netleştirmek için bir örnek görelim. Veri dosyamız CSV (virgülle ayrılmış değerler) veya XML gibi öntanımlı bir formatta ise çok az seçeneğimiz var. Ancak dosyayı kendi kullanımımız için yaratacak isek Lua oluşturucularını kendi formatımız olarak kullanabiliriz. Bu formatta her bir veri kaydını bir Lua oluşturucusu ile temsil ediyoruz. Veri dosyamıza şöyle bir şey yazmak yerine

Donald E. Knuth,Literate Programming,CSLI,1992
Jon Bentley,More Programming Pearls,Addison-Wesley,1990

şöyle yazarız

Giris{"Donald E. Knuth",
"Literate Programming",
"CSLI",
1992}

Giris{"Jon Bentley",
"More Programming Pearls",
"Addison-Wesley",
1990}


Giris{kod} ile Giris({kod}) aynı unutma, yani tek argümanı olarak bir tabloyla Giris fonksiyonuna bir çağrı bu. Bu şekilde önceki veri parçası bir Lua programı. Bu dosyayı okumak için Giris için makul bir tanımlamayla dosyayı çalıştırmamız gerekir yalnızca. Örneğin aşağıdaki program data isimli bir dosyadaki girişlerin sayısını sayar:

local sayac = 0
function Giris(_) sayac = sayac + 1 end
dofile("data")
print("girişlerin sayısı: " .. sayac)

Bir sonraki program, dosyada bulunan tüm yazarların adlarını bir kümeye toplar ve daha sonra bunları yazdırır (mutlaka dosyada olduğu gibi aynı sırada):

local yazarlar= {} -- yazarların toplanacağı küme
function Giris(b) yazarlar[b[1]] = true end
dofile("data")
for ismi in pairs(yazarlar) do print(ismi) end

Bu üstteki program parçalarındaki olay odaklı(event-driven) yaklaşıma dikkat: Giris fonksiyonu, data dosyasındaki her bir giriş için dofile 'da çağrılan bir callback fonksiyonu görevini görür.

Dosya boyutu büyük bir endişe olmadığında temsilimiz için isim-değer çiftlerini kullanabiliriz.
(Bu format size Bibtex'i hatırlatıyorsa bu bir tesadüf değil. BibTeX, Lua oluşturucu sözdiziminin ilhamlarından biriydi.).

Giris{
 yazar = "Donald E. Knuth",
 kitap= "Literate Programming",
 yayinevi= "CSLI",
 yil = 1992
}

Giris{
 yazar = "Jon Bentley",
 kitap = "More Programming Pearls",
 yil = 1990,
 yayinevi= "Addison-Wesley",
}

Bu format, kendini tanımlayan(self-describing) veri biçimi olarak adlandırılır, çünkü her bir veri parçası , anlamının kısa bir açıklamasına bağlanmıştır. Kendini tanımlayan veriler, CSV veya diğer kompakt notasyonlardan daha okunaklıdır (en azından insanlar tarafından); gerektiğinde elle edit yapmak kolay; ve veri dosyasını değiştirmek zorunda kalmadan temel formatta küçük değişiklikler yapmamıza imkan verir. Misal , veri dosyasına yeni bir alan eklersek okuma yapan programımızda alan olmadığında varsayılan değer sağlayacak küçük bir değişikliğe ihtiyacımız olur yalnızca.

İsim-değer formatı ile yazarları toplamak için programımız şöyle olur

local yazarlar = {} -- yazarları bir kümeye topla
function Giris (b) yazarlar[b.yazar] = true end
dofile("data")
for ismi in pairs(yazarlar) do print(ismi) end

Artık alanların sırası önemli değil. Şimdi bazı girişlerin yazarı olmadığı durum için Giris fonksiyonunu uyarlamamız gerek:

function Giris(b)
    if b.yazar then yazarlar[b.yazar] = true end
end

Lua 'nın hızlı çalışması yanında hızlı da derler. Örneğin, yazar listelemesi yapan yukarıdaki program 2 megabaytlık bir veriyi 1 saniyeden daha az sürede işler.  Bu tesadüf değil. Veri tanımlaması, Lua'nın yaratılışından bu yana ana uygulamalarından biri olmuştur ve derleyicisini büyük programlar için hızlı hale getirmek için büyük özen gösterdik.


12.2 Serializasyon

Sıklıkla bazı verileri seri haline getirmeye yani verileri  bayt veya karakter akışına dönüştürmeye ihtiyaç duyarız bu şekilde bir dosyaya kaydetme veya bir ağ bağlantısı üzerinden gönderim yapabiliriz. Bu yöntemde ki gibi serileştirilmiş verileri Lua kodu olarak temsil edebiliriz , kodu çalıştırdığımızda kaydedilen değerleri okuma yapan programda yeniden oluşturur.

Normal olarak, global bir değişkene bir değer yüklemek istersek öbeğimiz degiskenismi=ifade gibi bir şey olacak, ifade burada değer üretecek Lua kodu, degiskenismi kısmı kolay, değer oluşturacak kodu nasıl yazacağımızı görelim. Sayısal bir değer için görev kolay:

function serialize (o)
    if type(o) == "number" then
      io.write(o)
    else <diger durumlar>
    end
 end

string bir değer için toy yaklaşım devamdaki bir şey olurdu:

if type(o) == "string" then
    io.write("'", o, "'")

Ancak, string özel karakterler (tırnak işareti veya yenisatır "\n" gibi) içeriyorsa ortaya çıkan kod geçerli bir Lua programı olamayacak.

tırnakları değiştirmek size sorunu çözmede cazip görünebilir:

if type(o) == "string" then
    io.write("[[", o, "]]")

Aman dikkat! Kötü niyetli bir kullanıcı programınızı '' ]]..os.execute('rm *')..[['' gibi bir şeyi (misal, adresi olarak bu stringi sağlayabilir) kaydetmeye yönlendirmeyi başarırsa, nihai öbeğiniz şöyle olacak:

degiskenismi = [[ ]]..os.execute('rm *')..[[ ]]

Bu "verileri" yüklemeye çalışırken kötü bir sürpriz ile karşılaşacaksınız.

Bir stringi güvenli bir şekilde alıntılama yapmanın basit yolu, string.format fonksiyonundan "%q" seçeneğiyledir. Bu, stringi çift tırnak içine alır  ve çift tırnak, yenisatır ve string içindeki bazı diğer karakterler için  uygun kaçışlar sağlar:

a = 'a "şübheli" \\string'
print(string.format("%q", a)) --> "a \"şüpheli\" \\string"

Bu özelliği kullanarak, serialize fonksiyonumuz şimdi şu şekilde görünür:

function serialize (o)
    if type(o) == "number" then
      io.write(o)
    elseif type(o) == "string" then
      io.write(string.format("%q", o))
    else <diger durumlar>
    end
 end

Lua 5.1, güvenli şekilde gelişi güzel stringlerden alıntılama yapmak için başka bir seçenek sunar,  [=[...]=] , uzun stringler için yeni notasyon. Tabi, bu yeni notasyon esas olarak ,  herhangi bir şekilde stringin değişmesini istemediğimiz yerlerde, elle-yazılan kod için tasarlanmıştır. Otomatik oluşturulan kodda, şüpheli karakterlerden kaçmada string.format'dan “%q” seçeneği daha kolaydır.

Otomatik üretilen kodlar için uzun string notasyonunu yine de kullanmak istiyorsanız bazı ayrıntılara dikkat etmelisiniz. İlkin, eşittir işaretleri için uygun sayıyı seçmelisiniz. İyi bir uygun sayı, orijinal stringde görünen miktardan bir fazladır. uzun stringlerin eşittir işaretleri içermemesi nadir olduğundan (örneğin, kaynak kodlarla yorum kısımlarını ayırma), kapalı köşeli parantez öncesindeki eşittir işaretleri serisine dikkatimizi vermeliyiz ki diğer sekanslar hatalı bir string sonu üretemesin. İkinci detay, uzun stringin başındaki "\n" her zaman yok sayılır; bu problemden kaçınmak için basit yol, yok sayılacak "\n" 'i daima eklemektir.

Liste 12.1. Rasgele uzun stringlerde alıntılama:

function quote(s)
    -- denk işaretli dizilerinin maksimum uzunluğunu bul
    local n = -1
    for w in string.gmatch(s, "]=*") do
        n = math.max(n, #w - 1)
    end

    -- ’n’ artı 1 denk işaretli bir string üret
    local eq = string.rep("=", n + 1)

    -- alıntı stringini oluştur
    return string.format(" [%s[\n%s]%s] ", eq, s, eq)
end

quote fonksiyonu (Liste 12.1) önceki uyarılarımızın neticesi. Keyfi stringi alır ve uzun string olarak biçimlendirilmişini döndürür. string.gmatch çağrısı, s stringindeki ']=*' (yani, kapalı köşeli parantez ardından sıfır veya eşit işareti serisi) şablonunun tüm oluşumlarının aranması için bir iteratör oluşturur. (20. bölümde şablon eşleştirmelerini ele alacağız.). Her bir oluşumda, döngü n'i o ana kadar ki eşit işaretlerin sayısı ile  günceller. Döngüden sonra kullandığımız string.rep, n+1 şekilde eşit işaretini çoğaltmak içindir , bir stringdeki oluşumların bir fazlası yani. Son olarak, string.format , köşeli paranntezler arasına eşittir işaretlerin uygun sayısıyla s'i çevreler ve alıntılanan string etrafına ekstra boşluklar artı çevrelenen stringin başına yenisatır-"\n" ekler

Düz tabloları kaydetme


Sıradaki (ve daha zor) görevimiz tabloları kaydetmek. Tabloda yaptığımız kayıtlama şekline göre tabloları  kaydetme yöntemleri çeşitlenir. Tüm durumlar için tek bir algoritma uygun değil.
Basit tablolar sadece basit algoritmalara ihtiyaç duymasının yanı sıra ortaya çıkan dosyalar da daha estetik olabilir.

Liste 12.2 Düz tabloları serileştirme

function serialize(o)
    if type(o) == "number" then
        io.write(o)
    elseif type(o) == "string" then
        io.write(string.format("%q", o))
    elseif type(o) == "table" then
        io.write("{\n")
        for k, v in pairs(o) do
            io.write(" ", k, " = ")
            serialize(v)
            io.write(",\n")
        end
        io.write("}\n")
    else
        error("cannot serialize a " .. type(o))
    end
end

İlk girişimimiz 12.2 listesinde. Basitliğine rağmen bu fonksiyon makul bir iş çıkartır. Tablo yapısı bir ağaç(yani, paylaşılan alt tablolar ve dolaşım yok) şeklinde uzandığı sürece iç içe geçmiş tabloları (diğer bir deyişle, tablolar içinde tablolar) bile işler. Küçük bir makyajlama, ara sıra iç içe geçmiş tablolara girinti yapmak olacaktır; bunu bir egzersiz olarak deneyebilirsiniz. (İpucu: serialize 'e girinti stringini ekstra bir parametre olarak ekleyin.)

Önceki fonksiyon, tablodaki tüm anahtarların geçerli tanımlayıcılar olduğunu varsayar. Bir tablo, sözdizimsel olarak Lua'da geçerli olmayan sayısal veya string anahtarlara sahipse başımız belada demektir. Bu sıkıntıyı gidermenin basit yolu, io.write(" ", k, " = ")  satırını io.write(" ["); serialize(k); io.write("] = ") 'a çevirmektir. Bu değişiklikle nihai dosyanın estetiği pahasına fonksiyonumuzun sağlamlığını arttırıyoruz. serialize'nin ilk versiyonu 

serialize{a=12, b='Lua', key='another "one"'}

ile sonuç:

{
 a = 12,
 b = "Lua",
 key = "another \"one\"",
}

İkinci sürümü ile karşılaştırın:

{
 ["a"] = 12,
 ["b"] = "Lua",
 ["key"] = "another \"one\"",
}

Her durum için köşeli parantezlere ihtiyaç duyulup duyulmadığını test ederek bu sonucu geliştirebiliriz. yine,  bir egzersiz olarak bu geliştirmeyi size bırakıyorum.

Dolambaçlı tabloları kaydetme


Kapsamlı topolojilili tabloları işlemek için (yani, dolambaçlı ve paylaşımlı alt tablolar) farklı bir yaklaşıma ihtiyacımız var. Kurucular bu tip tabloları temsil edemez bu yüzden onları kullanmayacağız. Dolambaçı temsil etmek için isimlere ihtiyacımız var, bu nedenle bir sonraki fonksiyonumuz, kaydedilecek değer artı  ismini argüman olarak alacak. Dahası, bir dolambaç tespit ettiğimizde onları döngülemek için hazırda kaydedilmiş tabloların adlarının izini kaybetmemeliyiz Bu iz sürme sürme için ekstra bir tablo kullanacağız. Bu tablo ,  değerlere ilişkilendirilmiş olarak adlarına ve indeksler olarak tablolara sahip olacak.

Liste 12.3. Dolambaçlı tabloları kaydetme:
function basicSerialize(o)
    if type(o) == "number" then
        return tostring(o)
    else -- bir string olduğu varsayılmakta
        return string.format("%q", o)
    end
end

function save(name, value, saved)
    saved = saved or {} -- ilk değer
    io.write(name, " = ")
    if type(value) == "number" or type(value) == "string" then
        io.write(basicSerialize(value), "\n")
    elseif type(value) == "table" then
        if saved[value] then -- değer hazırda kayıtlı?
            io.write(saved[value], "\n") -- önceki ismini kullan
        else
            saved[value] = name -- sonrası için ismini kaydet
            io.write("{}\n") -- yeni bir tablo oluştur
            for k, v in pairs(value) do -- alanlarını kaydet
                k = basicSerialize(k)
                local fname = string.format("%s[%s]", name, k)
                save(fname, v, saved)
            end
        end
    else
        error("cannot save a " .. type(value))
    end
end

Ortaya çıkan kod 12.3 listesinde. Kaydetmek istediğimiz tablolar yalnızca stringleri ve sayıları anahtar olarak kabul ediyor kısıtlamasını koruyoruz. basicSerialize fonksiyonu,  bu temel tipleri serileştirir ve sonucu döndürür. Bir sonraki fonksiyon save zor iş yapar. saved parametresi hazırda kaydedilmiş tabloları takip eden tablodur. Örnek olarak devamdaki gibi bir tablo oluşturursak:

a = {x=1, y=2; {3,4,5}}
a[2] = a      -- dolambaç
a.z = a[1]    -- paylaşımlı alt tablo

o zaman save("a", a) çağrısı aşağıdaki gibi onu kaydedecektir:

a = {}
a[1] = {}
a[1][1] = 3
a[1][2] = 4
a[1][3] = 5

a[2] = a
a["y"] = 2
a["x"] = 1
a["z"] = a[1]

Bu atamaların aktüel sırası değişebilir tablo gezintisine bağlı olarak. Bununla birlikte, algoritma, yeni tanımlamada ihtiyaç duyulan önceki her düğümün hazırda tanımlanmasını sağlar.

Paylaşılan kısımlarla bazı değerleri kaydetmek istiyorsak aynı saved tablosunu kullanarak save'e çağrı yapabiliriz. Örneğin aşağıdaki iki tabloya sahip olduğumuzu varsayalım:

a = {{"one", "two"}, 3}
b = {k = a[1]}

Onları bağımsız olarak kaydedersek sonuç ortak parçalara sahip olmayacaktır:

save("a", a)
save("b", b)

--> a = {}
--> a[1] = {}
--> a[1][1] = "one"
--> a[1][2] = "two"
--> a[2] = 3
--> b = {}
--> b["k"] = {}
--> b["k"][1] = "one"
--> b["k"][2] = "two"

Bununla birlikte, iki save çağrısı için aynı saved tablosunu kullanırsanız  o zaman sonuç ortak parçaları paylaşır:

local t = {}
save("a", a, t)
save("b", b, t)

--> a = {}
--> a[1] = {}
--> a[1][1] = "one"
--> a[1][2] = "two"
--> a[2] = 3
--> b = {}
--> b["k"] = a[1]

Lua'da her zamanki gibi birkaç diğer alternatif var. Bunlar arasında, global bir isim vermeden bir  (bunun yerine chunk lokal bir değer oluşturur ve onu döndürür) değeri kaydedebiliriz, fonksiyonları işleyebiliriz (her fonksiyon kendi adına ilişkilendirilen bir yardımcı tablo oluşturarak) vb. Lua size gücü verir; mekanizmaları siz inşa edersiniz.


Hiç yorum yok:

Yorum Gönder