From ad9c0630153ee11076afc5bac638a3674982fec3 Mon Sep 17 00:00:00 2001 From: maelrx Date: Thu, 21 May 2026 04:39:21 -0300 Subject: [PATCH 1/2] Add TechRadar publisher --- docs/supported_publishers.md | 19 ++ src/fundus/publishers/uk/__init__.py | 12 ++ src/fundus/publishers/uk/techradar.py | 77 +++++++ .../parser/test_data/uk/TechRadar.json | 186 ++++++++++++++++ .../test_data/uk/TechRadar_2026_05_21.html.gz | Bin 0 -> 324596 bytes tests/resources/parser/test_data/uk/meta.info | 200 +++++++++--------- 6 files changed, 396 insertions(+), 98 deletions(-) create mode 100644 src/fundus/publishers/uk/techradar.py create mode 100644 tests/resources/parser/test_data/uk/TechRadar.json create mode 100644 tests/resources/parser/test_data/uk/TechRadar_2026_05_21.html.gz diff --git a/docs/supported_publishers.md b/docs/supported_publishers.md index 011110587..8e1be98a4 100644 --- a/docs/supported_publishers.md +++ b/docs/supported_publishers.md @@ -3409,6 +3409,25 @@     + + + TechRadar + + +
TechRadar
+ + + + www.techradar.com + + + + en + +   +   +   + BBC diff --git a/src/fundus/publishers/uk/__init__.py b/src/fundus/publishers/uk/__init__.py index d266141a1..abce4e94e 100644 --- a/src/fundus/publishers/uk/__init__.py +++ b/src/fundus/publishers/uk/__init__.py @@ -14,6 +14,7 @@ from .i_news import INewsParser from .metro import MetroParser from .nature import NatureParser +from .techradar import TechRadarParser from .the_bbc import TheBBCParser from .the_guardian import TheGuardianParser from .the_independent import TheIndependentParser @@ -157,6 +158,17 @@ class UK(metaclass=PublisherGroup): ], ) + TechRadar = Publisher( + name="TechRadar", + domain="https://www.techradar.com/", + parser=TechRadarParser, + sources=[ + Sitemap("https://www.techradar.com/sitemap.xml", reverse=True), + NewsMap("https://www.techradar.com/sitemap-news.xml"), + ], + url_filter=regex_filter(r"/deals/compare|/html/|/outlink|/infinite-scroll-"), + ) + Express = Publisher( name="Daily Express", domain="https://www.express.co.uk/", diff --git a/src/fundus/publishers/uk/techradar.py b/src/fundus/publishers/uk/techradar.py new file mode 100644 index 000000000..728758e73 --- /dev/null +++ b/src/fundus/publishers/uk/techradar.py @@ -0,0 +1,77 @@ +import datetime +import re +from typing import List, Optional + +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 TechRadarParser(ParserProxy): + class V1(BaseParser): + _summary_selector = XPath("//article//header//*[contains(@class, 'strapline')]") + _subheadline_selector = XPath( + "//article//div[contains(concat(' ', normalize-space(@class), ' '), ' text-copy ')]" + "//*[self::h2 or self::h3][normalize-space()]" + ) + + _bloat_regex = ( + r"^When you purchase through links|" + r"^Sign up for breaking news|" + r"^Follow TechRadar on Google News|" + r"^Get daily insight|" + r"^You might also like" + ) + _paragraph_selector = XPath( + "//article//div[contains(concat(' ', normalize-space(@class), ' '), ' text-copy ')]" + "//*[self::p or self::li]" + "[normalize-space() and not(contains(@class, 'vanilla-image-block')) " + "and not(self::li[contains(@class, 'list-none')]) " + f"and not(re:test(normalize-space(string()), '{_bloat_regex}'))]", + namespaces={"re": "http://exslt.org/regular-expressions"}, + ) + + @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 publishing_date(self) -> Optional[datetime.datetime]: + return generic_date_parsing(self.precomputed.ld.bf_search("datePublished")) + + @attribute + def authors(self) -> List[str]: + return generic_author_parsing( + self.precomputed.ld.bf_search("author") or self.precomputed.meta.get("mrf:authors") + ) + + @attribute + def title(self) -> Optional[str]: + return self.precomputed.ld.bf_search("headline") + + @attribute + def topics(self) -> List[str]: + return generic_topic_parsing(self.precomputed.meta.get("article:tag")) + + @attribute + def images(self) -> List[Image]: + return image_extraction( + doc=self.precomputed.doc, + paragraph_selector=self._paragraph_selector, + upper_boundary_selector=XPath("//article"), + image_selector=XPath("//article//figure//img"), + caption_selector=XPath("./ancestor::figure//figcaption"), + author_selector=re.compile(r"(?i)image credit[s]?: (?P.*)"), + ) diff --git a/tests/resources/parser/test_data/uk/TechRadar.json b/tests/resources/parser/test_data/uk/TechRadar.json new file mode 100644 index 000000000..803f99095 --- /dev/null +++ b/tests/resources/parser/test_data/uk/TechRadar.json @@ -0,0 +1,186 @@ +{ + "V1": { + "authors": [ + "Lance Ulanoff" + ], + "body": { + "summary": [ + "A big shift, but with some wiggle room" + ], + "sections": [ + { + "headline": [], + "paragraphs": [ + "Google has been synonymous with search for more than 25 years, and so how it reimagines search matters to billions of people who rely on its powerful knowledge graph. In recent years, we've seen the steady encroachment of AI Overviews and AI mode on our search experience. Now, though the transition to inserting AI into your search results seems complete, I worry that this might alter Google Search in ways that no one wants or can reverse. Google, however, tells me that's not the case.", + "First, Google is now on record saying that in this next chapter of search, the change it unveiled during its Google I/O 2026 keynote is, according to Google Search lead Liz Reid, \"truly the biggest upgrade to our iconic search box since its debut over 25 years ago.\"", + "That's heady, terrifying, and maybe a bit of hyperbole. What's promised is a new search box that not only effortlessly expands to support your most long-winded queries but also carries intelligence that lets it decide on the fly what kind of AI smarts might help answer your, well, let's call it what it is: a prompt.", + "If that sounds like AI Mode is now inside the classic Google Search box, I think you're right. In the demo video I saw, I didn't even see the current \"AI Mode\" iconography. And instead of basic autocomplete, the new search box has AI-powered query suggestions and multi-modal capabilities (throw in some images and ask a question)." + ] + }, + { + "headline": [ + "Google vs. OpenAI" + ], + "paragraphs": [ + "If Google's long-term effort was to make AI, specifically, various Gemini models, inescapable in Search, I think the work is nearly complete. I don't blame Google for doing this. After all, OpenAI's ChatGPT has been surging in recent years, with some people saying they \"Chatted\" instead of \"Googled\".", + "Verb status aside, ChatGPT, though rising, remains by one measure at less than 25% of the search market, while Google hovers around 80%. But ChatGPT's trajectory is unmistakable in Google's eyes. It has no choice but to deeply infuse traditional search with AI.", + "How much AI, though, is too much?", + "There remains a large contingent who want nothing to do with AI from Google or ChatGPT. I wondered if they could opt out, and during a Google I/O 2026 pre-brief, I posed the question to Google. Later, I got an email reply from a Google representative.", + "\"The AI dimension of the Search box is giving you quick access to AI tools, and an updated query suggestion system that helps you formulate long questions, where an AI response is likely the most helpful. Using this new search box does not mean that you will only get AI responses - you'll continue to get a range of results on Search.\"", + "Using this new search box does not mean that you will only get AI responses.", + "What's notable is that there is no \"No, I'd rather not\" option here. You can't opt out of the Intelligent Search Box. But that doesn't mean your search results won't still include some of the classic link and summary results you've known and loved since 1998. As a Google spokesperson promised, \"No matter what you ask, you’ll continue to get a range of results from Search, just like you do today.\"", + "Those results, though, will likely be below the AI Overviews that already sit atop those classic results. If anything, Overviews may be even richer and more accurate thanks to the intelligent query guidance you received in the search box. Scrolling down below them might be pointless.", + "It doesn't take much imagination to envision a future in which the AI Overviews are your Google Search results, and there is nothing below because it's not as useful, or at least it doesn't \"speak\" to you in the same way the overviews do. They seem to get you because they're designed to respond to your intention in a way that traditional search results could never do.", + "For some, this is progress. For me? The jury's still out.", + "What about you? Share your thoughts on Google's new Intelligent Search Box in the comments below." + ] + } + ] + }, + "images": [ + { + "versions": [ + { + "url": "https://cdn.mos.cms.futurecdn.net/rERQRhw7xajcatmufxPXAM-320-80.jpg", + "query_width": null, + "size": { + "width": 320, + "height": 0 + }, + "type": "image/jpeg" + }, + { + "url": "https://cdn.mos.cms.futurecdn.net/rERQRhw7xajcatmufxPXAM-320-80.jpg.webp", + "query_width": null, + "size": { + "width": 320, + "height": 0 + }, + "type": "image/webp" + }, + { + "url": "https://cdn.mos.cms.futurecdn.net/rERQRhw7xajcatmufxPXAM-480-80.jpg", + "query_width": null, + "size": { + "width": 480, + "height": 0 + }, + "type": "image/jpeg" + }, + { + "url": "https://cdn.mos.cms.futurecdn.net/rERQRhw7xajcatmufxPXAM-480-80.jpg.webp", + "query_width": null, + "size": { + "width": 480, + "height": 0 + }, + "type": "image/webp" + }, + { + "url": "https://cdn.mos.cms.futurecdn.net/rERQRhw7xajcatmufxPXAM-650-80.jpg", + "query_width": null, + "size": { + "width": 650, + "height": 0 + }, + "type": "image/jpeg" + }, + { + "url": "https://cdn.mos.cms.futurecdn.net/rERQRhw7xajcatmufxPXAM-650-80.jpg.webp", + "query_width": null, + "size": { + "width": 650, + "height": 0 + }, + "type": "image/webp" + }, + { + "url": "https://cdn.mos.cms.futurecdn.net/rERQRhw7xajcatmufxPXAM-970-80.jpg", + "query_width": null, + "size": { + "width": 970, + "height": 0 + }, + "type": "image/jpeg" + }, + { + "url": "https://cdn.mos.cms.futurecdn.net/rERQRhw7xajcatmufxPXAM-970-80.jpg.webp", + "query_width": null, + "size": { + "width": 970, + "height": 0 + }, + "type": "image/webp" + }, + { + "url": "https://cdn.mos.cms.futurecdn.net/rERQRhw7xajcatmufxPXAM-1024-80.jpg", + "query_width": null, + "size": { + "width": 1024, + "height": 0 + }, + "type": "image/jpeg" + }, + { + "url": "https://cdn.mos.cms.futurecdn.net/rERQRhw7xajcatmufxPXAM-1024-80.jpg.webp", + "query_width": null, + "size": { + "width": 1024, + "height": 0 + }, + "type": "image/webp" + }, + { + "url": "https://cdn.mos.cms.futurecdn.net/rERQRhw7xajcatmufxPXAM-1200-80.jpg", + "query_width": null, + "size": { + "width": 1200, + "height": 0 + }, + "type": "image/jpeg" + }, + { + "url": "https://cdn.mos.cms.futurecdn.net/rERQRhw7xajcatmufxPXAM-1200-80.jpg.webp", + "query_width": null, + "size": { + "width": 1200, + "height": 0 + }, + "type": "image/webp" + } + ], + "is_cover": true, + "description": "Google search", + "caption": null, + "authors": [ + "TechRadar (Image credit: Google)" + ], + "position": 1851 + }, + { + "versions": [ + { + "url": "https://cdn.mos.cms.futurecdn.net/UDSETB5pZsBuBg37vf6B2R.gif", + "query_width": null, + "size": null, + "type": null + } + ], + "is_cover": false, + "description": "Google Intelligent Search Box", + "caption": "(", + "authors": [ + "Google)" + ], + "position": 1992 + } + ], + "publishing_date": "2026-05-19 21:00:00+00:00", + "title": "'This new search box does not mean that you'll only get AI responses': Google's Search makeover incorporates yet more AI, but Google promises to leave room for classic results", + "topics": [ + "Software", + "Computing", + "Google" + ] + } +} diff --git a/tests/resources/parser/test_data/uk/TechRadar_2026_05_21.html.gz b/tests/resources/parser/test_data/uk/TechRadar_2026_05_21.html.gz new file mode 100644 index 0000000000000000000000000000000000000000..ad36e01ce312a5e7fa7222136ef31b14ed17aa64 GIT binary patch literal 324596 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

(Lzz9W($H1_bDo$&0qmym?wX_(PCD#dk}PVW36 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