Files

290 lines
11 KiB
Python

#!/usr/bin/env python3
"""Scrape Gladbach & Bundesliga news from multiple sources."""
import requests
from bs4 import BeautifulSoup
import json
import re
import sys
from urllib.parse import urljoin
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;q=0.8",
}
def fetch_url(url, timeout=20):
try:
resp = requests.get(url, headers=HEADERS, timeout=timeout)
resp.encoding = resp.apparent_encoding or 'utf-8'
return BeautifulSoup(resp.text, 'html.parser')
except Exception as e:
print(f" ERROR fetching {url}: {e}", file=sys.stderr)
return None
def scrape_torfabrik():
print("=== TORFABRIK.DE ===", file=sys.stderr)
articles = []
soup = fetch_url("https://www.torfabrik.de/")
if not soup:
return articles
# TYPO3 news items
for item in soup.select(".news .article, .news-list-item, .article, .top-news, .frame-type-news_pi1 .article"):
title_el = item.select_one("h1, h2, h3, .news-header")
link_el = item.select_one("a")
date_el = item.select_one("time, .date, .news-list-date")
if not title_el:
continue
title = title_el.get_text(strip=True)
link = None
if link_el and link_el.get("href"):
link = urljoin("https://www.torfabrik.de/", link_el["href"])
elif title_el.name == "a":
link = urljoin("https://www.torfabrik.de/", title_el.get("href", ""))
date_str = date_el.get_text(strip=True) if date_el else ""
teaser_el = item.select_one(".teaser-text, .bodytext, p")
teaser = teaser_el.get_text(strip=True)[:300] if teaser_el else ""
if title and len(title) > 10:
articles.append({"title": title, "url": link, "date": date_str, "teaser": teaser, "source": "torfabrik.de"})
# All headings with links
for heading in soup.select("h1, h2, h3"):
a_tag = heading.select_one("a")
if a_tag and a_tag.get("href") and not a_tag.get("href").startswith("#"):
text = heading.get_text(strip=True)
if len(text) > 15:
link = urljoin("https://www.torfabrik.de/", a_tag["href"])
if not any(a.get("title") == text for a in articles):
articles.append({"title": text, "url": link, "date": "", "teaser": "", "source": "torfabrik.de"})
print(f" Found {len(articles)} articles", file=sys.stderr)
return articles
def scrape_fohlen_hautnah():
print("=== FOHLEN-HAUTNAH.DE ===", file=sys.stderr)
articles = []
# Try WP REST API first
try:
api_url = "https://fohlen-hautnah.de/wp-json/wp/v2/posts?per_page=10"
resp = requests.get(api_url, headers=HEADERS, timeout=15)
if resp.status_code == 200:
posts = resp.json()
for post in posts:
title = post.get("title", {}).get("rendered", "")
title = BeautifulSoup(title, "html.parser").get_text(strip=True)
link = post.get("link", "")
date = post.get("date", "")
excerpt = post.get("excerpt", {}).get("rendered", "")
excerpt = BeautifulSoup(excerpt, "html.parser").get_text(strip=True)[:300]
content = post.get("content", {}).get("rendered", "")
content_text = BeautifulSoup(content, "html.parser").get_text(strip=True)[:5000]
if title:
articles.append({
"title": title, "url": link, "date": date,
"teaser": excerpt, "fulltext": content_text,
"source": "fohlen-hautnah.de"
})
if articles:
print(f" WP API: Found {len(articles)} articles", file=sys.stderr)
return articles
except Exception as e:
print(f" WP API error: {e}", file=sys.stderr)
# Fallback: scrape HTML
soup = fetch_url("https://fohlen-hautnah.de/")
if not soup:
return articles
for article in soup.select("article, .post, .entry"):
title_el = article.select_one("h2 a, h3 a, .entry-title a")
if not title_el:
title_el = article.select_one("h2, h3, .entry-title")
if not title_el:
continue
title = title_el.get_text(strip=True)
link = title_el.get("href")
if not link and title_el.name == "a":
link = title_el.get("href")
if link and not link.startswith("http"):
link = urljoin("https://fohlen-hautnah.de/", link)
date_el = article.select_one("time, .entry-date, .date")
date_str = date_el.get("datetime") or date_el.get_text(strip=True) if date_el else ""
teaser_el = article.select_one(".entry-summary, .excerpt, p")
teaser = teaser_el.get_text(strip=True)[:300] if teaser_el else ""
if title and len(title) > 10:
articles.append({"title": title, "url": link, "date": date_str, "teaser": teaser, "source": "fohlen-hautnah.de"})
print(f" HTML scrape: Found {len(articles)} articles", file=sys.stderr)
return articles
def scrape_gladbachlive():
print("=== GLADBACHLIVE.DE ===", file=sys.stderr)
articles = []
soup = fetch_url("https://www.gladbachlive.de/")
if not soup:
return articles
# JSON-LD structured data
for script in soup.select("script[type='application/ld+json']"):
try:
data = json.loads(script.string)
items = data if isinstance(data, list) else [data]
for item in items:
if isinstance(item, dict) and item.get("@type") in ("NewsArticle", "Article"):
articles.append({
"title": item.get("headline", item.get("name", "")),
"url": item.get("url", item.get("mainEntityOfPage", "")),
"date": item.get("datePublished", ""),
"teaser": item.get("description", "")[:300],
"source": "gladbachlive.de"
})
except:
pass
# DM teaser cards
for item in soup.select("[class*='teaser'], [class*='card'], [class*='article']"):
title_el = item.select_one("h2, h3, [class*='headline'], [class*='title']")
link_el = item.select_one("a")
if not title_el:
continue
title = title_el.get_text(strip=True)
link = None
if link_el and link_el.get("href"):
link = urljoin("https://www.gladbachlive.de/", link_el["href"])
if title and len(title) > 15:
articles.append({"title": title, "url": link, "date": "", "teaser": "", "source": "gladbachlive.de"})
print(f" Found {len(articles)} articles", file=sys.stderr)
return articles
def scrape_kicker():
print("=== KICKER.DE ===", file=sys.stderr)
articles = []
soup = fetch_url("https://www.kicker.de/bundesliga/news")
if not soup:
return articles
for item in soup.select(".kick__item, .news-list__item, article, [class*='teaser'], [class*='news-item']"):
title_el = item.select_one("h2, h3, .headline, .title, [class*='title'], [class*='headline']")
link_el = item.select_one("a")
if not title_el:
continue
title = title_el.get_text(strip=True)
link = None
if link_el and link_el.get("href"):
link = urljoin("https://www.kicker.de/", link_el["href"])
if title and len(title) > 15:
articles.append({"title": title, "url": link, "date": "", "teaser": "", "source": "kicker.de"})
print(f" Found {len(articles)} articles", file=sys.stderr)
return articles
def scrape_sportschau():
print("=== SPORTSCHAU.DE ===", file=sys.stderr)
articles = []
soup = fetch_url("https://www.sportschau.de/fussball/bundesliga/")
if not soup:
return articles
for item in soup.select("article, .teaser, [class*='teaser'], [class*='news']"):
title_el = item.select_one("h2, h3, [class*='headline'], [class*='title']")
link_el = item.select_one("a")
if not title_el:
continue
title = title_el.get_text(strip=True)
link = None
if link_el and link_el.get("href"):
link = urljoin("https://www.sportschau.de/", link_el["href"])
if title and len(title) > 15:
articles.append({"title": title, "url": link, "date": "", "teaser": "", "source": "sportschau.de"})
print(f" Found {len(articles)} articles", file=sys.stderr)
return articles
def scrape_article_fulltext(url):
if not url:
return ""
soup = fetch_url(url)
if not soup:
return ""
for selector in ["article", ".article-content", ".post-content", ".entry-content",
".news-content", ".article-body", ".story-body", "main", ".content", "#content"]:
content_el = soup.select_one(selector)
if content_el:
for unwanted in content_el.select("script, style, nav, .ad, .advertisement, .comments, .related"):
unwanted.decompose()
text = content_el.get_text(separator="\n", strip=True)
if len(text) > 200:
return text[:5000]
paragraphs = soup.select("article p, .content p, main p")
if paragraphs:
return "\n".join(p.get_text(strip=True) for p in paragraphs[:20])[:5000]
return ""
def main():
all_articles = []
# Priority sources
all_articles.extend(scrape_torfabrik())
all_articles.extend(scrape_fohlen_hautnah())
all_articles.extend(scrape_gladbachlive())
# Deduplicate
seen = set()
unique = []
for a in all_articles:
key = a["title"].lower().strip()[:80]
if key and key not in seen:
seen.add(key)
unique.append(a)
print(f"\n=== TOTAL UNIQUE: {len(unique)} articles ===", file=sys.stderr)
# Scrape full text for top articles
for a in unique[:15]:
if a.get("fulltext"):
continue # already have it from API
url = a.get("url")
if url:
print(f"Fetching fulltext: {url}", file=sys.stderr)
a["fulltext"] = scrape_article_fulltext(url)
# Save to JSON
with open("/home/hermes/workspace/scraped_articles.json", "w") as f:
json.dump(unique, f, ensure_ascii=False, indent=2)
# Print summary
for i, a in enumerate(unique[:30]):
print(f"[{i+1}] [{a['source']}] {a['title']}")
if a.get('date'):
print(f" Date: {a['date']}")
if a.get('url'):
print(f" URL: {a['url']}")
if a.get('teaser'):
print(f" Teaser: {a['teaser'][:200]}")
if a.get('fulltext'):
print(f" Fulltext: {len(a['fulltext'])} chars")
print()
if __name__ == "__main__":
main()