Files
BSN-Chatsystem/scrape_gladbach.py
T

302 lines
11 KiB
Python
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.
#!/usr/bin/env python3
"""Scrape Gladbach-focused football news from German sources."""
import requests
from bs4 import BeautifulSoup
import json
import time
import re
HEADERS = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7',
}
GLADBACH_KW = [
'gladbach', 'borussia', 'mönchengladbach', 'fohlen', 'fohlenelf',
'borussia park', 'niederrhein', 'borussen',
'elvedi', 'plea', 'honorat', 'neuhaus', 'weigl', 'koné',
'scally', 'oultara', 'friedrich', 'itakura', 'ngoumou',
'cvančara', 'civancara', 'ranos', 'hack',
'reitz', 'netz', 'wöber', 'woeber', 'kleindienst',
'seoane', 'virkus',
]
def fetch_url(url, retries=2):
for attempt in range(retries + 1):
try:
resp = requests.get(url, headers=HEADERS, timeout=20)
resp.raise_for_status()
resp.encoding = resp.apparent_encoding or 'utf-8'
return resp
except Exception as e:
if attempt < retries:
time.sleep(2)
else:
print(f" FAILED: {url}{e}")
return None
def clean_text(text):
return re.sub(r'\s+', ' ', text).strip()
def is_recent_date(date_str):
patterns = [
r'01\.06\.2026', r'02\.06\.2026',
r'1\.\s*Juni\s*2026', r'2\.\s*Juni\s*2026',
r'01\.06\.26', r'02\.06\.26',
r'2026-06-01', r'2026-06-02',
r'vor \d+ Stunden', r'vor \d+ Minuten',
]
for pat in patterns:
if re.search(pat, date_str, re.IGNORECASE):
return True
return False
def has_gladbach_keyword(text, min_hits=1):
text_lower = text.lower()
hits = sum(1 for kw in GLADBACH_KW if kw in text_lower)
return hits >= min_hits
def extract_article_text(soup):
selectors = [
'article', '.article-content', '.article-body', '.post-content',
'.entry-content', '.content', '.story-body', '.article__body',
'.article-text', '.news-text', 'main', '.main-content',
'[itemprop="articleBody"]', '.article__content', '.post__content',
'.single-content', '#article-content',
]
for sel in selectors:
elem = soup.select_one(sel)
if elem:
for unwanted in elem.find_all(['script', 'style', 'nav', 'aside', 'footer', 'header', 'iframe', 'form']):
unwanted.decompose()
text = elem.get_text(separator=' ', strip=True)
text = clean_text(text)
if len(text) > 200:
return text
paragraphs = soup.find_all('p')
texts = [clean_text(p.get_text()) for p in paragraphs if len(p.get_text().strip()) > 50]
if texts:
return ' '.join(texts)
body = soup.find('body')
if body:
for unwanted in body.find_all(['script', 'style', 'nav', 'footer', 'header', 'iframe']):
unwanted.decompose()
return clean_text(body.get_text(separator=' ', strip=True))
return ""
def extract_date(soup):
date_patterns = [
('meta[property="article:published_time"]', 'content'),
('meta[name="date"]', 'content'),
('meta[name="pubdate"]', 'content'),
('time', 'datetime'),
('time[datetime]', 'datetime'),
]
for selector, attr in date_patterns:
elem = soup.select_one(selector)
if elem:
val = elem.get(attr, '') if attr else elem.get_text(strip=True)
if val and len(val) > 5:
return val
text = soup.get_text()
m = re.search(r'(\d{1,2}\.\s*\d{1,2}\.\s*(?:20)?2[456])', text)
return m.group(1) if m else ""
def generic_scrape(name, base_urls, url_filter=None, title_cleanup=None, gladbach_check=True):
"""Generic scraper for a news site."""
print(f"\n{'='*60}\nSCRAPING: {name}\n{'='*60}")
results = []
for base_url in base_urls:
print(f" Trying: {base_url}")
resp = fetch_url(base_url)
if not resp:
continue
soup = BeautifulSoup(resp.text, 'html.parser')
links = soup.find_all('a', href=True)
article_urls = set()
for link in links:
href = link['href']
if not href or href.startswith('#') or href.startswith('javascript:') or href.startswith('mailto:'):
continue
if href.endswith(('.jpg', '.png', '.gif', '.pdf', '.css', '.js', '.ico')):
continue
# Build full URL
if href.startswith('http'):
full_url = href
elif href.startswith('//'):
full_url = 'https:' + href
elif href.startswith('/'):
# Extract domain from base_url
domain = re.match(r'https?://[^/]+', base_url)
if domain:
full_url = domain.group(0) + href
else:
continue
else:
continue
if full_url in article_urls:
continue
# Apply filter if provided
if url_filter:
if url_filter(full_url):
article_urls.add(full_url)
else:
article_urls.add(full_url)
print(f" Found {len(article_urls)} potential article URLs (sampling first 30)")
for art_url in list(article_urls)[:30]:
print(f" Fetching: {art_url}")
art_resp = fetch_url(art_url)
if not art_resp:
continue
art_soup = BeautifulSoup(art_resp.text, 'html.parser')
# Extract title
title = None
title_elem = art_soup.find('title')
if title_elem:
title = clean_text(title_elem.get_text())
if title_cleanup:
title = title_cleanup(title)
if not title or len(title) < 15:
h1 = art_soup.find('h1')
if h1:
title = clean_text(h1.get_text())
if not title or len(title) < 15:
continue
date_str = extract_date(art_soup)
full_text = extract_article_text(art_soup)
if not full_text or len(full_text) < 100:
print(f" Skipping (no usable text)")
continue
if date_str and not is_recent_date(date_str):
print(f" Skipping (date: {date_str})")
continue
if gladbach_check:
combined = (title + ' ' + full_text).lower()
if not has_gladbach_keyword(combined, min_hits=1):
print(f" Not Gladbach-related, skipping")
continue
print(f" ✓ FOUND: {title[:80]}")
results.append({
'source': name,
'title': title,
'url': art_url,
'date': date_str or 'Kein Datum gefunden',
'full_text': full_text[:3000],
})
if len(results) >= 2:
break
if results:
break
return results
def main():
all_results = []
# 1. torfabrik.de
all_results.extend(generic_scrape(
'torfabrik.de',
['https://www.torfabrik.de/', 'https://www.torfabrik.de/borussia-moenchengladbach/'],
url_filter=lambda u: 'torfabrik.de' in u,
title_cleanup=lambda t: re.sub(r'\s*[-|]\s*[Tt]orfabrik.*$', '', t).strip(),
))
# 2. fohlen-hautnah.de
all_results.extend(generic_scrape(
'fohlen-hautnah.de',
['https://www.fohlen-hautnah.de/'],
url_filter=lambda u: 'fohlen-hautnah.de' in u,
title_cleanup=lambda t: re.sub(r'\s*[-|]\s*[Ff]ohlen[-\s]*[Hh]autnah.*$', '', t).strip(),
gladbach_check=False, # All articles are Gladbach-related
))
# 3. gladbachlive.de
all_results.extend(generic_scrape(
'gladbachlive.de',
['https://www.gladbachlive.de/', 'https://www.gladbachlive.de/news/'],
url_filter=lambda u: 'gladbachlive.de' in u,
title_cleanup=lambda t: re.sub(r'\s*[-|]\s*[Gg]ladbach[Ll]ive.*$', '', t).strip(),
))
# 4. kicker.de
all_results.extend(generic_scrape(
'kicker.de',
['https://www.kicker.de/borussia-moenchengladbach/news', 'https://www.kicker.de/bundesliga/aktuell'],
url_filter=lambda u: 'kicker.de' in u and any(kw in u.lower() for kw in ['gladbach', 'borussia-mg', 'bundesliga', 'news', 'transfer']),
title_cleanup=lambda t: re.sub(r'\s*[-|]\s*[Kk]icker.*$', '', t).strip(),
))
# 5. sportschau.de (general Bundesliga, no strict Gladbach filter)
all_results.extend(generic_scrape(
'sportschau.de',
['https://www.sportschau.de/fussball/bundesliga/'],
url_filter=lambda u: 'sportschau.de' in u and '/fussball/' in u,
title_cleanup=lambda t: re.sub(r'\s*[-|]\s*[Ss]portschau.*$', '', t).strip(),
gladbach_check=False,
))
# 6. fussballtransfers.com
all_results.extend(generic_scrape(
'fussballtransfers.com',
['https://www.fussballtransfers.com/bundesliga', 'https://www.fussballtransfers.com/'],
url_filter=lambda u: 'fussballtransfers.com' in u,
title_cleanup=lambda t: re.sub(r'\s*[-|]\s*[Ff]ussball\s*[Tt]ransfers.*$', '', t).strip(),
))
# Deduplicate by URL
seen_urls = set()
deduped = []
for r in all_results:
if r['url'] not in seen_urls:
seen_urls.add(r['url'])
deduped.append(r)
# Prioritize: Gladbach articles first (with 2+ keyword hits), then the rest
gladbach_heavy = [r for r in deduped if has_gladbach_keyword(r['title'] + ' ' + r['full_text'], min_hits=2)]
gladbach_light = [r for r in deduped if r not in gladbach_heavy and has_gladbach_keyword(r['title'] + ' ' + r['full_text'], min_hits=1)]
other = [r for r in deduped if r not in gladbach_heavy and r not in gladbach_light]
# Take 5: prefer heavy Gladbach, then light Gladbach, then other
final = gladbach_heavy + gladbach_light + other
final = final[:5]
print("\n\n" + "=" * 60)
print(f"FINAL RESULTS: {len(final)} articles")
print("=" * 60)
for i, r in enumerate(final, 1):
print(f"\n--- Artikel {i} ---")
print(f"Quelle: {r['source']}")
print(f"Titel: {r['title']}")
print(f"Datum: {r['date']}")
print(f"URL: {r['url']}")
print(f"Textlänge: {len(r['full_text'])} Zeichen")
with open('/home/hermes/workspace/gladbach_news.json', 'w', encoding='utf-8') as f:
json.dump(final, f, ensure_ascii=False, indent=2)
print(f"\nResults saved to /home/hermes/workspace/gladbach_news.json")
return final
if __name__ == '__main__':
main()