webpageinfo 2022.5.27__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.
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2022, Vincent Poulailleau
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.1
2
+ Name: webpageinfo
3
+ Version: 2022.5.27
4
+ Summary: Web page information (title, content, tags…)
5
+ Home-page: https://github.com/vpoulailleau/webpageinfo
6
+ License: BSD-3-Clause
7
+ Keywords: web,page,information
8
+ Author: Vincent Poulailleau
9
+ Author-email: vpoulailleau@gmail.com
10
+ Requires-Python: >=3.10,<4.0
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: BSD License
14
+ Classifier: Natural Language :: English
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Topic :: Utilities
19
+ Requires-Dist: beautifulsoup4 (>=4,<5)
20
+ Requires-Dist: httpx (>=0,<1)
21
+ Requires-Dist: pdfminer.six (>=20220524,<20220525)
22
+ Requires-Dist: pillow (>=9,<10)
23
+ Requires-Dist: splinter[selenium4] (>=0,<1)
24
+ Project-URL: Documentation, https://webpageinfo.readthedocs.io/en/latest/
25
+ Project-URL: Repository, https://github.com/vpoulailleau/webpageinfo
26
+ Description-Content-Type: text/markdown
27
+
28
+ # WebPageInfo
29
+
30
+ Web page information (title, content, tags…)
31
+
32
+ Internal tool, no public documentation…
33
+
@@ -0,0 +1,5 @@
1
+ # WebPageInfo
2
+
3
+ Web page information (title, content, tags…)
4
+
5
+ Internal tool, no public documentation…
@@ -0,0 +1,53 @@
1
+ [tool.poetry]
2
+ name = "webpageinfo"
3
+ version = "2022.5.27"
4
+ description = "Web page information (title, content, tags…)"
5
+ classifiers=[
6
+ "Development Status :: 4 - Beta",
7
+ "Intended Audience :: Developers",
8
+ "License :: OSI Approved :: BSD License",
9
+ "Natural Language :: English",
10
+ "Programming Language :: Python :: 3",
11
+ "Programming Language :: Python :: 3.10",
12
+ "Programming Language :: Python :: 3.11",
13
+ "Topic :: Utilities",
14
+ ]
15
+ authors = ["Vincent Poulailleau <vpoulailleau@gmail.com>"]
16
+ readme = "README.md"
17
+ repository = "https://github.com/vpoulailleau/webpageinfo"
18
+ homepage = "https://github.com/vpoulailleau/webpageinfo"
19
+ documentation = "https://webpageinfo.readthedocs.io/en/latest/"
20
+ keywords = ["web", "page", "information"]
21
+ license = "BSD-3-Clause"
22
+ include = ["webpageinfo/**/*.py"]
23
+
24
+ [tool.poetry.dependencies]
25
+ python = ">=3.10,<4.0"
26
+ beautifulsoup4 = "^4"
27
+ httpx = "^0"
28
+ "pdfminer.six" = "^20220524"
29
+ pillow = "^9"
30
+ splinter = {version = "^0", extras = ["selenium4"]}
31
+
32
+ [tool.poetry.dev-dependencies]
33
+ python-dev-tools = "^2022"
34
+
35
+ #[tool.poetry.scripts]
36
+ #webpageinfo = "webpageinfo.cli:main"
37
+
38
+ [tool.pytest.ini_options]
39
+ minversion = "7.0"
40
+ addopts = "-ra -q -s -vv --cov=webpageinfo"
41
+ testpaths = [
42
+ "tests",
43
+ ]
44
+
45
+ [tool.coverage.report]
46
+ exclude_lines = [
47
+ "pragma: no cover",
48
+ "if TYPE_CHECKING:",
49
+ ]
50
+
51
+ [build-system]
52
+ requires = ["poetry_core>=1.0.0"]
53
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,34 @@
1
+ # -*- coding: utf-8 -*-
2
+ from setuptools import setup
3
+
4
+ packages = \
5
+ ['webpageinfo', 'webpageinfo.config']
6
+
7
+ package_data = \
8
+ {'': ['*']}
9
+
10
+ install_requires = \
11
+ ['beautifulsoup4>=4,<5',
12
+ 'httpx>=0,<1',
13
+ 'pdfminer.six>=20220524,<20220525',
14
+ 'pillow>=9,<10',
15
+ 'splinter[selenium4]>=0,<1']
16
+
17
+ setup_kwargs = {
18
+ 'name': 'webpageinfo',
19
+ 'version': '2022.5.27',
20
+ 'description': 'Web page information (title, content, tags…)',
21
+ 'long_description': '# WebPageInfo\n\nWeb page information (title, content, tags…)\n\nInternal tool, no public documentation…\n',
22
+ 'author': 'Vincent Poulailleau',
23
+ 'author_email': 'vpoulailleau@gmail.com',
24
+ 'maintainer': None,
25
+ 'maintainer_email': None,
26
+ 'url': 'https://github.com/vpoulailleau/webpageinfo',
27
+ 'packages': packages,
28
+ 'package_data': package_data,
29
+ 'install_requires': install_requires,
30
+ 'python_requires': '>=3.10,<4.0',
31
+ }
32
+
33
+
34
+ setup(**setup_kwargs)
File without changes
@@ -0,0 +1,89 @@
1
+ from collections.abc import Iterable
2
+
3
+ import splinter
4
+ from splinter.driver.webdriver import BaseWebDriver
5
+
6
+
7
+ class WebElementNotFound(Exception):
8
+ pass
9
+
10
+
11
+ def _find_elements_by(
12
+ root, tag: str = "", class_name: str = "", css: str = "", wait_time: float = 0
13
+ ) -> Iterable["WebElement"]:
14
+ if tag:
15
+ for elem in root.find_by_tag(tag, wait_time):
16
+ yield WebElement(elem)
17
+ if class_name:
18
+ for elem in root.find_by_css(f".{class_name}", wait_time):
19
+ yield WebElement(elem)
20
+ if css:
21
+ for elem in root.find_by_css(css, wait_time):
22
+ yield WebElement(elem)
23
+
24
+
25
+ class _Findable:
26
+ @property
27
+ def _root(self):
28
+ raise NotImplemented
29
+
30
+ def find_elements_by(
31
+ self, tag: str = "", class_name: str = "", css: str = "", wait_time: float = 0
32
+ ) -> Iterable["WebElement"]:
33
+ yield from _find_elements_by(self._root, tag, class_name, css, wait_time)
34
+
35
+ def find_element_by(
36
+ self, tag: str = "", class_name: str = "", css: str = "", wait_time: float = 0
37
+ ) -> "WebElement":
38
+ try:
39
+ return next(
40
+ iter(_find_elements_by(self._root, tag, class_name, css, wait_time))
41
+ )
42
+ except StopIteration:
43
+ raise WebElementNotFound
44
+
45
+ def is_element_present_by(self, tag: str = "", wait_time: float = 0) -> bool:
46
+ if tag:
47
+ return self._root.is_element_present_by_tag(tag)
48
+
49
+
50
+ class WebElement(_Findable):
51
+ def __init__(self, implementation_element):
52
+ self._element = implementation_element
53
+
54
+ @property
55
+ def _root(self):
56
+ return self._element
57
+
58
+ def __getattr__(self, name):
59
+ """Proxy for internal implementation."""
60
+ return getattr(self._element, name)
61
+
62
+ def __getitem__(self, key):
63
+ """Proxy for internal implementation."""
64
+ return self._element[key]
65
+
66
+
67
+ class Browser(_Findable):
68
+ def __init__(self, *args, **kwargs):
69
+ profile_preferences = {"intl.accept_languages": "fr-FR,en-US,en-GB"}
70
+ self.driver: BaseWebDriver = splinter.Browser(
71
+ "firefox",
72
+ *args,
73
+ incognito=True,
74
+ profile_preferences=profile_preferences,
75
+ **kwargs,
76
+ )
77
+ self.selenium_driver.set_window_size(1024, 1024)
78
+
79
+ @property
80
+ def _root(self):
81
+ return self.driver
82
+
83
+ @property
84
+ def selenium_driver(self):
85
+ return self.driver.driver
86
+
87
+ def __getattr__(self, name):
88
+ """Proxy for internal implementation."""
89
+ return getattr(self.driver, name)
File without changes
@@ -0,0 +1,13 @@
1
+ ignored_html_tags = {
2
+ "article",
3
+ "aside",
4
+ "figure",
5
+ "footer",
6
+ "header",
7
+ "label",
8
+ "main",
9
+ "section",
10
+ "show",
11
+ "time",
12
+ "tt",
13
+ }
@@ -0,0 +1,63 @@
1
+ ignored_word_in_tags = {
2
+ "about",
3
+ "apprenants",
4
+ "available",
5
+ "cette",
6
+ "command",
7
+ "créé",
8
+ "créée",
9
+ "default",
10
+ "des",
11
+ "different",
12
+ "directory",
13
+ "elles",
14
+ "est",
15
+ "était",
16
+ "example",
17
+ "exemple",
18
+ "first",
19
+ "following",
20
+ "for",
21
+ "from",
22
+ "how",
23
+ "however",
24
+ "http",
25
+ "https",
26
+ "ils",
27
+ "import",
28
+ "included",
29
+ "information",
30
+ "jusqu",
31
+ "learn",
32
+ "les",
33
+ "name",
34
+ "named",
35
+ "never",
36
+ "note",
37
+ "other",
38
+ "petit",
39
+ "project",
40
+ "readme",
41
+ "should",
42
+ "section",
43
+ "source",
44
+ "specify",
45
+ "super",
46
+ "sur",
47
+ "system",
48
+ "that",
49
+ "the",
50
+ "there",
51
+ "these",
52
+ "this",
53
+ "un",
54
+ "une",
55
+ "use",
56
+ "using",
57
+ "version",
58
+ "vous",
59
+ "which",
60
+ "with",
61
+ "you",
62
+ "your",
63
+ }
@@ -0,0 +1,10 @@
1
+ rejected_content_class = (
2
+ "author-description",
3
+ "author-info",
4
+ "d-md-none",
5
+ "d-none",
6
+ "footer",
7
+ "jp-relatedposts",
8
+ "modal",
9
+ "sharedaddy",
10
+ )
@@ -0,0 +1,5 @@
1
+ rejected_content_id = (
2
+ "author",
3
+ "reader-comments",
4
+ "sidebar-toc",
5
+ )
@@ -0,0 +1,13 @@
1
+ removed_html_tags = {
2
+ "button",
3
+ "canvas",
4
+ "form",
5
+ "hr",
6
+ "input",
7
+ "link",
8
+ "meta",
9
+ "nav",
10
+ "script",
11
+ "style",
12
+ "video-js",
13
+ }
@@ -0,0 +1,41 @@
1
+ simple_html_tags = {
2
+ "a",
3
+ "b",
4
+ "bdi",
5
+ "blockquote",
6
+ "br",
7
+ "code",
8
+ "del",
9
+ "details",
10
+ "div",
11
+ "em",
12
+ "figcaption",
13
+ "h1",
14
+ "h2",
15
+ "h3",
16
+ "h4",
17
+ "h5",
18
+ "h6",
19
+ "h7",
20
+ "i",
21
+ "ins",
22
+ "li",
23
+ "ol",
24
+ "p",
25
+ "picture",
26
+ "pre",
27
+ "small",
28
+ "source",
29
+ "span",
30
+ "strong",
31
+ "sub",
32
+ "summary",
33
+ "sup",
34
+ "table",
35
+ "td",
36
+ "textarea",
37
+ "th",
38
+ "tr",
39
+ "u",
40
+ "ul",
41
+ }
@@ -0,0 +1,20 @@
1
+ svg_tags = {
2
+ "area",
3
+ "circle",
4
+ "defs",
5
+ "fecolormatrix",
6
+ "fecomponenttransfer",
7
+ "fecomposite",
8
+ "fefunca",
9
+ "fefuncb",
10
+ "fefuncg",
11
+ "fefuncr",
12
+ "filter",
13
+ "g",
14
+ "map",
15
+ "path",
16
+ "polygon",
17
+ "polyline",
18
+ "svg",
19
+ "symbol",
20
+ }
@@ -0,0 +1,267 @@
1
+ """Page information."""
2
+ # TODO get canonical url: <link rel="canonical" href="https://leszexpertsfle.com/ressources-fle/raconter-une-histoire-damour-b1-et/" />
3
+ # TODO regarder dans les iframe pour trouver des liens et du contenu et des PDF
4
+
5
+ import math
6
+ import re
7
+ import urllib.parse
8
+ import uuid
9
+ from collections import Counter
10
+ from contextlib import suppress
11
+ from pathlib import Path
12
+ from typing import Iterable
13
+
14
+ import httpx
15
+ import pdfminer.pdfparser
16
+ from pdfminer.high_level import extract_text
17
+ from PIL import Image
18
+
19
+ from webpageinfo.browser import Browser, WebElementNotFound
20
+ from webpageinfo.config.ignored_word_in_tags import ignored_word_in_tags
21
+ from webpageinfo.safe_html import html2text, safe_html
22
+
23
+
24
+ def get_random_temp_file_name(extension: str = "") -> str:
25
+ """Return a random filename in /tmp."""
26
+ return f"/tmp/{uuid.uuid4()}.{extension}"
27
+
28
+
29
+ def extract_tags_from_words(words: list[str]) -> Iterable[str]:
30
+ counter = Counter(words)
31
+ nb_words = len(words)
32
+ threshold = math.sqrt(nb_words) / 5
33
+ # print(threshold)
34
+ result = []
35
+ result_lower = set()
36
+ for word, nb_apparitions in sorted(
37
+ counter.most_common(),
38
+ key=lambda c: c[1],
39
+ reverse=True,
40
+ ):
41
+ if nb_apparitions < max(threshold, 5.1):
42
+ break
43
+ if len(word) <= 4 and not word[0].isupper():
44
+ continue
45
+ if len(word) <= 2:
46
+ continue
47
+ if word.lower() in ignored_word_in_tags:
48
+ continue
49
+ if word.lower() not in result_lower:
50
+ result_lower.add(word.lower())
51
+ result.append(word)
52
+ # print(word, nb_apparitions)
53
+ return result
54
+
55
+
56
+ class Page:
57
+ """Page information."""
58
+
59
+ def _fetch_title(self: "Page", browser: Browser) -> None:
60
+ # wordpress
61
+ with suppress(WebElementNotFound):
62
+ title = browser.find_element_by(class_name="entry-title").text
63
+ if title:
64
+ self.title = title
65
+ return
66
+
67
+ with suppress(WebElementNotFound):
68
+ head = browser.find_element_by(tag="head")
69
+ title = title = head.find_element_by(tag="title").text
70
+ if title:
71
+ self.title = title
72
+ return
73
+
74
+ self.title = browser.title
75
+
76
+ def _fetch_links(self: "Page", browser: Browser) -> None:
77
+ # avoid javascript, read from source
78
+ html = httpx.get(self.url, follow_redirects=True)
79
+ html.raise_for_status()
80
+ for match in re.finditer(r'<a[^>]*href="([^"]+)"', html.text, re.MULTILINE):
81
+ self.links.add(self.to_absolute_url(match[1]))
82
+
83
+ # use javascript
84
+ for a_tag in browser.find_elements_by(tag="a"):
85
+ href = a_tag["href"]
86
+ if href:
87
+ self.links.add(self.to_absolute_url(href))
88
+
89
+ def _fetch_tags(self: "Page", browser: Browser) -> None:
90
+ for link in browser.find_elements_by(tag="a"):
91
+ if "tag" in link["rel"]:
92
+ self.tags.add(link.text)
93
+ for tag in browser.find_elements_by(class_name="tag__link"):
94
+ self.tags.add(tag.text)
95
+
96
+ def _fetch_description(self: "Page", browser: Browser) -> None:
97
+ metas = list(browser.find_elements_by(tag="meta"))
98
+
99
+ # should be ordered by priority, first is most relevant
100
+ meta_keywords = (
101
+ "description",
102
+ "og:description",
103
+ "twitter:description",
104
+ "keywords",
105
+ )
106
+
107
+ for keyword in meta_keywords:
108
+ for meta in metas:
109
+ name = meta["name"] or meta["property"]
110
+ if name == keyword and meta["content"].strip():
111
+ self.description = meta["content"].strip()
112
+ return
113
+
114
+ def _fetch_content(self: "Page", browser: Browser) -> None:
115
+ content_element = browser.find_element_by(tag="body")
116
+ try:
117
+ content_elements = list(browser.find_elements_by(tag="article"))
118
+ if len(content_elements) == 1:
119
+ content_element = content_elements[0]
120
+ except WebElementNotFound:
121
+ pass
122
+
123
+ content_classes = (
124
+ "et_pb_post_content",
125
+ "blog-post-content",
126
+ "entry-content",
127
+ "post-article",
128
+ )
129
+ for class_ in content_classes:
130
+ try:
131
+ content_element = browser.find_element_by(class_name=class_)
132
+ break
133
+ except WebElementNotFound:
134
+ pass
135
+
136
+ self.content = content_element.html
137
+
138
+ def _fetch_screenshot(self: "Page", browser: Browser) -> None:
139
+ self.screenshot = get_random_temp_file_name(extension="png")
140
+ browser.selenium_driver.save_screenshot(self.screenshot)
141
+ image = Image.open(self.screenshot)
142
+ image.thumbnail((1000, 1000))
143
+ image.save(self.screenshot, optimize=True, quality=40)
144
+
145
+ def _fetch_pdf_content(self: "Page") -> None:
146
+ for pdf_url in self.attached_pdf:
147
+ print("Downloading", pdf_url)
148
+ pdf = httpx.get(pdf_url, follow_redirects=True)
149
+ pdf.raise_for_status()
150
+ tmp_file_path = get_random_temp_file_name(extension="pdf")
151
+ Path(tmp_file_path).write_bytes(pdf.content)
152
+ print("Saved in", tmp_file_path)
153
+ with suppress(pdfminer.pdfparser.PDFSyntaxError):
154
+ self.pdf_content += f"{extract_text(tmp_file_path)} "
155
+
156
+ def _fetch_information(self: "Page") -> None:
157
+ browser = Browser(headless=True)
158
+ browser.visit(self.url)
159
+ if not browser.is_element_present_by(tag="title", wait_time=10):
160
+ raise ValueError("No internet access")
161
+ if not browser.is_element_present_by(tag="body", wait_time=10):
162
+ raise ValueError("No internet access")
163
+ # browser.find_by_name('btnK').click()
164
+ # if browser.is_text_present('splinter.readthedocs.io'):
165
+ # browser.is_text_present('splinter', wait_time=10) # True, using wait_time
166
+ # browser.is_element_present_by_tag('h1')
167
+ # screenshot_path = browser.screenshot('absolute_path/your_screenshot.png', full=True)
168
+ # time.sleep(10)
169
+ self._fetch_title(browser)
170
+ self._fetch_links(browser)
171
+ self._fetch_tags(browser)
172
+ self._fetch_description(browser)
173
+ self._fetch_content(browser)
174
+ self._fetch_screenshot(browser)
175
+ self._fetch_pdf_content()
176
+
177
+ self.safe_html = safe_html(self.content, self.server)
178
+ self.text = html2text(self.safe_html)
179
+ self.total_text = re.sub(
180
+ r"[\n\s\W_]+",
181
+ " ",
182
+ f"{self.text} {self.pdf_content} {self.description} {' '.join(self.tags)}",
183
+ flags=re.DOTALL | re.MULTILINE,
184
+ )
185
+ self.words = self.total_text.split()
186
+ self.words_tags = extract_tags_from_words(self.words)
187
+ browser.quit()
188
+
189
+ def __init__(self: "Page", url: str) -> None:
190
+ self.url: str = url
191
+ url = urllib.parse.urlparse(self.url)
192
+ self.server = f"http://{url.netloc}"
193
+ self.title: str = ""
194
+ self.links: set[str] = set()
195
+ self.tags: set[str] = set()
196
+ self.description: str = ""
197
+ self.content: str = ""
198
+ self.safe_html: str = ""
199
+ self.text: str = ""
200
+ self.total_text: str = ""
201
+ self.words: list[str] = []
202
+ self.words_tags: list[str] = []
203
+ self.screenshot: str = ""
204
+ self.pdf_content: str = ""
205
+ self._fetch_information()
206
+
207
+ def to_absolute_url(self, url: str) -> str:
208
+ if url.startswith("/"):
209
+ return self.server + url
210
+ if url.startswith(("#", "?")):
211
+ return (
212
+ self.url + url
213
+ ) # TODO remove fragment + query from self.url (urlparse)
214
+ return url.replace("http://", "https://")
215
+
216
+ @property
217
+ def attached_pdf(self) -> Iterable[str]:
218
+ return [
219
+ self.to_absolute_url(link) for link in self.links if link.endswith(".pdf")
220
+ ]
221
+
222
+ @property
223
+ def all_tags(self) -> list[str]:
224
+ result = []
225
+ result_lower = set()
226
+ for tag in self.tags:
227
+ if tag.lower() not in result_lower:
228
+ result_lower.add(tag.lower())
229
+ result.append(tag)
230
+ for tag in self.words_tags:
231
+ if tag.lower() not in result_lower:
232
+ result_lower.add(tag.lower())
233
+ result.append(tag)
234
+ return result
235
+
236
+
237
+ if __name__ == "__main__":
238
+ pages = [
239
+ # "https://leszexpertsfle.com/ressources-fle/raconter-une-histoire-damour-b1-et/",
240
+ # "https://littefle.wordpress.com/2019/10/16/le-prix-nobel-de-litterature-en-5-minutes/",
241
+ # "https://jancovici.com/changement-climatique/aspects-physiques/variation-du-climat-et-augmentation-de-leffet-de-serre-due-a-lhomme-cest-pareil/",
242
+ # "https://adamj.eu/tech/2022/03/30/how-to-make-django-error-for-undefined-template-variables/",
243
+ # "https://realpython.com/pypi-publish-python-package/",
244
+ "https://agi.to/enseigner/ressource/carte-didentite-de-super-heros/",
245
+ ]
246
+ for url in pages:
247
+ print("=" * 80)
248
+ page = Page(url)
249
+ print("URL", page.url)
250
+ print("title", page.title)
251
+ # print("links", page.links)
252
+ print("tags", page.tags)
253
+ print("words tags", page.words_tags)
254
+ print("all_tags", page.all_tags)
255
+ print("description:")
256
+ print(page.description)
257
+ print("pdfs", page.attached_pdf)
258
+ print("screenshot", page.screenshot)
259
+ # print("pdf content", page.pdf_content)
260
+ print("text", page.total_text[:2000].replace("\n", " "))
261
+
262
+ with open("index.html", "w", encoding="utf-8") as f:
263
+ f.write(
264
+ '<html><head><link rel="stylesheet" href="https://www.lecalamar.fr/css/main.css"></head><body style="max-width: 17cm;">'
265
+ )
266
+ f.write(page.safe_html)
267
+ f.write("</body></html>")
@@ -0,0 +1,103 @@
1
+ import bs4
2
+
3
+ from webpageinfo.config.ignored_html_tags import ignored_html_tags
4
+ from webpageinfo.config.rejected_content_class import rejected_content_class
5
+ from webpageinfo.config.rejected_content_id import rejected_content_id
6
+ from webpageinfo.config.removed_html_tags import removed_html_tags
7
+ from webpageinfo.config.simple_html_tags import simple_html_tags
8
+ from webpageinfo.config.svg_tags import svg_tags
9
+
10
+ # TODO mettre in id sur les titres si pas déjà existant
11
+ # TODO conserver les id (pour les liens intradocuments)
12
+ # TODO display none https://stackoverflow.com/questions/47862023/splinter-find-by-style
13
+
14
+ _result = ""
15
+ _server_url = ""
16
+
17
+ BS4Element = (
18
+ bs4.element.Tag
19
+ | bs4.BeautifulSoup
20
+ | bs4.element.NavigableString
21
+ | bs4.element.Comment
22
+ )
23
+
24
+
25
+ def _manage_element_start(element: BS4Element) -> bool:
26
+ """Return False to ignore."""
27
+ global _result
28
+ if isinstance(element, bs4.element.Tag):
29
+ if "class" in element.attrs and any(
30
+ class_ in element["class"] for class_ in rejected_content_class
31
+ ):
32
+ return False
33
+ if "id" in element.attrs and any(
34
+ id_ == element["id"] for id_ in rejected_content_id
35
+ ):
36
+ return False
37
+ if element.name == "a" and "href" in element.attrs:
38
+ url = element["href"]
39
+ if url.startswith("/"):
40
+ url = _server_url + url
41
+ _result += f'<a href="{url}">'
42
+ elif element.name == "img":
43
+ url = element.attrs.get("src")
44
+ if url is not None:
45
+ if url.startswith("/"):
46
+ url = _server_url + url
47
+ _result += f'<img src="{url}"'
48
+ if "alt" in element.attrs:
49
+ _result += f' alt="{element["alt"]}"'
50
+ if "width" in element.attrs:
51
+ _result += f' width="{element["width"]}"'
52
+ if "height" in element.attrs:
53
+ _result += f' height="{element["height"]}"'
54
+ _result += ">"
55
+ elif element.name in simple_html_tags:
56
+ _result += f"<{element.name}>"
57
+ elif element.name in svg_tags:
58
+ pass # TODO l'utiliser tel quel
59
+ elif element.name == ("iframe"):
60
+ return False # TODO utiliser son contenu
61
+ elif element.name in removed_html_tags:
62
+ return False
63
+ elif element.name in ignored_html_tags:
64
+ return True
65
+ elif element.name == "[document]":
66
+ pass # Beautiful soup root
67
+ else:
68
+ raise ValueError(f"Unknown tag: {element.name}, {element}, {element.attrs}")
69
+ elif isinstance(element, bs4.element.Comment):
70
+ return False
71
+ elif isinstance(element, bs4.element.NavigableString):
72
+ _result += str(element)
73
+ return False
74
+ return True
75
+
76
+
77
+ def _manage_element_end(element: BS4Element):
78
+ global _result
79
+ if isinstance(element, bs4.element.Tag):
80
+ if element.name == "a" and "href" in element.attrs:
81
+ _result += "</a>"
82
+ elif element.name in simple_html_tags:
83
+ _result += f"</{element.name}>"
84
+
85
+
86
+ def _visit(element):
87
+ if _manage_element_start(element):
88
+ for child in getattr(element, "children", []):
89
+ _visit(child)
90
+ _manage_element_end(element)
91
+
92
+
93
+ def safe_html(html: str, server_url: str) -> str:
94
+ global _result, _server_url
95
+ _server_url = server_url
96
+ _result = ""
97
+ soup = bs4.BeautifulSoup(html, features="html.parser")
98
+ _visit(soup)
99
+ return _result.replace("<div></div>", "").replace("<p></p>", "")
100
+
101
+
102
+ def html2text(safe_html: str) -> str:
103
+ return bs4.BeautifulSoup(safe_html, features="html.parser").text