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.
Files changed (34) hide show
  1. parallelhtmlscraper-0.2.0/LICENSE +21 -0
  2. parallelhtmlscraper-0.2.0/MANIFEST.in +1 -0
  3. parallelhtmlscraper-0.2.0/PKG-INFO +164 -0
  4. parallelhtmlscraper-0.2.0/README.md +104 -0
  5. parallelhtmlscraper-0.2.0/parallelhtmlscraper/__init__.py +11 -0
  6. {parallelhtmlscraper-0.0.0 → parallelhtmlscraper-0.2.0}/parallelhtmlscraper/exceptions.py +20 -18
  7. parallelhtmlscraper-0.2.0/parallelhtmlscraper/html_analyzer.py +22 -0
  8. parallelhtmlscraper-0.2.0/parallelhtmlscraper/html_request_coroutine.py +75 -0
  9. parallelhtmlscraper-0.2.0/parallelhtmlscraper/html_text_load_coroutine.py +33 -0
  10. parallelhtmlscraper-0.2.0/parallelhtmlscraper/parallel_html_request_coroutine.py +48 -0
  11. parallelhtmlscraper-0.2.0/parallelhtmlscraper/parallel_html_scraper.py +64 -0
  12. parallelhtmlscraper-0.2.0/parallelhtmlscraper.egg-info/PKG-INFO +164 -0
  13. {parallelhtmlscraper-0.0.0 → parallelhtmlscraper-0.2.0}/parallelhtmlscraper.egg-info/SOURCES.txt +9 -2
  14. parallelhtmlscraper-0.2.0/parallelhtmlscraper.egg-info/not-zip-safe +1 -0
  15. parallelhtmlscraper-0.2.0/pyproject.toml +233 -0
  16. {parallelhtmlscraper-0.0.0 → parallelhtmlscraper-0.2.0}/setup.cfg +4 -4
  17. parallelhtmlscraper-0.2.0/tests/test_hrml_analyzer.py +32 -0
  18. parallelhtmlscraper-0.2.0/tests/test_html_request_coroutine.py +53 -0
  19. parallelhtmlscraper-0.2.0/tests/test_html_text_load_coroutine.py +44 -0
  20. parallelhtmlscraper-0.2.0/tests/test_parallel_url_scraper.py +76 -0
  21. parallelhtmlscraper-0.0.0/PKG-INFO +0 -15
  22. parallelhtmlscraper-0.0.0/README.md +0 -3
  23. parallelhtmlscraper-0.0.0/parallelhtmlscraper/__init__.py +0 -11
  24. parallelhtmlscraper-0.0.0/parallelhtmlscraper/html_analyzer.py +0 -13
  25. parallelhtmlscraper-0.0.0/parallelhtmlscraper/html_request_coroutine.py +0 -54
  26. parallelhtmlscraper-0.0.0/parallelhtmlscraper/html_text_load_coroutine.py +0 -26
  27. parallelhtmlscraper-0.0.0/parallelhtmlscraper/parallel_html_request_coroutine.py +0 -26
  28. parallelhtmlscraper-0.0.0/parallelhtmlscraper/parallel_html_scraper.py +0 -30
  29. parallelhtmlscraper-0.0.0/parallelhtmlscraper.egg-info/PKG-INFO +0 -15
  30. parallelhtmlscraper-0.0.0/setup.py +0 -33
  31. {parallelhtmlscraper-0.0.0 → parallelhtmlscraper-0.2.0}/parallelhtmlscraper/py.typed +0 -0
  32. {parallelhtmlscraper-0.0.0 → parallelhtmlscraper-0.2.0}/parallelhtmlscraper.egg-info/dependency_links.txt +0 -0
  33. {parallelhtmlscraper-0.0.0 → parallelhtmlscraper-0.2.0}/parallelhtmlscraper.egg-info/requires.txt +0 -0
  34. {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
+ [![Test](https://github.com/yukihiko-shinoda/parallel-html-scraper/workflows/Test/badge.svg)](https://github.com/yukihiko-shinoda/parallel-html-scraper/actions?query=workflow%3ATest)
64
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/5f07cb148a0770db4be4/test_coverage)](https://codeclimate.com/github/yukihiko-shinoda/parallel-html-scraper/test_coverage)
65
+ [![Maintainability](https://api.codeclimate.com/v1/badges/5f07cb148a0770db4be4/maintainability)](https://codeclimate.com/github/yukihiko-shinoda/parallel-html-scraper/maintainability)
66
+ [![Code Climate technical debt](https://img.shields.io/codeclimate/tech-debt/yukihiko-shinoda/parallel-html-scraper)](https://codeclimate.com/github/yukihiko-shinoda/parallel-html-scraper)
67
+ [![Updates](https://pyup.io/repos/github/yukihiko-shinoda/parallel-html-scraper/shield.svg)](https://pyup.io/repos/github/yukihiko-shinoda/parallel-html-scraper/)
68
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/parallelhtmlscraper)](https://pypi.org/project/parallelhtmlscraper/)
69
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/parallelhtmlscraper)](https://pypi.org/project/parallelhtmlscraper/)
70
+ [![Twitter URL](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2Fyukihiko-shinoda%2Fparallel-html-scraper)](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
+ [![Test](https://github.com/yukihiko-shinoda/parallel-html-scraper/workflows/Test/badge.svg)](https://github.com/yukihiko-shinoda/parallel-html-scraper/actions?query=workflow%3ATest)
4
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/5f07cb148a0770db4be4/test_coverage)](https://codeclimate.com/github/yukihiko-shinoda/parallel-html-scraper/test_coverage)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/5f07cb148a0770db4be4/maintainability)](https://codeclimate.com/github/yukihiko-shinoda/parallel-html-scraper/maintainability)
6
+ [![Code Climate technical debt](https://img.shields.io/codeclimate/tech-debt/yukihiko-shinoda/parallel-html-scraper)](https://codeclimate.com/github/yukihiko-shinoda/parallel-html-scraper)
7
+ [![Updates](https://pyup.io/repos/github/yukihiko-shinoda/parallel-html-scraper/shield.svg)](https://pyup.io/repos/github/yukihiko-shinoda/parallel-html-scraper/)
8
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/parallelhtmlscraper)](https://pypi.org/project/parallelhtmlscraper/)
9
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/parallelhtmlscraper)](https://pypi.org/project/parallelhtmlscraper/)
10
+ [![Twitter URL](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2Fyukihiko-shinoda%2Fparallel-html-scraper)](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
- """This module implements exceptions for this package."""
2
- __all__ = ['Error', 'HttpTimeoutError']
3
-
4
-
5
- class Error(Exception):
6
- """
7
- Base class for exceptions in this module.
8
- @see https://docs.python.org/3/tutorial/errors.html#user-defined-exceptions
9
- """
10
-
11
-
12
- class HttpTimeoutError(Error):
13
- def __init__(self, *args, url: str):
14
- super().__init__(*args)
15
- self.url = url
16
-
17
- def __str__(self) -> str:
18
- return f'HttpTimeoutError. URL = {self.url}'
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
+ )