nordabiz/scripts/fix_google_news_sources.py
Maciej Pienczyn c13ad09e3a feat(zopk): Skrypt do naprawy źródeł newsów z Google News
Problem: Newsy z Google News RSS miały source_domain='news.google.com'
i favicon Google zamiast prawdziwego źródła.

Rozwiązanie: Nowy skrypt fix_google_news_sources.py który:
- Wyciąga nazwę źródła z tytułu (po " - ")
- Mapuje 59 źródeł na ich prawdziwe domeny
- Aktualizuje source_domain i image_url (favicon)

Wynik: 143/143 newsów zaktualizowanych z poprawnymi źródłami.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 08:06:40 +01:00

221 lines
7.0 KiB
Python
Raw Permalink 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.

#!/usr/bin/env python3
"""
Skrypt do naprawy źródeł newsów z Google News.
Problem: Newsy z Google News RSS mają source_domain='news.google.com'
i favicon Google zamiast prawdziwego źródła.
Rozwiązanie: Wyciągnij nazwę źródła z tytułu (po " - ") i zaktualizuj:
- source_domain na prawdziwą domenę
- image_url na favicon prawdziwej domeny
Użycie:
python scripts/fix_google_news_sources.py --dry-run # Test
python scripts/fix_google_news_sources.py # Produkcja
"""
import os
import sys
import argparse
# Dodaj ścieżkę projektu
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, PROJECT_ROOT)
from dotenv import load_dotenv
load_dotenv(os.path.join(PROJECT_ROOT, '.env'))
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
DATABASE_URL = os.getenv('DATABASE_URL')
if not DATABASE_URL:
print("❌ Błąd: Brak zmiennej DATABASE_URL w .env")
sys.exit(1)
# Mapowanie nazw źródeł na domeny
# Klucz: nazwa źródła z tytułu (po " - ")
# Wartość: domena do użycia w favicon URL
SOURCE_TO_DOMAIN = {
# Portale z .pl w nazwie - użyj bezpośrednio
"Bankier.pl": "bankier.pl",
"Bizblog.pl": "bizblog.pl",
"Bydgoszcz.Wyborcza.pl": "bydgoszcz.wyborcza.pl",
"CIRE.pl": "cire.pl",
"GazetaPrawna.pl": "gazetaprawna.pl",
"GospodarkaMorska.pl": "gospodarkamorska.pl",
"Gov.pl": "gov.pl",
"Gramwzielone.pl": "gramwzielone.pl",
"Green-news.pl": "green-news.pl",
"Inzynieria.com": "inzynieria.com",
"Money.pl": "money.pl",
"PolsatNews.pl": "polsatnews.pl",
"Trojmiasto.pl": "trojmiasto.pl",
"ekoszalin.pl": "ekoszalin.pl",
"enerad.pl": "enerad.pl",
"naTemat.pl": "natemat.pl",
"polskieradio.pl": "polskieradio.pl",
"trojmiasto.wyborcza.pl": "trojmiasto.wyborcza.pl",
"wnp.pl": "wnp.pl",
"www.wejherowo.pl": "wejherowo.pl",
"xyz.pl": "xyz.pl",
# Portale biznesowe
"Biznes Interia": "biznes.interia.pl",
"Business Insider Polska": "businessinsider.com.pl",
"Forbes": "forbes.pl",
"Forsal": "forsal.pl",
"Newsweek": "newsweek.pl",
"Obserwator Finansowy": "obserwatorfinansowy.pl",
"Rzeczpospolita": "rp.pl",
"wGospodarce": "wgospodarce.pl",
"Strefa Biznesu": "strefabiznesu.pl",
# Portale branżowe
"Defence24": "defence24.pl",
"Energetyka24": "energetyka24.com",
"GlobEnergia": "globenergia.pl",
"Investmap": "investmap.pl",
"Portal Morski": "portalmorski.pl",
"Portal Obronny": "portalobronny.pl",
"Portal Samorządowy": "portalsamorzadowy.pl",
"Polska Morska": "polska-morska.pl",
"Rynek Infrastruktury": "rynekinfrastruktury.pl",
"Top-Oze": "top-oze.pl",
"FOCUS ON Business": "focusonbusiness.eu",
# Regionalne
"Dziennik Bałtycki": "dziennikbaltycki.pl",
"Głos Pomorza": "gp24.pl",
"Kaszuby24": "kaszuby24.pl",
"Nadmorski24": "nadmorski24.pl",
"Portal Kujawski": "portalkujawski.pl",
"Pracodawcy Pomorza": "pracodawcypomorza.pl",
"Rumia naturalnie pomysłowa": "rumia.eu",
"Tygodnik Bydgoski": "tygodnikbydgoski.pl",
"Zawsze Pomorze": "zawszepomorze.pl",
# Radio i TV
"Radio Gdańsk": "radiogdansk.pl",
"Radio Weekend FM": "weekendfm.pl",
"Polskie Radio 24": "polskieradio24.pl",
"Polskie Radio Koszalin": "prkoszalin.pl",
"TVP Gdańsk": "gdansk.tvp.pl",
"TVP Bydgoszcz": "bydgoszcz.tvp.pl",
"TVP Info": "tvp.info",
# Inne
"Polska Agencja Prasowa SA": "pap.pl",
"OKO.press": "oko.press",
}
def get_domain_favicon(domain: str) -> str:
"""Zwróć URL favicona przez Google API."""
return f"https://www.google.com/s2/favicons?domain={domain}&sz=128"
def extract_source_from_title(title: str) -> str | None:
"""Wyciągnij źródło z tytułu (po ostatnim ' - ')."""
if ' - ' not in title:
return None
return title.rsplit(' - ', 1)[-1].strip()
def main():
parser = argparse.ArgumentParser(description='Napraw źródła newsów z Google News')
parser.add_argument('--dry-run', action='store_true', help='Tryb testowy - nie zapisuj')
parser.add_argument('--limit', type=int, default=None, help='Limit newsów')
args = parser.parse_args()
print("=" * 70)
print("Google News Source Fixer")
print("=" * 70)
if args.dry_run:
print("🔍 TRYB TESTOWY - zmiany NIE będą zapisane\n")
engine = create_engine(DATABASE_URL)
Session = sessionmaker(bind=engine)
session = Session()
try:
from database import ZOPKNews
# Pobierz newsy z Google News z favicon
query = session.query(ZOPKNews).filter(
ZOPKNews.status.in_(['approved', 'auto_approved']),
ZOPKNews.source_domain == 'news.google.com',
ZOPKNews.image_url.like('%s2/favicons%')
).order_by(ZOPKNews.published_at.desc())
if args.limit:
query = query.limit(args.limit)
news_items = query.all()
print(f"📰 Znaleziono {len(news_items)} newsów do przetworzenia\n")
stats = {
'processed': 0,
'mapped': 0,
'unknown': 0,
'no_pattern': 0
}
unknown_sources = set()
for i, news in enumerate(news_items, 1):
source_name = extract_source_from_title(news.title)
if not source_name:
stats['no_pattern'] += 1
print(f"[{i}] ⚠ Brak wzorca ' - ' w tytule: {news.title[:50]}...")
continue
domain = SOURCE_TO_DOMAIN.get(source_name)
if domain:
stats['processed'] += 1
stats['mapped'] += 1
favicon_url = get_domain_favicon(domain)
if not args.dry_run:
news.source_domain = domain
news.image_url = favicon_url
session.commit()
print(f"[{i}] ✓ {source_name}{domain}")
else:
print(f"[{i}] [DRY-RUN] {source_name}{domain}")
else:
stats['unknown'] += 1
unknown_sources.add(source_name)
print(f"[{i}] ✗ Nieznane źródło: {source_name}")
print("\n" + "=" * 70)
print("PODSUMOWANIE")
print("=" * 70)
print(f"Przetworzono: {stats['processed']}")
print(f" - Zmapowane: {stats['mapped']}")
print(f" - Nieznane źródła: {stats['unknown']}")
print(f" - Brak wzorca w tytule: {stats['no_pattern']}")
if unknown_sources:
print(f"\n⚠ Nieznane źródła ({len(unknown_sources)}) - dodaj do SOURCE_TO_DOMAIN:")
for src in sorted(unknown_sources):
print(f' "{src}": "",')
if args.dry_run:
print("\n⚠️ To był tryb testowy. Uruchom bez --dry-run aby zapisać.")
except Exception as e:
print(f"❌ Błąd: {e}")
import traceback
traceback.print_exc()
session.rollback()
finally:
session.close()
if __name__ == '__main__':
main()