Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/supported_publishers.md
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,28 @@
<td>&#160;</td>
<td>&#160;</td>
</tr>
<tr>
<td>
<code>KlasseGegenKlasse</code>
</td>
<td>
<div>Klasse Gegen Klasse</div>
</td>
<td>
<a href="https://www.klassegegenklasse.org/">
<span>www.klassegegenklasse.org</span>
</a>
</td>
<td>
<code>de</code>
</td>
<td>
<code>images</code>
<code>title</code>
</td>
<td>&#160;</td>
<td>&#160;</td>
</tr>
<tr>
<td>
<code>Krautreporter</code>
Expand Down
14 changes: 14 additions & 0 deletions src/fundus/publishers/de/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from .hessenschau import HessenschauParser
from .junge_welt import JungeWeltParser
from .kicker import KickerParser
from .klassegegenklasse import KlasseGegenKlasseParser
from .krautreporter import KrautreporterParser
from .mdr import MDRParser
from .merkur import MerkurParser
Expand Down Expand Up @@ -595,3 +596,16 @@ class DE(metaclass=PublisherGroup):
Sitemap("https://www.gamestar.de/artikel_archiv_index.xml"),
],
)

KlasseGegenKlasse = Publisher(
name="Klasse Gegen Klasse",
domain="https://www.klassegegenklasse.org/",
parser=KlasseGegenKlasseParser,
sources=[
RSSFeed("https://www.klassegegenklasse.org/feed/"),
Sitemap(
"https://www.klassegegenklasse.org/wp-sitemap.xml",
),
Comment on lines +606 to +608
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the sitemap includes a lot of unnecessary entries. You can use the sitemap_filter parameter of Sitemap to exclude unwanted sitemap URLs. For example, sitemap_filter=inverse(regex_filter("wp-sitemap-posts-post")) will exclude any sitemap that does not contain the substring wp-sitemap-posts-post.

],
request_header={"user-agent": "Fundus"},
)
151 changes: 151 additions & 0 deletions src/fundus/publishers/de/klassegegenklasse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import re
from datetime import datetime
from typing import List, Optional

from lxml.cssselect import CSSSelector
from lxml.etree import XPath

from fundus.parser import ArticleBody, BaseParser, Image, ParserProxy, attribute
from fundus.parser.utility import (
extract_article_body_with_selector,
generic_author_parsing,
generic_date_parsing,
generic_topic_parsing,
image_extraction,
)


class KlasseGegenKlasseParser(ParserProxy):
class V1(BaseParser):
_paragraph_selector = CSSSelector("article p, main article p, .post-content p, .entry-content p, .content p")
_summary_selector = CSSSelector(
"article .entry-content > p:first-child, article > p:first-child, .post-content > p:first-child"
)
_subheadline_selector = CSSSelector("article h2, .entry-content h2, .post-content h2")

@attribute
def body(self) -> Optional[ArticleBody]:
return extract_article_body_with_selector(
self.precomputed.doc,
summary_selector=self._summary_selector,
subheadline_selector=self._subheadline_selector,
paragraph_selector=self._paragraph_selector,
)

@attribute
def authors(self) -> List[str]:
# 1) nur Meta (kein LD)
res = generic_author_parsing(self.precomputed.meta.get("author"))
if res:
return res

# 2) DOM-Fallbacks (WP-typisch)
nodes = self.precomputed.doc.xpath(
"//a[@rel='author']/text()"
" | //span[contains(@class,'author')]//a/text()"
" | //div[contains(@class,'author')]//a/text()"
" | //div[contains(@class,'byline')]//a/text()"
" | //span[contains(@class,'byline')]//a/text()"
" | //a[contains(@href,'/autor/') or contains(@href,'/author/')]/text()"
)
Comment on lines +43 to +50
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clicking through some of the articles it seems to me that the author can usually be found at the same place. Could you elaborate on your reasoning for adding all the extra selectors?

vals = [t.strip() for t in nodes if t and t.strip()]
seen, out = set(), []
for v in vals:
if v not in seen:
seen.add(v)
out.append(v)
return out

@attribute
def publishing_date(self) -> Optional[datetime]:
Comment on lines +59 to +60
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as with authors. It seems that the publishing date of an article is displayed at the same location for the articles i encountered so far. Could you elaborate why you added the extra fall-backs?

# 1) Meta
for cand in (
self.precomputed.meta.get("article:published_time"),
self.precomputed.meta.get("og:article:published_time"),
self.precomputed.meta.get("date"),
):
dt = generic_date_parsing(cand)
if dt:
return dt

# 2) <time datetime>
for val in self.precomputed.doc.xpath("//time[@datetime]/@datetime"):
dt = generic_date_parsing(val)
if dt:
return dt

# 3) sichtbarer Datumstext (inkl. dd.mm.yyyy)
texts: list[str] = []
texts += [t.strip() for t in self.precomputed.doc.xpath("//time//text()") if t and t.strip()]
texts += [
t.strip()
for t in self.precomputed.doc.xpath(
"//*[contains(@class,'date') or contains(@class,'datum') or contains(@class,'entry-date') "
"or contains(@class,'meta-date') or contains(@class,'posted-on') or contains(@class,'post-meta')]//text()"
)
if t and t.strip()
]

for t in texts:
dt = generic_date_parsing(t)
if dt:
return dt

m = None
for t in texts:
m = re.search(r"\b(\d{1,2}\.\d{1,2}\.\d{4})(?:\s+(\d{1,2}:\d{2}))?\b", t)
if m:
d, tm = m.group(1), m.group(2)
try:
if tm:
return datetime.strptime(f"{d} {tm}", "%d.%m.%Y %H:%M")
return datetime.strptime(d, "%d.%m.%Y")
except ValueError:
pass

# 4) notfalls Roh-HTML
m = re.search(r"\b(\d{1,2}\.\d{1,2}\.\d{4})\b", self.precomputed.html or "")
if m:
try:
return datetime.strptime(m.group(1), "%d.%m.%Y")
except ValueError:
pass

# 5) optional: modified als Fallback
for cand in (
self.precomputed.meta.get("article:modified_time"),
self.precomputed.meta.get("og:updated_time"),
):
dt = generic_date_parsing(cand)
if dt:
return dt

return None

@attribute
def topics(self) -> List[str]:
# 1) Meta
res = generic_topic_parsing(
self.precomputed.meta.get("keywords") or self.precomputed.meta.get("news_keywords")
)
if res:
return res

# 2) DOM: WP-typische Orte + Permalinks
nodes = self.precomputed.doc.xpath(
"//a[@rel='tag']/text() | //a[@rel='category tag']/text()"
" | //div[contains(@class,'tags') or contains(@class,'tags-links') or contains(@class,'post-tags') "
" or contains(@class,'cat-links') or contains(@class,'post-categories') "
" or contains(@class,'entry-taxonomy') or contains(@class,'entry-taxonomies')]//a/text()"
" | //a[contains(@href,'/schlagwort/') or contains(@href,'/thema/') or contains(@href,'/kategorie/') "
" or contains(@href,'/tag/') or contains(@href,'/category/')]/text()"
" | //meta[@property='article:tag']/@content"
)
vals = [s.strip().lstrip("#") for s in nodes if s and s.strip()]

seen, out = set(), []
for v in vals:
if v not in seen:
seen.add(v)
out.append(v)
return out
31 changes: 31 additions & 0 deletions tests/resources/parser/test_data/de/KlasseGegenKlasse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"V1": {
"authors": [
"Joschua Klein"
],
"body": {
"summary": [],
"sections": [
{
"headline": [],
"paragraphs": [
"Der Angriff auf palästinasolidarische Menschen ist ein Angriff auf die demokratischen Freiheiten in diesem Land. Und das geht uns alle etwas an.",
"Die Palästina-Solidaritätsbewegung erfährt besonders in Berlin seit dem 7. Oktober 2025 noch gesteigerte Repression durch den deutschen Staat. Diese Ausweitung von massiver Polizeigewalt, willkürlichen Festnahmen, fadenscheinigen Versammlungsverboten und rassistischer Diskriminierung sind nicht nur direkte Angriffe auf die Pro-Palästina-Bewegung, sondern stellen auch drastische Einschnitte der allgemeinen Meinungs- und Versammlungsfreiheit und Rechtsstaatlichkeit dar – vermeintliche Grundpfeiler unserer Demokratie.",
"Dies wird auch längst international bestätigt, zuletzt durch die UN. Diese ruft Deutschland dazu auf, die Kriminalisierung und Polizeigewalt gegen palästinasolidarische Menschen einzustellen. Neben den schon lange anhaltenden Beschränkungen und Verboten von Pro-Palästina Versammlungen, der Kriminalisierung der Verteidigung von Menschenrechten und palästinischer Identität, und unverhältnismäßiger Polizeigewalt wurden insbesondere die Großeinkesslung, die gewaltsamen Festnahmen, und das willkürliche Versammlungsverbot des 7. Oktober in Berlin benannt. Seitdem wurden sogar ausgewiesene parlamentarische Beobachter von der Links-Partei durch die Polizei grundlos angegriffen und gewaltsam festgenommen.",
"Meinungsfreiheit, Versammlungsfreiheit und Rechtsstaatlichkeit werden von der Regierung konsequent untergraben. Deutschland ist in Europa ein trauriger Vorreiter in der Normalisierung von Grundrechtsverletzungen und dem Rückbau von demokratischen Freiheiten.",
"Die massive Repression wird nicht auf die Pro-Palästina-Bewegung beschränkt bleiben. Vielmehr muss sie als Barometer der autoritären Wende in Deutschland allgemein verstanden werden. Sie zeigt, wie der Staat auch dem gesellschaftlichen Widerstand gegen Maßnahmen wie die unbeliebte Wiedereinführung des Wehrdienstes und das Spardiktat zugunsten von massiver Aufrüstung begegnen möchte. Dies sehen wir mit der Ausweitung der brutalen Polizeigewalt auf Proteste von Rheinmetall-Entwaffnen und dem Berliner Bündnis gegen Waffenproduktion. Jegliche Kritik am Staat soll erstickt und seine Rechenschaft für die unrechtmäßige Unterdrückung der Bevölkerung – ganz zu schweigen von der Unterstützung des Genozids – entflohen werden.",
"Es ist leider kaum zu erwarten, dass die verantwortlichen parlamentarischen Parteien und staatlichen Organe den Repressionskurs spontan ändern. Denn die Exekutive, Judikative und Legislative kollaborieren in der Repression und delegitimieren sich damit zunehmend. Auch die Links-Partei muss viel mehr Tatkraft zeigen, die Polizeigewalt aufzuklären und ihr Einhalt zu gebieten.",
"Denn gerade Linke und Gewerkschaften müssen erwarten, dass ihre Aktionen im Zuge des allgemeinen Rechtsrucks aller parlamentarischen Parteien auch zunehmend ins Visier der staatlichen Repression kommen werden. Daher fordern wir diese auf, sich grundsätzlich solidarisch mit Palästina und palästinasolidarischen Menschen, die Repression erfahren, zu stellen und es nicht bei der Mitorganisation von wenigen Großdemos zu belassen.",
"Gehen wir zusammen weiter in Solidarität mit Palästina und gegen Militarisierung auf die Straße, so stehen wir auch für die Versammlungsfreiheit und Meinungsfreiheit ein – für alle, aber besonders gegen Rechts. Sprechen wir Palästina und die Repression der Solidaritätsbewegung in unseren Universitäten, Betrieben, Vereinen und Freundeskreisen an, so finden wir ungeahnte Mitstreiter:innen. Hören und verbreiten wir palästinensische Stimmen, so leisten wir Widerstand im Kleinen gegen Versuche, diese zu unterdrücken. Der unrechtmäßige Angriff auf palästinasolidarische Menschen ist auch ein Angriff auf die demokratischen Grundfreiheiten in diesem Land. Diesen gilt es gemeinsam abzuwehren."
]
}
]
},
"publishing_date": "2025-10-22 00:00:00",
"topics": [
"Palästina",
"Polizeigewalt",
"Repression und Militarismus"
]
}
}
Binary file not shown.
4 changes: 4 additions & 0 deletions tests/resources/parser/test_data/de/meta.info
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@
"url": "https://www.kicker.de/interimstrainer-schubert-nicht-mehr-viel-zu-verlieren-1019241/artikel#omrss",
"crawl_date": "2024-04-26 15:37:39.609270"
},
"KlasseGegenKlasse_2025_10_23.html.gz": {
"url": "https://www.klassegegenklasse.org/ausufernde-repression-als-spiegel-unserer-demokratie/",
"crawl_date": "2025-10-23 15:02:22.060737"
},
"Krautreporter_2024_08_22.html.gz": {
"url": "https://krautreporter.de/politik-und-macht/5481-ich-wollte-dass-sie-mich-mogen-so-uberlebt-man",
"crawl_date": "2024-08-22 18:41:33.860723"
Expand Down
Loading