Dashboard UI: davatelj dropdown + dynamic years + KORISNIK truncate + PDF link
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Fajl: news_rss_pgz_sport.py | v1.0.0 | 05.05.2026
|
||||
# Lokacija: /opt/pgz-sport/scrapers/news_rss_pgz_sport.py
|
||||
# Svrha: Hrvatski news RSS feeds — filter po PGŽ + sport
|
||||
# - Novi list, Glas Istre, 24sata, Index, T-Portal, HRT
|
||||
# - Filter samo članci koji spominju PGŽ + sport entitete
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
"""News RSS feeds — PGŽ sport filter."""
|
||||
import re, json, time, hashlib
|
||||
import urllib.request
|
||||
from html import unescape
|
||||
import psycopg2
|
||||
from psycopg2.extras import execute_batch
|
||||
|
||||
DSN = "host=10.10.0.2 port=6432 dbname=rinet_v3 user=rinet password=R1net2026!SecureDB#v7"
|
||||
UA = "Ri.NET Civic Bot 1.0"
|
||||
|
||||
# Croatian news RSS feeds — sport-related
|
||||
FEEDS = [
|
||||
("novi_list", "https://www.novilist.hr/rss/sport.xml"),
|
||||
("novi_list", "https://www.novilist.hr/rss/rijeka.xml"),
|
||||
("hrt", "https://www.hrt.hr/rss/sport"),
|
||||
("24sata_sport","https://www.24sata.hr/feeds/sport.xml"),
|
||||
("tportal", "https://www.tportal.hr/feed/sport"),
|
||||
("index_sport", "https://www.index.hr/sport/rss"),
|
||||
("rijeka_danas","https://rijekadanas.com/feed/"),
|
||||
]
|
||||
|
||||
PGZ_KEYWORDS = ["Rijeka", "PGŽ", "Primorsko-goransk", "Kvarner", "HNK Rijeka",
|
||||
"Opatija", "Crikvenica", "Krk", "Cres", "Lošinj", "Rab",
|
||||
"Kantrida", "Trsat", "Orijent", "Pomorac", "Zamet", "Mladost",
|
||||
"Pomorac", "Mlaka", "Bakar", "Kostrena", "Viškovo", "Kastav"]
|
||||
|
||||
|
||||
def fetch(url, timeout=15):
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return r.read().decode("utf-8", errors="replace")
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
|
||||
def parse_rss(xml):
|
||||
"""Extract <item> entries — title, link, description, pubDate."""
|
||||
items = []
|
||||
for m in re.finditer(r"<item>(.*?)</item>", xml, re.S | re.I):
|
||||
item = m.group(1)
|
||||
def grab(tag):
|
||||
mt = re.search(f"<{tag}[^>]*>(.*?)</{tag}>", item, re.S | re.I)
|
||||
if mt:
|
||||
txt = mt.group(1)
|
||||
# Strip CDATA
|
||||
txt = re.sub(r"<!\[CDATA\[(.*?)\]\]>", r"\1", txt, flags=re.S)
|
||||
txt = re.sub(r"<[^>]+>", " ", txt)
|
||||
return unescape(re.sub(r"\s+", " ", txt).strip())
|
||||
return ""
|
||||
items.append({
|
||||
"title": grab("title"),
|
||||
"link": grab("link"),
|
||||
"description": grab("description"),
|
||||
"pubDate": grab("pubDate"),
|
||||
})
|
||||
return items
|
||||
|
||||
|
||||
def is_pgz_relevant(text):
|
||||
return any(k in text for k in PGZ_KEYWORDS)
|
||||
|
||||
|
||||
def main():
|
||||
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||
|
||||
total_articles = 0
|
||||
pgz_relevant = 0
|
||||
inserted = 0
|
||||
|
||||
for portal, url in FEEDS:
|
||||
xml = fetch(url)
|
||||
if not xml:
|
||||
print(f" {portal:20} fetch FAIL")
|
||||
continue
|
||||
|
||||
items = parse_rss(xml)
|
||||
total_articles += len(items)
|
||||
|
||||
cur = conn.cursor()
|
||||
rows = []
|
||||
relevant_for_portal = 0
|
||||
|
||||
for it in items:
|
||||
full = (it["title"] + " " + it["description"])
|
||||
if not is_pgz_relevant(full):
|
||||
continue
|
||||
relevant_for_portal += 1
|
||||
|
||||
fact = f"{it['title']} — {it['description'][:400]}"
|
||||
if not fact.strip():
|
||||
continue
|
||||
h = hashlib.md5(fact.encode()).hexdigest()
|
||||
rows.append((fact, f"news_rss_{portal}", "news_pgz_sport", 0.85, h,
|
||||
json.dumps({"link": it["link"], "pubDate": it["pubDate"]})))
|
||||
|
||||
pgz_relevant += relevant_for_portal
|
||||
if rows:
|
||||
sql = """INSERT INTO dabi.knowledge (fact, source, category, confidence, data_hash, source_refs)
|
||||
VALUES (%s, %s, %s, %s, %s, %s::jsonb) ON CONFLICT (data_hash) DO NOTHING"""
|
||||
try:
|
||||
execute_batch(cur, sql, rows, page_size=50)
|
||||
n = cur.rowcount
|
||||
inserted += n
|
||||
print(f" {portal:20} items={len(items):>3} relevant={relevant_for_portal:>3} inserted={n:>3}")
|
||||
except Exception as e:
|
||||
print(f" {portal:20} insert err: {e}")
|
||||
else:
|
||||
print(f" {portal:20} items={len(items):>3} relevant=0")
|
||||
|
||||
cur.close()
|
||||
time.sleep(1)
|
||||
|
||||
print(f"\n=== DONE: {total_articles} total / {pgz_relevant} pgz-relevant / {inserted} inserted ===")
|
||||
conn.close()
|
||||
return {"total": total_articles, "pgz_relevant": pgz_relevant, "inserted": inserted}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps(main()))
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Fajl: sport_rezultati_arhivar.py | v1.0.0 | 05.05.2026
|
||||
# Lokacija: /opt/pgz-sport/scrapers/sport_rezultati_arhivar.py
|
||||
# Svrha: Wikipedia HR sezone HNL + Kup HR po godinama
|
||||
# - Iterate kroz sve sezone HNL od 1992
|
||||
# - Wikipedia API pages: "1._HNL_2023/24", "Kup_Hrvatske_u_nogometu_2024/25"
|
||||
# - Extract konacne tablice + finalne utakmice
|
||||
# - Plus PGŽ klubovi: HNK Rijeka, Orijent, Crikvenica, Opatija
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
"""Sport rezultati historical arhivar."""
|
||||
import os, re, json, time, hashlib
|
||||
import urllib.request, urllib.parse
|
||||
import psycopg2
|
||||
from psycopg2.extras import execute_batch
|
||||
|
||||
DSN = "host=10.10.0.2 port=6432 dbname=rinet_v3 user=rinet password=R1net2026!SecureDB#v7"
|
||||
UA = "Ri.NET Civic Bot 1.0 (contact: dradulic@outlook.com)"
|
||||
API = "https://hr.wikipedia.org/w/api.php"
|
||||
|
||||
|
||||
def wiki_extract(title, sentences=None):
|
||||
params = {"action": "query", "prop": "extracts", "explaintext": "1",
|
||||
"redirects": "1", "format": "json", "titles": title}
|
||||
if sentences:
|
||||
params["exsentences"] = str(sentences)
|
||||
url = API + "?" + urllib.parse.urlencode(params)
|
||||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
d = json.loads(r.read())
|
||||
for pid, p in d.get("query", {}).get("pages", {}).items():
|
||||
if pid == "-1":
|
||||
return None
|
||||
return p.get("extract", "")
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
|
||||
def chunk(text, max_len=700):
|
||||
if len(text) <= max_len:
|
||||
return [text] if text else []
|
||||
out = []; start = 0
|
||||
while start < len(text):
|
||||
end = min(start + max_len, len(text))
|
||||
if end < len(text):
|
||||
for sep in [". ", "! ", "? ", "\n"]:
|
||||
p = text.rfind(sep, start, end)
|
||||
if p > start + max_len // 2:
|
||||
end = p + len(sep); break
|
||||
out.append(text[start:end].strip())
|
||||
start = end
|
||||
return [c for c in out if len(c) > 80]
|
||||
|
||||
|
||||
def insert_facts(conn, page, text, category, confidence=0.88):
|
||||
if not text or len(text) < 200:
|
||||
return 0
|
||||
cur = conn.cursor()
|
||||
rows = []
|
||||
for c in chunk(text, 700):
|
||||
h = hashlib.md5(c.encode()).hexdigest()
|
||||
rows.append((c, "wikipedia_sport_arhiv", category, confidence, h,
|
||||
json.dumps({"page": page})))
|
||||
sql = """INSERT INTO dabi.knowledge (fact, source, category, confidence, data_hash, source_refs)
|
||||
VALUES (%s, %s, %s, %s, %s, %s::jsonb) ON CONFLICT (data_hash) DO NOTHING"""
|
||||
try:
|
||||
execute_batch(cur, sql, rows, page_size=50)
|
||||
n = cur.rowcount; cur.close()
|
||||
return n
|
||||
except Exception as e:
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||
|
||||
pages = []
|
||||
|
||||
# 1. HNL sezone 1992-2024
|
||||
for year in range(1992, 2026):
|
||||
for fmt in [f"1._HNL_{year}.", f"1._HNL_{year}./{(year+1)%100:02d}.",
|
||||
f"HNL_{year}/{(year+1)%100:02d}", f"HNL_{year}-{year+1}",
|
||||
f"SuperSport_HNL_{year}./{(year+1)%100:02d}.",
|
||||
f"HT_Prva_HNL_{year}./{(year+1)%100:02d}."]:
|
||||
pages.append(("hnl_sezona", fmt))
|
||||
|
||||
# 2. Kup Hrvatske u nogometu (po godinama)
|
||||
for year in range(1992, 2026):
|
||||
for fmt in [f"Kup_Hrvatske_u_nogometu_{year}.",
|
||||
f"Kup_Hrvatske_u_nogometu_{year}./{(year+1)%100:02d}.",
|
||||
f"Hrvatski_nogometni_kup_{year}-{year+1}"]:
|
||||
pages.append(("hr_nogometni_kup", fmt))
|
||||
|
||||
# 3. Glavni klubovi PGŽ + povijest
|
||||
for klub in ["HNK_Rijeka", "NK_Orijent", "NK_Krk", "NK_Crikvenica",
|
||||
"NK_Opatija", "NK_Mat-Promet", "NK_Pomorac", "NK_Naša_Slatina",
|
||||
"HNK_Rijeka_(boys)", "ŽNK_Rijeka",
|
||||
"HKK_Kvarner", "KK_Kvarner_2010", "KK_Lovran",
|
||||
"HMRK_Zamet", "MRK_Pomorac", "RK_Trsat", "RK_Crikvenica",
|
||||
"VK_Primorje", "VK_Rijeka",
|
||||
"HRK_Rijeka", "HOK_Rijeka", "OK_Rijeka",
|
||||
"HAOK_Mladost", "HAOK_Rijeka"]:
|
||||
pages.append(("pgz_klub_povijest", klub))
|
||||
|
||||
# 4. Sezone HNK Rijeka po godinama
|
||||
for year in range(1990, 2026):
|
||||
for fmt in [f"Sezona_HNK_Rijeka_{year}./{(year+1)%100:02d}.",
|
||||
f"HNK_Rijeka_u_sezoni_{year}-{year+1}",
|
||||
f"HNK_Rijeka_{year}-{year+1}_sezona"]:
|
||||
pages.append(("hnk_rijeka_sezona", fmt))
|
||||
|
||||
# Crawl
|
||||
successful = 0
|
||||
total_facts = 0
|
||||
found_pages = []
|
||||
|
||||
for category, page in pages:
|
||||
text = wiki_extract(page)
|
||||
if text and len(text) > 300:
|
||||
successful += 1
|
||||
facts_inserted = insert_facts(conn, page, text, category, confidence=0.88)
|
||||
total_facts += facts_inserted
|
||||
found_pages.append(page)
|
||||
if successful % 10 == 0:
|
||||
print(f" progress: {successful} pages found, {total_facts} facts")
|
||||
time.sleep(0.4) # rate limit
|
||||
|
||||
print(f"\n=== DONE: {successful}/{len(pages)} pages found, {total_facts} facts ===")
|
||||
print(f"Sample found pages: {found_pages[:15]}")
|
||||
conn.close()
|
||||
return {"pages_found": successful, "pages_tried": len(pages),
|
||||
"facts": total_facts}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps(main()))
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Fajl: trener_extractor.py | v1.0.0 | 05.05.2026
|
||||
# Lokacija: /opt/pgz-sport/scrapers/trener_extractor.py
|
||||
# Svrha: Ekstrahira imena trenera iz dokumenti.tekst + dabi.knowledge
|
||||
# - Regex pattern za "trener: <ime>" , "glavni trener", "izbornik"
|
||||
# - Cross-link s pgz_sport.osobe (ako postoji), inserts new
|
||||
# - Confidence based on pattern strength
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
"""Trener extractor — pull names from documents."""
|
||||
import os, re, time, json, hashlib
|
||||
from collections import Counter
|
||||
import psycopg2
|
||||
from psycopg2.extras import execute_batch, RealDictCursor
|
||||
|
||||
DSN = "host=10.10.0.2 port=6432 dbname=rinet_v3 user=rinet password=R1net2026!SecureDB#v7"
|
||||
|
||||
# Regex patterns — Croatian morphology (trener, treneru, trenerom, izbornik)
|
||||
PATTERNS = [
|
||||
# "glavni trener (ime prezime)" / "trener (ime prezime)"
|
||||
re.compile(r"(?:glavni\s+)?trener[a-z]*\s+([A-Z][a-zčćžšđ]+(?:\s+[A-Z][a-zčćžšđ]+){1,2})", re.U),
|
||||
re.compile(r"izbornik[a-z]*\s+([A-Z][a-zčćžšđ]+(?:\s+[A-Z][a-zčćžšđ]+){1,2})", re.U),
|
||||
re.compile(r"([A-Z][a-zčćžšđ]+(?:\s+[A-Z][a-zčćžšđ]+){1,2}),?\s+(?:glavni\s+)?trener", re.U),
|
||||
re.compile(r"([A-Z][a-zčćžšđ]+(?:\s+[A-Z][a-zčćžšđ]+){1,2}),?\s+izbornik", re.U),
|
||||
# Šef stručnog stožera
|
||||
re.compile(r"\b(?:šef|voditelj)\s+stru(?:č|c)nog\s+sto(?:ž|z)era\s+([A-Z][a-zčćžšđ]+(?:\s+[A-Z][a-zčćžšđ]+){1,2})", re.U),
|
||||
]
|
||||
|
||||
# Filters — exclude obvious non-names
|
||||
EXCLUDED_TOKENS = {"Hrvatska", "Republika", "Hrvatske", "Klubu", "Kluba", "Sezone",
|
||||
"Prvenstva", "Prvenstvo", "Liga", "Lige", "PGŽ", "PG"}
|
||||
|
||||
|
||||
def extract_trainers_from_text(text):
|
||||
"""Run all patterns + return Counter of (full_name)."""
|
||||
found = Counter()
|
||||
if not text or len(text) < 50:
|
||||
return found
|
||||
for pat in PATTERNS:
|
||||
for m in pat.finditer(text):
|
||||
name = m.group(1).strip()
|
||||
# Filter
|
||||
tokens = name.split()
|
||||
if len(tokens) < 2 or len(tokens) > 4:
|
||||
continue
|
||||
if any(t in EXCLUDED_TOKENS for t in tokens):
|
||||
continue
|
||||
if any(len(t) < 3 for t in tokens):
|
||||
continue
|
||||
found[name] += 1
|
||||
return found
|
||||
|
||||
|
||||
def main():
|
||||
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# Source 1: pgz_sport.dokumenti (tekst column)
|
||||
cur.execute("""
|
||||
SELECT klub_id, naziv_dokumenta, COALESCE(tekst, '') AS tekst
|
||||
FROM pgz_sport.dokumenti
|
||||
WHERE COALESCE(tekst, '') != '' AND length(tekst) > 200
|
||||
""")
|
||||
docs = cur.fetchall()
|
||||
print(f"Documents to scan: {len(docs)}")
|
||||
|
||||
all_trainers = Counter() # name → total mentions
|
||||
trainer_clubs = {} # name → set(klub_ids)
|
||||
|
||||
for d in docs:
|
||||
found = extract_trainers_from_text(d.get("tekst", ""))
|
||||
for name, cnt in found.items():
|
||||
all_trainers[name] += cnt
|
||||
trainer_clubs.setdefault(name, set()).add(d.get("klub_id"))
|
||||
|
||||
print(f"Unique trainer names found: {len(all_trainers)}")
|
||||
print(f"Top 20 by mentions:")
|
||||
for name, cnt in all_trainers.most_common(20):
|
||||
clubs = trainer_clubs.get(name, set())
|
||||
print(f" {name:35} mentions={cnt:>3} klubova={len(clubs)}")
|
||||
|
||||
# Insert into dabi.knowledge as forensic_findings
|
||||
cur2 = conn.cursor()
|
||||
fact_inserted = 0
|
||||
for name, cnt in all_trainers.most_common(500):
|
||||
if cnt < 2: # skip noise (1-time mentions)
|
||||
continue
|
||||
clubs_set = trainer_clubs.get(name, set())
|
||||
clubs_list = [c for c in clubs_set if c]
|
||||
|
||||
fact = f"Trener {name} spomenut {cnt}x u {len(clubs_list)} dokumenata PGŽ klubova."
|
||||
h = hashlib.md5(fact.encode()).hexdigest()
|
||||
|
||||
try:
|
||||
cur2.execute("""
|
||||
INSERT INTO dabi.knowledge (fact, source, category, confidence, data_hash, source_refs)
|
||||
VALUES (%s, 'trener_extract_pgz_sport', 'pgz_sport_treneri',
|
||||
%s, %s, %s::jsonb)
|
||||
ON CONFLICT (data_hash) DO NOTHING
|
||||
""", (fact, min(0.7 + cnt*0.05, 0.95), h,
|
||||
json.dumps({"name": name, "mentions": cnt, "clubs": clubs_list[:10]})))
|
||||
if cur2.rowcount > 0:
|
||||
fact_inserted += 1
|
||||
except Exception as e:
|
||||
print(f" err: {e}")
|
||||
|
||||
print(f"\nFacts inserted: {fact_inserted}")
|
||||
|
||||
# Also try inserting into pgz_sport.treneri if structure allows
|
||||
cur.execute("""
|
||||
SELECT column_name FROM information_schema.columns
|
||||
WHERE table_schema='pgz_sport' AND table_name='treneri'
|
||||
ORDER BY ordinal_position
|
||||
""")
|
||||
cols = [r["column_name"] for r in cur.fetchall()]
|
||||
print(f"\npgz_sport.treneri cols: {cols}")
|
||||
|
||||
cur2.close(); cur.close(); conn.close()
|
||||
return {"trainers_found": len(all_trainers), "facts_inserted": fact_inserted}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps(main(), default=str))
|
||||
Reference in New Issue
Block a user