"""Gradio explorer — metehan777/cc-turkish-fulltext-CC-MAIN-2026-21 (index, not fulltext)"""
import gradio as gr
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
import pyarrow.compute as pc
from huggingface_hub import hf_hub_download
from urllib.parse import urlparse
import html as html_mod
import json, re, os
DATASET_ID = os.environ.get("HF_DATASET_ID", "metehan777/cc-turkish-fulltext-CC-MAIN-2026-21")
_cache = {}
_meta_cache = None
_lb_clean = None
_urls_table = None
_browse_tbl = None
_BLOCKED_PATTERN = (
r"casino|slots?|porn|porno|xxx|bahis|escort|rulet|roulette|jackpot|"
r"betting|gambl|seks-|sextube|adult|nsfw|hentai|viagra|cialis"
)
_BLOCKED_RE = re.compile(_BLOCKED_PATTERN, re.I)
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
:root {
--bg: #f8fafc; --bg2: #fff; --bg3: #f1f5f9; --border: #e2e8f0;
--text: #0f172a; --muted: #64748b; --accent: #dc2626; --accent-soft: #fef2f2;
--row-hover: #fff5f5; --th-bg: #f8fafc; --shadow: 0 1px 3px rgba(15,23,42,.06);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0c0e14; --bg2: #141824; --bg3: #1c2233; --border: #2a3347;
--text: #e8edf5; --muted: #8b9ab5; --accent: #f87171; --accent-soft: #2a1a1a;
--row-hover: #1f2433; --th-bg: #181d2a; --shadow: none;
}
}
body, .gradio-container {
background: var(--bg) !important; color: var(--text) !important;
font-family: 'Inter', system-ui, sans-serif !important; font-size: 13px !important;
}
.gradio-container { max-width: 1280px !important; margin: 0 auto !important; padding: 16px !important; }
.hero {
background: linear-gradient(135deg, var(--bg2) 0%, var(--bg3) 100%);
border: 1px solid var(--border); border-radius: 12px; padding: 20px 24px; margin-bottom: 16px;
box-shadow: var(--shadow);
}
.hero h1 { margin: 0 0 6px; font-size: 1.35rem !important; font-weight: 700 !important; color: var(--text) !important; }
.hero p { margin: 0; color: var(--muted); font-size: 0.85rem; line-height: 1.5; }
.hero a { color: var(--accent) !important; text-decoration: none; font-weight: 500; }
.stats-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
.stat-card {
background: var(--bg2); border: 1px solid var(--border); border-radius: 8px;
padding: 10px 14px; min-width: 120px; flex: 1;
}
.stat-card .lbl { font-size: 0.7rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.stat-card .val { font-size: 1.1rem; font-weight: 700; color: var(--text); margin-top: 2px; }
.toolbar {
background: var(--bg2) !important; border: 1px solid var(--border) !important;
border-radius: 10px !important; padding: 12px 14px !important; margin-bottom: 10px !important;
box-shadow: var(--shadow);
}
.toolbar label { font-size: 0.75rem !important; color: var(--muted) !important; font-weight: 600 !important; }
.toolbar input, .toolbar textarea, .toolbar .wrap select {
background: var(--bg3) !important; border: 1px solid var(--border) !important;
border-radius: 8px !important; color: var(--text) !important;
}
.toolbar input:focus { border-color: var(--accent) !important; box-shadow: 0 0 0 2px var(--accent-soft) !important; }
.toolbar button {
border-radius: 8px !important; font-weight: 600 !important; min-height: 38px !important;
}
.toolbar button.primary {
background: var(--accent) !important; color: #fff !important; border: none !important;
}
.toolbar button.secondary {
background: var(--bg3) !important; border: 1px solid var(--border) !important; color: var(--text) !important;
}
.page-bar {
display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 8px 4px; color: var(--muted); font-size: 0.82rem;
}
.page-bar b { color: var(--text); }
.tab-nav { border-bottom: 1px solid var(--border) !important; gap: 4px !important; }
.tab-nav button {
font-size: 0.85rem !important; font-weight: 500 !important; padding: 10px 16px !important;
border-radius: 8px 8px 0 0 !important;
}
.tab-nav button.selected { color: var(--accent) !important; border-bottom: 2px solid var(--accent) !important; }
.panel { background: var(--bg2); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; box-shadow: var(--shadow); }
.cc-table-wrap { overflow: auto; max-height: 62vh; border: 1px solid var(--border); border-radius: 10px; background: var(--bg2); }
.cc-table-wrap table { border-collapse: collapse; width: 100%; }
.cc-table-wrap thead th {
background: var(--th-bg); color: var(--muted); padding: 9px 12px; text-align: left;
font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: .03em;
border-bottom: 1px solid var(--border); white-space: nowrap;
}
.cc-table-wrap tbody td {
padding: 8px 12px; border-bottom: 1px solid var(--border); font-size: 0.82rem;
vertical-align: middle; line-height: 1.4;
}
.cc-table-wrap tbody tr:hover td { background: var(--row-hover) !important; }
.cc-table-wrap .c-num { white-space: nowrap; width: 1%; }
.cc-table-wrap .c-domain { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cc-table-wrap .c-url { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cc-table-wrap .c-url a { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
.badge {
display: inline-block; background: var(--bg3); border: 1px solid var(--border);
border-radius: 999px; padding: 2px 8px; font-size: 0.72rem; color: var(--muted);
}
.loading-box {
padding: 48px 20px; text-align: center; color: var(--muted);
background: var(--bg2); border: 1px dashed var(--border); border-radius: 10px;
}
.lb-frame .table-wrap { max-height: 62vh !important; overflow: auto !important; border-radius: 10px !important; }
.lb-frame table { font-size: 0.82rem !important; }
.lb-frame td, .lb-frame th { padding: 7px 10px !important; white-space: nowrap !important; }
.lb-frame td:last-child, .lb-frame th:last-child {
max-width: 360px !important; overflow: hidden !important; text-overflow: ellipsis !important;
}
a { color: var(--accent) !important; text-decoration: none; }
a.url-long { word-break: break-all; white-space: normal; }
a.url-clip { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-block; max-width: 100%; }
"""
def _is_blocked(text):
return bool(text and _BLOCKED_RE.search(str(text)))
def _is_pure_turkish(lang):
if lang is None or (isinstance(lang, float) and pd.isna(lang)):
return False
parts = [p.strip().lower() for p in str(lang).split(",") if p.strip()]
return parts == ["tur"]
def _load_json(fname):
if fname not in _cache:
try:
path = hf_hub_download(DATASET_ID, fname, repo_type="dataset")
_cache[fname] = json.load(open(path))
except Exception as e:
_cache[fname] = {"_error": str(e)}
return _cache[fname]
def get_meta():
global _meta_cache
if _meta_cache is None:
m = _load_json("turkish_index_summary.json")
_meta_cache = m if isinstance(m, dict) and "_error" not in m else {}
return _meta_cache
def _leaderboard_path():
if "lb:path" not in _cache:
for fname in (
"turkish_CC-MAIN-2026-21_domain_leaderboard_webgraph.parquet",
"turkish_CC-MAIN-2026-21_domain_leaderboard.parquet",
):
try:
_cache["lb:path"] = hf_hub_download(DATASET_ID, fname, repo_type="dataset")
_cache["lb:fname"] = fname
break
except Exception:
continue
if "lb:path" not in _cache:
_cache["lb:path"] = None
return _cache.get("lb:path")
def _prepare_leaderboard():
"""Bir kez yükle + spam filtrele — her istekte tekrar etme."""
global _lb_clean
if _lb_clean is not None:
return _lb_clean
path = _leaderboard_path()
if not path:
return None
cols = ["rank", "domain", "page_count", "sample_url", "hc_rank", "pr_rank", "tier"]
try:
table = pq.read_table(path)
have = [c for c in cols if c in table.column_names]
df = table.select(have).to_pandas()
for c in ("hc_rank", "pr_rank", "page_count", "rank"):
if c in df.columns:
df[c] = pd.to_numeric(df[c], errors="coerce").fillna(0).astype(int)
for c in df.columns:
if df[c].dtype == object:
df[c] = df[c].astype(str)
df = df[~df["domain"].map(_is_blocked)]
if "sample_url" in df.columns:
df = df[~df["sample_url"].map(_is_blocked)]
_lb_clean = df.reset_index(drop=True)
return _lb_clean
except Exception:
return None
def _urls_path():
if "urls:path" not in _cache:
try:
_cache["urls:path"] = hf_hub_download(
DATASET_ID, "turkish_CC-MAIN-2026-21_urls.parquet", repo_type="dataset"
)
except Exception:
_cache["urls:path"] = None
return _cache["urls:path"]
def _urls_total():
if "urls:total" not in _cache:
path = _urls_path()
_cache["urls:total"] = pq.read_metadata(path).num_rows if path else 0
return _cache["urls:total"]
def _url_domain(url):
try:
h = urlparse(url or "").netloc.lower()
return h[4:] if h.startswith("www.") else h
except Exception:
return ""
def _clip(s, n=72):
s = str(s or "")
return s if len(s) <= n else s[: n - 1] + "…"
def _hero_html():
meta = get_meta()
return f"""
🇹🇷 Common Crawl Türkçe Explorer
CC-MAIN-2026-21 · columnar index · tur · HTTP 200 ·
Dataset →
URL
{meta.get('total_urls', 0):,}
Domain
{meta.get('unique_domains', 0):,}
"""
def _page_bar(start, end, total, pg, total_pages, extra=""):
return (
f''
f'Gösterilen #{start:,}–{end:,} · toplam {total:,}'
f'Sayfa {pg:,} / {total_pages:,} {extra}'
f"
"
)
_COL_CLASS = {
"#": "c-num", "sayfa": "c-num", "tier": "c-num", "hc": "c-num", "pr": "c-num",
"lang": "c-num", "fetch": "c-num",
"domain": "c-domain", "örnek": "c-url", "url": "c-url",
}
_LB_COLS = ["Sıra", "Domain", "Sayfa", "Tier", "HC", "PR", "Örnek URL"]
def _rows_to_html(rows, cols, url_col="url", clip_url=False):
if not rows:
return 'Sonuç bulunamadı.
'
head = "".join(
f"{html_mod.escape(str(c))} | " for c in cols
)
body = ""
for r in rows:
cells = ""
for c in cols:
v = r.get(c, "")
cls = _COL_CLASS.get(c, "")
if c == url_col and v:
sv = str(v)
link_cls = "url-clip" if clip_url else "url-long"
cells += (
f"{html_mod.escape(_clip(sv) if clip_url else sv)} | "
)
else:
cells += (
f""
f"{html_mod.escape(str(v)) if v not in (None, '') else '—'} | "
)
body += f"{cells}
"
return f''
def show_domains(kw, per_page, page, sort_by):
df = _prepare_leaderboard()
if df is None or len(df) == 0:
return [], _page_bar(0, 0, 0, 1, 1), 1
if kw and str(kw).strip():
mask = df["domain"].str.contains(re.escape(str(kw).strip()), case=False, na=False)
df = df[mask].copy()
if sort_by == "Domain A→Z":
df = df.sort_values("domain", kind="stable")
elif sort_by == "HC ↑":
df = df.assign(_sort=df["hc_rank"].where(df["hc_rank"] > 0, 10**12))
df = df.sort_values(["_sort", "page_count"], ascending=[True, False], kind="stable").drop(columns="_sort")
elif sort_by == "PR ↑":
df = df.assign(_sort=df["pr_rank"].where(df["pr_rank"] > 0, 10**12))
df = df.sort_values(["_sort", "page_count"], ascending=[True, False], kind="stable").drop(columns="_sort")
else:
df = df.sort_values("page_count", ascending=False, kind="stable")
df = df.reset_index(drop=True)
pp = max(1, int(per_page))
total = len(df)
total_pages = max(1, (total + pp - 1) // pp)
pg = max(1, min(int(page or 1), total_pages))
start = (pg - 1) * pp
chunk = df.iloc[start:start + pp]
out_rows = []
for j, (_, r) in enumerate(chunk.iterrows()):
hc = int(r["hc_rank"]) if "hc_rank" in r.index and pd.notna(r["hc_rank"]) else 0
pr = int(r["pr_rank"]) if "pr_rank" in r.index and pd.notna(r["pr_rank"]) else 0
sample = _clip(r.get("sample_url", ""), 90)
out_rows.append([
start + j + 1,
r["domain"],
f"{int(r['page_count']):,}",
r.get("tier") or "—",
f"#{hc:,}" if hc > 0 else "—",
f"#{pr:,}" if pr > 0 else "—",
sample,
])
end = start + len(out_rows)
extra = 'filtreli' if kw and str(kw).strip() else ""
bar = _page_bar(start + 1 if out_rows else 0, end, total, pg, total_pages, extra)
return out_rows, bar, pg
def _load_urls_table():
"""Tüm URL parquet'ini belleğe BİR kez yükle (3 sütun)."""
global _urls_table
if _urls_table is not None:
return _urls_table
path = _urls_path()
if not path:
return None
try:
t = pq.read_table(path, columns=["url", "content_languages", "fetch_time"])
new_cols = []
for name in t.column_names:
col = t.column(name)
if pa.types.is_large_string(col.type):
col = pc.cast(col, pa.string())
new_cols.append(col)
_urls_table = pa.table(dict(zip(t.column_names, new_cols)))
_cache["urls:total"] = _urls_table.num_rows
return _urls_table
except Exception as e:
print(f"URL table load error: {e}", flush=True)
return None
def _browse_table():
"""content_languages == 'tur' ve spam olmayan satırlar — bir kez filtrele."""
global _browse_tbl
if _browse_tbl is not None:
return _browse_tbl
t = _load_urls_table()
if t is None:
return None
lang = pc.utf8_trim_whitespace(t.column("content_languages"))
pure = pc.equal(lang, "tur")
not_blocked = pc.invert(
pc.match_substring_regex(t.column("url"), _BLOCKED_PATTERN, ignore_case=True)
)
_browse_tbl = t.filter(pc.and_(pure, not_blocked))
return _browse_tbl
def _search_table(url_kw, domain_kw):
"""Arama sonucu pyarrow tablo — sorgu başına cache'lenir."""
key = f"search:{url_kw.strip().lower()}|{domain_kw.strip().lower()}"
if key in _cache:
return _cache[key]
t = _load_urls_table()
if t is None:
return None
url_col = t.column("url")
mask = pc.invert(pc.match_substring_regex(url_col, _BLOCKED_PATTERN, ignore_case=True))
if domain_kw.strip():
mask = pc.and_(mask, pc.match_substring(url_col, domain_kw.strip(), ignore_case=True))
if url_kw.strip():
mask = pc.and_(mask, pc.match_substring(url_col, url_kw.strip(), ignore_case=True))
res = t.filter(mask)
if len(_cache) > 40:
for k in [k for k in _cache if k.startswith("search:")][:20]:
_cache.pop(k, None)
_cache[key] = res
return res
def _table_rows(table, start, count):
"""Sadece sayfa dilimini Python'a çevir."""
sl = table.slice(start, count).to_pydict()
urls = sl.get("url", [])
langs = sl.get("content_languages", [])
fetches = sl.get("fetch_time", [])
rows = []
for i, u in enumerate(urls):
rows.append({
"url": u or "",
"domain": _url_domain(u),
"lang": langs[i] if i < len(langs) else "",
"fetch": str(fetches[i])[:19] if i < len(fetches) and fetches[i] else "",
})
return rows
def show_urls(kw, domain_kw, per_page, page):
pp = max(1, int(per_page))
has_filter = bool((kw or "").strip()) or bool((domain_kw or "").strip())
table = _search_table(kw or "", domain_kw or "") if has_filter else _browse_table()
if table is None:
return (
'URL listesi henüz hazır değil.
',
_page_bar(0, 0, 0, 1, 1),
1,
)
total = table.num_rows
total_pages = max(1, (total + pp - 1) // pp)
pg = max(1, min(int(page or 1), total_pages))
start = (pg - 1) * pp
rows = _table_rows(table, start, pp)
if has_filter:
extra = 'aramaspam gizli'
else:
extra = 'yalnızca turspam gizli'
end = start + len(rows)
html = _rows_to_html(rows, ["url", "domain", "lang", "fetch"], url_col="url", clip_url=True)
bar = _page_bar(start + 1 if rows else 0, end, total, pg, total_pages, extra)
return html, bar, pg
# ── Startup: veriyi Space açılmadan önce belleğe yükle ──────────────────────
print("Preloading Turkish explorer data…", flush=True)
try:
get_meta()
_prepare_leaderboard()
print(f"Leaderboard ready: {len(_lb_clean):,} domains", flush=True)
_browse_table()
if _browse_tbl is not None:
print(f"URL table ready: {_urls_table.num_rows:,} rows · "
f"{_browse_tbl.num_rows:,} pure-tur", flush=True)
except Exception as e:
print(f"Preload warning: {e}", flush=True)
SORT_OPTS = ["Sayfa ↓", "HC ↑", "PR ↑", "Domain A→Z"]
PAGE_OPTS = ["25", "50", "100"]
with gr.Blocks(title="CC Turkish Explorer", css=CSS, theme=gr.themes.Soft(primary_hue="red")) as demo:
gr.HTML(_hero_html())
with gr.Tabs():
with gr.Tab("🏆 Domainler", id="domains"):
with gr.Group(elem_classes=["toolbar"]):
with gr.Row(equal_height=True):
kwD = gr.Textbox(
label="Ara", placeholder="hurriyet, .tr, cumhuriyet…",
scale=5, container=True,
)
soD = gr.Dropdown(SORT_OPTS, value="Sayfa ↓", label="Sırala", scale=2)
ppD = gr.Dropdown(PAGE_OPTS, value="50", label="Sayfa başı", scale=1)
srD = gr.Button("Ara", variant="primary", scale=1, elem_classes=["primary"])
with gr.Row(equal_height=True):
prevD = gr.Button("◀ Önceki", scale=1, elem_classes=["secondary"])
nextD = gr.Button("Sonraki ▶", scale=1, elem_classes=["secondary"])
stD = gr.State(1)
barD = gr.HTML()
outD = gr.Dataframe(
headers=_LB_COLS,
datatype=["number"] + ["str"] * 6,
interactive=False,
wrap=False,
column_widths=["6%", "16%", "10%", "8%", "9%", "9%", "42%"],
elem_classes=["lb-frame"],
)
def _refresh_d(kw, pp, so, page):
rows, bar, pg = show_domains(kw, pp, page, so)
return rows, bar, pg
def _go_d(kw, pp, so):
return _refresh_d(kw, pp, so, 1)
def _page_d(kw, pp, so, delta, cur):
return _refresh_d(kw, pp, so, int(cur or 1) + delta)
d_in = [kwD, ppD, soD, stD]
demo.load(lambda: _refresh_d("", "50", "Sayfa ↓", 1), outputs=[outD, barD, stD])
srD.click(_go_d, [kwD, ppD, soD], [outD, barD, stD])
kwD.submit(_go_d, [kwD, ppD, soD], [outD, barD, stD])
soD.change(_go_d, [kwD, ppD, soD], [outD, barD, stD])
ppD.change(_go_d, [kwD, ppD, soD], [outD, barD, stD])
prevD.click(lambda k, p, s, n: _page_d(k, p, s, -1, n), d_in, [outD, barD, stD])
nextD.click(lambda k, p, s, n: _page_d(k, p, s, 1, n), d_in, [outD, barD, stD])
with gr.Tab("🔗 URL'ler", id="urls"):
with gr.Group(elem_classes=["toolbar"]):
with gr.Row(equal_height=True):
domU = gr.Textbox(label="Domain", placeholder="cumhuriyet.com.tr", scale=3)
kwU = gr.Textbox(label="URL metni", placeholder="opsiyonel", scale=3)
ppU = gr.Dropdown(PAGE_OPTS, value="50", label="Sayfa başı", scale=1)
srU = gr.Button("Ara", variant="primary", scale=1)
with gr.Row(equal_height=True):
prevU = gr.Button("◀ Önceki", scale=1, elem_classes=["secondary"])
nextU = gr.Button("Sonraki ▶", scale=1, elem_classes=["secondary"])
stU = gr.State(1)
barU = gr.HTML()
outU = gr.HTML()
def _refresh_u(kw, dom, pp, page):
html, bar, pg = show_urls(kw, dom, pp, page)
return html, bar, pg
def _go_u(kw, dom, pp):
return _refresh_u(kw, dom, pp, 1)
def _page_u(kw, dom, pp, delta, cur):
return _refresh_u(kw, dom, pp, int(cur or 1) + delta)
u_in = [kwU, domU, ppU, stU]
demo.load(lambda: _refresh_u("", "", "50", 1), outputs=[outU, barU, stU])
srU.click(_go_u, [kwU, domU, ppU], [outU, barU, stU])
domU.submit(_go_u, [kwU, domU, ppU], [outU, barU, stU])
kwU.submit(_go_u, [kwU, domU, ppU], [outU, barU, stU])
ppU.change(_go_u, [kwU, domU, ppU], [outU, barU, stU])
prevU.click(lambda k, d, p, n: _page_u(k, d, p, -1, n), u_in, [outU, barU, stU])
nextU.click(lambda k, d, p, n: _page_u(k, d, p, 1, n), u_in, [outU, barU, stU])
demo.launch()