Skip to content

Commit 7455d33

Browse files
authored
Merge pull request #700 from ruggsea/other-italian-newspapers
Support for 2 new italian newspapers - Corriere della Sera & Il Giornale
2 parents babf123 + 0bdd503 commit 7455d33

10 files changed

+359
-14
lines changed

docs/supported_publishers.md

+16-3
Original file line numberDiff line numberDiff line change
@@ -1276,16 +1276,29 @@
12761276
<code>CorriereDellaSera</code>
12771277
</td>
12781278
<td>
1279-
<div>Corriere Della Sera</div>
1279+
<div>Corriere della Sera</div>
12801280
</td>
12811281
<td>
1282-
<a href="https://www.corriere.it/">
1282+
<a href="https://www.corriere.it">
12831283
<span>www.corriere.it</span>
12841284
</a>
12851285
</td>
1286+
<td>&#160;</td>
1287+
<td>&#160;</td>
1288+
</tr>
1289+
<tr>
12861290
<td>
1287-
<code>topics</code>
1291+
<code>IlGiornale</code>
12881292
</td>
1293+
<td>
1294+
<div>Il Giornale</div>
1295+
</td>
1296+
<td>
1297+
<a href="https://www.ilgiornale.it">
1298+
<span>www.ilgiornale.it</span>
1299+
</a>
1300+
</td>
1301+
<td>&#160;</td>
12891302
<td>&#160;</td>
12901303
</tr>
12911304
<tr>

src/fundus/publishers/it/__init__.py

+69-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from datetime import datetime, timedelta
2+
from typing import Optional
23

34
from dateutil.rrule import MONTHLY, rrule
45

56
from fundus.publishers.base_objects import Publisher, PublisherGroup
67
from fundus.publishers.it.corriere_della_sera import CorriereDellaSeraParser
8+
from fundus.publishers.it.il_giornale import IlGiornaleParser
79
from fundus.publishers.it.la_repubblica import LaRepubblicaParser
8-
from fundus.scraping.url import RSSFeed, Sitemap
10+
from fundus.scraping.filter import regex_filter
11+
from fundus.scraping.url import NewsMap, RSSFeed, Sitemap
912

1013

1114
class IT(metaclass=PublisherGroup):
@@ -25,11 +28,73 @@ class IT(metaclass=PublisherGroup):
2528
)
2629

2730
CorriereDellaSera = Publisher(
28-
name="Corriere Della Sera",
29-
domain="https://www.corriere.it/",
31+
name="Corriere della Sera",
32+
domain="https://www.corriere.it",
3033
parser=CorriereDellaSeraParser,
3134
sources=[
32-
RSSFeed("https://www.corriere.it/feed-hp/homepage.xml"),
35+
# Main RSS feeds
36+
RSSFeed("https://www.corriere.it/rss/homepage.xml"),
37+
RSSFeed("https://www.corriere.it/rss/ultimora.xml"), ## Current empty but could be in use
38+
RSSFeed("https://www.corriere.it/dynamic-feed/rss/section/Dataroom.xml"),
39+
RSSFeed("https://www.corriere.it/dynamic-feed/rss/section/lettere-al-direttore.xml"),
40+
RSSFeed("https://www.corriere.it/dynamic-feed/rss/section/lo-dico-al-corriere.xml"),
41+
RSSFeed("https://www.corriere.it/dynamic-feed/rss/section/frammenti-di-ferruccio-de-bortoli.xml"),
42+
# Main sitemaps
3343
Sitemap("https://www.corriere.it/rss/sitemap_v2.xml"),
44+
# Dynamic sitemaps - Last 100 articles
45+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Economia.xml"),
46+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Salute.xml"),
47+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Scienze.xml"),
48+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Interni.xml"),
49+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Esteri.xml"),
50+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Sport.xml"),
51+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Politica.xml"),
52+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Salute__Figli__e__Genitori.xml"),
53+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Salute__Sportello__Cancro.xml"),
54+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Elezioni.xml"),
55+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Tecnologia.xml"),
56+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Offerte__recensioni.xml"),
57+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Lotterie.xml"),
58+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Spettacoli.xml"),
59+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Scuola.xml"),
60+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Animali.xml"),
61+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Opinioni.xml"),
62+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Caffe-gramellini.xml"),
63+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Ultimo-banco.xml"),
64+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Letti-da-rifarei.xml"),
65+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Piccole-dosi.xml"),
66+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/L-angolo.xml"),
67+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Padiglione-italia.xml"),
68+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Facce-nuove.xml"),
69+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Ritorno-in-solferino.xml"),
70+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Oriente-occidente.xml"),
71+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Sette.xml"),
72+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Moda.xml"),
73+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/BuoneNotizie.xml"),
74+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/lettere__al__direttore.xml"),
75+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/lo__dico__al__corriere.xml"),
76+
Sitemap("https://www.corriere.it/dynamic-sitemap/sitemap-last-100/Frammenti-ferruccio-de-bortoli.xml"),
77+
# Section sitemaps
78+
Sitemap("https://www.corriere.it/rss/sitemap/Motori.xml"),
79+
Sitemap("https://www.corriere.it/rss/sitemap/Cultura.xml"),
80+
Sitemap("https://www.corriere.it/rss/sitemap/lettere-al-direttore.xml"),
81+
Sitemap("https://www.corriere.it/rss/sitemap/lo-dico-al-corriere.xml"),
82+
Sitemap("https://www.corriere.it/rss/sitemap/Cook-Last.xml"),
83+
],
84+
)
85+
86+
IlGiornale = Publisher(
87+
name="Il Giornale",
88+
domain="https://www.ilgiornale.it",
89+
parser=IlGiornaleParser,
90+
sources=[
91+
# Main RSS feed (removed the one returning 404)
92+
RSSFeed("https://www.ilgiornale.it/feed.xml"),
93+
# Main sitemaps - excluding video and image sitemaps
94+
NewsMap("https://www.ilgiornale.it/sitemap/google-news.xml"),
95+
Sitemap(
96+
"https://www.ilgiornale.it/sitemap/indice.xml",
97+
sitemap_filter=regex_filter(r"\*/video/|\*/image/"),
98+
),
3499
],
35100
)

src/fundus/publishers/it/corriere_della_sera.py

+10
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@ def publishing_date(self) -> Optional[datetime]:
5050
date_str = self.precomputed.ld.xpath_search("//NewsArticle/datePublished", scalar=True)
5151
return generic_date_parsing(date_str)
5252

53+
@attribute
54+
def topics(self) -> List[str]:
55+
breadcrumb_items = self.precomputed.ld.xpath_search("//BreadcrumbList/itemListElement/*/name")
56+
if breadcrumb_items:
57+
return generic_topic_parsing(breadcrumb_items[1:])
58+
section = self.precomputed.ld.xpath_search("//NewsArticle/articleSection", scalar=True)
59+
if section:
60+
return generic_topic_parsing([section])
61+
return []
62+
5363
@attribute
5464
def images(self) -> List[Image]:
5565
return image_extraction(
+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import re
2+
from datetime import datetime
3+
from typing import List, Optional
4+
5+
from lxml.cssselect import CSSSelector
6+
from lxml.etree import XPath, tostring
7+
from lxml.html import HtmlElement, document_fromstring
8+
9+
from fundus.parser import ArticleBody, BaseParser, Image, ParserProxy, attribute
10+
from fundus.parser.data import ImageVersion
11+
from fundus.parser.utility import (
12+
extract_article_body_with_selector,
13+
generic_author_parsing,
14+
generic_date_parsing,
15+
generic_topic_parsing,
16+
image_extraction,
17+
transform_breaks_to_paragraphs,
18+
)
19+
20+
21+
class IlGiornaleParser(ParserProxy):
22+
class V1(BaseParser):
23+
# Selectors for article body parts
24+
_paragraph_selector = XPath(
25+
"//div[contains(@class, 'typography--content')]//p[text() or strong or em] | //div[@class='banner banner--spaced-block banner-evo' and (text() or em or strong)]"
26+
)
27+
_subheadline_selector = CSSSelector("div.typography--content h2:not([class])")
28+
_summary_selector = CSSSelector("p.article__abstract, div.article__abstract")
29+
_image_selector = XPath(
30+
"//div[contains(@class, 'article__media')]//img | //section[contains(@class, 'article__content')]//img"
31+
)
32+
33+
@attribute
34+
def title(self) -> Optional[str]:
35+
# First try JSON-LD
36+
title = self.precomputed.ld.xpath_search("//NewsArticle/headline", scalar=True)
37+
if title:
38+
return str(title)
39+
# Fallback to meta tags
40+
return self.precomputed.meta.get("og:title")
41+
42+
@attribute
43+
def authors(self) -> List[str]:
44+
# Extract authors from schema.org NewsArticle data
45+
authors = self.precomputed.ld.xpath_search("//NewsArticle/author")
46+
if authors:
47+
return generic_author_parsing(authors)
48+
return []
49+
50+
@attribute
51+
def publishing_date(self) -> Optional[datetime]:
52+
# Try JSON-LD first
53+
date_str = self.precomputed.ld.xpath_search("//NewsArticle/datePublished", scalar=True)
54+
if not date_str:
55+
# Fallback to meta tags
56+
date_str = self.precomputed.meta.get("article:published_time")
57+
return generic_date_parsing(date_str)
58+
59+
@attribute
60+
def body(self) -> Optional[ArticleBody]:
61+
# Clean up HTML by removing ads and handling em/strong/cite tags
62+
html_string = tostring(self.precomputed.doc).decode("utf-8")
63+
html_string = re.sub(r"</?(em|strong|cite)>", "", html_string)
64+
html_string = re.sub(r"<!-- EVOLUTION ADV -->", "", html_string)
65+
doc = document_fromstring(html_string)
66+
67+
# Transform br tags to paragraphs for better structure
68+
doc = transform_breaks_to_paragraphs(doc)
69+
70+
# Extract article body using utility function
71+
return extract_article_body_with_selector(
72+
doc,
73+
paragraph_selector=self._paragraph_selector,
74+
subheadline_selector=self._subheadline_selector,
75+
summary_selector=self._summary_selector,
76+
)
77+
78+
@attribute
79+
def topics(self) -> List[str]:
80+
# Try to get topics from keywords
81+
keywords = self.precomputed.ld.bf_search("keywords")
82+
if keywords:
83+
return generic_topic_parsing(keywords)
84+
85+
# Fallback to articleSection
86+
section = self.precomputed.ld.xpath_search("//NewsArticle/articleSection", scalar=True)
87+
if section:
88+
return generic_topic_parsing([section])
89+
90+
return []
91+
92+
@attribute
93+
def images(self) -> List[Image]:
94+
# Extract images using the utility function
95+
return image_extraction(
96+
doc=self.precomputed.doc,
97+
paragraph_selector=self._paragraph_selector,
98+
image_selector=self._image_selector,
99+
caption_selector=XPath(".//figcaption/text()"),
100+
)

tests/resources/parser/test_data/it/CorriereDellaSera.json

+11-7
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@
55
],
66
"body": {
77
"summary": [
8-
"La premier italiana Giorgia Meloni è partita (a sorpresa) per una missione in Florida dal presidente eletto degli Usa. Segreta l'agenda dell'incontro, ma il New York Times rivela:«La premier ha premuto sul caso di Cecilia Sala»"
8+
"La premier italiana Giorgia Meloni è partita (a sorpresa) per una missione in Florida dal presidente eletto degli Usa. L'elogio di Trump: «Ha preso d'assalto l'Europa». Segreta l'agenda dell'incontro, ma il New York Times rivela:«La premier ha premuto sul caso di Cecilia Sala»"
99
],
1010
"sections": [
1111
{
1212
"headline": [],
1313
"paragraphs": [
14-
"DALLA NOSTRA CORRISPONDENTENEW YORK - Giorgia Meloni è arrivata a Mar-a-Lago alle 19:29 locali, dopo l’atterraggio del suo volo partito da Ciampino all’aeroporto di Palm Beach, per una missione lampo - e nata nel riserbo più assoluto - che l'ha portata a incontrare il presidente eletto degli Stati Uniti, Donald Trump.",
14+
"DALLA NOSTRA CORRISPONDENTENEW YORK - Giorgia Meloni è arrivata a Mar-a-Lago alle 19:29 locali - l'1:29 delmattino di domenica, ora italiana - dopo l’atterraggio del suo volo partito da Ciampino all’aeroporto di Palm Beach, per una missione lampo - e nata nel riserbo più assoluto - che l'ha portata a incontrare il presidente eletto degli Stati Uniti, Donald Trump.",
15+
"«Una bella serata, lo ringrazio per l'accoglienza. Pronti a lavorare insieme», ha twittato poi la premier nella serata di domenica.",
1516
"In una delle prime foto scattate dagli ospiti nel salone della residenza in Florida - che è anche un resort - , si vede la presidente del Consiglio italiana insieme a Trump. Al loro fianco, il senatore della Florida Marco Rubio nominato segretario di Stato da Trump, il deputato della Florida Mike Waltz, nominato consigliere per la sicurezza nazionale, Scott Bessent, nominato segretario del Tesoro, l’ambasciatrice d’Italia negli Usa Mariangela Zappia e l’imprenditore texano Tilman Fertitta, nominato ambasciatore Usa in Italia.",
1617
"Trump ha salutato gli ospiti nel salone d'onore, predisposto per la proiezione di un film e, seguito da Meloni e dalla delegazione, si è recato al piano di sopra, tornando intorno alle nove di sera. Non si è visto invece Elon Musk - che potrebbe però aver giocato un ruolo per l'organizzazione dell'incontro."
1718
]
1819
},
1920
{
2021
"headline": [
21-
"Trump su Meloni: «Ha preso d'assalto l'Europa». Poi il film"
22+
"Donald Trump su Giorgia Meloni: «Ha preso d'assalto l'Europa». Poi il film"
2223
],
2324
"paragraphs": [
2425
"«È molto emozionante, sono qui con una donna fantastica, la premier italiana», ha detto Trump agli ospiti riuniti nel salone. «Ha davvero preso d'assalto l'Europa». Rubio, dando il benvenuto alla premier, l'ha poi definita: «Un'ottima alleata, un leader forte».",
@@ -55,7 +56,7 @@
5556
{
5657
"versions": [
5758
{
58-
"url": "https://dimages2.corriereobjects.it/files/main_image/files/fp/uploads/2025/01/05/677a43307deb3.r_d.580-548-6933.jpeg",
59+
"url": "https://dimages2.corriereobjects.it/files/main_image/files/fp/uploads/2025/01/05/677aba3f831f8.r_d.1558-1018-2445.jpeg",
5960
"query_width": null,
6061
"size": {
6162
"width": 572,
@@ -65,13 +66,16 @@
6566
}
6667
],
6768
"is_cover": true,
68-
"description": "Trump e Meloni a mar a lago",
69+
"description": "Meloni e Trump a Mar-a-Lago",
6970
"caption": null,
7071
"authors": [],
71-
"position": 1449
72+
"position": 1450
7273
}
7374
],
7475
"publishing_date": "2025-01-05 03:56:38+01:00",
75-
"title": "Trump accoglie Meloni a Mar-a-Lago: «Ha preso d'assalto l'Europa». Il Nyt: «La premier ha premuto aggressivamente per Cecilia Sala». Salvini: «Bene Giorgia. E noi diciamo \"go Donald go\"»"
76+
"title": "Giorgia Meloni da Donald Trump a Mar-a-Lago: «Bella serata, pronti a lavorare insieme». Il Nyt: «Su Cecilia Sala la premier ha premuto aggressivamente»",
77+
"topics": [
78+
"Esteri"
79+
]
7680
}
7781
}
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)