parallelhtmlscraper 0.0.0__tar.gz → 0.2.0__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.
- parallelhtmlscraper-0.2.0/LICENSE +21 -0
- parallelhtmlscraper-0.2.0/MANIFEST.in +1 -0
- parallelhtmlscraper-0.2.0/PKG-INFO +164 -0
- parallelhtmlscraper-0.2.0/README.md +104 -0
- parallelhtmlscraper-0.2.0/parallelhtmlscraper/__init__.py +11 -0
- {parallelhtmlscraper-0.0.0 → parallelhtmlscraper-0.2.0}/parallelhtmlscraper/exceptions.py +20 -18
- parallelhtmlscraper-0.2.0/parallelhtmlscraper/html_analyzer.py +22 -0
- parallelhtmlscraper-0.2.0/parallelhtmlscraper/html_request_coroutine.py +75 -0
- parallelhtmlscraper-0.2.0/parallelhtmlscraper/html_text_load_coroutine.py +33 -0
- parallelhtmlscraper-0.2.0/parallelhtmlscraper/parallel_html_request_coroutine.py +48 -0
- parallelhtmlscraper-0.2.0/parallelhtmlscraper/parallel_html_scraper.py +64 -0
- parallelhtmlscraper-0.2.0/parallelhtmlscraper.egg-info/PKG-INFO +164 -0
- {parallelhtmlscraper-0.0.0 → parallelhtmlscraper-0.2.0}/parallelhtmlscraper.egg-info/SOURCES.txt +9 -2
- parallelhtmlscraper-0.2.0/parallelhtmlscraper.egg-info/not-zip-safe +1 -0
- parallelhtmlscraper-0.2.0/pyproject.toml +233 -0
- {parallelhtmlscraper-0.0.0 → parallelhtmlscraper-0.2.0}/setup.cfg +4 -4
- parallelhtmlscraper-0.2.0/tests/test_hrml_analyzer.py +32 -0
- parallelhtmlscraper-0.2.0/tests/test_html_request_coroutine.py +53 -0
- parallelhtmlscraper-0.2.0/tests/test_html_text_load_coroutine.py +44 -0
- parallelhtmlscraper-0.2.0/tests/test_parallel_url_scraper.py +76 -0
- parallelhtmlscraper-0.0.0/PKG-INFO +0 -15
- parallelhtmlscraper-0.0.0/README.md +0 -3
- parallelhtmlscraper-0.0.0/parallelhtmlscraper/__init__.py +0 -11
- parallelhtmlscraper-0.0.0/parallelhtmlscraper/html_analyzer.py +0 -13
- parallelhtmlscraper-0.0.0/parallelhtmlscraper/html_request_coroutine.py +0 -54
- parallelhtmlscraper-0.0.0/parallelhtmlscraper/html_text_load_coroutine.py +0 -26
- parallelhtmlscraper-0.0.0/parallelhtmlscraper/parallel_html_request_coroutine.py +0 -26
- parallelhtmlscraper-0.0.0/parallelhtmlscraper/parallel_html_scraper.py +0 -30
- parallelhtmlscraper-0.0.0/parallelhtmlscraper.egg-info/PKG-INFO +0 -15
- parallelhtmlscraper-0.0.0/setup.py +0 -33
- {parallelhtmlscraper-0.0.0 → parallelhtmlscraper-0.2.0}/parallelhtmlscraper/py.typed +0 -0
- {parallelhtmlscraper-0.0.0 → parallelhtmlscraper-0.2.0}/parallelhtmlscraper.egg-info/dependency_links.txt +0 -0
- {parallelhtmlscraper-0.0.0 → parallelhtmlscraper-0.2.0}/parallelhtmlscraper.egg-info/requires.txt +0 -0
- {parallelhtmlscraper-0.0.0 → parallelhtmlscraper-0.2.0}/parallelhtmlscraper.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Yukihiko Shinoda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
recursive-include docs *
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: parallelhtmlscraper
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: This project helps you to web scrape html file.
|
|
5
|
+
Author-email: Yukihiko Shinoda <yuk.hik.future@gmail.com>
|
|
6
|
+
Maintainer-email: Yukihiko Shinoda <yuk.hik.future@gmail.com>
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2019 Yukihiko Shinoda
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
|
|
29
|
+
Project-URL: homepage, https://github.com/yukihiko-shinoda/parallel-html-scraper
|
|
30
|
+
Project-URL: repository, https://github.com/yukihiko-shinoda/parallel-html-scraper
|
|
31
|
+
Keywords: parallel,HTML,web,scraping,scrape,aiohttp,beautifulsoup,beautifulsoup4
|
|
32
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
33
|
+
Classifier: Framework :: AsyncIO
|
|
34
|
+
Classifier: Framework :: aiohttp
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Classifier: Intended Audience :: Information Technology
|
|
37
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
38
|
+
Classifier: Natural Language :: English
|
|
39
|
+
Classifier: Operating System :: OS Independent
|
|
40
|
+
Classifier: Programming Language :: Python
|
|
41
|
+
Classifier: Programming Language :: Python :: 3
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
46
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
47
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
48
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
49
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
50
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
51
|
+
Classifier: Topic :: System :: Archiving
|
|
52
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
53
|
+
Classifier: Typing :: Typed
|
|
54
|
+
Requires-Python: >=3.7
|
|
55
|
+
Description-Content-Type: text/markdown
|
|
56
|
+
License-File: LICENSE
|
|
57
|
+
Requires-Dist: aiohttp
|
|
58
|
+
Requires-Dist: beautifulsoup4
|
|
59
|
+
Dynamic: license-file
|
|
60
|
+
|
|
61
|
+
# Parallel HTML Scraper
|
|
62
|
+
|
|
63
|
+
[](https://github.com/yukihiko-shinoda/parallel-html-scraper/actions?query=workflow%3ATest)
|
|
64
|
+
[](https://codeclimate.com/github/yukihiko-shinoda/parallel-html-scraper/test_coverage)
|
|
65
|
+
[](https://codeclimate.com/github/yukihiko-shinoda/parallel-html-scraper/maintainability)
|
|
66
|
+
[](https://codeclimate.com/github/yukihiko-shinoda/parallel-html-scraper)
|
|
67
|
+
[](https://pyup.io/repos/github/yukihiko-shinoda/parallel-html-scraper/)
|
|
68
|
+
[](https://pypi.org/project/parallelhtmlscraper/)
|
|
69
|
+
[](https://pypi.org/project/parallelhtmlscraper/)
|
|
70
|
+
[](http://twitter.com/share?text=Parallel%20HTML%20Scraper&url=https://pypi.org/project/parallelhtmlscraper/&hashtags=python)
|
|
71
|
+
|
|
72
|
+
Helps you to web scrape html file in parallel without async / await syntax.
|
|
73
|
+
|
|
74
|
+
## Feature
|
|
75
|
+
|
|
76
|
+
This project helps you to web scrape html file in parallel without async / await syntax.
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
```console
|
|
81
|
+
pip install parallelhtmlscraper
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
Minimum example:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from bs4 import BeautifulSoup
|
|
90
|
+
|
|
91
|
+
from parallelhtmlscraper.html_analyzer import HtmlAnalyzer
|
|
92
|
+
from parallelhtmlscraper.parallel_html_scraper import ParallelHtmlScraper
|
|
93
|
+
|
|
94
|
+
class AnalyzerExample(HtmlAnalyzer):
|
|
95
|
+
async def execute(self, soup: BeautifulSoup) -> str:
|
|
96
|
+
return soup.find('title').text
|
|
97
|
+
|
|
98
|
+
host_google = 'https://www.google.com'
|
|
99
|
+
path_and_content = [
|
|
100
|
+
'', # Google Search
|
|
101
|
+
'/imghp?hl=EN', # Google Images
|
|
102
|
+
'/shopping?hl=en', # Google Shopping
|
|
103
|
+
'/save', # Collection
|
|
104
|
+
'https://www.google.com/maps?hl=en', # Google Maps
|
|
105
|
+
'https://www.google.com/drive/apps.html', # Google drive
|
|
106
|
+
'https://www.google.com/mail/help/intl/en/about.html?vm=r', # GMail
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
list_response = ParallelHtmlScraper.execute(host_google, path_and_content, AnalyzerExample())
|
|
110
|
+
print(list_response)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```console
|
|
114
|
+
$ python test.py
|
|
115
|
+
['\n Gmail - Email from Google\n ', 'Google Images', ' Google Maps ', 'Using Google Drive - New Features, Benefits & Advantages of Google Cloud Storage', 'Google Shopping', 'Google', 'Collections']
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## API
|
|
119
|
+
|
|
120
|
+
### ParallelMediaDownloader.execute
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
class ParallelHtmlScraper:
|
|
124
|
+
"""API of parallel HTML scraping."""
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def execute(
|
|
128
|
+
base_url: str,
|
|
129
|
+
list_url: Iterable[str],
|
|
130
|
+
analyzer: HtmlAnalyzer[_T],
|
|
131
|
+
*,
|
|
132
|
+
limit: int = 5
|
|
133
|
+
interval: int = 1
|
|
134
|
+
) -> List[_T]:
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### base_url: str
|
|
138
|
+
|
|
139
|
+
Common part of request URL.
|
|
140
|
+
This will be help to download URLs got from HTML.
|
|
141
|
+
|
|
142
|
+
#### list_url: Iterable[str]
|
|
143
|
+
|
|
144
|
+
List of URL. Method will download them in parallel.
|
|
145
|
+
Absolute URL having same base URL as `base_url` also can be specified.
|
|
146
|
+
|
|
147
|
+
#### analyzer: HtmlAnalyzer[_T]
|
|
148
|
+
|
|
149
|
+
The instance extends `HtmlAnalyzer` to analyze HTML by using [BeautifulSoup](https://pypi.org/project/beautifulsoup4/).
|
|
150
|
+
Following example will be help to understand its roll:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
class AnalyzerExample(HtmlAnalyzer):
|
|
154
|
+
async def execute(self, soup: BeautifulSoup) -> str:
|
|
155
|
+
return soup.find('title').text
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### limit: int = 5
|
|
159
|
+
|
|
160
|
+
Limit number of parallel processes.
|
|
161
|
+
|
|
162
|
+
#### interval: int = 1
|
|
163
|
+
|
|
164
|
+
Interval between each request(second).
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Parallel HTML Scraper
|
|
2
|
+
|
|
3
|
+
[](https://github.com/yukihiko-shinoda/parallel-html-scraper/actions?query=workflow%3ATest)
|
|
4
|
+
[](https://codeclimate.com/github/yukihiko-shinoda/parallel-html-scraper/test_coverage)
|
|
5
|
+
[](https://codeclimate.com/github/yukihiko-shinoda/parallel-html-scraper/maintainability)
|
|
6
|
+
[](https://codeclimate.com/github/yukihiko-shinoda/parallel-html-scraper)
|
|
7
|
+
[](https://pyup.io/repos/github/yukihiko-shinoda/parallel-html-scraper/)
|
|
8
|
+
[](https://pypi.org/project/parallelhtmlscraper/)
|
|
9
|
+
[](https://pypi.org/project/parallelhtmlscraper/)
|
|
10
|
+
[](http://twitter.com/share?text=Parallel%20HTML%20Scraper&url=https://pypi.org/project/parallelhtmlscraper/&hashtags=python)
|
|
11
|
+
|
|
12
|
+
Helps you to web scrape html file in parallel without async / await syntax.
|
|
13
|
+
|
|
14
|
+
## Feature
|
|
15
|
+
|
|
16
|
+
This project helps you to web scrape html file in parallel without async / await syntax.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```console
|
|
21
|
+
pip install parallelhtmlscraper
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
Minimum example:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from bs4 import BeautifulSoup
|
|
30
|
+
|
|
31
|
+
from parallelhtmlscraper.html_analyzer import HtmlAnalyzer
|
|
32
|
+
from parallelhtmlscraper.parallel_html_scraper import ParallelHtmlScraper
|
|
33
|
+
|
|
34
|
+
class AnalyzerExample(HtmlAnalyzer):
|
|
35
|
+
async def execute(self, soup: BeautifulSoup) -> str:
|
|
36
|
+
return soup.find('title').text
|
|
37
|
+
|
|
38
|
+
host_google = 'https://www.google.com'
|
|
39
|
+
path_and_content = [
|
|
40
|
+
'', # Google Search
|
|
41
|
+
'/imghp?hl=EN', # Google Images
|
|
42
|
+
'/shopping?hl=en', # Google Shopping
|
|
43
|
+
'/save', # Collection
|
|
44
|
+
'https://www.google.com/maps?hl=en', # Google Maps
|
|
45
|
+
'https://www.google.com/drive/apps.html', # Google drive
|
|
46
|
+
'https://www.google.com/mail/help/intl/en/about.html?vm=r', # GMail
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
list_response = ParallelHtmlScraper.execute(host_google, path_and_content, AnalyzerExample())
|
|
50
|
+
print(list_response)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```console
|
|
54
|
+
$ python test.py
|
|
55
|
+
['\n Gmail - Email from Google\n ', 'Google Images', ' Google Maps ', 'Using Google Drive - New Features, Benefits & Advantages of Google Cloud Storage', 'Google Shopping', 'Google', 'Collections']
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
### ParallelMediaDownloader.execute
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
class ParallelHtmlScraper:
|
|
64
|
+
"""API of parallel HTML scraping."""
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def execute(
|
|
68
|
+
base_url: str,
|
|
69
|
+
list_url: Iterable[str],
|
|
70
|
+
analyzer: HtmlAnalyzer[_T],
|
|
71
|
+
*,
|
|
72
|
+
limit: int = 5
|
|
73
|
+
interval: int = 1
|
|
74
|
+
) -> List[_T]:
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### base_url: str
|
|
78
|
+
|
|
79
|
+
Common part of request URL.
|
|
80
|
+
This will be help to download URLs got from HTML.
|
|
81
|
+
|
|
82
|
+
#### list_url: Iterable[str]
|
|
83
|
+
|
|
84
|
+
List of URL. Method will download them in parallel.
|
|
85
|
+
Absolute URL having same base URL as `base_url` also can be specified.
|
|
86
|
+
|
|
87
|
+
#### analyzer: HtmlAnalyzer[_T]
|
|
88
|
+
|
|
89
|
+
The instance extends `HtmlAnalyzer` to analyze HTML by using [BeautifulSoup](https://pypi.org/project/beautifulsoup4/).
|
|
90
|
+
Following example will be help to understand its roll:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
class AnalyzerExample(HtmlAnalyzer):
|
|
94
|
+
async def execute(self, soup: BeautifulSoup) -> str:
|
|
95
|
+
return soup.find('title').text
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### limit: int = 5
|
|
99
|
+
|
|
100
|
+
Limit number of parallel processes.
|
|
101
|
+
|
|
102
|
+
#### interval: int = 1
|
|
103
|
+
|
|
104
|
+
Interval between each request(second).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Copyright (C) 2019 Yukihiko Shinoda
|
|
2
|
+
"""Helps to scrape html file."""
|
|
3
|
+
|
|
4
|
+
from parallelhtmlscraper.exceptions import Error
|
|
5
|
+
from parallelhtmlscraper.exceptions import HttpTimeoutError
|
|
6
|
+
from parallelhtmlscraper.html_analyzer import HtmlAnalyzer
|
|
7
|
+
from parallelhtmlscraper.parallel_html_scraper import ParallelHtmlScraper
|
|
8
|
+
|
|
9
|
+
__version__ = "0.2.0"
|
|
10
|
+
|
|
11
|
+
__all__ = ["Error", "HtmlAnalyzer", "HttpTimeoutError", "ParallelHtmlScraper"]
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
# Copyright (C) 2019 Yukihiko Shinoda
|
|
2
|
+
"""This module implements exceptions for this package."""
|
|
3
|
+
|
|
4
|
+
__all__ = ["Error", "HttpTimeoutError"]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Error(Exception):
|
|
8
|
+
"""Base class for exceptions in this module.
|
|
9
|
+
|
|
10
|
+
@see https://docs.python.org/3/tutorial/errors.html#user-defined-exceptions
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HttpTimeoutError(Error):
|
|
15
|
+
def __init__(self, url: str) -> None:
|
|
16
|
+
super().__init__(url)
|
|
17
|
+
self.url = url
|
|
18
|
+
|
|
19
|
+
def __str__(self) -> str:
|
|
20
|
+
return f"HttpTimeoutError. URL = {self.url}"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright (C) 2019 Yukihiko Shinoda
|
|
2
|
+
"""Base classes of HTML analyzer."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from abc import ABC
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import Generic
|
|
9
|
+
from typing import TypeVar
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from bs4 import BeautifulSoup
|
|
13
|
+
|
|
14
|
+
__all__ = ["HtmlAnalyzer"]
|
|
15
|
+
|
|
16
|
+
# noinspection PyShadowingBuiltins
|
|
17
|
+
_T = TypeVar("_T")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HtmlAnalyzer(ABC, Generic[_T]):
|
|
21
|
+
async def execute(self, soup: BeautifulSoup) -> _T:
|
|
22
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Copyright (C) 2019 Yukihiko Shinoda
|
|
2
|
+
"""Controlls traffic of parallel processes for HTML request."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from logging import getLogger
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
from typing import TypeVar
|
|
10
|
+
|
|
11
|
+
from aiohttp import ClientTimeout
|
|
12
|
+
|
|
13
|
+
from parallelhtmlscraper.exceptions import HttpTimeoutError
|
|
14
|
+
from parallelhtmlscraper.html_text_load_coroutine import HtmlTextLoadCoroutine
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from aiohttp import ClientSession
|
|
18
|
+
from bs4 import BeautifulSoup
|
|
19
|
+
|
|
20
|
+
from parallelhtmlscraper.html_analyzer import HtmlAnalyzer
|
|
21
|
+
|
|
22
|
+
# noinspection PyShadowingBuiltins
|
|
23
|
+
_T = TypeVar("_T")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HtmlRequestCoroutine:
|
|
27
|
+
"""Controlls traffic of parallel processes for HTML request."""
|
|
28
|
+
|
|
29
|
+
# ↓ Reason: Based on experience
|
|
30
|
+
SEMAPHORE_MAX_VALUE = 5
|
|
31
|
+
# ↓ Reason: Librahack incident @see https://www.google.com/search?q=Librahack%E4%BA%8B%E4%BB%B6
|
|
32
|
+
MINIMUM_INTERVAL_REQUEST = 1
|
|
33
|
+
# ↓ Reason: Based on experience
|
|
34
|
+
TIME_OUT = 30
|
|
35
|
+
# ↓ Reason: Based on experience
|
|
36
|
+
INTERVAL_ACCESS_TO_RESPONSE_BODY = 0.12
|
|
37
|
+
|
|
38
|
+
def __init__(self, *, semaphore: asyncio.Semaphore | None = None) -> None:
|
|
39
|
+
self.semaphore = asyncio.Semaphore(self.SEMAPHORE_MAX_VALUE) if semaphore is None else semaphore
|
|
40
|
+
self.semaphore_for_interval = asyncio.Semaphore(1)
|
|
41
|
+
self.logger = getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
async def execute(
|
|
44
|
+
self,
|
|
45
|
+
client_session: ClientSession,
|
|
46
|
+
url: str,
|
|
47
|
+
analyzer: HtmlAnalyzer[_T],
|
|
48
|
+
*,
|
|
49
|
+
interval: int = MINIMUM_INTERVAL_REQUEST,
|
|
50
|
+
) -> _T:
|
|
51
|
+
"""Request the URL with limiting the number of parallel requests and the interval between them."""
|
|
52
|
+
await self._acquire_slot(interval)
|
|
53
|
+
try:
|
|
54
|
+
soup = await self._request(client_session, url)
|
|
55
|
+
finally:
|
|
56
|
+
self.semaphore.release()
|
|
57
|
+
return await analyzer.execute(soup)
|
|
58
|
+
|
|
59
|
+
async def _acquire_slot(self, interval: int) -> None:
|
|
60
|
+
async with self.semaphore_for_interval:
|
|
61
|
+
# Waits for a free slot while holding the interval lock
|
|
62
|
+
# so that the next request can't start until this one has started.
|
|
63
|
+
await asyncio.sleep(interval)
|
|
64
|
+
await self.semaphore.acquire()
|
|
65
|
+
|
|
66
|
+
async def _request(self, client_session: ClientSession, url: str) -> BeautifulSoup:
|
|
67
|
+
try:
|
|
68
|
+
response = await client_session.get(url, timeout=ClientTimeout(total=self.TIME_OUT))
|
|
69
|
+
except asyncio.TimeoutError as error:
|
|
70
|
+
raise HttpTimeoutError(url=url) from error
|
|
71
|
+
# ↓ Because response raises error when access to response body too fast
|
|
72
|
+
await asyncio.sleep(self.INTERVAL_ACCESS_TO_RESPONSE_BODY)
|
|
73
|
+
response.raise_for_status()
|
|
74
|
+
self.logger.info("status = %s", response.status)
|
|
75
|
+
return await HtmlTextLoadCoroutine.execute(response)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Copyright (C) 2019 Yukihiko Shinoda
|
|
2
|
+
"""Parser for response body to BeautifulSoup."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from logging import getLogger
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from bs4 import BeautifulSoup
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from aiohttp import ClientResponse
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HtmlTextLoadCoroutine:
|
|
16
|
+
"""Parser for response body to BeautifulSoup."""
|
|
17
|
+
|
|
18
|
+
LOGGER = getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
async def execute(cls, response: ClientResponse) -> BeautifulSoup:
|
|
22
|
+
return BeautifulSoup(await cls._execute(response), "html.parser")
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
async def _execute(cls, response: ClientResponse) -> str:
|
|
26
|
+
try:
|
|
27
|
+
return await response.text()
|
|
28
|
+
except UnicodeDecodeError:
|
|
29
|
+
# if response header includes no encoding information,
|
|
30
|
+
# aiohttp try to detect encoding by chardet.
|
|
31
|
+
# However chardet doesn't support Emoji.
|
|
32
|
+
cls.LOGGER.exception("UnicodeDecodeError. URL = %s, encoding = %s", response.url, response.get_encoding())
|
|
33
|
+
return await response.text("utf-8")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Copyright (C) 2019 Yukihiko Shinoda
|
|
2
|
+
"""Executes HTML requests as parallel."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import TypeVar
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
|
|
12
|
+
from parallelhtmlscraper.html_request_coroutine import HtmlRequestCoroutine
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Iterable
|
|
16
|
+
|
|
17
|
+
from parallelhtmlscraper.html_analyzer import HtmlAnalyzer
|
|
18
|
+
|
|
19
|
+
# noinspection PyShadowingBuiltins
|
|
20
|
+
_T = TypeVar("_T")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ParallelHtmlRequestCoroutine:
|
|
24
|
+
"""Executes HTML requests as parallel."""
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
async def execute(
|
|
28
|
+
base_url: str,
|
|
29
|
+
list_url: Iterable[str],
|
|
30
|
+
analyzer: HtmlAnalyzer[_T],
|
|
31
|
+
*,
|
|
32
|
+
semaphore: asyncio.Semaphore | None = None,
|
|
33
|
+
interval: int = HtmlRequestCoroutine.MINIMUM_INTERVAL_REQUEST,
|
|
34
|
+
) -> list[_T]:
|
|
35
|
+
"""Execute HTML requests in parallel."""
|
|
36
|
+
semaphore = asyncio.Semaphore(HtmlRequestCoroutine.SEMAPHORE_MAX_VALUE) if semaphore is None else semaphore
|
|
37
|
+
html_request_coroutine = HtmlRequestCoroutine(semaphore=semaphore)
|
|
38
|
+
async with aiohttp.ClientSession() as session:
|
|
39
|
+
tasks = [
|
|
40
|
+
html_request_coroutine.execute(
|
|
41
|
+
session,
|
|
42
|
+
url if base_url in url else f"{base_url}{url}",
|
|
43
|
+
analyzer,
|
|
44
|
+
interval=interval,
|
|
45
|
+
)
|
|
46
|
+
for url in list_url
|
|
47
|
+
]
|
|
48
|
+
return await asyncio.gather(*tasks)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Copyright (C) 2019 Yukihiko Shinoda
|
|
2
|
+
"""API of parallel HTML scraping."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import TypeVar
|
|
9
|
+
|
|
10
|
+
from parallelhtmlscraper.html_request_coroutine import HtmlRequestCoroutine
|
|
11
|
+
from parallelhtmlscraper.parallel_html_request_coroutine import ParallelHtmlRequestCoroutine
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Iterable
|
|
15
|
+
|
|
16
|
+
from parallelhtmlscraper.html_analyzer import HtmlAnalyzer
|
|
17
|
+
|
|
18
|
+
__all__ = ["ParallelHtmlScraper"]
|
|
19
|
+
# noinspection PyShadowingBuiltins
|
|
20
|
+
_T = TypeVar("_T")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ParallelHtmlScraper:
|
|
24
|
+
"""API of parallel HTML scraping.
|
|
25
|
+
|
|
26
|
+
Following steps hasn't implemented:
|
|
27
|
+
- don't follow link when a tag has rel="nofollow"
|
|
28
|
+
- Follow attributes of robots meta tag
|
|
29
|
+
- Follow X-Robots-Tag in HTTP header
|
|
30
|
+
- Follow robots.txt
|
|
31
|
+
- Set User-agent and etc...
|
|
32
|
+
@see https://vaaaaaanquish.hatenablog.com/entry/2017/12/01/064227
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def execute(
|
|
37
|
+
base_url: str,
|
|
38
|
+
list_url: Iterable[str],
|
|
39
|
+
analyzer: HtmlAnalyzer[_T],
|
|
40
|
+
*,
|
|
41
|
+
limit: int = HtmlRequestCoroutine.SEMAPHORE_MAX_VALUE,
|
|
42
|
+
interval: int = HtmlRequestCoroutine.MINIMUM_INTERVAL_REQUEST,
|
|
43
|
+
) -> list[_T]:
|
|
44
|
+
"""Execute parallel HTML scraping."""
|
|
45
|
+
return asyncio.run(ParallelHtmlScraper._execute(base_url, list_url, analyzer, limit=limit, interval=interval))
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
async def _execute(
|
|
49
|
+
base_url: str,
|
|
50
|
+
list_url: Iterable[str],
|
|
51
|
+
analyzer: HtmlAnalyzer[_T],
|
|
52
|
+
*,
|
|
53
|
+
limit: int,
|
|
54
|
+
interval: int,
|
|
55
|
+
) -> list[_T]:
|
|
56
|
+
# Reason: Before Python 3.10, asyncio.Semaphore binds the event loop when instantiated,
|
|
57
|
+
# so it has to be instantiated in the event loop which asyncio.run() creates.
|
|
58
|
+
return await ParallelHtmlRequestCoroutine.execute(
|
|
59
|
+
base_url,
|
|
60
|
+
list_url,
|
|
61
|
+
analyzer,
|
|
62
|
+
semaphore=asyncio.Semaphore(limit),
|
|
63
|
+
interval=interval,
|
|
64
|
+
)
|