Asyncio Python web scraper projelerinde PostgreSQL ile tarih tiplerini nasıl etkili bir şekilde yöneteceğinizi adım adım öğrenin. Veri tutarlılığı ve performans için en iyi uygulamalar.
Web kazıma (web scraping) projelerinde tarih ve saat verileriyle uğraşmak, başlangıçta basit görünse de, derinlemesine bilgi ve dikkat gerektiren bir alandır. İnternet dünyasında dolaşan verilerin önemli bir kısmı, olayların ne zaman gerçekleştiği, bir içeriğin ne zaman güncellendiği veya bir kullanıcının ne zaman yorum yaptığı gibi tarihsel bilgiler içerir. Bu tarihsel veriler, sadece ham bilgi olmaktan öte, scraper’ınızın elde ettiği verinin bağlamını, güncelliğini ve analiz edilebilirliğini doğrudan etkiler. Örneğin, bir e-ticaret sitesinden ürün yorumlarını topluyorsanız, yorumun ne zaman yapıldığı bilgisi, ürünün popülaritesini, trendleri veya yorumların güncelliğini anlamak için hayati öneme sahiptir. Aynı şekilde, haber sitelerinden makale çekiyorsanız, haberin yayınlanma tarihi, kronolojik bir akış oluşturmanıza ve eski, güncelliğini yitirmiş içerikleri filtrelemenize olanak tanır.
Bu bağlamda, tarih ve saat tiplerini doğru bir şekilde yönetmek, scraper’ınızın topladığı verinin kalitesini ve güvenilirliğini artırmakla kalmaz, aynı zamanda veri depolama ve analiz süreçlerini de büyük ölçüde kolaylaştırır. Farklı web siteleri, tarih ve saat bilgilerini sayısız farklı formatta sunabilir: “2 gün önce”, “12 Mayıs 2023”, “2023-05-12T14:30:00Z”, “12/05/23 14:30” gibi. Bu çeşitlilik, verileri standart bir biçime dönüştürme ihtiyacını doğurur. Dahası, dünya genelindeki farklı zaman dilimleri (timezones) sorunu, verilerin tutarlı bir şekilde depolanması ve karşılaştırılması için merkezi bir UTC (Coordinated Universal Time) formatına çevrilmesini neredeyse zorunlu kılar. Aksi takdirde, aynı olayın farklı zaman dilimlerinde farklı kaydedilmesi, veri tutarsızlıklarına ve hatalı analizlere yol açabilir.
Asyncio tabanlı bir Python web scraper kullanmak, eşzamanlı (concurrent) işlemleri verimli bir şekilde yönetmenizi sağlar, ancak tarih veri tiplerinin işlenmesi hala dikkatli bir yaklaşım gerektirir. Python’ın kendi datetime modülü, bu tür karmaşık tarih ve saat işlemleri için güçlü araçlar sunar. Bu modül sayesinde, farklı string formatlarındaki tarihleri datetime objelerine dönüştürebilir, zaman dilimi dönüşümleri yapabilir ve verileri PostgreSQL gibi bir veritabanına kaydetmeye hazır hale getirebilirsiniz. PostgreSQL ise, çeşitli tarih ve saat tipleriyle (DATE, TIME, TIMESTAMP, TIMESTAMPTZ) bu dönüştürülmüş verileri güvenli ve etkin bir şekilde depolama kapasitesi sunar. Bu entegrasyon, web kazıma projelerinizin sadece hızlı ve verimli olmakla kalmayıp, aynı zamanda doğru ve anlamlı veriler üretmesini de sağlar. Kısacası, tarih tiplerini yönetmek, bir web scraper projesinin temel taşlarından biridir ve bu konuya hakim olmak, veri biliminden iş zekasına kadar birçok alanda size avantaj sağlayacaktır.
PostgreSQL’de Tarih ve Saat Tiplerini Tanıma: Hangisini Ne Zaman Kullanmalı?
PostgreSQL, tarih ve saat verilerini depolamak için oldukça zengin bir tip yelpazesi sunar. Bu tipleri doğru anlamak ve projenizin ihtiyaçlarına göre seçmek, veri tutarlılığı, depolama alanı ve sorgu performansı açısından kritik öneme sahiptir. Bir web scraper projesinde, genellikle karşılaşılan verilerin doğası gereği, zaman dilimi farkındalığına sahip tipler büyük avantaj sağlar.
PostgreSQL’in başlıca tarih ve saat tipleri şunlardır:
DATE: Yalnızca tarihi (yıl, ay, gün) saklar. Saat bilgisi içermez. Doğum tarihleri veya belirli bir olayın sadece gününü kaydetmek gibi durumlarda idealdir.TIME: Yalnızca saati (saat, dakika, saniye) saklar. Tarih bilgisi içermez. Bir mağazanın açılış saati gibi durumlarda kullanılabilir.TIMESTAMP WITHOUT TIME ZONE(kısacaTIMESTAMP): Tarih ve saati saklar, ancak herhangi bir zaman dilimi bilgisi içermez. Veritabanına kaydedilen değer, tam olarak sizin belirttiğiniz değerdir ve bu, “hangi zaman dilimine ait olduğu” bilgisini dışarıda bırakır. Eğer tüm tarih-saat verileriniz zaten UTC’ye dönüştürülmüşse ve bu dönüşümün her zaman Python tarafında garantilendiğini düşünüyorsanız kullanılabilir. Ancak bu, potansiyel hatalara açık bir yaklaşımdır.TIMESTAMP WITH TIME ZONE(kısacaTIMESTAMPTZ): Bu tip, bir web scraper için genellikle en iyi seçimdir. Tarih ve saati saklar ve beraberinde zaman dilimi bilgisini de içerir. PostgreSQL, bu tipi kullanırken, gelen tüm tarih-saat değerlerini veritabanının kendi zaman dilimine (genellikle UTC olarak yapılandırılır) dönüştürerek saklar. Sorgulama yapıldığında ise, istemcinin zaman dilimi ayarına göre (veya belirtilen bir zaman dilimine göre) tekrar dönüştürülerek gösterilir. Bu özellik, farklı coğrafyalardan gelen veya farklı zaman dilimlerinde oluşturulmuş verileri tutarlı bir şekilde yönetmek için vazgeçilmezdir.INTERVAL: İki tarih/saat arasındaki farkı saklar. Örneğin, “5 gün 3 saat 10 dakika” gibi bir süreyi temsil eder. Bir işlemin ne kadar sürdüğünü veya belirli bir periyodu depolamak için kullanılabilir.
Peki, web kazıma projelerinde hangi tipi ne zaman kullanmalıyız?
Genellikle TIMESTAMPTZ, web scraper’lar için altın standarttır. Bunun ana nedeni, web’den çekilen verilerin genellikle belirli bir zaman dilimine ait olması (örneğin, bir web sitesinin sunucusunun yerel saati) veya zaman dilimi bilgisinin belirsiz olmasıdır. TIMESTAMPTZ kullanarak, tüm verilerinizi merkezi bir referans noktasına (UTC) göre depolayabilir ve bu, gelecekteki analizlerinizde zaman dilimi karmaşasını ortadan kaldırır. Örneğin, farklı ülkelerdeki e-ticaret sitelerinden yorum tarihleri çekiyorsunuz. Her sitenin yorum zamanı kendi yerel zaman dilimindedir. Eğer hepsini UTC’ye dönüştürüp TIMESTAMPTZ olarak saklarsanız, tüm yorumları tek bir kronolojik sıralamada doğru bir şekilde görebilirsiniz. Aksi takdirde, örneğin İstanbul’dan gelen “15:00” ve Londra’dan gelen “15:00” aynı anda gerçekleşmiş gibi görünebilir, oysa gerçekte aralarında 3 saat fark vardır.
Aşağıdaki tablo, PostgreSQL tarih ve saat tiplerinin temel özelliklerini ve kullanım senaryolarını özetlemektedir:
| Tip | Depoladığı | Zaman Dilimi Farkındalığı | Kullanım Senaryosu (Web Scraper) |
|---|---|---|---|
DATE |
Sadece tarih | Yok | Bir etkinliğin sadece günü (saat önemsizse) |
TIME |
Sadece saat | Yok | Bir web sitesinin güncelleme zamanı (tarih önemsizse) |
TIMESTAMP |
Tarih ve saat | Yok (veritabanı zaman dilimine çevrilmez) | Tüm verilerin zaten UTC olduğu garanti edilen özel durumlar |
TIMESTAMPTZ |
Tarih ve saat (UTC’ye çevrilir) | Var (önerilen) | Web kazıma verileri (yorum tarihleri, yayınlanma zamanları vb.) |
INTERVAL |
Zaman aralığı | Uygulanamaz | İki olay arasındaki süre, bir işlemin süresi |
Bu seçimleri yaparken, projenizin gelecekteki ihtiyaçlarını ve verilerinizi kimlerin, hangi coğrafyada analiz edeceğini göz önünde bulundurmak önemlidir. Yanlış tip seçimi, ileride büyük veri düzeltme operasyonlarına veya hatalı raporlamalara yol açabilir.
Python’da datetime Modülü ile Tarih Verilerini Hazırlama: Scraper Verileri Nasıl Dönüştürülür?
Web scraper’ınızın topladığı ham tarih ve saat string’lerini, PostgreSQL’e doğru ve tutarlı bir şekilde kaydedebilmek için Python’ın güçlü datetime modülünü kullanmamız gerekir. Bu modül, tarih string’lerini Python’ın kendi datetime nesnelerine dönüştürme, bu nesneler üzerinde aritmetik işlemler yapma ve zaman dilimi dönüşümleri gerçekleştirme gibi yetenekler sunar.
Farklı Tarih Formatlarını Anlama ve Dönüştürme
Web siteleri, tarihleri standart olmayan birçok farklı biçimde sunabilir. datetime.strptime() fonksiyonu, belirli bir format string’ini kullanarak bir tarih string’ini datetime nesnesine dönüştürmek için kullanılır. İşte birkaç örnek:
from datetime import datetime
# Örnek 1: 'YYYY-MM-DD HH:MM:SS' formatı
date_str_1 = "2023-05-12 14:30:00"
dt_obj_1 = datetime.strptime(date_str_1, "%Y-%m-%d %H:%M:%S")
print(f"Format 1: {dt_obj_1}")
# Örnek 2: 'DD.MM.YYYY' formatı
date_str_2 = "12.05.2023"
dt_obj_2 = datetime.strptime(date_str_2, "%d.%m.%Y")
print(f"Format 2: {dt_obj_2}")
# Örnek 3: 'May 12, 2023 2:30 PM' formatı
date_str_3 = "May 12, 2023 2:30 PM"
dt_obj_3 = datetime.strptime(date_str_3, "%b %d, %Y %I:%M %p")
print(f"Format 3: {dt_obj_3}")
# Örnek 4: ISO 8601 formatı (genellikle API'lerden gelir)
date_str_4 = "2023-05-12T14:30:00Z" # 'Z' UTC olduğunu belirtir
# 'Z' için 'utcfromtimestamp' veya dateutil.parser daha uygun olabilir
# Ya da strptime'ı 'Z'siz kısmı için kullanıp manuel olarak UTC'yi ekleyebiliriz.
dt_obj_4 = datetime.strptime(date_str_4.replace('Z', ''), "%Y-%m-%dT%H:%M:%S")
print(f"Format 4 (ISO): {dt_obj_4}")
Farklı format kodları için strftime ve strptime davranış belgelerini incelemek faydalı olacaktır. Eğer çok sayıda farklı formatla karşılaşıyorsanız ve bunları tek tek yönetmek istemiyorsanız, dateutil.parser.parse gibi kütüphaneler daha esnek bir çözüm sunabilir. Bu kütüphane, çoğu yaygın formatı otomatik olarak algılayabilir.
from dateutil.parser import parse
date_str_flexible_1 = "2023-05-12 14:30"
date_str_flexible_2 = "12th May, 2023"
date_str_flexible_3 = "Yesterday at 3 PM" # Bu tür göreceli ifadeler için ek mantık gerekir.
print(f"Flexible Parse 1: {parse(date_str_flexible_1)}")
print(f"Flexible Parse 2: {parse(date_str_flexible_2)}")
# Göreceli ifadeler için genellikle özel bir yorumlama veya "now" parametresi gerekir.
# Örneğin, "Yesterday" için dateutil.relativedelta kullanılabilir veya scraper'ın çalıştığı an göz önünde bulundurulur.
# Basitçe, parse fonksiyonu "Yesterday" gibi ifadeleri belirli bir referans tarihi olmadan doğrudan datetime objesine dönüştürmekte zorlanabilir.
Zaman Dilimi Yönetimi ve UTC Dönüşümü
Veri bütünlüğü için en iyi uygulama, tüm tarih-saat verilerini UTC'ye (Coordinated Universal Time) dönüştürerek depolamaktır. Python 3.9 ve sonrası için yerleşik zoneinfo modülü veya eski sürümler için üçüncü taraf pytz kütüphanesi bu konuda yardımcı olur.
from datetime import datetime, timezone
import zoneinfo # Python 3.9+
# Eğer sistemde zoneinfo yoksa veya Python 3.9 öncesiyse:
# pip install pytz
# import pytz
# Şu anki UTC zamanı
now_utc = datetime.now(timezone.utc)
print(f"Şu anki UTC zamanı: {now_utc}")
# Belirli bir zaman dilimine sahip bir datetime objesi oluşturma
try:
istanbul_tz = zoneinfo.ZoneInfo("Europe/Istanbul")
london_tz = zoneinfo.ZoneInfo("Europe/London")
except zoneinfo.ZoneInfoNotFoundError:
print("ZoneInfo bulunamadı. pytz kullanılıyor.")
# import pytz
# istanbul_tz = pytz.timezone("Europe/Istanbul")
# london_tz = pytz.timezone("Europe/London")
# Bir string'i İstanbul zaman diliminde varsayarak işleme
date_str_istanbul = "2023-05-12 14:30:00"
dt_istanbul_naive = datetime.strptime(date_str_istanbul, "%Y-%m-%d %H:%M:%S")
dt_istanbul_aware = dt_istanbul_naive.replace(tzinfo=istanbul_tz)
# Veya daha doğru bir yol:
# dt_istanbul_aware = istanbul_tz.localize(dt_istanbul_naive) # pytz ile
print(f"İstanbul'da belirlenen zaman: {dt_istanbul_aware}")
# İstanbul zamanını UTC'ye dönüştürme
dt_istanbul_to_utc = dt_istanbul_aware.astimezone(timezone.utc)
print(f"İstanbul zamanının UTC karşılığı: {dt_istanbul_to_utc}")
# London zamanını UTC'ye dönüştürme (karşılaştırma için)
date_str_london = "2023-05-12 12:30:00" # İstanbul 14:30'a denk gelmesi için
dt_london_naive = datetime.strptime(date_str_london, "%Y-%m-%d %H:%M:%S")
dt_london_aware = dt_london_naive.replace(tzinfo=london_tz)
dt_london_to_utc = dt_london_aware.astimezone(timezone.utc)
print(f"London zamanının UTC karşılığı: {dt_london_to_utc}")
# Görüldüğü üzere, her iki farklı yerel saat de UTC'ye çevrildiğinde aynı oluyor.
# Bu da PostgreSQL'e TIMESTAMPTZ olarak kaydedilecek doğru formatı sağlıyor.
Bu süreç, web scraper'ınızın farklı kaynaklardan topladığı tarih verilerini tek bir, evrensel ve tutarlı formatta birleştirmenizi sağlar. Bu, daha sonra veriler üzerinde yapacağınız her türlü analiz veya raporlama için temel bir adımdır.
Asyncio Ortamında datetime Kullanımı: Performans İpuçları Nelerdir?
Asyncio, Python'da eşzamanlı (concurrent) programlama için tasarlanmış bir yapıdır ve I/O yoğun (disk, ağ) işlemlerde blokajı önleyerek performansı artırır. datetime modülü ile yapılan işlemler genellikle CPU yoğun (CPU-bound) ve çok hızlıdır. Bu tür küçük, anlık hesaplamalar event loop'u bloklamaz ve asyncio ortamında doğrudan kullanılabilir.
Ancak, bazı durumlarda (örneğin, çok büyük bir veri setindeki her öğe için karmaşık tarih hesaplamaları yapılıyorsa), bu işlemlerin toplam süresi dikkate değer hale gelebilir. Bu tür senaryolarda:
asyncio.to_thread()kullanılarak CPU yoğun işlemlerin ayrı bir iş parçacığına taşınması düşünülebilir. Bu, ana event loop'un serbest kalmasını sağlar. Ancakdatetimedönüşümleri genellikle o kadar hızlıdır ki bu, çoğu zaman gereksiz bir ek yük getirir.- Veri dönüşümlerini mümkün olduğunca erken bir aşamada ve veritabanına kaydetmeden önce tek seferde yapmak, genel veri akışınızı optimize eder.
- Karmaşık tarih/saat işleme mantığını daha optimize edilmiş kütüphanelerle (örneğin,
numpyveyapandasgibi kütüphanelerin tarih/saat yetenekleri) hızlandırmak, eğer büyük veri kümeleri üzerinde çalışıyorsanız bir seçenek olabilir. Ancak web kazıma bağlamında,datetimevedateutilçoğu senaryo için yeterli performansı sağlar.
Özetle, datetime modülünün temel kullanımı asyncio performansını olumsuz etkilemez. Ana odak noktanız, veri çekme ve veritabanı yazma gibi I/O işlemlerini await anahtar kelimesiyle doğru bir şekilde yönetmek olmalıdır.
Asyncpg ile PostgreSQL'e Tarih Verisi Kaydetme ve Geri Okuma: En İyi Yöntemler
Asyncio tabanlı bir Python web scraper için PostgreSQL ile etkileşime geçmenin en modern ve verimli yollarından biri asyncpg kütüphanesini kullanmaktır. asyncpg, özellikle asyncio ortamında çalışmak üzere tasarlanmış, hızlı ve güvenilir bir PostgreSQL veritabanı sürücüsüdür.
PostgreSQL Bağlantısı Kurma ve Tablo Oluşturma
Öncelikle, asyncpg kütüphanesini kurmanız gerekir:
pip install asyncpg
Daha sonra, veritabanı bağlantınızı oluşturabilir ve tarih verilerini saklamak için bir tablo tanımlayabilirsiniz. Önceki bölümde bahsettiğimiz gibi, TIMESTAMPTZ kullanmak en iyi uygulamadır.
CREATE TABLE IF NOT EXISTS scraped_data (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT,
scraped_at TIMESTAMPTZ NOT NULL, -- Verinin çekildiği zaman (UTC)
published_at TIMESTAMPTZ, -- Kaynakta belirtilen yayınlanma zamanı (UTC)
url VARCHAR(2048) UNIQUE NOT NULL
);
Bu tablo yapısı, bir başlık, içerik, verinin ne zaman çekildiği (scraped_at), kaynağın ne zaman yayınlandığını iddia ettiği (published_at) ve benzersiz bir URL içerir. TIMESTAMPTZ tipi, zaman dilimi farkındalığına sahip tarih-saat verilerini saklamak için idealdir.
Asyncpg ile Tarih Verilerini Kaydetme
Python tarafında datetime nesnelerini asyncpg aracılığıyla PostgreSQL'e kaydederken, asyncpg bu nesneleri otomatik olarak PostgreSQL'in TIMESTAMPTZ tipine uygun şekilde dönüştürür. Bu, büyük bir kolaylık sağlar ve manuel formatlama hatalarını önler. Önemli olan, datetime nesnelerinizin UTC ve zaman dilimi farkındalığına sahip (tzinfo set edilmiş) olmasıdır.
import asyncpg
from datetime import datetime, timezone
import zoneinfo
async def save_data_to_db(data_list):
conn = None # Bağlantıyı başlangıçta None olarak ayarla
try:
# Veritabanı bağlantısı kurma
conn = await asyncpg.connect(user='your_user', password='your_password',
database='your_db', host='127.0.0.1')
# Tek bir kayıt ekleme
# Örnek datetime nesnesi oluşturma (UTC, zaman dilimi farkındalığına sahip)
# published_at muhtemelen scraped_at'tan farklı bir kaynaktan gelecektir
now_utc = datetime.now(timezone.utc)
published_dt = datetime(2023, 5, 10, 10, 0, 0, tzinfo=timezone.utc) # Örnek yayınlanma tarihi
await conn.execute('''
INSERT INTO scraped_data(title, content, scraped_at, published_at, url)
VALUES($1, $2, $3, $4, $5)
''', 'Örnek Başlık', 'Bu bir örnek içeriktir.', now_utc, published_dt, 'https://example.com/example-article')
print("Tek kayıt başarıyla eklendi.")
# Toplu kayıt ekleme (daha verimli)
# executemany asyncpg'nin toplu insertler için önerilen yoludur.
records_to_insert = []
for i, data in enumerate(data_list):
scraped_at_val = data.get('scraped_at', datetime.now(timezone.utc))
published_at_val = data.get('published_at') # None olabilir
# published_at string ise datetime objesine çevir
if isinstance(published_at_val, str):
try:
# 'Z' ile biten ISO formatlarını işlemek için
if published_at_val.endswith('Z'):
published_at_val = datetime.strptime(published_at_val.replace('Z', '+00:00'), "%Y-%m-%dT%H:%M:%S%z")
else:
published_at_val = datetime.fromisoformat(published_at_val) # ISO formatları için
# Eğer parsed datetime zaman dilimi bilgisinden yoksunsa UTC olarak varsay
if published_at_val.tzinfo is None:
published_at_val = published_at_val.replace(tzinfo=timezone.utc)
else:
# Başka bir zaman dilimindeyse UTC'ye çevir
published_at_val = published_at_val.astimezone(timezone.utc)
except ValueError:
print(f"Hata: published_at string formatı dönüştürülemedi: {published_at_val}. None olarak ayarlandı.")
published_at_val = None
# Ensure published_at is timezone-aware if not None
if published_at_val and published_at_val.tzinfo is None:
published_at_val = published_at_val.replace(tzinfo=timezone.utc)
records_to_insert.append((
data.get('title', f"Otomatik Başlık {i}"),
data.get('content', f"Otomatik içerik {i}."),
scraped_at_val,
published_at_val,
data.get('url', f"https://example.com/auto-article-{i}")
))
if records_to_insert:
await conn.executemany('''
INSERT INTO scraped_data(title, content, scraped_at, published_at, url)
VALUES($1, $2, $3, $4, $5)
ON CONFLICT (url) DO UPDATE SET
title = EXCLUDED.title,
content = EXCLUDED.content,
scraped_at = EXCLUDED.scraped_at,
published_at = EXCLUDED.published_at;
''', records_to_insert)
print(f"{len(records_to_insert)} kayıt başarıyla toplu olarak eklendi/güncellendi.")
except Exception as e:
print(f"Veritabanı işlemi sırasında bir hata oluştu: {e}")
finally:
if conn:
await conn.close()
# Örnek veri listesi
sample_data = [
{'title': 'Makale A', 'content': 'İçerik A.', 'scraped_at': datetime.now(timezone.utc), 'published_at': '2023-05-11T08:00:00Z', 'url': 'https://example.com/a'},
{'title': 'Makale B', 'content': 'İçerik B.', 'scraped_at': datetime.now(timezone.utc), 'published_at': datetime(2023, 5, 9, 15, 30, tzinfo=zoneinfo.ZoneInfo("America/New_York")).astimezone(timezone.utc), 'url': 'https://example.com/b'},
{'title': 'Makale C', 'content': 'İçerik C.', 'scraped_at': datetime.now(timezone.utc), 'published_at': '2023-05-12 10:00:00', 'url': 'https://example.com/c'} # Naive string
]
# async def main():
# await save_data_to_db(sample_data)
# if __name__ == '__main__':
# import asyncio
# asyncio.run(main())
Yukarıdaki kod bloğu, executemany kullanarak toplu ekleme örneği sunar. Bu yöntem, tek tek INSERT sorguları göndermekten çok daha performanslıdır ve büyük veri kümeleri için önerilir. Ayrıca, ON CONFLICT (url) DO UPDATE ifadesi, benzersiz url sütununu kullanarak zaten mevcut olan kayıtları güncellemenizi sağlar, böylece veri tekrarını önlersiniz.
Tarih Verilerini PostgreSQL'den Geri Okuma ve Python'da Kullanma
PostgreSQL'den TIMESTAMPTZ tipindeki verileri sorguladığınızda, asyncpg bunları otomatik olarak Python'ın zaman dilimi farkındalığına sahip datetime nesnelerine dönüştürür. Bu, verileri hemen kullanmaya hazır hale getirir.
import asyncpg
from datetime import datetime, timezone
import zoneinfo
async def fetch_data_from_db():
conn = None
try:
conn = await asyncpg.connect(user='your_user', password='your_password',
database='your_db', host='127.0.0.1')
# Son 24 saat içinde kazınan verileri çekme
one_day_ago = datetime.now(timezone.utc) - timedelta(days=1)
records = await conn.fetch('''
SELECT title, scraped_at, published_at FROM scraped_data
WHERE scraped_at >= $1
ORDER BY scraped_at DESC
''', one_day_ago)
print("\n--- Son 24 Saatte Kazınan Veriler ---")
for record in records:
scraped_at_utc = record['scraped_at'] # asyncpg otomatik olarak datetime objesi döndürür (UTC)
published_at_utc = record['published_at'] # Bu da datetime objesidir
# İsterseniz yerel zaman dilimine dönüştürebilirsiniz (örn. İstanbul)
try:
istanbul_tz = zoneinfo.ZoneInfo("Europe/Istanbul")
scraped_at_istanbul = scraped_at_utc.astimezone(istanbul_tz)
published_at_istanbul = published_at_utc.astimezone(istanbul_tz) if published_at_utc else None
except zoneinfo.ZoneInfoNotFoundError:
print("ZoneInfo bulunamadı, dönüşüm yapılmıyor.")
scraped_at_istanbul = scraped_at_utc
published_at_istanbul = published_at_utc
print(f"Başlık: {record['title']}")
print(f"Kazınma Tarihi (UTC): {scraped_at_utc}")
print(f"Kazınma Tarihi (İstanbul): {scraped_at_istanbul}")
print(f"Yayınlanma Tarihi (UTC): {published_at_utc}")
print(f"Yayınlanma Tarihi (İstanbul): {published_at_istanbul}")
print("-" * 20)
except Exception as e:
print(f"Veri okuma sırasında bir hata oluştu: {e}")
finally:
if conn:
await conn.close()
# async def main():
# await fetch_data_from_db()
# if __name__ == '__main__':
# import asyncio
# from datetime import timedelta # timedelta'yı burada tanımladık
# asyncio.run(main())
Bu bölüm, asyncpg kullanarak veritabanına tarih verisi kaydetmenin ve geri okumanın temel adımlarını ve en iyi uygulamalarını göstermektedir. Önemli nokta, Python datetime nesnelerinin UTC ve zaman dilimi farkındalığına sahip olması ve asyncpg'nin bu nesnelerle sorunsuz bir şekilde çalışmasıdır. Bu yaklaşım, web scraper'ınızın topladığı tarihsel verilerin tutarlı, doğru ve analiz edilebilir olmasını sağlar.
Gerçek Dünya Senaryosu: Bir E-Ticaret Sitesinden Ürün Yorum Tarihlerini Kazıma ve Yönetme
Şimdiye kadar öğrendiğimiz teorik bilgileri ve kod örneklerini gerçek bir senaryoda uygulayalım. Bir e-ticaret sitesinden ürün yorumlarını çekmek istediğinizi ve bu yorumların yayınlanma tarihlerini doğru bir şekilde yönetmeniz gerektiğini varsayalım. E-ticaret siteleri genellikle yorum tarihlerini çok çeşitli ve kullanıcı dostu ancak makine tarafından okunması zor formatlarda sunar. Örneğin:
- "2 gün önce"
- "1 hafta önce"
- "12 Mayıs 2023"
- "12.05.2023"
- "2022-11-20"
Bu farklı formatları işlemek ve PostgreSQL'e TIMESTAMPTZ olarak kaydetmek için bir iş akışı oluşturalım.
Adım 1: Tarih Stringlerini Tanımlama ve Ayıklama
Web sayfasından yorumları çekerken, tarih stringlerini HTML'den veya API yanıtlarından ayıklamanız gerekir. Bu genellikle BeautifulSoup veya lxml gibi kütüphanelerle yapılır.
import re
from datetime import datetime, timedelta, timezone
from dateutil.parser import parse, ParserError
import zoneinfo # Python 3.9+ veya pytz
def extract_date_string(html_element):
# Bu sadece bir örnek. Gerçek uygulamada CSS seçiciler veya XPath kullanılır.
date_text = html_element.find('span', class_='comment-date').text.strip()
return date_text
# Örnek HTML elementleri simüle edelim:
sample_html_elements = [
"2 gün önce",
"1 hafta önce",
"12 Mayıs 2023",
"12.05.2023",
"2022-11-20",
"Dün 14:30",
"Bugün 10:00",
"3 ay önce",
"Geçen sene"
]
Adım 2: Çeşitli Tarih Stringlerini datetime Nesnelerine Dönüştürme
Bu adım, dateutil.parser.parse ve bazı özel mantık kombinasyonunu gerektirir. Göreceli ifadeleri (örn. "2 gün önce") işlemek için scraper'ın çalıştığı zamanı referans almamız gerekecek.
def parse_flexible_date(date_string, reference_time=None):
if reference_time is None:
reference_time = datetime.now(timezone.utc) # UTC olarak referans alın
date_string = date_string.lower().strip()
# Göreceli zaman ifadeleri
if "gün önce" in date_string:
days = int(re.search(r'(\d+)', date_string).group(1))
return reference_time - timedelta(days=days)
elif "hafta önce" in date_string:
weeks = int(re.search(r'(\d+)', date_string).group(1))
return reference_time - timedelta(weeks=weeks)
elif "ay önce" in date_string:
months = int(re.search(r'(\d+)', date_string).group(1))
# dateutil.relativedelta aylık çıkarmalar için daha doğru
from dateutil.relativedelta import relativedelta
return reference_time - relativedelta(months=months)
elif "sene önce" in date_string or "yıl önce" in date_string:
years = int(re.search(r'(\d+)', date_string).group(1)) if re.search(r'(\d+)', date_string) else 1
from dateutil.relativedelta import relativedelta
return reference_time - relativedelta(years=years)
elif "dün" in date_string:
# Saati de içerebilir "Dün 14:30"
time_part = re.search(r'(\d{1,2}:\d{2})', date_string)
if time_part:
dt_naive = datetime.strptime(time_part.group(1), "%H:%M")
return (reference_time - timedelta(days=1)).replace(hour=dt_naive.hour, minute=dt_naive.minute, second=0, microsecond=0)
return reference_time - timedelta(days=1)
elif "bugün" in date_string:
time_part = re.search(r'(\d{1,2}:\d{2})', date_string)
if time_part:
dt_naive = datetime.strptime(time_part.group(1), "%H:%M")
return reference_time.replace(hour=dt_naive.hour, minute=dt_naive.minute, second=0, microsecond=0)
return reference_time # Bugün, şu an
# Diğer formatlar için dateutil.parser kullan
try:
# Türkçe ay isimlerini İngilizce'ye çevirme veya yerel ayar kullanarak parse etme
# Örneğin: "12 Mayıs 2023" -> "12 May 2023"
turkish_month_map = {
'ocak': 'january', 'şubat': 'february', 'mart': 'march', 'nisan': 'april',
'mayıs': 'may', 'haziran': 'june', 'temmuz': 'july', 'ağustos': 'august',
'eylül': 'september', 'ekim': 'october', 'kasım': 'november', 'aralık': 'december'
}
for tr_month, en_month in turkish_month_map.items():
if tr_month in date_string:
date_string = date_string.replace(tr_month, en_month)
break
parsed_dt = parse(date_string, fuzzy_with_tokens=False)
# Parse edilen datetime objesi naive ise (zaman dilimi yoksa), UTC olarak varsay
if parsed_dt.tzinfo is None:
return parsed_dt.replace(tzinfo=timezone.utc)
else:
return parsed_dt.astimezone(timezone.utc) # Başka bir zaman dilimindeyse UTC'ye çevir
except ParserError:
print(f"Bilinmeyen tarih formatı: {date_string}")
return None
# Test etme
for ds in sample_html_elements:
parsed_date = parse_flexible_date(ds)
print(f"'{ds}' -> {parsed_date}")
# Çıktı örnekleri (referans zamanına göre değişebilir):
# '2 gün önce' -> 2023-05-10 14:30:00+00:00
# '1 hafta önce' -> 2023-05-05 14:30:00+00:00
# '12 Mayıs 2023' -> 2023-05-12 00:00:00+00:00
# '12.05.2023' -> 2023-05-12 00:00:00+00:00
# '2022-11-20' -> 2022-11-20 00:00:00+00:00
# 'Dün 14:30' -> 2023-05-11 14:30:00+00:00
# 'Bugün 10:00' -> 2023-05-12 10:00:00+00:00
# '3 ay önce' -> 2023-02-12 14:30:00+00:00
# 'Geçen sene' -> 2022-05-12 14:30:00+00:00
Adım 3: PostgreSQL'e Kaydetme
Yukarıdaki parse_flexible_date fonksiyonu ile elde ettiğimiz UTC ve zaman dilimi farkındalığına sahip datetime objelerini, önceki bölümde gösterildiği gibi asyncpg ile PostgreSQL'e kolayca kaydedebiliriz.
import asyncpg
from datetime import datetime, timezone, timedelta
from dateutil.parser import parse, ParserError
from dateutil.relativedelta import relativedelta # Sadece relativedelta için
# parse_flexible_date fonksiyonu yukarıdaki gibi burada da olmalı.
async def save_review(product_id, review_text, date_string_from_scraper):
conn = None
try:
conn = await asyncpg.connect(user='your_user', password='your_password',
database='your_db', host='127.0.0.1')
# Tarihi dönüştür
published_at_utc = parse_flexible_date(date_string_from_scraper)
if published_at_utc is None:
print(f"Tarih dönüştürülemedi: {date_string_from_scraper}. Kayıt atlandı.")
return
# Scraped_at her zaman kazımanın yapıldığı an olacaktır (UTC)
scraped_at_utc = datetime.now(timezone.utc)
await conn.execute('''
INSERT INTO product_reviews(product_id, review_text, published_at, scraped_at)
VALUES($1, $2, $3, $4)
''', product_id, review_text, published_at_utc, scraped_at_utc)
print(f"Yorum başarıyla kaydedildi: Product ID {product_id}, Tarih: {published_at_utc}")
except Exception as e:
print(f"Yorum kaydetme sırasında hata oluştu: {e}")
finally:
if conn:
await conn.close()
# Örnek kullanım:
# async def main_scenario():
# # Önce tabloyu oluşturduğumuzdan emin olalım:
# # CREATE TABLE IF NOT EXISTS product_reviews (
# # id SERIAL PRIMARY KEY,
# # product_id VARCHAR(255) NOT NULL,
# # review_text TEXT,
# # published_at TIMESTAMPTZ,
# # scraped_at TIMESTAMPTZ NOT NULL
# # );
#
# await save_review("PRD001", "Harika bir ürün!", "2 gün önce")
# await save_review("PRD002", "Fiyatı yüksek.", "12 Mayıs 2023")
# await save_review("PRD003", "Kargo yavaştı.", "2022-11-20")
# await save_review("PRD004", "Çok iyi!", "1 hafta önce")
# await save_review("PRD005", "Beğendim.", "Dün 14:30")
# await save_review("PRD006", "Kötü.", "18 Ağustos 2021") # Türkçe ay ismi örneği
# await save_review("PRD007", "Fiyatı iyi.", "Bugün 10:00")
# await save_review("PRD008", "Vasat", "3 ay önce")
# await save_review("PRD009", "Yorum tarihi yok!") # Parse edilemeyen durum
# if __name__ == '__main__':
# import asyncio
# asyncio.run(main_scenario())
Bu gerçek dünya senaryosu, web sitelerinden gelen düzensiz tarih formatlarını nasıl temizleyeceğinizi, standartlaştıracağınızı (UTC ve zaman dilimi farkındalığına sahip datetime objeleri) ve PostgreSQL'e güvenli bir şekilde nasıl kaydedeceğinizi göstermektedir. Bu yaklaşım, scraper'ınızın topladığı verinin hem tutarlı hem de analiz edilebilir olmasını sağlar.
İleri Düzey İpuçları ve Performans Optimizasyonları: Büyük Veri Kümeleri İçin Neler Yapılabilir?
Web kazıma projeleri genellikle büyük hacimli verilerle uğraşmayı gerektirir. Tarih ve saat verileri de bu büyük kümelerin önemli bir parçasıdır. Büyük veri kümeleriyle çalışırken, performans ve veritabanı yönetimini optimize etmek için bazı ileri düzey tekniklere başvurmak gerekebilir.
Veritabanı İndeksleme: Tarih Sütunları İçin Neden Önemli?
Tarih sütunları üzerinde sıkça sorgulama (filtreleme, sıralama veya birleştirme) yapıyorsanız, bu sütunlara indeks eklemek sorgu performansını dramatik bir şekilde artırabilir. Örneğin, son 24 saatteki yorumları veya belirli bir ay içindeki tüm makaleleri arıyorsanız, indeksler veritabanının verileri çok daha hızlı bulmasını sağlar.
-- scraped_data tablosundaki scraped_at sütunu için indeks oluşturma
CREATE INDEX IF NOT EXISTS idx_scraped_data_scraped_at ON scraped_data (scraped_at);
-- product_reviews tablosundaki published_at sütunu için indeks oluşturma
CREATE INDEX IF NOT EXISTS idx_product_reviews_published_at ON product_reviews (published_at);
İndeksler, veritabanının belirli bir sütundaki değerlere hızlıca erişmesini sağlayan bir tür arama tablosu gibidir. Ancak, indeksler ek depolama alanı kullanır ve veri ekleme/güncelleme işlemlerinde küçük bir ek yük getirir. Bu nedenle, sıkça sorgulanan sütunlara indeks eklemek en mantıklı yaklaşımdır.
Partitioning (Bölümleme): Çok Büyük Tarih Serileri İçin
Milyonlarca veya milyarlarca satır içeren tablolar için, veritabanı bölümleme (partitioning) ciddi bir performans artışı sağlayabilir. Özellikle tarih veya zaman aralığına göre bölümleme (range partitioning), çok büyük tabloları daha küçük, yönetilebilir parçalara böler. Bu, sorguların sadece ilgili bölümlere odaklanmasını sağlayarak performansı artırır ve bakım işlemlerini (yedekleme, indeksleme, arşivleme) kolaylaştırır.
-- product_reviews tablosunu published_at sütununa göre yıla göre bölümleme örneği
CREATE TABLE product_reviews_partitioned (
id SERIAL,
product_id VARCHAR(255) NOT NULL,
review_text TEXT,
published_at TIMESTAMPTZ,
scraped_at TIMESTAMPTZ NOT NULL
) PARTITION BY RANGE (published_at);
-- Yıllara göre bölümler oluşturma
CREATE TABLE product_reviews_y2022 PARTITION OF product_reviews_partitioned
FOR VALUES FROM ('2022-01-01 00:00:00+00') TO ('2023-01-01 00:00:00+00');
CREATE TABLE product_reviews_y2023 PARTITION OF product_reviews_partitioned
FOR VALUES FROM ('2023-01-01 00:00:00+00') TO ('2024-01-01 00:00:00+00');
-- Gelecek için varsayılan bir bölüm de oluşturulabilir
CREATE TABLE product_reviews_default PARTITION OF product_reviews_partitioned DEFAULT;
Bu yapı sayesinde, 2022 yılına ait yorumları sorguladığınızda, veritabanı sadece product_reviews_y2022 tablosuna bakar, diğer milyonlarca satırı taramak zorunda kalmaz.
Verimli Toplu Ekleme: COPY FROM Kullanımı
asyncpg'nin executemany metodu zaten oldukça verimlidir, ancak çok büyük veri kümeleri (on binlerce, yüz binlerce satır ve üzeri) için PostgreSQL'in yerel COPY FROM komutu en hızlı toplu ekleme yöntemidir. asyncpg, bu işlevselliği copy_records_to_table veya copy_from_query gibi yöntemlerle destekler. Bu, genellikle bir CSV dosyası veya bellek içi bir veri akışı aracılığıyla verileri doğrudan veritabanına aktarır.
# asyncpg ile copy_records_to_table kullanımı
import asyncpg
from datetime import datetime, timezone
async def bulk_copy_data(records):
conn = None
try:
conn = await asyncpg.connect(user='your_user', password='your_password',
database='your_db', host='127.0.0.1')
# records, tuple'lardan oluşan bir liste olmalı ve sütun sırasına uymalıdır.
# Örneğin: [(title, content, scraped_at, published_at, url), ...]
# Sütun isimlerini belirtmek önemlidir.
await conn.copy_records_to_table(
'scraped_data',
records=records,
columns=['title', 'content', 'scraped_at', 'published_at', 'url']
)
print(f"{len(records)} kayıt COPY FROM ile başarıyla eklendi.")
except Exception as e:
print(f"Toplu kopyalama sırasında hata oluştu: {e}")
finally:
if conn:
await conn.close()
# Örnek kullanım:
# sample_records_for_copy = [
# ('Başlık X', 'İçerik X', datetime.now(timezone.utc), datetime(2023, 4, 1, tzinfo=timezone.utc), 'https://example.com/x'),
# ('Başlık Y', 'İçerik Y', datetime.now(timezone.utc), datetime(2023, 3, 15, tzinfo=timezone.utc), 'https://example.com/y'),
# ]
# async def main_copy():
# await bulk_copy_data(sample_records_for_copy)
# if __name__ == '__main__':
# import asyncio
# asyncio.run(main_copy())
Bu yöntem, saniyede binlerce hatta on binlerce satır ekleyebilme kapasitesine sahiptir, bu da onu büyük ölçekli veri alımı için ideal kılar.
Medya Sorguları ve Mobil Uyumluluk (HTML/CSS bağlamında)
Makalenin formatında belirtildiği gibi, mobil uyumlu HTML üretmek için media query örnekleri eklemek önemlidir. Bu doğrudan Python veya PostgreSQL ile ilgili olmasa da, web scraper'ınızdan elde ettiğiniz verileri sunan bir arayüz geliştiriyorsanız hayati olabilir. Örneğin, tarih tablolarınızı mobil cihazlarda daha okunaklı hale getirmek için:
/* Genel tablo stilleri */
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
}
/* Mobil cihazlar için stil (768px'ten küçük ekranlar) */
@media screen and (max-width: 768px) {
table, thead, tbody, th, td, tr {
display: block; /* Tablo elemanlarını blok elementler gibi davranmaya zorla */
}
thead tr {
position: absolute; /* Başlık satırını gizle */
top: -9999px;
left: -9999px;
}
tr {
border: 1px solid #ccc; /* Her satır için bir çerçeve */
margin-bottom: 1em;
}
td {
border: none;
border-bottom: 1px solid #eee;
position: relative;
padding-left: 50%; /* İçeriği başlıklar için yer açarak kaydır */
text-align: right;
}
td:before {
/* Mobil görünümde her hücreye ait başlığı dinamik olarak ekle */
position: absolute;
left: 6px;
width: 45%;
padding-right: 10px;
white-space: nowrap;
text-align: left;
font-weight: bold;
/* content: attr(data-label); HTML'deki data-label özniteliğinden başlık çekilebilir */
}
/* Örnek: İlk sütun başlığını dinamik olarak ekle */
td:nth-of-type(1):before { content: "Başlık:"; }
td:nth-of-type(2):before { content: "Kazınma Tarihi:"; }
td:nth-of-type(3):before { content: "Yayınlanma Tarihi:"; }
}
Bu CSS medya sorgusu örneği, büyük tablolardaki tarih verilerinin mobil ekranlarda kaydırılmak yerine dikey olarak yığılmasını sağlar, böylece okunabilirlik artar. Her
data-label özniteliği ekleyerek, mobil görünümde başlıkları da gösterebilirsiniz. Bu, kullanıcı deneyimini önemli ölçüde iyileştirir.
Bu ileri düzey ipuçları, web kazıma projelerinizin sadece işlevsel olmakla kalmayıp, aynı zamanda büyük veri kümeleriyle çalışırken ölçeklenebilir ve performanslı olmasını sağlar. Doğru indeksleme, bölümleme ve toplu ekleme stratejileri, veritabanı performansınızı optimize etmenin anahtarlarıdır.
Sonuç: Asyncio Scraper'larda Tarih Yönetiminde Ustalaşmak
Asyncio Python web scraper projelerinde tarih ve saat tiplerini doğru bir şekilde yönetmek, veri kalitesini, tutarlılığını ve gelecekteki analizlerin doğruluğunu doğrudan etkileyen kritik bir beceridir. Bu makalede, tarih tiplerinin neden bu kadar önemli olduğundan, PostgreSQL'deki farklı tarih tiplerini ne zaman kullanmanız gerektiğine, Python'ın datetime modülü ile karmaşık tarih stringlerini nasıl dönüştüreceğinize ve asyncpg ile veritabanına nasıl güvenle kaydedeceğinize kadar birçok konuyu ele aldık.
Özellikle zaman dilimi farkındalığına sahip TIMESTAMPTZ tipini PostgreSQL'de kullanmak ve tüm tarih verilerini Python tarafında UTC'ye dönüştürmek, uluslararası web kazıma projeleri için altın standarttır. Bu yaklaşım, veri karışıklığını önler ve evrensel bir referans noktası sağlar. Ayrıca, dateutil.parser gibi kütüphanelerle esnek tarih stringlerini dönüştürmek, farklı web sitelerinden gelen düzensiz formatlarla başa çıkmak için vazgeçilmezdir. Büyük veri kümeleri için indeksleme, bölümleme ve COPY FROM gibi ileri düzey teknikler ise, veritabanı performansını optimize etmenin anahtarlarıdır.
Bu kılavuz, web kazıma projelerinizde tarih ve saat verilerini etkili bir şekilde yönetmeniz için sağlam bir temel sunmaktadır. Bu prensipleri uygulayarak, sadece daha güvenilir ve doğru veriler toplamakla kalmayacak, aynı zamanda veri işleme ve analiz süreçlerinizi de daha verimli hale getireceksiniz. Unutmayın, veri temizliği ve standardizasyonu, her başarılı veri projesinin temelidir.
Sıkça Sorulan Sorular (SSS)
Q1: Tarih formatları web kazıma projelerinde neden bu kadar sorunlu?
C1: Web siteleri, tarih ve saat bilgilerini genellikle kullanıcı dostu olacak şekilde, ancak makineler tarafından işlenmesi zor birçok farklı formatta sunar (örn. "2 gün önce", "12 Mayıs 2023", "2023-05-12T14:30:00Z"). Bu çeşitlilik, verileri standart bir datetime objesine dönüştürme ihtiyacını doğurur. Ayrıca, farklı coğrafi konumlar nedeniyle zaman dilimi farklılıkları da veri tutarsızlıklarına yol açabilir.
Q2: PostgreSQL'de TIMESTAMP ve TIMESTAMPTZ arasındaki temel fark nedir?
C2: TIMESTAMP WITHOUT TIME ZONE (kısaca TIMESTAMP), tarih ve saati depolar ancak herhangi bir zaman dilimi bilgisi içermez. Veritabanı, kaydedilen değeri olduğu gibi alır. TIMESTAMP WITH TIME ZONE (kısaca TIMESTAMPTZ) ise tarih ve saati depolar ve bu değeri veritabanının kendi zaman dilimine (genellikle UTC) dönüştürerek saklar. Sorgulandığında, istemcinin zaman dilimine göre tekrar dönüştürülebilir. TIMESTAMPTZ, web kazıma gibi zaman dilimi farkındalığı gerektiren projeler için önerilir.
Q3: Python'da timezone dönüşümlerini yaparken dikkat etmem gerekenler nelerdir?
C3: En önemli nokta, tüm tarih-saat verilerini UTC'ye dönüştürerek depolamaktır. Python'da datetime.now(timezone.utc) ile zaman dilimi farkındalığına sahip UTC objeleri oluşturabilir veya pytz (Python 3.9 öncesi) ya da zoneinfo (Python 3.9+) kütüphanelerini kullanarak belirli zaman dilimlerini yönetebilirsiniz. String'ten datetime objesi oluştururken (strptime veya dateutil.parser) eğer objenin zaman dilimi bilgisi yoksa (naive ise), onu bir zaman dilimine atamayı (.replace(tzinfo=...)) veya doğrudan UTC olarak varsaymayı unutmayın.
Q4: Büyük veri kümeleri için tarih sütunlarında indeksleme neden önemlidir?
C4: Tarih sütunları üzerinde sıkça filtreleme (örn. WHERE published_at >= '2023-01-01'), sıralama (ORDER BY published_at DESC) veya birleştirme (JOIN) işlemleri yapılıyorsa, indeksler bu sorguların performansını önemli ölçüde artırır. İndeksler, veritabanının ilgili verileri disk üzerinde daha hızlı bulmasını sağlayan bir arama dizini görevi görür. Bu, milyonlarca satırlık tablolarda sorgu sürelerini saniyelerden milisaniyelere düşürebilir.