diff --git a/recipes/economist_world_ahead.recipe b/recipes/economist_world_ahead.recipe
index c3000fc32059..33f1d5585949 100644
--- a/recipes/economist_world_ahead.recipe
+++ b/recipes/economist_world_ahead.recipe
@@ -3,19 +3,14 @@
import json
import time
-from collections import defaultdict
from datetime import datetime, timedelta
-from urllib.parse import quote, urlencode
from html5_parser import parse
from lxml import etree
-from calibre import replace_entities
from calibre.ebooks.BeautifulSoup import NavigableString, Tag
-from calibre.ptempfile import PersistentTemporaryFile
from calibre.web.feeds.news import BasicNewsRecipe
-use_archive = True
def E(parent, name, text='', **attrs):
ans = parent.makeelement(name, **attrs)
@@ -24,24 +19,32 @@ def E(parent, name, text='', **attrs):
return ans
-def process_node(node, html_parent):
- ntype = node.get('type')
- if ntype == 'tag':
- c = html_parent.makeelement(node['name'])
- c.attrib.update({k: v or '' for k, v in node.get('attribs', {}).items()})
- html_parent.append(c)
- for nc in node.get('children', ()):
- process_node(nc, c)
- elif ntype == 'text':
- text = node.get('data')
- if text:
- text = replace_entities(text)
- if len(html_parent):
- t = html_parent[-1]
- t.tail = (t.tail or '') + text
- else:
- html_parent.text = (html_parent.text or '') + text
-
+def process_node(node):
+ ntype = node.get('type', '')
+ if ntype == 'CROSSHEAD':
+ if node.get('textHtml'):
+ return f'
{node.get("textHtml")}
'
+ return f'{node.get("tex", "")}
'
+ if ntype == 'PARAGRAPH':
+ if node.get('textHtml'):
+ return f'{node.get("textHtml")}
'
+ return f'{node.get("tex", "")}
'
+ elif ntype == 'IMAGE':
+ alt = "" if node.get("altText") is None else node.get("altText")
+ cap = ""
+ if node.get('caption'):
+ if node['caption'].get('textHtml') is not None:
+ cap = node['caption']['textHtml']
+ return f'{cap}
'
+ elif ntype == 'PULL_QUOTE':
+ if node.get('textHtml'):
+ return f'{node.get("textHtml")}
'
+ return f'{node.get("text", "")}
'
+ elif ntype == 'DIVIDER':
+ return '
'
+ elif ntype:
+ print('** ', ntype)
+ return ''
def safe_dict(data, *names):
ans = data
@@ -54,63 +57,29 @@ class JSONHasNoContent(ValueError):
pass
-if use_archive:
- def load_article_from_json(raw, root):
- # open('/t/raw.json', 'w').write(raw)
- data = json.loads(raw)
- body = root.xpath('//body')[0]
- article = E(body, 'article')
- E(article, 'div', data['flyTitle'] , style='color: red; font-size:small; font-weight:bold;')
- E(article, 'h1', data['title'], title=safe_dict(data, "url", "canonical") or '')
- E(article, 'div', data['rubric'], style='font-style: italic; color:#202020;')
- try:
- date = data['dateModified']
- except Exception:
- date = data['datePublished']
- dt = datetime.fromisoformat(date[:-1]) + timedelta(seconds=time.timezone)
- dt = dt.strftime('%b %d, %Y, %I:%M %p')
- if data['dateline'] is None:
- E(article, 'p', dt, style='color: gray; font-size:small;')
- else:
- E(article, 'p', dt + ' | ' + (data['dateline']), style='color: gray; font-size:small;')
- main_image_url = safe_dict(data, 'image', 'main', 'url').get('canonical')
- if main_image_url:
- div = E(article, 'div')
- try:
- E(div, 'img', src=main_image_url)
- except Exception:
- pass
- for node in data.get('text') or ():
- process_node(node, article)
-else:
- def load_article_from_json(raw, root):
- # open('/t/raw.json', 'w').write(raw)
- try:
- data = json.loads(raw)['props']['pageProps']['content']
- except KeyError as e:
- raise JSONHasNoContent(e)
- if isinstance(data, list):
- data = data[0]
- body = root.xpath('//body')[0]
- for child in tuple(body):
- body.remove(child)
- article = E(body, 'article')
- E(article, 'div', replace_entities(data['subheadline']) , style='color: red; font-size:small; font-weight:bold;')
- E(article, 'h1', replace_entities(data['headline']))
- E(article, 'div', replace_entities(data['description']), style='font-style: italic; color:#202020;')
- if data['dateline'] is None:
- E(article, 'p', (data['datePublishedString'] or ''), style='color: gray; font-size:small;')
- else:
- E(article, 'p', (data['datePublishedString'] or '') + ' | ' + (data['dateline']), style='color: gray; font-size:small;')
- main_image_url = safe_dict(data, 'image', 'main', 'url').get('canonical')
- if main_image_url:
- div = E(article, 'div')
- try:
- E(div, 'img', src=main_image_url)
- except Exception:
- pass
- for node in data.get('text') or ():
- process_node(node, article)
+def load_article_from_json(raw):
+ # open('/t/raw.json', 'w').write(raw)
+ body = ''
+ data = json.loads(raw)['props']['pageProps']['cp2Content']
+ body += f'{data.get("flyTitle", "")}
'
+ body += f'{data["headline"]}
'
+ body += f'{data.get("rubric", "")}
'
+ try:
+ date = data['dateModified']
+ except Exception:
+ date = data['datePublished']
+ dt = datetime.fromisoformat(date[:-1]) + timedelta(seconds=time.timezone)
+ dt = dt.strftime('%b %d, %Y %I:%M %p')
+ if data.get('dateline') is None:
+ body += f'{dt}
'
+ else:
+ body += f'{dt + " | " + (data["dateline"])}
'
+ main_image_url = safe_dict(data, 'leadComponent') or ''
+ if main_image_url:
+ body += process_node(data['leadComponent'])
+ for node in data.get('body'):
+ body += process_node(node)
+ return '' + body + ''
def cleanup_html_article(root):
@@ -150,14 +119,13 @@ def process_url(url):
return url
-class Economist(BasicNewsRecipe):
-
+class EconomistWorld(BasicNewsRecipe):
title = 'The Economist World Ahead'
language = 'en'
encoding = 'utf-8'
masthead_url = 'https://www.livemint.com/lm-img/dev/economist-logo-oneline.png'
- __author__ = "Kovid Goyal"
+ __author__ = "unkn0wn"
description = (
'The World Ahead is The Economist’s future-gazing publication. It prepares audiences for what is to '
'come with mind-stretching insights and expert analysis—all in The Economist’s clear, elegant style.'
@@ -166,25 +134,31 @@ class Economist(BasicNewsRecipe):
extra_css = '''
em { color:#202020; }
img {display:block; margin:0 auto;}
+ .sub { font-size:small; }
+ #subhead { color: #404040; font-size:small; font-weight:bold; }'
+ #descrip { font-style: italic; color:#202020; }
+ #date { color: gray; font-size:small; }
'''
- oldest_article = 7.0
+
resolve_internal_links = True
remove_tags = [
- dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent', 'aside', 'footer']),
+ dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent', 'aside', 'footer', 'svg']),
dict(attrs={'aria-label': "Article Teaser"}),
+ dict(attrs={'id': 'player'}),
dict(attrs={
'class': [
'dblClkTrk', 'ec-article-info', 'share_inline_header',
'related-items', 'main-content-container', 'ec-topic-widget',
'teaser', 'blog-post__bottom-panel-bottom', 'blog-post__comments-label',
'blog-post__foot-note', 'blog-post__sharebar', 'blog-post__bottom-panel',
- 'newsletter-form','share-links-header','teaser--wrapped', 'latest-updates-panel__container',
- 'latest-updates-panel__article-link','blog-post__section'
+ 'newsletter-form', 'share-links-header', 'teaser--wrapped', 'latest-updates-panel__container',
+ 'latest-updates-panel__article-link', 'blog-post__section'
]
}
),
dict(attrs={
'class': lambda x: x and 'blog-post__siblings-list-aside' in x.split()}),
+ dict(attrs={'id': lambda x: x and 'gpt-ad-slot' in x}),
classes(
'share-links-header teaser--wrapped latest-updates-panel__container'
' latest-updates-panel__article-link blog-post__section newsletter-form blog-post__bottom-panel'
@@ -195,28 +169,32 @@ class Economist(BasicNewsRecipe):
remove_attributes = ['data-reactid', 'width', 'height']
# economist.com has started throttling after about 60% of the total has
# downloaded with connection reset by peer (104) errors.
- delay = 2
+ delay = 3
+ remove_empty_feeds = True
+ ignore_duplicate_articles = {'title'}
+
+ needs_subscription = False
recipe_specific_options = {
'res': {
'short': 'For hi-res images, select a resolution from the\nfollowing options: 834, 960, 1096, 1280, 1424',
'long': 'This is useful for non e-ink devices, and for a lower file size\nthan the default, use from 480, 384, 360, 256.',
- 'default': '600'
- }
+ 'default': '600',
+ },
}
- needs_subscription = False
-
def get_browser(self, *args, **kwargs):
- # Needed to bypass cloudflare
- kwargs['user_agent'] = 'common_words/based'
+ kwargs['user_agent'] = 'Mozilla/5.0 (Linux; Android 14; 330333QCG Build/AP1A.140705.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/125.0.6422.165 Mobile Safari/537.36 Lamarr/3.37.0-3037003 (android)' # noqa
br = BasicNewsRecipe.get_browser(self, *args, **kwargs)
- br.addheaders += [('Accept-Language', 'en-GB,en-US;q=0.9,en;q=0.8')]
+ br.addheaders += [
+ ('x-requested-with', 'com.economist.lamarr')
+ ]
return br
def economist_test_article(self):
+ self.cover_url = None
return [('Articles', [{'title':'test',
- 'url':'https://www.economist.com/the-americas/2024/04/14/elon-musk-is-feuding-with-brazils-powerful-supreme-court'
+ 'url':'https://www.economist.com/the-world-ahead/2024/11/20/what-the-superforecasters-predict-for-major-events-in-2025'
}])]
def economist_return_index(self, ans):
@@ -229,152 +207,62 @@ class Economist(BasicNewsRecipe):
)
return ans
- if use_archive:
- def parse_index(self):
- # return self.economist_test_article()
- soup = self.index_to_soup('https://www.economist.com/the-world-ahead')
- script_tag = soup.find("script", id="__NEXT_DATA__")
- if script_tag is None:
- raise ValueError('No script tag with JSON data found in the weeklyedition archive')
+ def parse_index(self):
+ # return self.economist_test_article()
+ raw = self.index_to_soup('https://www.economist.com/the-world-ahead')
+ ans = self.economist_parse_index(raw)
+ return self.economist_return_index(ans)
+
+ def economist_parse_index(self, soup):
+ script_tag = soup.find("script", id="__NEXT_DATA__")
+ if script_tag is not None:
data = json.loads(script_tag.string)
- content_id = data['props']['pageProps']['content']['tegID'].split('/')[-1]
- query = {
- 'query': 'query HubsDataQuery($id:String!$size:Int!){canonical(ref:$id){id headline description url{canonical __typename}image{ident{url{canonical __typename}width height __typename}__typename}text(mode:"hub" format:"json")hasPart(size:$size){parts{id title:headline isPartOf{context{title:headline __typename}__typename}hasPart{parts{...ArticleFragment isPartOf{id context{title:headline flyTitle:subheadline rubric:description dateline image{...ImageMainFragment ...ImagePromoFragment __typename}__typename}__typename}__typename}__typename}__typename}__typename}__typename}}fragment ArticleFragment on Content{ad{grapeshot{channels{name __typename}__typename}__typename}articleSection{internal{id title:headline __typename}__typename}audio{main{id duration(format:"seconds")source:channel{id __typename}url{canonical __typename}__typename}__typename}byline dateline dateModified datePublished dateRevised flyTitle:subheadline id image{...ImageInlineFragment ...ImageMainFragment ...ImagePromoFragment __typename}print{title:headline flyTitle:subheadline rubric:description section{id title:headline __typename}__typename}publication{id tegID title:headline flyTitle:subheadline datePublished regionsAllowed url{canonical __typename}__typename}rubric:description source:channel{id __typename}tegID text(format:"json")title:headline type url{canonical __typename}topic contentIdentity{forceAppWebview mediaType articleType __typename}__typename}fragment ImageInlineFragment on Media{inline{url{canonical __typename}width height __typename}__typename}fragment ImageMainFragment on Media{main{url{canonical __typename}width height __typename}__typename}fragment ImagePromoFragment on Media{promo{url{canonical __typename}id width height __typename}__typename}', # noqa
- 'operationName': 'HubsDataQuery',
- 'variables': '{{"id":"/content/{}","size":40}}'.format(content_id),
- }
- url = 'https://cp2-graphql-gateway.p.aws.economist.com/graphql?' + urlencode(query, safe='()!', quote_via=quote)
- try:
- raw = self.index_to_soup(url, raw=True)
- except Exception:
- raise ValueError('Server is not reachable, try again some other time.')
- ans = self.economist_parse_index(raw)
- return self.economist_return_index(ans)
-
- def economist_parse_index(self, raw):
- data = json.loads(raw)['data']['canonical']
- self.description = data['description']
-
- feeds_dict = defaultdict(list)
- for part in safe_dict(data, "hasPart", "parts"):
- section = part['title']
+ # open('/t/raw.json', 'w').write(json.dumps(data, indent=2, sort_keys=True))
+ self.title = safe_dict(data, "props", "pageProps", "content", "headline")
+ self.cover_url = 'https://mma.prnewswire.com/media/2561745/The_Economist_World_Ahead_2025_cover.jpg?w=600'
+
+ feeds = []
+
+ for coll in safe_dict(data, "props", "pageProps", "content", "components"):
+ section = safe_dict(coll, "headline") or ''
self.log(section)
- for art in safe_dict(part, "hasPart", "parts"):
- title = safe_dict(art, "title")
- desc = safe_dict(art, "rubric") or ''
- sub = safe_dict(art, "flyTitle") or ''
+ articles = []
+ for part in safe_dict(coll, "items"):
+ title = safe_dict(part, "headline") or ''
+ url = process_url(safe_dict(part, "url") or '')
+ desc = safe_dict(part, "rubric") or ''
+ sub = safe_dict(part, "flyTitle") or ''
if sub and section != sub:
desc = sub + ' :: ' + desc
- pt = PersistentTemporaryFile('.html')
- pt.write(json.dumps(art).encode('utf-8'))
- pt.close()
- url = 'file:///' + pt.name
- feeds_dict[section].append({"title": title, "url": url, "description": desc})
- self.log('\t', title, '\n\t\t', desc)
- return [(section, articles) for section, articles in feeds_dict.items()]
-
- def populate_article_metadata(self, article, soup, first):
- article.url = soup.find('h1')['title']
-
- def preprocess_html(self, soup):
- width = '600'
- w = self.recipe_specific_options.get('res')
- if w and isinstance(w, str):
- width = w
- for img in soup.findAll('img', src=True):
- qua = 'economist.com/cdn-cgi/image/width=' + width + ',quality=80,format=auto/'
- img['src'] = img['src'].replace('economist.com/', qua)
- return soup
-
- else: # Load articles from individual article pages {{{
-
- def __init__(self, *args, **kwargs):
- BasicNewsRecipe.__init__(self, *args, **kwargs)
- if self.output_profile.short_name.startswith('kindle'):
- # Reduce image sizes to get file size below amazon's email
- # sending threshold
- self.web2disk_options.compress_news_images = True
- self.web2disk_options.compress_news_images_auto_size = 5
- self.log.warn('Kindle Output profile being used, reducing image quality to keep file size below amazon email threshold')
-
- def parse_index(self):
- # return [('Articles', [{'title':'test',
- # 'url':'https://www.economist.com/interactive/briefing/2022/06/11/huge-foundation-models-are-turbo-charging-ai-progress'
- # }])]
- url = 'https://www.economist.com/the-world-ahead'
- # raw = open('/t/raw.html').read()
- raw = self.index_to_soup(url, raw=True)
- # with open('/t/raw.html', 'wb') as f:
- # f.write(raw)
- soup = self.index_to_soup(raw)
- # nav = soup.find(attrs={'class':'navigation__wrapper'})
- # if nav is not None:
- # a = nav.find('a', href=lambda x: x and '/printedition/' in x)
- # if a is not None:
- # self.log('Following nav link to current edition', a['href'])
- # soup = self.index_to_soup(process_url(a['href']))
- ans = self.economist_parse_index(soup)
- if not ans:
- raise NoArticles(
- 'Could not find any articles, either the '
- 'economist.com server is having trouble and you should '
- 'try later or the website format has changed and the '
- 'recipe needs to be updated.'
- )
- return ans
-
- def economist_parse_index(self, soup):
- script_tag = soup.find("script", id="__NEXT_DATA__")
- if script_tag is not None:
- data = json.loads(script_tag.string)
- # open('/t/raw.json', 'w').write(json.dumps(data, indent=2, sort_keys=True))
- self.title = safe_dict(data, "props", "pageProps", "content", "headline")
- # self.cover_url = 'https://mma.prnewswire.com/media/2275620/The_Economist_The_World_Ahead_2024.jpg?w=600'
-
- feeds = []
-
- for coll in safe_dict(data, "props", "pageProps", "content", "collections"):
- section = safe_dict(coll, "headline") or ''
- self.log(section)
- articles = []
- for part in safe_dict(coll, "hasPart", "parts"):
- title = safe_dict(part, "headline") or ''
- url = safe_dict(part, "url", "canonical") or ''
- if not title or not url:
- continue
- desc = safe_dict(part, "description") or ''
- sub = safe_dict(part, "subheadline") or ''
- if sub:
- desc = sub + ' :: ' + desc
- self.log('\t', title, '\n\t', desc, '\n\t\t', url)
- articles.append({'title': title, 'description':desc, 'url': url})
- if articles:
- feeds.append((section, articles))
- return feeds
-
- # }}}
-
+ self.log('\t', title, '\n\t', desc, '\n\t\t', url)
+ articles.append({'title': title, 'description':desc, 'url': url})
+ if articles:
+ feeds.append((section, articles))
+ return feeds
+
+ def preprocess_html(self, soup):
+ width = '600'
+ w = self.recipe_specific_options.get('res')
+ if w and isinstance(w, str):
+ width = w
+ for img in soup.findAll('img', src=True):
+ qua = 'economist.com/cdn-cgi/image/width=' + width + ',quality=80,format=auto/'
+ img['src'] = img['src'].replace('economist.com/', qua)
+ return soup
def preprocess_raw_html(self, raw, url):
# open('/t/raw.html', 'wb').write(raw.encode('utf-8'))
- if use_archive:
- body = ''
- root = parse(body)
- load_article_from_json(raw, root)
- else:
- root = parse(raw)
- script = root.xpath('//script[@id="__NEXT_DATA__"]')
- if script:
- try:
- load_article_from_json(script[0].text, root)
- except JSONHasNoContent:
- cleanup_html_article(root)
-
+ root_ = parse(raw)
if '/interactive/' in url:
- return '' + root.xpath('//h1')[0].text + '
' \
+ return '' + root_.xpath('//h1')[0].text + '
' \
+ 'This article is supposed to be read in a browser' \
+ ''
+ script = root_.xpath('//script[@id="__NEXT_DATA__"]')
+
+ html = load_article_from_json(script[0].text)
+
+ root = parse(html)
for div in root.xpath('//div[@class="lazy-image"]'):
noscript = list(div.iter('noscript'))
if noscript and noscript[0].text:
@@ -431,12 +319,3 @@ class Economist(BasicNewsRecipe):
if url.endswith('/print'):
url = url.rpartition('/')[0]
return BasicNewsRecipe.canonicalize_internal_url(self, url, is_link=is_link)
-
-
-def get_login_cookies(username, password):
- print(33333333333, username, password)
-
-
-if __name__ == '__main__':
- import sys
- get_login_cookies(sys.argv[-2], sys.argv[-1])