/** * YÖK Atlas API'sindeki program ÖZELLİKLERİNİ `program_ozellik` tablosuna yazar: * yıllık ücret, burs oranı, öğretim dili, öğretim türü, program ve üniversite * akreditasyonu, kadro (prof/doç/dr. öğr. üyesi/arş. gör.), tercih baraj * sırası, kampüs ilçesi ve yerleşme koşulu kodları. Koşul metinleri koda göre * `kosul` tablosunda tek kopya durur. * * refresh.ts'ten AYRI betik: bu alanların yıl anlamı yok (kılavuzun o anki * hâli), yıl kilidi gerekmez; ama --yil ve --db yine zorunlu ve tablo satırı * hangi yılın kılavuzundan geldiğini `yil` kolonunda taşır. Yalnız `programs` * tablosunda olan id'lere yazılır; eşleşmeyen kayıt sayısı raporlanır. * * Kaynağa nazik: sayfa başı 500 kayıt, istekler arası 0,5 sn, tek bağlantı. * Çekilen ham kayıtlar data/ham/ozellik--.jsonl.gz'e yazılır; * `--ham ` verilirse API'ye gidilmez, o dosya okunur. * * Kullanım: * pnpm ozellik --yil 2026 --db /tmp/deneme.db * pnpm ozellik --yil 2026 --db data/yokatlas.db --ham data/ham/ozellik-2026-2026-09-25.jsonl.gz */ import fs from "node:fs"; import path from "node:path"; import readline from "node:readline"; import zlib from "node:zlib"; import { argOku, calistir, dbAc, dbArgumani, dur, metaYaz, yilArgumani, type Db, } from "./yokatlas-goc"; const API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search"; const PAGE_SIZE = 500; const DELAY_MS = 500; type HamKayit = Record; type ApiPage = { content: HamKayit[]; totalPages: number; totalElements: number; }; const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); async function fetchPage(page: number): Promise { const res = await fetch(API, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filters: { puanTuru: null, universiteId: [], birimGrupId: [], ilKodu: [], birimTuruId: null, universiteTuru: null, bursOraniId: null, ogrenimTuruId: null, kilavuzKodu: null, minBasariSirasi: null, maxBasariSirasi: null, }, page, size: PAGE_SIZE, sortBy: "kilavuzKodu", direction: "ASC", }), }); if (!res.ok) throw new Error(`API ${res.status} (sayfa ${page})`); return (await res.json()) as ApiPage; } /** Pozitif tam sayı; 0 / boş / sayı değil → null (API "veri yok"u 0 ile verir). */ function intOrNull(v: unknown): number | null { const n = typeof v === "string" ? Number.parseFloat(v) : (v as number); return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.round(n) : null; } function metinOrNull(v: unknown): string | null { if (typeof v !== "string") return null; const t = v.trim(); return t ? t : null; } // ── Şema ───────────────────────────────────────────────────────────────── export function ozellikTablolariniGarantiEt(db: Db): void { db.exec(` CREATE TABLE IF NOT EXISTS program_ozellik ( id TEXT PRIMARY KEY REFERENCES programs(id), ucret INTEGER, burs_orani TEXT, ogrenim_dili TEXT, ogrenim_turu TEXT, akreditasyon TEXT, uni_akreditasyon TEXT, prof INTEGER, doc INTEGER, dou INTEGER, argor INTEGER, min_basari_sirasi INTEGER, ilce TEXT, kosul_kodlari TEXT, yil INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS kosul ( kod TEXT PRIMARY KEY, metin TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_program_ozellik_dili ON program_ozellik (ogrenim_dili); CREATE INDEX IF NOT EXISTS idx_program_ozellik_turu ON program_ozellik (ogrenim_turu); `); } // ── Ham dosya ──────────────────────────────────────────────────────────── async function hamOku(dosya: string, yil: number): Promise { if (!fs.existsSync(dosya)) dur(`Ham dosya yok: ${dosya}`); const rl = readline.createInterface({ input: fs.createReadStream(dosya).pipe(zlib.createGunzip()), }); const kayitlar: HamKayit[] = []; let metaVar = false; for await (const satir of rl) { if (!satir) continue; const obj = JSON.parse(satir) as HamKayit; if (obj._meta) { const meta = obj._meta as { yil: number }; if (meta.yil !== yil) dur(`Ham dosya ${meta.yil} yılı için, --yil ${yil} verildi.`); metaVar = true; continue; } kayitlar.push(obj); } if (!metaVar) dur(`Ham dosyada _meta satırı yok: ${dosya}`); return kayitlar; } async function apidenCek(yil: number): Promise { const ilk = await fetchPage(0); const toplamSayfa = ilk.totalPages; const kayitlar = [...ilk.content]; console.log(`API: ${ilk.totalElements} kayıt, ${toplamSayfa} sayfa`); for (let p = 1; p < toplamSayfa; p++) { await sleep(DELAY_MS); const sayfa = await fetchPage(p); kayitlar.push(...sayfa.content); if (p % 10 === 0) console.log(` sayfa ${p}/${toplamSayfa}`); } const benzersiz = new Set(kayitlar.map((r) => String(r.kilavuzKodu))).size; if (benzersiz !== ilk.totalElements) { dur(`Benzersiz kod ${benzersiz} ≠ totalElements ${ilk.totalElements} (sayfalama kaydı)`); } const dizin = path.join(process.cwd(), "data", "ham"); fs.mkdirSync(dizin, { recursive: true }); const tarih = new Date().toISOString().slice(0, 10); const dosya = path.join(dizin, `ozellik-${yil}-${tarih}.jsonl.gz`); const gz = zlib.createGzip(); const out = fs.createWriteStream(dosya); gz.pipe(out); gz.write(JSON.stringify({ _meta: { yil, totalElements: ilk.totalElements, tarih, kaynak: "program-ozellik" } }) + "\n"); for (const r of kayitlar) gz.write(JSON.stringify(r) + "\n"); await new Promise((res, rej) => { out.on("finish", () => res()); out.on("error", rej); gz.end(); }); console.log(`Ham dosya: ${dosya}`); return kayitlar; } // ── Yazım ──────────────────────────────────────────────────────────────── function kosulKodlari(r: HamKayit): { kodlar: string[]; metinler: Map } { const metinler = new Map(); const liste = Array.isArray(r.kosulList) ? (r.kosulList as Record[]) : []; for (const k of liste) { for (const [kod, metin] of Object.entries(k)) { if (typeof metin === "string" && metin.trim()) metinler.set(kod, metin.trim()); } } // `kosul` alanı "155,18,21" biçiminde; listedeki kodlarla birleşir. const hamKodlar = typeof r.kosul === "string" ? r.kosul.split(",") : []; const kodlar = Array.from( new Set([...hamKodlar.map((k) => k.trim()).filter(Boolean), ...metinler.keys()]) ).sort((a, b) => Number(a) - Number(b)); return { kodlar, metinler }; } calistir(async () => { const yil = yilArgumani(); const dbYolu = dbArgumani(); const hamArg = argOku("ham"); const kayitlar = hamArg ? await hamOku(path.resolve(process.cwd(), hamArg), yil) : await apidenCek(yil); const db = dbAc(dbYolu); ozellikTablolariniGarantiEt(db); const mevcut = new Set( (db.prepare("SELECT id FROM programs").all() as { id: string }[]).map((r) => r.id) ); const yaz = db.prepare(` INSERT INTO program_ozellik (id, ucret, burs_orani, ogrenim_dili, ogrenim_turu, akreditasyon, uni_akreditasyon, prof, doc, dou, argor, min_basari_sirasi, ilce, kosul_kodlari, yil) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET ucret = excluded.ucret, burs_orani = excluded.burs_orani, ogrenim_dili = excluded.ogrenim_dili, ogrenim_turu = excluded.ogrenim_turu, akreditasyon = excluded.akreditasyon, uni_akreditasyon = excluded.uni_akreditasyon, prof = excluded.prof, doc = excluded.doc, dou = excluded.dou, argor = excluded.argor, min_basari_sirasi = excluded.min_basari_sirasi, ilce = excluded.ilce, kosul_kodlari = excluded.kosul_kodlari, yil = excluded.yil `); const kosulYaz = db.prepare( "INSERT INTO kosul (kod, metin) VALUES (?, ?) ON CONFLICT(kod) DO UPDATE SET metin = excluded.metin" ); let yazilan = 0; let eslesmeyen = 0; const tumKosullar = new Map(); db.transaction(() => { for (const r of kayitlar) { const id = String(r.kilavuzKodu ?? ""); if (!mevcut.has(id)) { eslesmeyen++; continue; } const { kodlar, metinler } = kosulKodlari(r); for (const [k, m] of metinler) tumKosullar.set(k, m); yaz.run( id, intOrNull(r.ucret), metinOrNull(r.bursOraniAdi), metinOrNull(r.ogrenimDiliAdi), metinOrNull(r.ogrenimTuruAdi), metinOrNull(r.akreditasyon), metinOrNull(r.uniAkreditasyon), intOrNull(r.prof), intOrNull(r.doc), intOrNull(r.dou), intOrNull(r.arGor), intOrNull(r.minBasariSirasi), metinOrNull(r.ilceAdi), kodlar.length ? kodlar.join(",") : null, yil ); yazilan++; } for (const [k, m] of tumKosullar) kosulYaz.run(k, m); metaYaz(db, "program_ozellik_yil", yil); metaYaz(db, "program_ozellik_tarih", new Date().toISOString().slice(0, 10)); })(); const ozet = db .prepare( `SELECT COUNT(*) AS n, SUM(ucret IS NOT NULL) AS ucret, SUM(burs_orani IS NOT NULL) AS burs, SUM(ogrenim_dili IS NOT NULL) AS dil, SUM(akreditasyon IS NOT NULL) AS akr, SUM(prof IS NOT NULL) AS prof, SUM(min_basari_sirasi IS NOT NULL) AS baraj, SUM(kosul_kodlari IS NOT NULL) AS kosul FROM program_ozellik` ) .get() as Record; db.pragma("wal_checkpoint(TRUNCATE)"); db.close(); console.log( `Yazıldı ${yazilan}, programs'ta olmayan ${eslesmeyen}, koşul metni ${tumKosullar.size}.\n` + `Doluluk: ${JSON.stringify(ozet)}` ); });