IO4IT 4.0.2__tar.gz → 4.0.4__tar.gz
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.
- {io4it-4.0.2 → io4it-4.0.4}/PKG-INFO +1 -1
- {io4it-4.0.2 → io4it-4.0.4}/io4it.egg-info/PKG-INFO +1 -1
- io4it-4.0.4/orangecontrib/IO4IT/widgets/OWWebSearch.py +647 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owwebsearch.ui +71 -4
- {io4it-4.0.2 → io4it-4.0.4}/setup.py +1 -1
- io4it-4.0.2/orangecontrib/IO4IT/widgets/OWWebSearch.py +0 -307
- {io4it-4.0.2 → io4it-4.0.4}/io4it.egg-info/SOURCES.txt +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/io4it.egg-info/dependency_links.txt +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/io4it.egg-info/entry_points.txt +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/io4it.egg-info/namespace_packages.txt +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/io4it.egg-info/requires.txt +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/io4it.egg-info/top_level.txt +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/__init__.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/ocr_function/__init__.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/ocr_function/word_converter.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/toolViews/designer/key_manager_ui.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/toolViews/designer/link_workflow_manager_ui.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/toolViews/key_manager_ui.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/toolViews/linkHTLMWorkflow_manager_ui.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/utils/__init__.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/utils/keys_manager.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/utils/mail.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/utils/pool_exec_utils.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/utils/secret_manager.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/utils/utils_md.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWChatGpt.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWDeep_Search.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWDoclingToMarkdown.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWExportMarkdown.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWExtractTablesDocxToXlsx.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWInboxMailMonitoring.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWJsonToDataTable.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWMD2HTML.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWMarkdownLoader.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWMarkdownizer.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWOfficeNormalizer.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWParserHTML.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWPdfType.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWProcessPoolExecutor.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWS3Uploader.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWS3downloader.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWS3list.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWmailLoader.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWmailSender.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWwordpdf2docx.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/__init__.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/__init__.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/nogui.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/ow_extract_tables_docx_to_xlsx.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/ow_file_ext_selector.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/ow_json_to_data_table.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owchatgpt.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owdeepsearch.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owdoclingasr.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owdoclingtomarkdown.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owexportmarkdown.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owinboxmailmonitoring.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owmailloader.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owmailsender.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owmarkdownizer.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owmarkdownloader.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owmd2html.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owofficenormalizer.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owparserhtml.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owpdftype.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owprocesspoolexecutor.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owspeechtotext.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owvisualizationer.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/wordpdf2docx.ui +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/__init__.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/chatgpt.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/check_pdf.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/deepsearch.svg +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/dep_md_old.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/download.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/export_md.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/extract_table.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/file_extensor.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/html.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/json-file.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/list_aws.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/load_md.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/mail_loader.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/mail_writer.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/md.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/monitor-email.svg +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/office_normalizer.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/owmd2html.svg +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/process_pool_executor.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/speech_to_text.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/upload.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/visualizationer.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/websearch.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/wordpdf2docx.png +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons_dev/__init__.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/__init__.py +0 -0
- {io4it-4.0.2 → io4it-4.0.4}/setup.cfg +0 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
import unicodedata
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
import Orange
|
|
9
|
+
from Orange.widgets.widget import Input, Output
|
|
10
|
+
from AnyQt.QtWidgets import QApplication, QPushButton, QLabel, QWidget, QVBoxLayout, QGroupBox
|
|
11
|
+
from AnyQt.QtCore import QUrl, QTimer, Signal
|
|
12
|
+
from Orange.widgets.settings import Setting
|
|
13
|
+
from AnyQt.QtWebEngineWidgets import QWebEngineView
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
from bs4 import BeautifulSoup
|
|
17
|
+
from urllib.parse import quote, urlparse, urljoin, parse_qs
|
|
18
|
+
|
|
19
|
+
if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
|
|
20
|
+
from Orange.widgets.orangecontrib.AAIT.utils import thread_management, base_widget
|
|
21
|
+
from Orange.widgets.orangecontrib.HLIT_dev.remote_server_smb import convert
|
|
22
|
+
else:
|
|
23
|
+
from orangecontrib.AAIT.utils import thread_management, base_widget
|
|
24
|
+
from orangecontrib.HLIT_dev.remote_server_smb import convert
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WebSearch(base_widget.BaseListWidget):
|
|
31
|
+
name = "WebSearch"
|
|
32
|
+
description = "Search url website from a query with DDG."
|
|
33
|
+
icon = "icons/websearch.png"
|
|
34
|
+
if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
|
|
35
|
+
icon = "icons_dev/websearch.png"
|
|
36
|
+
priority = 3000
|
|
37
|
+
gui = ""
|
|
38
|
+
want_control_area = False
|
|
39
|
+
category = "AAIT - TOOLBOX"
|
|
40
|
+
gui = os.path.join(os.path.dirname(os.path.abspath(__file__)), "designer/owwebsearch.ui")
|
|
41
|
+
|
|
42
|
+
selected_column_name = Setting("content")
|
|
43
|
+
|
|
44
|
+
browser_request_signal = Signal(str, int)
|
|
45
|
+
|
|
46
|
+
class Inputs:
|
|
47
|
+
data = Input("Data", Orange.data.Table)
|
|
48
|
+
|
|
49
|
+
class Outputs:
|
|
50
|
+
data = Output("Data", Orange.data.Table)
|
|
51
|
+
|
|
52
|
+
@Inputs.data
|
|
53
|
+
def set_data(self, in_data):
|
|
54
|
+
self.data = in_data
|
|
55
|
+
if in_data is None:
|
|
56
|
+
self.Outputs.data.send(None)
|
|
57
|
+
return
|
|
58
|
+
if self.data:
|
|
59
|
+
self.var_selector.add_variables(self.data.domain)
|
|
60
|
+
self.var_selector.select_variable_by_name(self.selected_column_name)
|
|
61
|
+
self.run()
|
|
62
|
+
|
|
63
|
+
def __init__(self):
|
|
64
|
+
super().__init__()
|
|
65
|
+
|
|
66
|
+
self.setFixedWidth(480)
|
|
67
|
+
self.setFixedHeight(760)
|
|
68
|
+
|
|
69
|
+
self.pushButton_run = self.findChild(QPushButton, 'pushButton_send')
|
|
70
|
+
self.pushButton_run.clicked.connect(self.run)
|
|
71
|
+
|
|
72
|
+
# UI viewer embarqué
|
|
73
|
+
self.groupBox_browser = self.findChild(QGroupBox, "groupBox_browser")
|
|
74
|
+
self.label_browser_status = self.findChild(QLabel, "label_browser_status")
|
|
75
|
+
self.web_placeholder = self.findChild(QWidget, "web_placeholder")
|
|
76
|
+
self.pushButton_browser_check = self.findChild(QPushButton, "pushButton_browser_check")
|
|
77
|
+
self.pushButton_browser_close = self.findChild(QPushButton, "pushButton_browser_close")
|
|
78
|
+
|
|
79
|
+
self.browser_view = None
|
|
80
|
+
self.browser_timer = QTimer(self)
|
|
81
|
+
self.browser_timer.timeout.connect(self._browser_periodic_check)
|
|
82
|
+
|
|
83
|
+
self._browser_request_event = None
|
|
84
|
+
self._browser_request_data = None
|
|
85
|
+
self._browser_wait_deadline = 0.0
|
|
86
|
+
self._browser_active = False
|
|
87
|
+
self._browser_last_html = ""
|
|
88
|
+
|
|
89
|
+
# Cookies du viewer Qt -> requests.Session
|
|
90
|
+
self.browser_cookies = []
|
|
91
|
+
self.browser_cookie_lock = threading.Lock()
|
|
92
|
+
|
|
93
|
+
self.browser_request_signal.connect(self._open_browser_for_query)
|
|
94
|
+
self._init_embedded_browser_ui()
|
|
95
|
+
|
|
96
|
+
def _init_embedded_browser_ui(self):
|
|
97
|
+
if self.groupBox_browser is not None:
|
|
98
|
+
self.groupBox_browser.hide()
|
|
99
|
+
|
|
100
|
+
if self.pushButton_browser_check is not None:
|
|
101
|
+
self.pushButton_browser_check.clicked.connect(self._browser_check_current_page)
|
|
102
|
+
|
|
103
|
+
if self.pushButton_browser_close is not None:
|
|
104
|
+
self.pushButton_browser_close.clicked.connect(self._browser_cancel_request)
|
|
105
|
+
|
|
106
|
+
if QWebEngineView is None:
|
|
107
|
+
if self.label_browser_status is not None:
|
|
108
|
+
self.label_browser_status.setText("QWebEngineView is not available.")
|
|
109
|
+
if self.pushButton_browser_check is not None:
|
|
110
|
+
self.pushButton_browser_check.setEnabled(False)
|
|
111
|
+
if self.pushButton_browser_close is not None:
|
|
112
|
+
self.pushButton_browser_close.setEnabled(False)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
if self.web_placeholder is None:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
layout = self.web_placeholder.layout()
|
|
119
|
+
if layout is None:
|
|
120
|
+
layout = QVBoxLayout(self.web_placeholder)
|
|
121
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
122
|
+
layout.setSpacing(0)
|
|
123
|
+
|
|
124
|
+
self.browser_view = QWebEngineView(self.web_placeholder)
|
|
125
|
+
layout.addWidget(self.browser_view)
|
|
126
|
+
self.browser_view.hide()
|
|
127
|
+
self.browser_view.loadFinished.connect(self._browser_on_load_finished)
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
cookie_store = self.browser_view.page().profile().cookieStore()
|
|
131
|
+
cookie_store.cookieAdded.connect(self._on_cookie_added)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
print(f"[WARN] Impossible de brancher le cookie store Qt: {e}")
|
|
134
|
+
|
|
135
|
+
def _on_cookie_added(self, cookie):
|
|
136
|
+
try:
|
|
137
|
+
name = bytes(cookie.name()).decode("utf-8", errors="ignore")
|
|
138
|
+
value = bytes(cookie.value()).decode("utf-8", errors="ignore")
|
|
139
|
+
domain = cookie.domain() if hasattr(cookie, "domain") else ""
|
|
140
|
+
path = cookie.path() if hasattr(cookie, "path") else "/"
|
|
141
|
+
|
|
142
|
+
item = {
|
|
143
|
+
"name": name,
|
|
144
|
+
"value": value,
|
|
145
|
+
"domain": domain or "",
|
|
146
|
+
"path": path or "/",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
with self.browser_cookie_lock:
|
|
150
|
+
replaced = False
|
|
151
|
+
for i, existing in enumerate(self.browser_cookies):
|
|
152
|
+
if (
|
|
153
|
+
existing["name"] == item["name"]
|
|
154
|
+
and existing["domain"] == item["domain"]
|
|
155
|
+
and existing["path"] == item["path"]
|
|
156
|
+
):
|
|
157
|
+
self.browser_cookies[i] = item
|
|
158
|
+
replaced = True
|
|
159
|
+
break
|
|
160
|
+
if not replaced:
|
|
161
|
+
self.browser_cookies.append(item)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
print(f"[WARN] _on_cookie_added error: {e}")
|
|
164
|
+
|
|
165
|
+
def _copy_qt_cookies_to_session(self, session):
|
|
166
|
+
with self.browser_cookie_lock:
|
|
167
|
+
cookies_snapshot = list(self.browser_cookies)
|
|
168
|
+
|
|
169
|
+
for c in cookies_snapshot:
|
|
170
|
+
try:
|
|
171
|
+
name = c.get("name", "")
|
|
172
|
+
value = c.get("value", "")
|
|
173
|
+
domain = c.get("domain", "")
|
|
174
|
+
path = c.get("path", "/") or "/"
|
|
175
|
+
|
|
176
|
+
if not name:
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
if domain:
|
|
180
|
+
session.cookies.set(name, value, domain=domain, path=path)
|
|
181
|
+
# variante sans le point initial éventuel
|
|
182
|
+
if domain.startswith("."):
|
|
183
|
+
session.cookies.set(name, value, domain=domain.lstrip("."), path=path)
|
|
184
|
+
else:
|
|
185
|
+
session.cookies.set(name, value, path=path)
|
|
186
|
+
except Exception as e:
|
|
187
|
+
print(f"[WARN] copy cookie error: {e}")
|
|
188
|
+
|
|
189
|
+
def _get_browser_user_agent(self):
|
|
190
|
+
try:
|
|
191
|
+
if self.browser_view is not None:
|
|
192
|
+
return self.browser_view.page().profile().httpUserAgent()
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
return "Mozilla/5.0"
|
|
196
|
+
|
|
197
|
+
def _is_duckduckgo_challenge(self, html):
|
|
198
|
+
if not html:
|
|
199
|
+
return False
|
|
200
|
+
h = html.lower()
|
|
201
|
+
markers = [
|
|
202
|
+
"anomaly-modal",
|
|
203
|
+
"bots use duckduckgo too",
|
|
204
|
+
"please complete the following challenge",
|
|
205
|
+
"select all squares containing",
|
|
206
|
+
"challenge-form",
|
|
207
|
+
"anomaly.js",
|
|
208
|
+
]
|
|
209
|
+
return any(m in h for m in markers)
|
|
210
|
+
|
|
211
|
+
def _parse_ddg_results_from_html(self, html, max_results):
|
|
212
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
213
|
+
resultats = []
|
|
214
|
+
|
|
215
|
+
for a in soup.select("a.result__a")[:max_results]:
|
|
216
|
+
titre = a.get_text(strip=True)
|
|
217
|
+
lien = a.get("href", "")
|
|
218
|
+
|
|
219
|
+
parsed = urlparse(lien)
|
|
220
|
+
if parsed.netloc and "duckduckgo.com" in parsed.netloc and parsed.path.startswith("/l/"):
|
|
221
|
+
qs = parse_qs(parsed.query)
|
|
222
|
+
if "uddg" in qs:
|
|
223
|
+
lien = qs["uddg"][0]
|
|
224
|
+
|
|
225
|
+
if lien.startswith("/"):
|
|
226
|
+
lien = urljoin("https://duckduckgo.com", lien)
|
|
227
|
+
|
|
228
|
+
resultats.append((titre, lien))
|
|
229
|
+
|
|
230
|
+
return resultats
|
|
231
|
+
|
|
232
|
+
def _open_browser_for_query(self, query, wait_timeout):
|
|
233
|
+
if self._browser_request_data is None:
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
if self.browser_view is None:
|
|
237
|
+
self._browser_finish_request(html=None, cancelled=True, message="QWebEngineView unavailable.")
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
q = quote(query)
|
|
241
|
+
url = f"https://duckduckgo.com/html/?q={q}"
|
|
242
|
+
|
|
243
|
+
self._browser_active = True
|
|
244
|
+
self._browser_last_html = ""
|
|
245
|
+
self._browser_wait_deadline = time.time() + wait_timeout
|
|
246
|
+
|
|
247
|
+
if self.groupBox_browser is not None:
|
|
248
|
+
self.groupBox_browser.show()
|
|
249
|
+
|
|
250
|
+
if self.label_browser_status is not None:
|
|
251
|
+
self.label_browser_status.setText(
|
|
252
|
+
"Captcha detected.\n"
|
|
253
|
+
"Solve it in the embedded browser."
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
self.browser_view.show()
|
|
257
|
+
self.browser_view.setUrl(QUrl(url))
|
|
258
|
+
self.browser_timer.start(2000)
|
|
259
|
+
|
|
260
|
+
def _browser_on_load_finished(self, ok):
|
|
261
|
+
if not self._browser_active:
|
|
262
|
+
return
|
|
263
|
+
if not ok:
|
|
264
|
+
if self.label_browser_status is not None:
|
|
265
|
+
self.label_browser_status.setText("DuckDuckGo load error.")
|
|
266
|
+
return
|
|
267
|
+
self._browser_check_current_page()
|
|
268
|
+
|
|
269
|
+
def _browser_periodic_check(self):
|
|
270
|
+
if not self._browser_active:
|
|
271
|
+
self.browser_timer.stop()
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
if time.time() >= self._browser_wait_deadline:
|
|
275
|
+
self._browser_finish_request(html=None, cancelled=True, message="Captcha timeout.")
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
self._browser_check_current_page()
|
|
279
|
+
|
|
280
|
+
def _browser_check_current_page(self):
|
|
281
|
+
if not self._browser_active or self.browser_view is None:
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
page = self.browser_view.page()
|
|
286
|
+
if page is None:
|
|
287
|
+
return
|
|
288
|
+
page.toHtml(self._browser_process_html)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
if self.label_browser_status is not None:
|
|
291
|
+
self.label_browser_status.setText(f"Check error: {e}")
|
|
292
|
+
|
|
293
|
+
def _browser_process_html(self, html):
|
|
294
|
+
if not self._browser_active:
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
html = html or ""
|
|
298
|
+
self._browser_last_html = html
|
|
299
|
+
|
|
300
|
+
if self._is_duckduckgo_challenge(html):
|
|
301
|
+
if self.label_browser_status is not None:
|
|
302
|
+
self.label_browser_status.setText(
|
|
303
|
+
"Captcha still present.\n"
|
|
304
|
+
"Solve it, then wait for automatic detection."
|
|
305
|
+
)
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
if self.label_browser_status is not None:
|
|
309
|
+
self.label_browser_status.setText("Results detected.")
|
|
310
|
+
|
|
311
|
+
self._browser_finish_request(html=html, cancelled=False, message=None)
|
|
312
|
+
|
|
313
|
+
def _browser_cancel_request(self):
|
|
314
|
+
self._browser_finish_request(html=None, cancelled=True, message="Validation cancelled.")
|
|
315
|
+
|
|
316
|
+
def _browser_finish_request(self, html=None, cancelled=False, message=None):
|
|
317
|
+
self.browser_timer.stop()
|
|
318
|
+
self._browser_active = False
|
|
319
|
+
|
|
320
|
+
if self.browser_view is not None:
|
|
321
|
+
try:
|
|
322
|
+
self.browser_view.stop()
|
|
323
|
+
except Exception:
|
|
324
|
+
pass
|
|
325
|
+
try:
|
|
326
|
+
self.browser_view.setUrl(QUrl("about:blank"))
|
|
327
|
+
except Exception:
|
|
328
|
+
pass
|
|
329
|
+
self.browser_view.hide()
|
|
330
|
+
|
|
331
|
+
if self.groupBox_browser is not None:
|
|
332
|
+
self.groupBox_browser.hide()
|
|
333
|
+
|
|
334
|
+
if self.label_browser_status is not None:
|
|
335
|
+
if message:
|
|
336
|
+
self.label_browser_status.setText(message)
|
|
337
|
+
else:
|
|
338
|
+
self.label_browser_status.setText(
|
|
339
|
+
"The embedded browser will appear here only if manual validation is needed."
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if self._browser_request_data is not None:
|
|
343
|
+
self._browser_request_data["html"] = html
|
|
344
|
+
self._browser_request_data["cancelled"] = cancelled
|
|
345
|
+
|
|
346
|
+
if self._browser_request_event is not None:
|
|
347
|
+
self._browser_request_event.set()
|
|
348
|
+
|
|
349
|
+
def normaliser_texte(self, txt: str) -> str:
|
|
350
|
+
"""
|
|
351
|
+
Met en minuscules, retire les accents et trim.
|
|
352
|
+
"""
|
|
353
|
+
if not txt:
|
|
354
|
+
return ""
|
|
355
|
+
txt = txt.lower()
|
|
356
|
+
txt = unicodedata.normalize("NFD", txt)
|
|
357
|
+
txt = "".join(c for c in txt if not unicodedata.combining(c))
|
|
358
|
+
return txt.strip()
|
|
359
|
+
|
|
360
|
+
def extraire_mots_cles(self, requete: str):
|
|
361
|
+
"""
|
|
362
|
+
Découpe la requête en mots-clés simples, en retirant
|
|
363
|
+
les mots très fréquents (du, de, le, la, etc.).
|
|
364
|
+
"""
|
|
365
|
+
stopwords = {"du", "de", "des", "le", "la", "les", "un", "une", "au", "aux", "et", "en", "pour", "sur", "a"}
|
|
366
|
+
req_norm = self.normaliser_texte(requete)
|
|
367
|
+
mots = re.findall(r"\w+", req_norm)
|
|
368
|
+
mots_cles = [m for m in mots if m not in stopwords and len(m) > 2]
|
|
369
|
+
return mots_cles or mots
|
|
370
|
+
|
|
371
|
+
def recherche_duckduckgo(self, query, max_results=10):
|
|
372
|
+
q = quote(query)
|
|
373
|
+
url = f"https://duckduckgo.com/html/?q={q}"
|
|
374
|
+
|
|
375
|
+
session = requests.Session()
|
|
376
|
+
headers = {
|
|
377
|
+
"User-Agent": self._get_browser_user_agent(),
|
|
378
|
+
# "Accept-Language": "fr-FR,fr;q=0.9,en;q=0.8",
|
|
379
|
+
# "Referer": "https://duckduckgo.com/",
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
def do_request():
|
|
383
|
+
r = session.get(url, headers=headers, timeout=10)
|
|
384
|
+
r.raise_for_status()
|
|
385
|
+
return r.text
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
r = session.get(url, headers=headers, timeout=10)
|
|
389
|
+
r.raise_for_status()
|
|
390
|
+
except requests.RequestException as e:
|
|
391
|
+
print(f"[ERREUR] Problème lors de la requête DuckDuckGo : {e}")
|
|
392
|
+
return []
|
|
393
|
+
|
|
394
|
+
html = r.text
|
|
395
|
+
|
|
396
|
+
# Cas normal = ton code d'origine
|
|
397
|
+
if not self._is_duckduckgo_challenge(html):
|
|
398
|
+
return self._parse_ddg_results_from_html(html, max_results)
|
|
399
|
+
|
|
400
|
+
# Cas captcha = viewer Qt
|
|
401
|
+
print("[INFO] Challenge DuckDuckGo détecté.")
|
|
402
|
+
print("[INFO] Ouverture du navigateur embarqué pour résolution manuelle...")
|
|
403
|
+
|
|
404
|
+
if QWebEngineView is None or self.browser_view is None:
|
|
405
|
+
print("[ERREUR] QWebEngineView non disponible.")
|
|
406
|
+
return []
|
|
407
|
+
|
|
408
|
+
wait_event = threading.Event()
|
|
409
|
+
request_data = {
|
|
410
|
+
"html": None,
|
|
411
|
+
"cancelled": False,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
self._browser_request_event = wait_event
|
|
415
|
+
self._browser_request_data = request_data
|
|
416
|
+
|
|
417
|
+
self.browser_request_signal.emit(query, 120)
|
|
418
|
+
wait_event.wait(125)
|
|
419
|
+
|
|
420
|
+
data = self._browser_request_data
|
|
421
|
+
self._browser_request_event = None
|
|
422
|
+
self._browser_request_data = None
|
|
423
|
+
|
|
424
|
+
if not data or data.get("cancelled"):
|
|
425
|
+
print("[ERREUR] Validation DuckDuckGo annulée ou timeout.")
|
|
426
|
+
return []
|
|
427
|
+
|
|
428
|
+
viewer_html = data.get("html") or ""
|
|
429
|
+
if not viewer_html:
|
|
430
|
+
print("[ERREUR] HTML vide après validation.")
|
|
431
|
+
return []
|
|
432
|
+
|
|
433
|
+
if self._is_duckduckgo_challenge(viewer_html):
|
|
434
|
+
print("[ERREUR] Captcha toujours présent après validation.")
|
|
435
|
+
return []
|
|
436
|
+
|
|
437
|
+
# On copie les cookies Qt -> requests.Session
|
|
438
|
+
self._copy_qt_cookies_to_session(session)
|
|
439
|
+
|
|
440
|
+
# 1) on tente de relancer requests avec les mêmes cookies
|
|
441
|
+
try:
|
|
442
|
+
html_after = do_request()
|
|
443
|
+
if not self._is_duckduckgo_challenge(html_after):
|
|
444
|
+
resultats = self._parse_ddg_results_from_html(html_after, max_results)
|
|
445
|
+
if resultats:
|
|
446
|
+
return resultats
|
|
447
|
+
except requests.RequestException as e:
|
|
448
|
+
print(f"[WARN] Requête requests après synchro cookies en échec: {e}")
|
|
449
|
+
|
|
450
|
+
# 2) fallback: on parse directement le HTML du viewer
|
|
451
|
+
resultats = self._parse_ddg_results_from_html(viewer_html, max_results)
|
|
452
|
+
if resultats:
|
|
453
|
+
return resultats
|
|
454
|
+
|
|
455
|
+
print("[ERREUR] Aucun résultat parsable après validation captcha.")
|
|
456
|
+
return []
|
|
457
|
+
|
|
458
|
+
def extraire_domaines(self, resultats_search):
|
|
459
|
+
domaines = set()
|
|
460
|
+
for _titre, url in resultats_search:
|
|
461
|
+
try:
|
|
462
|
+
parsed = urlparse(url)
|
|
463
|
+
scheme = parsed.scheme or "https"
|
|
464
|
+
if not parsed.netloc:
|
|
465
|
+
continue
|
|
466
|
+
domaine = f"{scheme}://{parsed.netloc}"
|
|
467
|
+
domaines.add(domaine)
|
|
468
|
+
except Exception:
|
|
469
|
+
pass
|
|
470
|
+
return list(domaines)
|
|
471
|
+
|
|
472
|
+
def trouver_flux_rss(self, url_site):
|
|
473
|
+
try:
|
|
474
|
+
r = requests.get(url_site, headers={"User-Agent": "Mozilla/5.0"}, timeout=10)
|
|
475
|
+
r.raise_for_status()
|
|
476
|
+
except requests.RequestException as e:
|
|
477
|
+
print(f"[ERREUR] Impossible d'accéder à {url_site} : {e}")
|
|
478
|
+
return []
|
|
479
|
+
|
|
480
|
+
soup = BeautifulSoup(r.text, "html.parser")
|
|
481
|
+
flux = []
|
|
482
|
+
|
|
483
|
+
for link in soup.find_all("link", type="application/rss+xml"):
|
|
484
|
+
href = link.get("href")
|
|
485
|
+
if href:
|
|
486
|
+
href = urljoin(url_site, href)
|
|
487
|
+
flux.append(href)
|
|
488
|
+
|
|
489
|
+
for link in soup.find_all("a"):
|
|
490
|
+
href = link.get("href", "")
|
|
491
|
+
if not href:
|
|
492
|
+
continue
|
|
493
|
+
href_norm = href.lower()
|
|
494
|
+
if "rss" in href_norm or "feed" in href_norm:
|
|
495
|
+
flux.append(urljoin(url_site, href))
|
|
496
|
+
|
|
497
|
+
return list(set(flux))
|
|
498
|
+
|
|
499
|
+
def rechercher_articles_dans_flux(self, requete, flux_list, max_results=20):
|
|
500
|
+
mots_cles = self.extraire_mots_cles(requete)
|
|
501
|
+
articles = []
|
|
502
|
+
|
|
503
|
+
headers = {"User-Agent": "Mozilla/5.0"}
|
|
504
|
+
|
|
505
|
+
for flux in flux_list:
|
|
506
|
+
try:
|
|
507
|
+
r = requests.get(flux, headers=headers, timeout=10)
|
|
508
|
+
r.raise_for_status()
|
|
509
|
+
# Parse du flux en XML
|
|
510
|
+
soup = BeautifulSoup(r.content, "xml")
|
|
511
|
+
except requests.RequestException as e:
|
|
512
|
+
print(f"[ERREUR] Problème lors de la lecture du flux {flux} : {e}")
|
|
513
|
+
continue
|
|
514
|
+
|
|
515
|
+
# Gestion RSS (<item>) et Atom (<entry>)
|
|
516
|
+
for entry in soup.find_all(["item", "entry"]):
|
|
517
|
+
# Titre
|
|
518
|
+
titre_tag = entry.find("title")
|
|
519
|
+
titre = titre_tag.get_text(strip=True) if titre_tag else ""
|
|
520
|
+
|
|
521
|
+
# Résumé / description
|
|
522
|
+
resume_tag = entry.find("description") or entry.find("summary")
|
|
523
|
+
resume = resume_tag.get_text(strip=True) if resume_tag else ""
|
|
524
|
+
|
|
525
|
+
# Lien
|
|
526
|
+
lien_tag = entry.find("link")
|
|
527
|
+
lien = ""
|
|
528
|
+
if lien_tag:
|
|
529
|
+
# Atom : <link href="...">
|
|
530
|
+
if lien_tag.has_attr("href"):
|
|
531
|
+
lien = lien_tag["href"]
|
|
532
|
+
else:
|
|
533
|
+
# RSS : <link>https://...</link>
|
|
534
|
+
lien = lien_tag.get_text(strip=True)
|
|
535
|
+
|
|
536
|
+
# Date
|
|
537
|
+
date_tag = (
|
|
538
|
+
entry.find("pubDate")
|
|
539
|
+
or entry.find("published")
|
|
540
|
+
or entry.find("updated")
|
|
541
|
+
)
|
|
542
|
+
date = date_tag.get_text(strip=True) if date_tag else "Date inconnue"
|
|
543
|
+
|
|
544
|
+
texte_complet = self.normaliser_texte(titre + " " + resume)
|
|
545
|
+
|
|
546
|
+
# Condition : au moins un mot-clé présent dans titre+résumé
|
|
547
|
+
if any(mot in texte_complet for mot in mots_cles):
|
|
548
|
+
articles.append(
|
|
549
|
+
{
|
|
550
|
+
"titre": titre,
|
|
551
|
+
"url": lien,
|
|
552
|
+
"date": date,
|
|
553
|
+
"source_flux": flux,
|
|
554
|
+
}
|
|
555
|
+
)
|
|
556
|
+
if len(articles) >= max_results:
|
|
557
|
+
return articles
|
|
558
|
+
|
|
559
|
+
return articles
|
|
560
|
+
|
|
561
|
+
def pipeline_veille_requete(self, requete):
|
|
562
|
+
resultats = self.recherche_duckduckgo(requete)
|
|
563
|
+
if not resultats:
|
|
564
|
+
print("Aucun résultat trouvé sur DuckDuckGo.")
|
|
565
|
+
return []
|
|
566
|
+
print(resultats)
|
|
567
|
+
domaines = self.extraire_domaines(resultats)
|
|
568
|
+
|
|
569
|
+
flux = []
|
|
570
|
+
for d in domaines:
|
|
571
|
+
found = self.trouver_flux_rss(d)
|
|
572
|
+
if found:
|
|
573
|
+
print(f"Flux trouvés sur {d}:")
|
|
574
|
+
for f in found:
|
|
575
|
+
print(" ->", f)
|
|
576
|
+
flux.extend(found)
|
|
577
|
+
|
|
578
|
+
flux = list(set(flux))
|
|
579
|
+
|
|
580
|
+
if not flux:
|
|
581
|
+
return [
|
|
582
|
+
{"titre": t, "url": u, "date": None, "source_flux": None, "source": "web"}
|
|
583
|
+
for t, u in resultats
|
|
584
|
+
]
|
|
585
|
+
|
|
586
|
+
articles = self.rechercher_articles_dans_flux(requete, flux)
|
|
587
|
+
|
|
588
|
+
if not articles:
|
|
589
|
+
return [
|
|
590
|
+
{"titre": t, "url": u, "date": None, "source_flux": None, "source": "web"}
|
|
591
|
+
for t, u in resultats
|
|
592
|
+
]
|
|
593
|
+
return articles
|
|
594
|
+
|
|
595
|
+
def run(self):
|
|
596
|
+
self.error("")
|
|
597
|
+
self.warning("")
|
|
598
|
+
if self.data is None:
|
|
599
|
+
self.Outputs.data.send(None)
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
if not self.selected_column_name in self.data.domain:
|
|
603
|
+
self.warning(f'Previously selected column "{self.selected_column_name}" does not exist in your data.')
|
|
604
|
+
return
|
|
605
|
+
|
|
606
|
+
self.query = self.data.get_column(self.selected_column_name)[0]
|
|
607
|
+
|
|
608
|
+
self.progressBarInit()
|
|
609
|
+
self.thread = thread_management.Thread(self.pipeline_veille_requete, self.query)
|
|
610
|
+
self.thread.progress.connect(self.handle_progress)
|
|
611
|
+
self.thread.result.connect(self.handle_result)
|
|
612
|
+
self.thread.finish.connect(self.handle_finish)
|
|
613
|
+
self.thread.start()
|
|
614
|
+
|
|
615
|
+
def handle_progress(self, progress) -> None:
|
|
616
|
+
value = progress[0]
|
|
617
|
+
text = progress[1]
|
|
618
|
+
if value is not None:
|
|
619
|
+
self.progressBarSet(value)
|
|
620
|
+
if text is None:
|
|
621
|
+
self.textBrowser.setText("")
|
|
622
|
+
else:
|
|
623
|
+
self.textBrowser.insertPlainText(text)
|
|
624
|
+
|
|
625
|
+
def handle_result(self, result):
|
|
626
|
+
if result is None or len(result) == 0:
|
|
627
|
+
self.Outputs.data.send(None)
|
|
628
|
+
return
|
|
629
|
+
data = convert.convert_json_implicite_to_data_table(result)
|
|
630
|
+
self.Outputs.data.send(data)
|
|
631
|
+
|
|
632
|
+
def handle_finish(self):
|
|
633
|
+
self.progressBarFinished()
|
|
634
|
+
|
|
635
|
+
def post_initialized(self):
|
|
636
|
+
pass
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
if __name__ == "__main__":
|
|
640
|
+
app = QApplication(sys.argv)
|
|
641
|
+
my_widget = WebSearch()
|
|
642
|
+
my_widget.show()
|
|
643
|
+
|
|
644
|
+
if hasattr(app, "exec"):
|
|
645
|
+
sys.exit(app.exec())
|
|
646
|
+
else:
|
|
647
|
+
sys.exit(app.exec_())
|