bertrend 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bertrend/BERTopicModel.py +320 -0
- bertrend/BERTrend.py +1211 -0
- bertrend/__init__.py +59 -0
- bertrend/config/bertopic_default_config.toml +48 -0
- bertrend/config/bertrend_default_config.toml +16 -0
- bertrend/config/parameters.py +92 -0
- bertrend/config/services_default_config.toml +32 -0
- bertrend/demos/__init__.py +4 -0
- bertrend/demos/demos_utils/__init__.py +13 -0
- bertrend/demos/demos_utils/data_loading_component.py +267 -0
- bertrend/demos/demos_utils/embed_documents_component.py +52 -0
- bertrend/demos/demos_utils/icons.py +41 -0
- bertrend/demos/demos_utils/messages.py +11 -0
- bertrend/demos/demos_utils/parameters_component.py +162 -0
- bertrend/demos/demos_utils/state_utils.py +99 -0
- bertrend/demos/start_summarizer_app.sh +9 -0
- bertrend/demos/start_topic_analysis_app.sh +9 -0
- bertrend/demos/start_weak_signals_app.sh +9 -0
- bertrend/demos/summarization/__init__.py +4 -0
- bertrend/demos/summarization/summarizer_app.py +98 -0
- bertrend/demos/topic_analysis/__init__.py +4 -0
- bertrend/demos/topic_analysis/app.py +87 -0
- bertrend/demos/topic_analysis/app_utils.py +69 -0
- bertrend/demos/topic_analysis/demo_pages/__init__.py +4 -0
- bertrend/demos/topic_analysis/demo_pages/explore_topics.py +455 -0
- bertrend/demos/topic_analysis/demo_pages/newsletters_generation.py +178 -0
- bertrend/demos/topic_analysis/demo_pages/temporal_visualizations.py +573 -0
- bertrend/demos/topic_analysis/demo_pages/topic_visualizations.py +280 -0
- bertrend/demos/topic_analysis/demo_pages/training_page.py +219 -0
- bertrend/demos/topic_analysis/messages.py +13 -0
- bertrend/demos/weak_signals/__init__.py +4 -0
- bertrend/demos/weak_signals/app.py +510 -0
- bertrend/demos/weak_signals/messages.py +32 -0
- bertrend/demos/weak_signals/visualizations_utils.py +403 -0
- bertrend/llm_utils/__init__.py +4 -0
- bertrend/llm_utils/newsletter.css +131 -0
- bertrend/llm_utils/newsletter_features.py +272 -0
- bertrend/llm_utils/openai_client.py +235 -0
- bertrend/llm_utils/prompts.py +94 -0
- bertrend/metrics/__init__.py +4 -0
- bertrend/metrics/temporal_metrics_embedding.py +861 -0
- bertrend/metrics/topic_metrics.py +199 -0
- bertrend/resources/common_ngrams.json +23 -0
- bertrend/resources/stopwords-en.json +3 -0
- bertrend/resources/stopwords-fr.json +1 -0
- bertrend/resources/stopwords-rte.json +15 -0
- bertrend/services/__init__.py +4 -0
- bertrend/services/authentication.py +66 -0
- bertrend/services/embedding_client.py +154 -0
- bertrend/services/embedding_service.py +380 -0
- bertrend/services/summarizer.py +45 -0
- bertrend/services/summary/__init__.py +4 -0
- bertrend/services/summary/abstractive_summarizer.py +74 -0
- bertrend/services/summary/chatgpt_summarizer.py +67 -0
- bertrend/services/summary/extractive_summarizer.py +368 -0
- bertrend/services/summary/lexrank.py +126 -0
- bertrend/services/summary/prompts.py +43 -0
- bertrend/tests/__init__.py +4 -0
- bertrend/tests/test_abstractive_summarizer.py +140 -0
- bertrend/tests/test_bertrend.py +1091 -0
- bertrend/tests/test_cache_utils.py +111 -0
- bertrend/tests/test_chatgpt_summarizer.py +150 -0
- bertrend/tests/test_config_utils.py +54 -0
- bertrend/tests/test_data_loading.py +326 -0
- bertrend/tests/test_data_structure.py +53 -0
- bertrend/tests/test_embedding_client.py +546 -0
- bertrend/tests/test_extractive_summarizer.py +250 -0
- bertrend/tests/test_installation.py +183 -0
- bertrend/tests/test_newsletter_features.py +354 -0
- bertrend/tests/test_openai_client.py +210 -0
- bertrend/tests/test_representative_docs.py +125 -0
- bertrend/tests/test_summarizer.py +115 -0
- bertrend/tests/test_temporal_metrics_embedding.py +349 -0
- bertrend/tests/test_topic_description.py +159 -0
- bertrend/tests/test_topic_metrics.py +233 -0
- bertrend/tests/test_topic_model.py +285 -0
- bertrend/tests/test_trend_analysis_data_structure.py +174 -0
- bertrend/tests/test_trend_analysis_prompts.py +225 -0
- bertrend/tests/test_trend_analysis_visualizations.py +307 -0
- bertrend/tests/test_trend_analysis_weak_signals.py +598 -0
- bertrend/tests/test_utils_init.py +70 -0
- bertrend/tests/test_visualizations.py +306 -0
- bertrend/topic_analysis/__init__.py +4 -0
- bertrend/topic_analysis/data_structure.py +13 -0
- bertrend/topic_analysis/prompts.py +42 -0
- bertrend/topic_analysis/representative_docs.py +57 -0
- bertrend/topic_analysis/topic_description.py +66 -0
- bertrend/topic_analysis/visualizations.py +315 -0
- bertrend/trend_analysis/__init__.py +4 -0
- bertrend/trend_analysis/data_structure.py +78 -0
- bertrend/trend_analysis/prompts.py +229 -0
- bertrend/trend_analysis/signal_llm_template_en.html +181 -0
- bertrend/trend_analysis/signal_llm_template_fr.html +181 -0
- bertrend/trend_analysis/visualizations.py +605 -0
- bertrend/trend_analysis/weak_signals.py +425 -0
- bertrend/utils/__init__.py +31 -0
- bertrend/utils/cache_utils.py +31 -0
- bertrend/utils/config_utils.py +41 -0
- bertrend/utils/data_loading.py +329 -0
- bertrend-0.3.1.dist-info/AUTHORS.txt +15 -0
- bertrend-0.3.1.dist-info/LICENSE.md +373 -0
- bertrend-0.3.1.dist-info/METADATA +400 -0
- bertrend-0.3.1.dist-info/RECORD +181 -0
- bertrend-0.3.1.dist-info/WHEEL +4 -0
- bertrend_apps/__init__.py +4 -0
- bertrend_apps/common/__init__.py +4 -0
- bertrend_apps/common/crontab_utils.py +113 -0
- bertrend_apps/common/mail_utils.py +91 -0
- bertrend_apps/config/feeds/arxiv_feed.toml +15 -0
- bertrend_apps/config/feeds/decarbonation_feed.toml +15 -0
- bertrend_apps/config/feeds/llm_en_feed.toml +15 -0
- bertrend_apps/config/feeds/llm_feed.toml +16 -0
- bertrend_apps/config/feeds/nlp_feed.toml +15 -0
- bertrend_apps/config/feeds/rte_feed.toml +15 -0
- bertrend_apps/config/gmail_credentials.json +1 -0
- bertrend_apps/config/newsletters/arxiv_newsletter_from_last.toml +39 -0
- bertrend_apps/config/newsletters/arxiv_newsletter_from_scratch.toml +39 -0
- bertrend_apps/config/newsletters/decarbonation_newsletter_from_last.toml +38 -0
- bertrend_apps/config/newsletters/decarbonation_newsletter_from_scratch.toml +38 -0
- bertrend_apps/config/newsletters/llm_en_newsletter_from_last.toml +38 -0
- bertrend_apps/config/newsletters/llm_newsletter_from_last.toml +39 -0
- bertrend_apps/config/newsletters/nlp_newsletter_from_last.toml +38 -0
- bertrend_apps/config/newsletters/rte_newsletter_from_last.toml +38 -0
- bertrend_apps/config/newsletters/rte_newsletter_from_scratch.toml +38 -0
- bertrend_apps/data_provider/__init__.py +9 -0
- bertrend_apps/data_provider/__main__.py +237 -0
- bertrend_apps/data_provider/arxiv_provider.py +161 -0
- bertrend_apps/data_provider/bing_news_provider.py +82 -0
- bertrend_apps/data_provider/curebot_provider.py +105 -0
- bertrend_apps/data_provider/data_provider.py +194 -0
- bertrend_apps/data_provider/google_news_provider.py +94 -0
- bertrend_apps/data_provider/newscatcher_provider.py +74 -0
- bertrend_apps/data_provider/utils.py +165 -0
- bertrend_apps/exploration/__init__.py +4 -0
- bertrend_apps/exploration/curebot/__init__.py +4 -0
- bertrend_apps/exploration/curebot/app.py +45 -0
- bertrend_apps/exploration/curebot/app_utils.py +317 -0
- bertrend_apps/exploration/curebot/config.toml +33 -0
- bertrend_apps/exploration/curebot/import_extraction_article.ipynb +108 -0
- bertrend_apps/exploration/curebot/old_veille_analyse.py +298 -0
- bertrend_apps/exploration/curebot/prompts.py +24 -0
- bertrend_apps/exploration/curebot/start_newsletter_generator.sh +8 -0
- bertrend_apps/exploration/curebot/tabs/tab1.py +150 -0
- bertrend_apps/exploration/curebot/tabs/tab2.py +95 -0
- bertrend_apps/exploration/curebot/tabs/tab3.py +125 -0
- bertrend_apps/exploration/dataviz/Main_page.py +10 -0
- bertrend_apps/exploration/dataviz/__init__.py +4 -0
- bertrend_apps/exploration/dataviz/pages/1_Data_feeds.py +105 -0
- bertrend_apps/exploration/geolocalization/__init__.py +4 -0
- bertrend_apps/exploration/geolocalization/entity_utils.py +118 -0
- bertrend_apps/exploration/geolocalization/spacy/__init__.py +4 -0
- bertrend_apps/exploration/geolocalization/spacy/common_factory.py +142 -0
- bertrend_apps/exploration/geolocalization/spacy/dbpedia.py +206 -0
- bertrend_apps/exploration/geolocalization/spacy/nlp.config +37 -0
- bertrend_apps/exploration/geolocalization/spacy/utils.py +39 -0
- bertrend_apps/install_feeds_and_newsletters.py +29 -0
- bertrend_apps/newsletters/__init__.py +4 -0
- bertrend_apps/newsletters/__main__.py +277 -0
- bertrend_apps/prospective_demo/.streamlit/config.toml +3 -0
- bertrend_apps/prospective_demo/.streamlit/secrets.toml +9 -0
- bertrend_apps/prospective_demo/__init__.py +70 -0
- bertrend_apps/prospective_demo/app.py +111 -0
- bertrend_apps/prospective_demo/authentication.py +44 -0
- bertrend_apps/prospective_demo/dashboard_analysis.py +132 -0
- bertrend_apps/prospective_demo/dashboard_common.py +79 -0
- bertrend_apps/prospective_demo/dashboard_signals.py +226 -0
- bertrend_apps/prospective_demo/feeds_common.py +38 -0
- bertrend_apps/prospective_demo/feeds_config.py +261 -0
- bertrend_apps/prospective_demo/feeds_data.py +95 -0
- bertrend_apps/prospective_demo/i18n.py +136 -0
- bertrend_apps/prospective_demo/llm_utils.py +34 -0
- bertrend_apps/prospective_demo/models_info.py +361 -0
- bertrend_apps/prospective_demo/perf_utils.py +23 -0
- bertrend_apps/prospective_demo/process_new_data.py +348 -0
- bertrend_apps/prospective_demo/report_generation.py +214 -0
- bertrend_apps/prospective_demo/report_template.html +148 -0
- bertrend_apps/prospective_demo/report_utils.py +132 -0
- bertrend_apps/prospective_demo/start_demo.sh +9 -0
- bertrend_apps/prospective_demo/streamlit_utils.py +37 -0
- bertrend_apps/start_apps.sh +15 -0
- bertrend_apps/stop_apps.sh +11 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Copyright (c) 2024, RTE (https://www.rte-france.com)
|
|
2
|
+
# See AUTHORS.txt
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This file is part of BERTrend.
|
|
5
|
+
# Define a pattern for a basic URL validation
|
|
6
|
+
URL_PATTERN = (
|
|
7
|
+
r"^(https?://)?([a-z0-9-]+\.)+[a-z]{2,6}(:\d+)?(/[\w.-]*)*$|"
|
|
8
|
+
r"^(https?://)?(localhost|(\d{1,3}\.){3}\d{1,3})(:\d+)?(/[\w.-]*)*$"
|
|
9
|
+
)
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Copyright (c) 2024, RTE (https://www.rte-france.com)
|
|
2
|
+
# See AUTHORS.txt
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This file is part of BERTrend.
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from datetime import timedelta, datetime
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from loguru import logger
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from bertrend import FEED_BASE_PATH, load_toml_config
|
|
15
|
+
from bertrend_apps.common.crontab_utils import schedule_scrapping
|
|
16
|
+
from bertrend_apps.data_provider.arxiv_provider import ArxivProvider
|
|
17
|
+
from bertrend_apps.data_provider.bing_news_provider import BingNewsProvider
|
|
18
|
+
from bertrend_apps.data_provider.curebot_provider import CurebotProvider
|
|
19
|
+
from bertrend_apps.data_provider.google_news_provider import GoogleNewsProvider
|
|
20
|
+
from bertrend_apps.data_provider.newscatcher_provider import NewsCatcherProvider
|
|
21
|
+
|
|
22
|
+
# Ensures to write with +rw for both user and groups
|
|
23
|
+
os.umask(0o002)
|
|
24
|
+
|
|
25
|
+
PROVIDERS = {
|
|
26
|
+
"arxiv": ArxivProvider,
|
|
27
|
+
"curebot": CurebotProvider,
|
|
28
|
+
"google": GoogleNewsProvider,
|
|
29
|
+
"bing": BingNewsProvider,
|
|
30
|
+
"newscatcher": NewsCatcherProvider,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
app = typer.Typer()
|
|
35
|
+
|
|
36
|
+
@app.command("scrape")
|
|
37
|
+
def scrape(
|
|
38
|
+
keywords: str = typer.Argument(help="keywords for data search engine."),
|
|
39
|
+
provider: str = typer.Option(
|
|
40
|
+
"google", help="source for data [arxiv, google, bing, newscatcher]"
|
|
41
|
+
),
|
|
42
|
+
after: str = typer.Option(
|
|
43
|
+
None, help="date after which to consider news [format YYYY-MM-DD]"
|
|
44
|
+
),
|
|
45
|
+
before: str = typer.Option(
|
|
46
|
+
None, help="date before which to consider news [format YYYY-MM-DD]"
|
|
47
|
+
),
|
|
48
|
+
max_results: int = typer.Option(
|
|
49
|
+
50, help="maximum number of results per request"
|
|
50
|
+
),
|
|
51
|
+
save_path: Path = typer.Option(
|
|
52
|
+
None, help="Path for writing results. File is in jsonl format."
|
|
53
|
+
),
|
|
54
|
+
language: str = typer.Option(None, help="Language filter"),
|
|
55
|
+
):
|
|
56
|
+
"""Scrape data from Arxiv, Google, Bing or NewsCatcher news (single request).
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
keywords: str
|
|
61
|
+
query described as keywords
|
|
62
|
+
provider: str
|
|
63
|
+
News data provider. Current authorized values [google, bing, newscatcher]
|
|
64
|
+
after: str
|
|
65
|
+
"from" date, formatted as YYYY-MM-DD
|
|
66
|
+
before: str
|
|
67
|
+
"to" date, formatted as YYYY-MM-DD
|
|
68
|
+
max_results: int
|
|
69
|
+
Maximum number of results per request
|
|
70
|
+
save_path: Path
|
|
71
|
+
Path to the output file (jsonl format)
|
|
72
|
+
language: str
|
|
73
|
+
Language filter
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
provider_class = PROVIDERS.get(provider)
|
|
80
|
+
provider = provider_class()
|
|
81
|
+
results = provider.get_articles(keywords, after, before, max_results, language)
|
|
82
|
+
provider.store_articles(results, save_path)
|
|
83
|
+
|
|
84
|
+
@app.command("auto-scrape")
|
|
85
|
+
def auto_scrape(
|
|
86
|
+
requests_file: str = typer.Argument(
|
|
87
|
+
help="path of jsonlines input file containing the expected queries."
|
|
88
|
+
),
|
|
89
|
+
max_results: int = typer.Option(
|
|
90
|
+
50, help="maximum number of results per request"
|
|
91
|
+
),
|
|
92
|
+
provider: str = typer.Option(
|
|
93
|
+
"google", help="source for news [google, bing, newscatcher]"
|
|
94
|
+
),
|
|
95
|
+
save_path: Path = typer.Option(None, help="Path for writing results."),
|
|
96
|
+
language: str = typer.Option(None, help="Language filter"),
|
|
97
|
+
):
|
|
98
|
+
"""Scrape data from Arxiv, Google, Bing news or NewsCatcher (multiple requests from a configuration file: each line of the file shall be compliant with the following format:
|
|
99
|
+
<keyword list>;<after_date, format YYYY-MM-DD>;<before_date, format YYYY-MM-DD>)
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
requests_file: str
|
|
104
|
+
Text file containing the list of requests to be processed
|
|
105
|
+
max_results: int
|
|
106
|
+
Maximum number of results per request
|
|
107
|
+
provider: str
|
|
108
|
+
News data provider. Current authorized values [google, bing, newscatcher]
|
|
109
|
+
save_path: Path
|
|
110
|
+
Path to the output file (jsonl format)
|
|
111
|
+
language: str
|
|
112
|
+
Language filter
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
provider_class = PROVIDERS.get(provider)
|
|
119
|
+
provider = provider_class()
|
|
120
|
+
logger.info(f"Opening query file: {requests_file}")
|
|
121
|
+
with open(requests_file) as file:
|
|
122
|
+
try:
|
|
123
|
+
requests = [line.rstrip().split(";") for line in file]
|
|
124
|
+
except:
|
|
125
|
+
logger.error("Bad file format")
|
|
126
|
+
return -1
|
|
127
|
+
results = provider.get_articles_batch(requests, max_results, language)
|
|
128
|
+
logger.info(f"Storing {len(results)} articles")
|
|
129
|
+
provider.store_articles(results, save_path)
|
|
130
|
+
|
|
131
|
+
@app.command("generate-query-file")
|
|
132
|
+
def generate_query_file(
|
|
133
|
+
keywords: str = typer.Argument(help="keywords for news search engine."),
|
|
134
|
+
after: str = typer.Option(
|
|
135
|
+
None, help="date after which to consider news [format YYYY-MM-DD]"
|
|
136
|
+
),
|
|
137
|
+
before: str = typer.Option(
|
|
138
|
+
None, help="date before which to consider news [format YYYY-MM-DD]"
|
|
139
|
+
),
|
|
140
|
+
save_path: str = typer.Option(
|
|
141
|
+
None, help="Path for writing results. File is in jsonl format."
|
|
142
|
+
),
|
|
143
|
+
interval: int = typer.Option(30, help="Range of days of atomic requests"),
|
|
144
|
+
):
|
|
145
|
+
"""Generates a query file to be used with the auto-scrape command. This is useful for queries generating many results.
|
|
146
|
+
This will split the broad query into many ones, each one covering an 'interval' (range) in days covered by each atomic
|
|
147
|
+
request.
|
|
148
|
+
If you want to cover several keywords, run the command several times with the same output file.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
keywords: str
|
|
153
|
+
query described as keywords
|
|
154
|
+
after: str
|
|
155
|
+
"from" date, formatted as YYYY-MM-DD
|
|
156
|
+
before: str
|
|
157
|
+
"to" date, formatted as YYYY-MM-DD
|
|
158
|
+
save_path: str
|
|
159
|
+
Path to the output file (jsonl format)
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
date_format = "%Y-%m-%d"
|
|
166
|
+
start_date = datetime.strptime(after, date_format)
|
|
167
|
+
end_date = datetime.strptime(before, date_format)
|
|
168
|
+
dates_l = list(_daterange(start_date, end_date, interval))
|
|
169
|
+
|
|
170
|
+
with open(save_path, "a") as query_file:
|
|
171
|
+
for elem in dates_l:
|
|
172
|
+
query_file.write(
|
|
173
|
+
f"{keywords};{elem[0].strftime(date_format)};{elem[1].strftime(date_format)}\n"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def _daterange(start_date, end_date, ndays):
|
|
177
|
+
for n in range(int((end_date - start_date).days / ndays)):
|
|
178
|
+
yield (
|
|
179
|
+
start_date + timedelta(ndays * n),
|
|
180
|
+
start_date + timedelta(ndays * (n + 1)),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
@app.command("scrape-feed")
|
|
184
|
+
def scrape_from_feed(
|
|
185
|
+
feed_cfg: Path = typer.Argument(help="Path of the data feed config file"),
|
|
186
|
+
):
|
|
187
|
+
"""Scrape data from Arxiv, Google, Bing news or NewsCatcher on the basis of a feed configuration file"""
|
|
188
|
+
data_feed_cfg = load_toml_config(feed_cfg)
|
|
189
|
+
current_date = datetime.today()
|
|
190
|
+
current_date_str = current_date.strftime("%Y-%m-%d")
|
|
191
|
+
days_to_subtract = data_feed_cfg["data-feed"].get("number_of_days")
|
|
192
|
+
provider = data_feed_cfg["data-feed"].get("provider")
|
|
193
|
+
keywords = data_feed_cfg["data-feed"].get("query")
|
|
194
|
+
max_results = data_feed_cfg["data-feed"].get("max_results")
|
|
195
|
+
before = current_date_str
|
|
196
|
+
after = (current_date - timedelta(days=days_to_subtract)).strftime("%Y-%m-%d")
|
|
197
|
+
language = data_feed_cfg["data-feed"].get("language")
|
|
198
|
+
save_path = (
|
|
199
|
+
FEED_BASE_PATH
|
|
200
|
+
/ data_feed_cfg["data-feed"].get("feed_dir_path")
|
|
201
|
+
/ f"{current_date_str}_{data_feed_cfg['data-feed'].get('id')}.jsonl"
|
|
202
|
+
)
|
|
203
|
+
save_path.parent.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
|
|
205
|
+
# Generate a query file
|
|
206
|
+
with tempfile.NamedTemporaryFile() as query_file:
|
|
207
|
+
if provider == "arxiv" or provider == "curebot": # already returns batches
|
|
208
|
+
scrape(
|
|
209
|
+
keywords=keywords,
|
|
210
|
+
provider=provider,
|
|
211
|
+
after=after,
|
|
212
|
+
before=before,
|
|
213
|
+
max_results=max_results,
|
|
214
|
+
save_path=save_path,
|
|
215
|
+
language=language,
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
generate_query_file(
|
|
219
|
+
keywords, after, before, interval=1, save_path=query_file.name
|
|
220
|
+
)
|
|
221
|
+
auto_scrape(
|
|
222
|
+
requests_file=query_file.name,
|
|
223
|
+
max_results=max_results,
|
|
224
|
+
provider=provider,
|
|
225
|
+
save_path=save_path,
|
|
226
|
+
language=language,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
@app.command("schedule-scrapping")
|
|
230
|
+
def automate_scrapping(
|
|
231
|
+
feed_cfg: Path = typer.Argument(help="Path of the data feed config file"),
|
|
232
|
+
):
|
|
233
|
+
"""Schedule data scrapping on the basis of a feed configuration file"""
|
|
234
|
+
schedule_scrapping(feed_cfg)
|
|
235
|
+
|
|
236
|
+
##################
|
|
237
|
+
app()
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Copyright (c) 2024, RTE (https://www.rte-france.com)
|
|
2
|
+
# See AUTHORS.txt
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This file is part of BERTrend.
|
|
5
|
+
|
|
6
|
+
import itertools
|
|
7
|
+
import os
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
|
|
11
|
+
import arxiv
|
|
12
|
+
|
|
13
|
+
from loguru import logger
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
from bertrend.utils.data_loading import TEXT_COLUMN
|
|
17
|
+
from bertrend_apps.data_provider.data_provider import DataProvider
|
|
18
|
+
from bertrend_apps.data_provider.utils import wait
|
|
19
|
+
|
|
20
|
+
PAGE_SIZE = 2000
|
|
21
|
+
DELAY_SECONDS = 3
|
|
22
|
+
NUM_RETRIES = 10
|
|
23
|
+
|
|
24
|
+
### Request parameters ###
|
|
25
|
+
query = "cat:cs.CL"
|
|
26
|
+
max_results = float("inf")
|
|
27
|
+
|
|
28
|
+
### Parameters Semantic Scholar ###
|
|
29
|
+
SEMANTIC_SCHOLAR_API_KEY = os.getenv("SEMANTIC_SCHOLAR_API_KEY")
|
|
30
|
+
if not SEMANTIC_SCHOLAR_API_KEY:
|
|
31
|
+
raise ValueError("SEMANTIC_SCHOLAR_API_KEY environment variable is not set")
|
|
32
|
+
|
|
33
|
+
PAPERS_PER_REQUEST = 500 # should not be more than 500
|
|
34
|
+
SEMANTIC_SCHOLAR_FIELDS = "title,abstract,citationCount,publicationDate,url"
|
|
35
|
+
|
|
36
|
+
DATE_FORMAT_YYYYMMDD = "%Y-%m-%d"
|
|
37
|
+
DATE_FORMAT_YYYYMMDD_TIME = "%Y-%m-%d %H:%M:%S"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ArxivProvider(DataProvider):
|
|
41
|
+
"""Scientific articles providers for Arxiv"""
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
self.client = arxiv.Client(
|
|
45
|
+
page_size=PAGE_SIZE,
|
|
46
|
+
delay_seconds=DELAY_SECONDS,
|
|
47
|
+
num_retries=NUM_RETRIES,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def get_articles(
|
|
51
|
+
self,
|
|
52
|
+
query: str,
|
|
53
|
+
after: str,
|
|
54
|
+
before: str,
|
|
55
|
+
max_results: int,
|
|
56
|
+
language: str = None,
|
|
57
|
+
) -> list[dict]:
|
|
58
|
+
"""Requests the news data provider, collects a set of URLs to be parsed, return results as json lines.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
query: str
|
|
63
|
+
keywords describing the request
|
|
64
|
+
after: str
|
|
65
|
+
date after which to consider articles, formatted as YYYY-MM-DD, currently ignored
|
|
66
|
+
before: str
|
|
67
|
+
date before which to consider articles, formatted as YYYY-MM-DD, currently ignored
|
|
68
|
+
max_results: int
|
|
69
|
+
Maximum number of results per request
|
|
70
|
+
language: str
|
|
71
|
+
Language filter
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
A list of dict entries, each one describing an article
|
|
76
|
+
"""
|
|
77
|
+
begin = datetime.strptime(after, DATE_FORMAT_YYYYMMDD)
|
|
78
|
+
end = datetime.strptime(before, DATE_FORMAT_YYYYMMDD)
|
|
79
|
+
|
|
80
|
+
logger.info(f"Querying Arxiv: {query}")
|
|
81
|
+
entries = list(
|
|
82
|
+
self.client.results(
|
|
83
|
+
arxiv.Search(
|
|
84
|
+
query=query,
|
|
85
|
+
max_results=max_results,
|
|
86
|
+
sort_by=arxiv.SortCriterion.SubmittedDate,
|
|
87
|
+
sort_order=arxiv.SortOrder.Descending,
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
results = self.process_entries(entries, language)
|
|
92
|
+
# post-filtering by date
|
|
93
|
+
results = [
|
|
94
|
+
res
|
|
95
|
+
for res in results
|
|
96
|
+
if begin
|
|
97
|
+
<= datetime.strptime(res["timestamp"], DATE_FORMAT_YYYYMMDD_TIME)
|
|
98
|
+
<= end
|
|
99
|
+
]
|
|
100
|
+
# add citations count
|
|
101
|
+
return self.add_citations_count(results)
|
|
102
|
+
|
|
103
|
+
def _parse_entry(self, entry: arxiv.Result) -> dict | None:
|
|
104
|
+
"""Parses a Arxiv entry"""
|
|
105
|
+
try:
|
|
106
|
+
id = entry.entry_id
|
|
107
|
+
title = entry.title
|
|
108
|
+
published = entry.published.strftime(
|
|
109
|
+
"%Y-%m-%d %H:%M:%S"
|
|
110
|
+
) # dateparser.parse(entry.published).strftime("%Y-%m-%d %H:%M:%S")
|
|
111
|
+
logger.debug(f"----- Title: {title},\tDate: {published}")
|
|
112
|
+
return {
|
|
113
|
+
"id": id,
|
|
114
|
+
"title": title,
|
|
115
|
+
"summary": entry.summary,
|
|
116
|
+
"text": entry.summary,
|
|
117
|
+
"timestamp": published,
|
|
118
|
+
}
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.error(str(e) + f"\nError occurred with parsing of: {entry}")
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
@wait(1)
|
|
124
|
+
def _request_semantic_scholar_chunk(self, chunk: list[dict]):
|
|
125
|
+
"""Get information from semantic scholar API per batch of articles IDs"""
|
|
126
|
+
ids_list = ["URL:" + entry["id"] for entry in chunk]
|
|
127
|
+
response = requests.post(
|
|
128
|
+
"https://api.semanticscholar.org/graph/v1/paper/batch",
|
|
129
|
+
params={"fields": SEMANTIC_SCHOLAR_FIELDS},
|
|
130
|
+
json={"ids": ids_list},
|
|
131
|
+
headers={"x-api-key": SEMANTIC_SCHOLAR_API_KEY},
|
|
132
|
+
)
|
|
133
|
+
return [item for item in response.json() if item is not None]
|
|
134
|
+
|
|
135
|
+
def add_citations_count(self, entries: list[dict]):
|
|
136
|
+
"""Uses the semantic_scholar API to get the number of counts per paper"""
|
|
137
|
+
# split list into chunks and request semantic scholar
|
|
138
|
+
chunks = [
|
|
139
|
+
entries[i : i + PAPERS_PER_REQUEST]
|
|
140
|
+
for i in range(0, len(entries), PAPERS_PER_REQUEST)
|
|
141
|
+
]
|
|
142
|
+
# request API
|
|
143
|
+
semantic_scholar_items_list = list(
|
|
144
|
+
itertools.chain(
|
|
145
|
+
[self._request_semantic_scholar_chunk(chunk) for chunk in chunks]
|
|
146
|
+
)
|
|
147
|
+
)[0]
|
|
148
|
+
|
|
149
|
+
# merge semantic scholar entries with arxiv entries
|
|
150
|
+
|
|
151
|
+
# update values of entries
|
|
152
|
+
d = defaultdict(dict)
|
|
153
|
+
|
|
154
|
+
for item in semantic_scholar_items_list + entries:
|
|
155
|
+
# careful, order is important, semantic_scholar_items may contain fewer items than entries
|
|
156
|
+
d[item["title"]].update(item)
|
|
157
|
+
|
|
158
|
+
# filter out possible missing values
|
|
159
|
+
new_entries = [item for item in list(d.values()) if TEXT_COLUMN in item.keys()]
|
|
160
|
+
|
|
161
|
+
return new_entries
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Copyright (c) 2024, RTE (https://www.rte-france.com)
|
|
2
|
+
# See AUTHORS.txt
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This file is part of BERTrend.
|
|
5
|
+
|
|
6
|
+
import urllib.parse
|
|
7
|
+
|
|
8
|
+
import dateparser
|
|
9
|
+
import feedparser
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
from bertrend_apps.data_provider.data_provider import DataProvider
|
|
13
|
+
from bertrend_apps.data_provider.utils import wait
|
|
14
|
+
|
|
15
|
+
PATTERN = "{QUERY}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BingNewsProvider(DataProvider):
|
|
19
|
+
"""News provider for Bing News.
|
|
20
|
+
Limitations:
|
|
21
|
+
- since of results limited to 12
|
|
22
|
+
- hard to request specific dates
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
URL_ENDPOINT = f"https://www.bing.com/news/search?q={PATTERN}&format=rss&setLang=fr&sortBy=Date"
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
super().__init__()
|
|
29
|
+
|
|
30
|
+
@wait(0.2)
|
|
31
|
+
def get_articles(
|
|
32
|
+
self,
|
|
33
|
+
query: str,
|
|
34
|
+
after: str,
|
|
35
|
+
before: str,
|
|
36
|
+
max_results: int,
|
|
37
|
+
language: str = None,
|
|
38
|
+
) -> list[dict]:
|
|
39
|
+
"""Requests the news data provider, collects a set of URLs to be parsed, return results as json lines"""
|
|
40
|
+
q = self._build_query(query, after, before)
|
|
41
|
+
logger.info(f"Querying Bing: {q}")
|
|
42
|
+
result = feedparser.parse(q)
|
|
43
|
+
entries = result["entries"][:max_results]
|
|
44
|
+
return self.process_entries(entries, language)
|
|
45
|
+
|
|
46
|
+
def _build_query(self, keywords: str, after: str = None, before: str = None) -> str:
|
|
47
|
+
# FIXME: don't know how to use after/before parameters with Bing news queries
|
|
48
|
+
query = self.URL_ENDPOINT.replace(PATTERN, f"{urllib.parse.quote(keywords)}")
|
|
49
|
+
return query
|
|
50
|
+
|
|
51
|
+
def _clean_url(self, bing_url) -> str:
|
|
52
|
+
"""Clean encoded URLs returned by Bing news such as "http://www.bing.com/news/apiclick.aspx?ref=FexRss&aid=&tid=649475a6257945d6900378c8310bcfde&url=https%3a%2f%2fwww.lemondeinformatique.fr%2factualites%2flire-avec-schema-gpt-translator-datastax-automatise-la-creation-de-pipelines-de-donnees-90737.html&c=15009376565431680830&mkt=fr-fr" """
|
|
53
|
+
try:
|
|
54
|
+
clean_url = bing_url.split("url=")[1].split("& ")[0]
|
|
55
|
+
return urllib.parse.unquote(clean_url)
|
|
56
|
+
except IndexError:
|
|
57
|
+
# fallback (the URL does not match the expected pattern)
|
|
58
|
+
return bing_url
|
|
59
|
+
|
|
60
|
+
def _parse_entry(self, entry: dict) -> dict | None:
|
|
61
|
+
"""Parses a Bing news entry, uses wait decorator to force delay between 2 successive calls"""
|
|
62
|
+
try:
|
|
63
|
+
link = entry["link"]
|
|
64
|
+
url = self._clean_url(link)
|
|
65
|
+
summary = entry["summary"]
|
|
66
|
+
published = dateparser.parse(entry["published"]).strftime(
|
|
67
|
+
"%Y-%m-%d %H:%M:%S"
|
|
68
|
+
)
|
|
69
|
+
text, title = self._get_text(url=link)
|
|
70
|
+
return {
|
|
71
|
+
"title": title,
|
|
72
|
+
"summary": summary,
|
|
73
|
+
"link": link,
|
|
74
|
+
"url": url,
|
|
75
|
+
"text": text,
|
|
76
|
+
"timestamp": published,
|
|
77
|
+
}
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(
|
|
80
|
+
str(e) + f"\nError occurred with text parsing of url in : {entry}"
|
|
81
|
+
)
|
|
82
|
+
return None
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Copyright (c) 2024, RTE (https://www.rte-france.com)
|
|
2
|
+
# See AUTHORS.txt
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This file is part of BERTrend.
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from bertrend_apps.data_provider import URL_PATTERN
|
|
12
|
+
from bertrend_apps.data_provider.data_provider import DataProvider
|
|
13
|
+
import feedparser
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CurebotProvider(DataProvider):
|
|
17
|
+
"""Class used to process exports from Curebot tool"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, curebot_export_file: Path = None, feed_url: str = None):
|
|
20
|
+
super().__init__()
|
|
21
|
+
self.data_file = curebot_export_file
|
|
22
|
+
if self.data_file:
|
|
23
|
+
self.df_dict = pd.read_excel(self.data_file, sheet_name=None, dtype=str)
|
|
24
|
+
else:
|
|
25
|
+
self.df_dict = None
|
|
26
|
+
self.feed_url = feed_url
|
|
27
|
+
|
|
28
|
+
def get_articles(
|
|
29
|
+
self,
|
|
30
|
+
query: str = None,
|
|
31
|
+
after: str = None,
|
|
32
|
+
before: str = None,
|
|
33
|
+
max_results: int = None,
|
|
34
|
+
language: str = "fr",
|
|
35
|
+
) -> list[dict]:
|
|
36
|
+
"""Requests the news data provider, collects a set of URLs to be parsed, return results as json lines"""
|
|
37
|
+
if query and re.match(URL_PATTERN, query):
|
|
38
|
+
# if using a config file, the "query" field may contain the feed url
|
|
39
|
+
self.feed_url = query
|
|
40
|
+
if self.feed_url:
|
|
41
|
+
return self.parse_ATOM_feed()
|
|
42
|
+
|
|
43
|
+
if self.df_dict:
|
|
44
|
+
entries = []
|
|
45
|
+
for k in self.df_dict.keys():
|
|
46
|
+
entries += self.df_dict[k].to_dict(orient="records")
|
|
47
|
+
results = [self._parse_entry(res) for res in entries]
|
|
48
|
+
return [
|
|
49
|
+
res for res in results if res is not None
|
|
50
|
+
] # sanity check to remove errors
|
|
51
|
+
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
def parse_ATOM_feed(self) -> list[dict]:
|
|
55
|
+
feed = feedparser.parse(self.feed_url)
|
|
56
|
+
# Initialize an empty list to store the entries
|
|
57
|
+
entries = []
|
|
58
|
+
# Extract information for each entry
|
|
59
|
+
for entry in feed.entries:
|
|
60
|
+
link = entry.get("link", "")
|
|
61
|
+
summary = entry.get("summary", "")
|
|
62
|
+
published = entry.get("published", "")
|
|
63
|
+
formatted_date = self.parse_date(published)
|
|
64
|
+
text, title = self._get_text(url=link)
|
|
65
|
+
if text is None or text == "":
|
|
66
|
+
continue
|
|
67
|
+
# Store the extracted information in a dictionary
|
|
68
|
+
entry_info = {
|
|
69
|
+
"title": title,
|
|
70
|
+
"link": link,
|
|
71
|
+
"url": link,
|
|
72
|
+
"summary": summary,
|
|
73
|
+
"text": text,
|
|
74
|
+
"timestamp": formatted_date,
|
|
75
|
+
}
|
|
76
|
+
# Add the dictionary to the list
|
|
77
|
+
entries.append(entry_info)
|
|
78
|
+
|
|
79
|
+
return entries
|
|
80
|
+
|
|
81
|
+
def _parse_entry(self, entry: dict) -> dict | None:
|
|
82
|
+
"""Parses a Curebot news entry"""
|
|
83
|
+
try:
|
|
84
|
+
# NB. we do not use the title from Gnews as it is sometimes truncated
|
|
85
|
+
link = entry["URL de la ressource"]
|
|
86
|
+
url = link
|
|
87
|
+
summary = entry["Contenu de la ressource"]
|
|
88
|
+
published = self.parse_date(entry["Date de trouvaille"])
|
|
89
|
+
text, title = self._get_text(url=url)
|
|
90
|
+
if text is None or text == "":
|
|
91
|
+
return None
|
|
92
|
+
logger.debug(f"----- Title: {title},\tDate: {published}")
|
|
93
|
+
return {
|
|
94
|
+
"title": title,
|
|
95
|
+
"summary": summary,
|
|
96
|
+
"link": link,
|
|
97
|
+
"url": url,
|
|
98
|
+
"text": text,
|
|
99
|
+
"timestamp": published,
|
|
100
|
+
}
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(
|
|
103
|
+
str(e) + f"\nError occurred with text parsing of url in : {entry}"
|
|
104
|
+
)
|
|
105
|
+
return None
|