Python JSONPath Örnekleri: Veri Çekme Sanatı
Giriş
Günümüzün yazılım dünyasında, veri alışverişi ve depolama formatları arasında JavaScript Object Notation (JSON) tartışmasız bir lider konumdadır. API’lar, web servisleri, mobil uygulamalar ve hatta birçok NoSQL veritabanı JSON’u tercih etmektedir. JSON’un basitliği ve insan tarafından okunabilirliği, onu geliştiriciler için cazip kılsa da, karmaşık ve iç içe geçmiş JSON yapılarından belirli verilere ulaşmak bazen zorlu bir görev haline gelebilir. İşte tam bu noktada JSONPath devreye girer.
JSONPath, XML için XPath ne ise, JSON için de odur. JSON belgeleri içindeki belirli öğeleri seçmek ve ayıklamak için güçlü ve esnek bir sorgu dili sunar. Bu makale, Python ortamında JSONPath’in nasıl kullanılacağını, temel ve gelişmiş operatörlerini örneklerle açıklayarak, veri çekme süreçlerinizi nasıl basitleştirebileceğinizi göstermeyi amaçlamaktadır. Amacımız, Python geliştiricilerinin JSONPath’i etkin bir şekilde kullanarak karmaşık JSON verilerini kolayca manipüle etmelerini sağlamaktır.
JSON ve Yapısal Veri İhtiyacı
JSON, hafif, veri değişim formatı olarak giderek daha fazla popülerlik kazanmaktadır. Nesneleri ve dizileri temsil etme yeteneği sayesinde, karmaşık hiyerarşik verileri yapılandırılmış bir şekilde ifade edebilir. Bir REST API’den gelen yanıt, bir yapılandırma dosyası veya bir log kaydı, genellikle iç içe geçmiş nesneler ve diziler içeren büyük bir JSON belgesi olabilir.
Bu tür belgelerden belirli bir bilgi parçasını çekmek, geleneksel Python sözlük ve liste manipülasyonlarıyla yorucu ve hataya açık bir süreç olabilir. Örneğin, yüzlerce kitaptan oluşan bir listeden, belirli bir yazarın adını veya fiyatı belirli bir değerin altında olan tüm kitapların başlıklarını bulmak için uzun döngüler ve koşullu ifadeler yazmanız gerekebilir. Bu yaklaşım, kodun okunabilirliğini azaltır, bakımı zorlaştırır ve JSON yapısı değiştiğinde kodun yeniden yazılmasını gerektirir.
JSONPath, bu sorunu ortadan kaldırır. Tıpkı bir veritabanı sorgusu gibi, istediğiniz veriye doğrudan bir “yol” belirterek ulaşmanızı sağlar. Bu, kodunuzu daha kısa, daha okunabilir ve JSON yapısındaki değişikliklere karşı daha dayanıklı hale getirir.
JSONPath Nedir?
JSONPath, JSON veri yapılarında belirli öğeleri bulmak için kullanılan bir sorgu dilidir. XPath’in XML belgeleri için yaptığı gibi, JSONPath de JSON belgeleri için benzer bir işlevsellik sunar. Ancak, JSON’un daha basit yapısına uygun olarak, JSONPath’in sözdizimi genellikle XPath’ten daha az karmaşıktır.
JSONPath’in temel sözdizimi elemanları şunlardır:
* $: Kök elemanı temsil eder. Sorgular her zaman bu sembolle başlar.
* . (nokta notasyonu): Bir nesnenin alt elemanına erişmek için kullanılır (örn. $.store.book).
* [] (köşeli parantez notasyonu): Bir dizinin elemanlarına veya bir nesnenin anahtarlarına erişmek için kullanılır. Ayrıca filtreleme ve dilimleme için de kullanılır (örn. $.store.book[0]).
: Tüm elemanları seçmek için kullanılır (örn. $.store.book[*]).
* .. (recursive descent): JSON yapısının herhangi bir derinliğindeki bir elemanı bulmak için kullanılır (örn. $..author).
* ?(): Filtre ifadelerini tanımlamak için kullanılır. İçindeki ifade true dönen elemanları seçer (örn. $.store.book[?(@.price < 10)]).
* @: Filtre ifadeleri içinde mevcut elemanı temsil eder.
Python'da JSONPath Ortamı: jsonpath-ng
Python ekosisteminde JSONPath uygulamaları için birkaç kütüphane bulunmaktadır. Bunlar arasında jsonpath-rw, jsonpath, ve jsonpath-ng öne çıkar. Bu makalede, güçlü özellik setleri, iyi bakımı ve esnek API'si nedeniyle jsonpath-ng kütüphanesini kullanacağız. jsonpath-ng, hem standart JSONPath sözdizimini destekler hem de regex filtreleme gibi ek özellikler sunar.
Kurulum
jsonpath-ng kütüphanesini kurmak için pip kullanabilirsiniz:
pip install jsonpath-ng
Temel Kullanım Deseni
jsonpath-ng ile JSONPath ifadelerini kullanmanın temel adımları şunlardır:
1. jsonpath_ng modülünden jsonpath ve parse fonksiyonlarını içe aktarın.
2. Bir JSONPath ifadesini bir JsonPath nesnesine ayrıştırmak için parse() fonksiyonunu kullanın.
3. Ayrıştırılmış JsonPath nesnesinin find() metodunu, sorgulamak istediğiniz JSON verisiyle çağırın. Bu metod, bulunan her eşleşme için bir Datum nesneleri listesi döndürür.
4. Datum nesnesinin value özelliğini kullanarak eşleşen veriye erişin.
Tüm örneklerimizde aşağıdaki örnek JSON verisini kullanacağız:
{
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
},
"expensive": 10
}
Bu JSON verisini Python'da bir sözlük olarak temsil edelim:
import json
from jsonpath_ng import jsonpath, parse
data = {
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
},
"expensive": 10
}
Temel JSONPath Operatörleri ve Python Örnekleri
Kök Eleman ($)
* $: Tüm JSON belgesini seçer.
jsonpath_expression = parse('$')
match = jsonpath_expression.find(data)
print("Tüm belge:", match[0].value)
# Çıktı: {'store': ..., 'expensive': 10}
* $.store: Kök elemanın store anahtarını seçer.
jsonpath_expression = parse('$.store')
match = jsonpath_expression.find(data)
print("Mağaza objesi:", match[0].value)
# Çıktı: {'book': [...], 'bicycle': {...}}
Nokta ve Köşeli Parantez (. ve [])
* $.store.book: store objesi içindeki book anahtarına erişir, bu bir kitap listesidir.
jsonpath_expression = parse('$.store.book')
matches = jsonpath_expression.find(data)
print("Tüm kitaplar:", [m.value for m in matches])
# Çıktı: [[{'category': 'reference', 'author': 'Nigel Rees', ...}]] (liste içinde liste)
* $.store.book[0]: Kitap listesindeki ilk kitabı seçer (indeks 0).
jsonpath_expression = parse('$.store.book[0]')
match = jsonpath_expression.find(data)
print("İlk kitap:", match[0].value)
# Çıktı: {'category': 'reference', 'author': 'Nigel Rees', ...}
* $.store.book[0].title: İlk kitabın title özelliğini seçer.
jsonpath_expression = parse('$.store.book[0].title')
match = jsonpath_expression.find(data)
print("İlk kitabın başlığı:", match[0].value)
# Çıktı: Sayings of the Century
$.store.book[].author: Tüm kitapların author özelliklerini seçer. * joker karakteri, bir dizideki tüm öğeleri veya bir nesnedeki tüm anahtarları temsil eder.
jsonpath_expression = parse('$.store.book[*].author')
matches = jsonpath_expression.find(data)
print("Tüm yazarlar:", [m.value for m in matches])
# Çıktı: ['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien']
* $.store.bicycle.color: Bisiklet objesinin color özelliğini seçer.
jsonpath_expression = parse('$.store.bicycle.color')
match = jsonpath_expression.find(data)
print("Bisikletin rengi:", match[0].value)
# Çıktı: red
Tüm Elemanlar (*)
$.store.: store objesi içindeki tüm doğrudan alt elemanları (hem book listesi hem de bicycle objesi) seçer.
jsonpath_expression = parse('$.store.*')
matches = jsonpath_expression.find(data)
print("Store içindeki tüm elemanlar:")
for m in matches:
print(m.value)
# Çıktı:
# [{'category': 'reference', ...}, {'category': 'fiction', ...}, ...] (book listesi)
# {'color': 'red', 'price': 19.95} (bicycle objesi)
$.store.book[]: book dizisindeki tüm kitap objelerini seçer. Bu, $.store.book ile benzer bir sonuç verir ancak her bir kitabı ayrı bir eşleşme olarak döndürür.
jsonpath_expression = parse('$.store.book[*]')
matches = jsonpath_expression.find(data)
print("Tüm kitap objeleri:")
for m in matches:
print(m.value['title']) # Sadece başlıklarını yazdıralım
# Çıktı:
# Sayings of the Century
# Sword of Honour
# Moby Dick
# The Lord of the Rings
Recursive Descent (..)
* $..author: JSON belgesinin herhangi bir derinliğindeki tüm author anahtarlarını seçer.
jsonpath_expression = parse('$..author')
matches = jsonpath_expression.find(data)
print("Tüm yazarlar (derinlikten bağımsız):", [m.value for m in matches])
# Çıktı: ['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien']
* $..price: JSON belgesinin herhangi bir derinliğindeki tüm price anahtarlarını seçer.
jsonpath_expression = parse('$..price')
matches = jsonpath_expression.find(data)
print("Tüm fiyatlar:", [m.value for m in matches])
# Çıktı: [8.95, 12.99, 8.99, 22.99, 19.95]
* $.store..price: Yalnızca store objesi içindeki tüm price anahtarlarını seçer.
jsonpath_expression = parse('$.store..price')
matches = jsonpath_expression.find(data)
print("Store içindeki tüm fiyatlar:", [m.value for m in matches])
# Çıktı: [8.95, 12.99, 8.99, 22.99, 19.95] (aynı, çünkü başka price yok)
Dizi Dilimleme (Array Slicing) ([start:end:step])
* $.store.book[0:2]: Kitap listesinden ilk iki kitabı seçer (0'dan başlayıp 2'ye kadar, 2 dahil değil).
jsonpath_expression = parse('$.store.book[0:2]')
matches = jsonpath_expression.find(data)
print("İlk iki kitap:")
for m in matches:
print(m.value['title'])
# Çıktı:
# Sayings of the Century
# Sword of Honour
* $.store.book[-1]: Kitap listesindeki son kitabı seçer.
jsonpath_expression = parse('$.store.book[-1]')
match = jsonpath_expression.find(data)
print("Son kitap:", match[0].value['title'])
# Çıktı: The Lord of the Rings
* $.store.book[1:]: İkinci kitaptan başlayarak tüm kitapları seçer.
jsonpath_expression = parse('$.store.book[1:]')
matches = jsonpath_expression.find(data)
print("İkinci kitaptan sonrası:")
for m in matches:
print(m.value['title'])
# Çıktı:
# Sword of Honour
# Moby Dick
# The Lord of the Rings
* $.store.book[::2]: Her iki kitaptan birini seçer (0, 2, 4...).
jsonpath_expression = parse('$.store.book[::2]')
matches = jsonpath_expression.find(data)
print("Her iki kitaptan biri:")
for m in matches:
print(m.value['title'])
# Çıktı:
# Sayings of the Century
# Moby Dick
Union Operatörü ([,])
* $.store.book[0,2]: Kitap listesindeki ilk ve üçüncü kitabı seçer.
jsonpath_expression = parse('$.store.book[0,2]')
matches = jsonpath_expression.find(data)
print("Birinci ve üçüncü kitap:")
for m in matches:
print(m.value['title'])
# Çıktı:
# Sayings of the Century
# Moby Dick
* $.store.book[0].author, $.store.book[1].author: İlk iki kitabın yazarlarını ayrı ayrı seçer. Birden fazla path ifadesini virgülle ayırarak birleştirebilirsiniz.
jsonpath_expression = parse('$.store.book[0].author, $.store.book[1].author')
matches = jsonpath_expression.find(data)
print("İlk iki kitabın yazarları:", [m.value for m in matches])
# Çıktı: ['Nigel Rees', 'Evelyn Waugh']
Gelişmiş JSONPath Özellikleri ve Filtrelemeler
Filtre İfadeleri (?())
Filtre ifadeleri, bir dizideki öğeleri veya bir nesnedeki özellikleri belirli koşullara göre seçmek için kullanılır. @ sembolü, mevcut öğeyi temsil eder.
* $.store.book[?(@.price < 10)]: Fiyatı 10'dan az olan tüm kitapları seçer.
jsonpath_expression = parse('$.store.book[?(@.price < 10)]')
matches = jsonpath_expression.find(data)
print("Fiyatı 10'dan az olan kitaplar:")
for m in matches:
print(f"{m.value['title']} ({m.value['price']})")
# Çıktı:
# Sayings of the Century (8.95)
# Moby Dick (8.99)
* $.store.book[?(@.category == 'fiction' && @.price > 10)]: Kategorisi 'fiction' olan ve fiyatı 10'dan büyük olan kitapları seçer.
jsonpath_expression = parse('$.store.book[?(@.category == "fiction" && @.price > 10)]')
matches = jsonpath_expression.find(data)
print("Kategorisi 'fiction' ve fiyatı 10'dan büyük kitaplar:")
for m in matches:
print(f"{m.value['title']} ({m.value['price']})")
# Çıktı:
# Sword of Honour (12.99)
# The Lord of the Rings (22.99)
* $.store.book[?(@.isbn)]: isbn anahtarı olan tüm kitapları seçer. Bir anahtarın varlığını kontrol etmek için kullanılır.
jsonpath_expression = parse('$.store.book[?(@.isbn)]')
matches = jsonpath_expression.find(data)
print("ISBN'i olan kitaplar:")
for m in matches:
print(m.value['title'])
# Çıktı:
# Moby Dick
# The Lord of the Rings
$.store.book[?(@.author =~ /.Tolkien.*/i)]: Yazar adında "Tolkien" geçen (büyük/küçük harf duyarsız) tüm kitapları seçer. jsonpath-ng kütüphanesi regex desteği sunar.
jsonpath_expression = parse('$.store.book[?(@.author =~ /.Tolkien./i)]')
matches = jsonpath_expression.find(data)
print("Yazar adı 'Tolkien' içeren kitaplar:")
for m in matches:
print(m.value['title'])
# Çıktı: The Lord of the Rings
* $.store.book[?(@.price > $.expensive)]: Fiyatı, JSON belgesindeki expensive anahtarının değerinden büyük olan kitapları seçer. Bu, filtre içinde başka bir JSONPath ifadesi kullanma yeteneğini gösterir.
jsonpath_expression = parse('$.store.book[?(@.price > $.expensive)]')
matches = jsonpath_expression.find(data)
print("Fiyatı 'expensive' değerinden büyük olan kitaplar:")
for m in matches:
print(f"{m.value['title']} ({m.value['price']})")
# Çıktı:
# Sword of Honour (12.99)
# The Lord of the Rings (22.99)
# Bisiklet de dahil olabilir eğer fiyatı uygunsa, ancak path sadece kitapları hedefledi.
İç İçe Filtreleme ve Zincirleme
JSONPath ifadelerini zincirleyerek daha karmaşık sorgular oluşturabilirsiniz.
* $.store.book[?(@.category == 'fiction')][?(@.price > 10)]: Kategorisi 'fiction' olan kitapları bulur, ardından bu sonuçlar arasından fiyatı 10'dan büyük olanları seçer.
jsonpath_expression = parse('$.store.book[?(@.category == "fiction")][?(@.price > 10)]')
matches = jsonpath_expression.find(data)
print("Kategorisi 'fiction' olan ve fiyatı 10'dan büyük kitaplar (zincirleme):")
for m in matches:
print(f"{m.value['title']} ({m.value['price']})")
# Çıktı:
# Sword of Honour (12.99)
# The Lord of the Rings (22.99)
Birden Fazla Alanı Çekme
Bazen bir listedeki her bir öğeden birden fazla belirli alanı çekmek isteyebilirsiniz. jsonpath-ng kütüphanesi, köşeli parantez içinde virgülle ayrılmış anahtar listesi kullanarak bunu destekler.
$.store.book[]['title', 'author']: Tüm kitapların başlık ve yazarlarını çeker.
jsonpath_expression = parse("$.store.book[*]['title', 'author']")
matches = jsonpath_expression.find(data)
print("Tüm kitapların başlık ve yazarları:")
for m in matches:
# jsonpath-ng, bu durumda bir liste içinde sözlükler döndürür.
# Her sözlükte sadece istenen anahtarlar bulunur.
print(m.value)
# Çıktı:
# {'title': 'Sayings of the Century', 'author': 'Nigel Rees'}
# {'title': 'Sword of Honour', 'author': 'Evelyn Waugh'}
# {'title': 'Moby Dick', 'author': 'Herman Melville'}
# {'title': 'The Lord of the Rings', 'author': 'J. R. R. Tolkien'}
jsonpath-ng Kütüphanesinin Detayları ve İpuçları
jsonpath-ng'nin find() metodu, Datum nesnelerinin bir listesini döndürür. Her Datum nesnesi, eşleşen öğe hakkında ek bilgi içerir:
* match.value: Eşleşen verinin kendisi (Python sözlüğü, liste, string, sayı vb.).
* match.path: Eşleşen öğeye giden JSONPath ifadesi.
* match.map: JSON belgesindeki eşleşen öğenin tam yolu.
jsonpath_expression = parse('$.store.book[0].title')
matches = jsonpath_expression.find(data)
if matches:
match = matches[0]
print(f"Değer: {match.value}")
print(f"Path ifadesi: {match.path}")
print(f"Harita yolu: {match.map}")
Çıktı:
Değer: Sayings of the Century
Path ifadesi: Child(This(), Fields('title'))
Harita yolu: Child(Child(Child(Root(), Fields('store')), Fields('book')), Index(0))
Bulunamayan Path'ler ve Hata Yönetimi
Eğer bir JSONPath ifadesi JSON belgesinde hiçbir eşleşme bulamazsa, find() metodu boş bir liste döndürür. Bu, hataları ele almayı kolaylaştırır, çünkü genellikle bir if matches: kontrolü ile durumu yönetebilirsiniz.
Geçersiz bir JSONPath ifadesi kullanmaya çalışırsanız (örneğin, sözdizimi hatası), parse() fonksiyonu jsonpath_ng.exceptions.JsonPathLexerError veya jsonpath_ng.exceptions.JsonPathParserError gibi istisnalar fırlatacaktır. Bu istisnaları try-except blokları ile yakalayarak uygulamanızın çökmesini önleyebilirsiniz.
try:
jsonpath_expression = parse('$.invalid.path')
matches = jsonpath_expression.find(data)
if not matches:
print("Path bulunamadı, boş liste döndürüldü.")
except Exception as e:
print(f"Bir hata oluştu: {e}")
Pratik Kullanım Senaryoları
JSONPath'in Python'daki pratik kullanım alanları oldukça geniştir:
* REST API Yanıtlarını İşleme: Web servislerinden alınan karmaşık JSON yanıtlarından belirli alanları kolayca ayıklamak. Örneğin, bir hava durumu API'sinden sadece sıcaklık değerini çekmek.
* Büyük JSON Log Dosyalarından Bilgi Çıkarma: Uygulama loglarını JSON formatında tutuyorsanız, belirli hata kodlarını, kullanıcı ID'lerini veya zaman damgalarını hızla filtrelemek için JSONPath kullanabilirsiniz.
* Yapılandırılmış Konfigürasyon Dosyalarını Okuma: Uygulamalarınızın karmaşık JSON tabanlı konfigürasyon dosyalarından belirli ayarları dinamik olarak çekmek.
* Veri Dönüştürme ve Raporlama: Büyük bir JSON veri setinden raporlama veya başka bir formata dönüştürme için gerekli olan belirli veri noktalarını çekmek.
* NoSQL Veritabanı Sorguları: Bazı NoSQL veritabanları (örneğin MongoDB), JSONPath benzeri sorgu dillerini destekler. JSONPath öğrenmek, bu veritabanlarıyla çalışırken de faydalı olacaktır.
JSONPath Alternatifleri ve Performans
JSONPath, çoğu senaryo için mükemmel bir araç olsa da, bazı durumlarda alternatifleri veya ek araçları değerlendirmek faydalı olabilir.
* JMESPath: JSONPath'e benzer başka bir JSON sorgu dilidir. Özellikle AWS CLI'da yaygın olarak kullanılır. JSONPath'ten biraz daha fazla fonksiyonel yetenek sunar (örneğin, sum, min, max gibi fonksiyonlar). Eğer AWS ekosisteminde çalışıyorsanız veya daha fonksiyonel bir sorgu diline ihtiyacınız varsa JMESPath'i inceleyebilirsiniz.
* Manuel Python Parsing: JSONPath kullanmak yerine, Python'ın yerleşik sözlük ve liste manipülasyonlarıyla da verilere ulaşabilirsiniz. Ancak bu, karmaşık yapılar için daha uzun, daha az okunabilir ve daha az sürdürülebilir kodla sonuçlanır. Basit, sığ JSON'lar için yeterli olabilir.
* Stream Parsing (örn. ijson): Çok büyük JSON dosyalarıyla (GB'lar mertebesinde) çalışırken, tüm dosyayı belleğe yüklemek kaynak sorunlarına yol açabilir. ijson gibi kütüphaneler, JSON verilerini parça parça okuyarak ve işleyerek bu sorunu çözer. Bu senaryolarda JSONPath doğrudan uygulanamaz, ancak ijson ile akış halindeki veriden belirli yolları dinleyerek JSONPath benzeri bir mantık uygulayabilirsiniz.
Performans: jsonpath-ng kütüphanesi, küçük ve orta boyutlu JSON belgeleri için genellikle yeterince hızlıdır. Ancak çok büyük belgelerde (birkaç yüz MB'tan fazla) performans darboğazları yaşanabilir. Bu tür durumlarda, sorgu karmaşıklığını ve JSON belgesinin boyutunu göz önünde bulundurarak farklı yaklaşımları (önbellekleme, veritabanı kullanımı veya stream parsing) değerlendirmek önemlidir.
Sonuç
Python JSONPath, özellikle jsonpath-ng gibi güçlü kütüphaneler aracılığıyla, karmaşık JSON verilerinden bilgi çekme sürecini radikal bir şekilde basitleştiren vazgeçilmez bir araçtır. Bu makalede ele aldığımız temel operatörler ($, ., [], *, ..) ve gelişmiş özellikler (dilimleme, filtreleme, regex) sayesinde, her türlü JSON yapısında istediğiniz verilere kolayca ulaşabilirsiniz.
JSONPath'i öğrenmek, API'larla çalışan, log dosyalarını analiz eden veya yapılandırılmış konfigürasyonlarla uğraşan her Python geliştiricisi için değerli bir beceridir. Kodunuzu daha kısa, daha okunabilir ve daha az hataya açık hale getirerek geliştirme verimliliğinizi artırır. Pratik yaparak ve farklı JSONPath ifadeleriyle denemeler yaparak bu güçlü aracı ustalıkla kullanabilirsiniz. JSONPath, veri çekme sanatını parmaklarınızın ucuna getirir.
