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.
- webpageinfo-2022.5.27/LICENSE +29 -0
- webpageinfo-2022.5.27/PKG-INFO +33 -0
- webpageinfo-2022.5.27/README.md +5 -0
- webpageinfo-2022.5.27/pyproject.toml +53 -0
- webpageinfo-2022.5.27/setup.py +34 -0
- webpageinfo-2022.5.27/webpageinfo/__init__.py +0 -0
- webpageinfo-2022.5.27/webpageinfo/browser.py +89 -0
- webpageinfo-2022.5.27/webpageinfo/config/__init__.py +0 -0
- webpageinfo-2022.5.27/webpageinfo/config/ignored_html_tags.py +13 -0
- webpageinfo-2022.5.27/webpageinfo/config/ignored_word_in_tags.py +63 -0
- webpageinfo-2022.5.27/webpageinfo/config/rejected_content_class.py +10 -0
- webpageinfo-2022.5.27/webpageinfo/config/rejected_content_id.py +5 -0
- webpageinfo-2022.5.27/webpageinfo/config/removed_html_tags.py +13 -0
- webpageinfo-2022.5.27/webpageinfo/config/simple_html_tags.py +41 -0
- webpageinfo-2022.5.27/webpageinfo/config/svg_tags.py +20 -0
- webpageinfo-2022.5.27/webpageinfo/page.py +267 -0
- webpageinfo-2022.5.27/webpageinfo/safe_html.py +103 -0
|
@@ -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,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,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,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
|