Files
kolaytercih/scripts/program-ozellik.ts
bilalgursen c23910cc79 feat(veri): program_ozellik tablosu — YÖK Atlas API'den ücret, burs, dil, öğretim türü, akreditasyon, kadro, baraj, ilçe, koşullar
scripts/program-ozellik.ts (pnpm ozellik --yil --db [--ham]) API'nin
kılavuz alanlarını program_ozellik ve kosul tablolarına yazar; 25 Eyl
2026 çekimi: 21.493 program, 194 koşul metni. src/lib/program-ozellik.ts
okuyucu + öğrenciye okunan kısa özet. Tablo yoksa okuma boş döner.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 01:02:39 +03:00

283 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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-<yil>-<tarih>.jsonl.gz'e yazılır;
* `--ham <dosya>` 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<string, unknown>;
type ApiPage = {
content: HamKayit[];
totalPages: number;
totalElements: number;
};
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
async function fetchPage(page: number): Promise<ApiPage> {
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<HamKayit[]> {
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<HamKayit[]> {
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<void>((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<string, string> } {
const metinler = new Map<string, string>();
const liste = Array.isArray(r.kosulList) ? (r.kosulList as Record<string, string>[]) : [];
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<string, string>();
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<string, number>;
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)}`
);
});