fetta 0.2.0__tar.gz → 0.3.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.
- {fetta-0.2.0 → fetta-0.3.0}/PKG-INFO +1 -1
- {fetta-0.2.0 → fetta-0.3.0}/pyproject.toml +1 -1
- {fetta-0.2.0 → fetta-0.3.0}/pyproject.toml.orig +1 -1
- {fetta-0.2.0 → fetta-0.3.0}/src/fetta/__init__.py +2 -1
- {fetta-0.2.0 → fetta-0.3.0}/src/fetta/base.py +6 -3
- fetta-0.3.0/src/fetta/browser.py +54 -0
- {fetta-0.2.0 → fetta-0.3.0}/src/fetta/fetchers.py +39 -1
- {fetta-0.2.0 → fetta-0.3.0}/src/fetta/httpx_fetcher.py +3 -1
- {fetta-0.2.0 → fetta-0.3.0}/src/fetta/json_fetcher.py +6 -4
- {fetta-0.2.0 → fetta-0.3.0}/src/fetta/models.py +5 -2
- {fetta-0.2.0 → fetta-0.3.0}/src/fetta/playwright_fetcher.py +5 -2
- fetta-0.3.0/src/fetta/protocol.py +11 -0
- fetta-0.3.0/src/fetta/smart_fetcher.py +137 -0
- fetta-0.2.0/src/fetta/browser.py +0 -49
- fetta-0.2.0/src/fetta/protocol.py +0 -8
- {fetta-0.2.0 → fetta-0.3.0}/LICENSE +0 -0
- {fetta-0.2.0 → fetta-0.3.0}/README.md +0 -0
- {fetta-0.2.0 → fetta-0.3.0}/src/fetta/page_parser/__init__.py +0 -0
- {fetta-0.2.0 → fetta-0.3.0}/src/fetta/page_parser/cleaning.py +0 -0
- {fetta-0.2.0 → fetta-0.3.0}/src/fetta/page_parser/constants.py +0 -0
- {fetta-0.2.0 → fetta-0.3.0}/src/fetta/page_parser/content.py +0 -0
- {fetta-0.2.0 → fetta-0.3.0}/src/fetta/page_parser/helpers.py +0 -0
- {fetta-0.2.0 → fetta-0.3.0}/src/fetta/page_parser/images.py +0 -0
- {fetta-0.2.0 → fetta-0.3.0}/src/fetta/page_parser/metadata.py +0 -0
- {fetta-0.2.0 → fetta-0.3.0}/src/fetta/page_parser/navigation.py +0 -0
- {fetta-0.2.0 → fetta-0.3.0}/src/fetta/page_parser/parser.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from .browser import BrowserManager
|
|
2
|
-
from .fetchers import BrowserFetcher, HttpFetcher
|
|
2
|
+
from .fetchers import BrowserFetcher, HttpFetcher, SmartFetcher
|
|
3
3
|
from .json_fetcher import JsonFetcher
|
|
4
4
|
from .models import Link, Image, PageContent, PageMetadata, FetchResult
|
|
5
5
|
from .protocol import FetcherProtocol
|
|
@@ -15,4 +15,5 @@ __all__ = [
|
|
|
15
15
|
"HttpFetcher",
|
|
16
16
|
"FetcherProtocol",
|
|
17
17
|
"JsonFetcher",
|
|
18
|
+
"SmartFetcher"
|
|
18
19
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
|
-
from .models import FetchResult, PageContent
|
|
4
|
+
from .models import FetchResult, PageContent, PageMetadata, FetcherKind
|
|
5
5
|
from .page_parser.constants import DEFAULT_SECTIONS
|
|
6
6
|
from .page_parser.parser import parse_html
|
|
7
7
|
|
|
@@ -11,6 +11,8 @@ class BaseFetcher(ABC):
|
|
|
11
11
|
Abstract base class for fetchers.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
fetcher_kind: FetcherKind
|
|
15
|
+
|
|
14
16
|
@abstractmethod
|
|
15
17
|
async def fetch(self, url: str, **kwargs: Any) -> FetchResult:
|
|
16
18
|
"""Fetch a page and return its parsed content."""
|
|
@@ -62,7 +64,7 @@ class BaseFetcher(ABC):
|
|
|
62
64
|
text=parsed_content.get("text"),
|
|
63
65
|
raw_html=parsed_content.get("raw_html"),
|
|
64
66
|
raw_text=parsed_content.get("raw_text"),
|
|
65
|
-
metadata=
|
|
67
|
+
metadata=parsed_content.get("metadata"),
|
|
66
68
|
links=tuple(parsed_content.get("links") or []),
|
|
67
69
|
images=parsed_content.get("images") or [],
|
|
68
70
|
csr_data=tuple(csr_data) if csr_data is not None else None
|
|
@@ -71,5 +73,6 @@ class BaseFetcher(ABC):
|
|
|
71
73
|
icon=page_metadata.get("icon"),
|
|
72
74
|
image=page_metadata.get("image"),
|
|
73
75
|
description=page_metadata.get("description"),
|
|
74
|
-
status=status
|
|
76
|
+
status=status,
|
|
77
|
+
fetcher_kind=self.fetcher_kind
|
|
75
78
|
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from asyncio import Semaphore
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
from typing import AsyncIterator
|
|
4
|
+
|
|
5
|
+
from playwright.async_api import (
|
|
6
|
+
Browser,
|
|
7
|
+
Playwright,
|
|
8
|
+
async_playwright,
|
|
9
|
+
ViewportSize,
|
|
10
|
+
Page
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BrowserManager:
|
|
15
|
+
def __init__(self, max_contexts: int = 10) -> None:
|
|
16
|
+
self._semaphore = Semaphore(max_contexts)
|
|
17
|
+
self._playwright: Playwright | None = None
|
|
18
|
+
self._browser: Browser | None = None
|
|
19
|
+
|
|
20
|
+
async def start(self) -> None:
|
|
21
|
+
self._playwright = await async_playwright().start()
|
|
22
|
+
self._browser = await self._playwright.chromium.launch(headless=True)
|
|
23
|
+
|
|
24
|
+
async def stop(self) -> None:
|
|
25
|
+
if self._browser is not None:
|
|
26
|
+
await self._browser.close()
|
|
27
|
+
self._browser = None
|
|
28
|
+
if self._playwright is not None:
|
|
29
|
+
await self._playwright.stop()
|
|
30
|
+
self._playwright = None
|
|
31
|
+
|
|
32
|
+
@asynccontextmanager
|
|
33
|
+
async def page(self) -> AsyncIterator[Page]:
|
|
34
|
+
if self._browser is None:
|
|
35
|
+
raise RuntimeError("BrowserManager is not started")
|
|
36
|
+
async with self._semaphore:
|
|
37
|
+
context = await self._browser.new_context(
|
|
38
|
+
user_agent=(
|
|
39
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
40
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
41
|
+
"Chrome/153.0.0.0 Safari/537.36"
|
|
42
|
+
),
|
|
43
|
+
viewport=ViewportSize(**{"width": 1920, "height": 1080}),
|
|
44
|
+
locale="en-GB",
|
|
45
|
+
extra_http_headers={
|
|
46
|
+
"Accept-Language": "en-GB,en;q=0.9,en-US;q=0.8,en;q=0.7",
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
page = await context.new_page()
|
|
50
|
+
try:
|
|
51
|
+
yield page
|
|
52
|
+
finally:
|
|
53
|
+
await context.close()
|
|
54
|
+
|
|
@@ -4,8 +4,9 @@ from .httpx_fetcher import HttpxFetcher
|
|
|
4
4
|
from .protocol import FetcherProtocol
|
|
5
5
|
from .base import BaseFetcher
|
|
6
6
|
from .browser import BrowserManager
|
|
7
|
-
from .models import FetchResult
|
|
7
|
+
from .models import FetchResult, FetcherKind
|
|
8
8
|
from .playwright_fetcher import PlaywrightFetcher
|
|
9
|
+
from .smart_fetcher import VanillaSmartFetcher
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class HttpFetcher(BaseFetcher):
|
|
@@ -16,6 +17,8 @@ class HttpFetcher(BaseFetcher):
|
|
|
16
17
|
HttpxFetcher (sync `requests` offloaded to a worker thread).
|
|
17
18
|
"""
|
|
18
19
|
|
|
20
|
+
fetcher_kind: FetcherKind = "http"
|
|
21
|
+
|
|
19
22
|
def __init__(
|
|
20
23
|
self,
|
|
21
24
|
*,
|
|
@@ -38,6 +41,8 @@ class BrowserFetcher(BaseFetcher):
|
|
|
38
41
|
used; alternative engines can be injected via the `engine` argument.
|
|
39
42
|
"""
|
|
40
43
|
|
|
44
|
+
fetcher_kind: FetcherKind = "browser"
|
|
45
|
+
|
|
41
46
|
def __init__(
|
|
42
47
|
self,
|
|
43
48
|
manager: BrowserManager,
|
|
@@ -51,3 +56,36 @@ class BrowserFetcher(BaseFetcher):
|
|
|
51
56
|
|
|
52
57
|
async def close(self) -> None:
|
|
53
58
|
await self._engine.close()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SmartFetcher(BaseFetcher):
|
|
63
|
+
"""
|
|
64
|
+
Fetches pages with the cheapest fetcher that can render them.
|
|
65
|
+
|
|
66
|
+
The page is first fetched via the HTML fetcher. If the response
|
|
67
|
+
looks client-side rendered, the browser fetcher is used instead.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
manager: BrowserManager | None = None,
|
|
73
|
+
*,
|
|
74
|
+
html_fetcher: FetcherProtocol | None = None,
|
|
75
|
+
browser_fetcher: FetcherProtocol | None = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
html_engine: FetcherProtocol = html_fetcher or HttpxFetcher()
|
|
78
|
+
if not browser_fetcher and not manager:
|
|
79
|
+
browser_engine = None
|
|
80
|
+
else:
|
|
81
|
+
browser_engine = browser_fetcher or PlaywrightFetcher(manager)
|
|
82
|
+
self._engine = VanillaSmartFetcher(
|
|
83
|
+
html_fetcher=html_engine,
|
|
84
|
+
browser_fetcher=browser_engine
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
async def fetch(self, url: str, **kwargs: Any) -> FetchResult:
|
|
88
|
+
return await self._engine.fetch(url, **kwargs)
|
|
89
|
+
|
|
90
|
+
async def close(self) -> None:
|
|
91
|
+
await self._engine.close()
|
|
@@ -2,7 +2,7 @@ from typing import Any
|
|
|
2
2
|
|
|
3
3
|
import httpx
|
|
4
4
|
|
|
5
|
-
from .models import FetchResult
|
|
5
|
+
from .models import FetchResult, FetcherKind
|
|
6
6
|
from .base import BaseFetcher
|
|
7
7
|
from .page_parser.constants import DEFAULT_SECTIONS
|
|
8
8
|
|
|
@@ -16,6 +16,8 @@ class HttpxFetcher(BaseFetcher):
|
|
|
16
16
|
TLS-handshake overhead).
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
+
fetcher_kind: FetcherKind = "http"
|
|
20
|
+
|
|
19
21
|
def __init__(self, *, timeout: float = 10.0, http2: bool = False) -> None:
|
|
20
22
|
self._client = httpx.AsyncClient(
|
|
21
23
|
timeout=timeout,
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
from
|
|
2
|
-
from
|
|
3
|
-
from
|
|
4
|
-
|
|
1
|
+
from .base import BaseFetcher
|
|
2
|
+
from .browser import BrowserManager
|
|
3
|
+
from .models import FetchResult, FetcherKind
|
|
5
4
|
|
|
6
5
|
_BLOCK = {"image", "media", "font"}
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class JsonFetcher(BaseFetcher):
|
|
9
|
+
|
|
10
|
+
fetcher_kind: FetcherKind = "browser"
|
|
11
|
+
|
|
10
12
|
def __init__(self, browser: BrowserManager) -> None:
|
|
11
13
|
self._browser = browser
|
|
12
14
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from typing import TypedDict
|
|
2
|
+
from typing import TypedDict, Literal
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class PageMetadata(TypedDict):
|
|
@@ -35,16 +35,19 @@ class PageContent:
|
|
|
35
35
|
text: str | None
|
|
36
36
|
raw_html: str | None
|
|
37
37
|
raw_text: str | None
|
|
38
|
-
metadata: PageMetadata
|
|
38
|
+
metadata: PageMetadata | None
|
|
39
39
|
links: tuple[Link, ...]
|
|
40
40
|
images: tuple[Image, ...]
|
|
41
41
|
csr_data: tuple[dict, ...] | None
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
FetcherKind = Literal["http", "browser"]
|
|
45
|
+
|
|
44
46
|
@dataclass(frozen=True, slots=True)
|
|
45
47
|
class FetchResult:
|
|
46
48
|
url: str
|
|
47
49
|
status: int
|
|
50
|
+
fetcher_kind: FetcherKind
|
|
48
51
|
# Meta
|
|
49
52
|
title: str | None
|
|
50
53
|
icon: str | None
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from fetta.base import BaseFetcher
|
|
2
2
|
from fetta.browser import BrowserManager
|
|
3
|
-
from fetta.models import FetchResult
|
|
3
|
+
from fetta.models import FetchResult, FetcherKind
|
|
4
4
|
from fetta.page_parser.constants import DEFAULT_SECTIONS
|
|
5
5
|
|
|
6
6
|
_BLOCK = {"image", "media", "font"}
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class PlaywrightFetcher(BaseFetcher):
|
|
10
|
+
|
|
11
|
+
fetcher_kind: FetcherKind = "browser"
|
|
12
|
+
|
|
10
13
|
def __init__(self, browser: BrowserManager) -> None:
|
|
11
14
|
self._browser = browser
|
|
12
15
|
|
|
@@ -45,7 +48,7 @@ class PlaywrightFetcher(BaseFetcher):
|
|
|
45
48
|
return self._parse(
|
|
46
49
|
html=html,
|
|
47
50
|
url=url,
|
|
48
|
-
status=response.status if response else
|
|
51
|
+
status=response.status if response else 500,
|
|
49
52
|
title=title,
|
|
50
53
|
include=include
|
|
51
54
|
)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import replace
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from bs4 import BeautifulSoup
|
|
6
|
+
|
|
7
|
+
from .protocol import FetcherProtocol
|
|
8
|
+
from .models import FetchResult
|
|
9
|
+
from .base import BaseFetcher
|
|
10
|
+
from .page_parser.constants import DEFAULT_SECTIONS
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
EMPTY_ROOT_IDS = frozenset({
|
|
16
|
+
"root", "app", "__next", "__nuxt", "___gatsby",
|
|
17
|
+
"react-root", "svelte", "main-app", "ember-app"
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
FRAMEWORK_MARKERS = (
|
|
21
|
+
"data-reactroot", "_reactRootContainer",
|
|
22
|
+
"__NEXT_DATA__", "__NUXT__", "__SVELTE__",
|
|
23
|
+
"___gatsby", "ng-version=", "ng-app=",
|
|
24
|
+
"window.__INITIAL_STATE__", "window.__APOLLO_STATE__",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
SSR_MARKERS = (
|
|
28
|
+
'data-server-rendered="true"', # Vue SSR
|
|
29
|
+
"data-reactroot", # React SSR (hydrated)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
NON_CONTAINER_TAGS = frozenset({"script", "style", "link", "meta"})
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class VanillaSmartFetcher(BaseFetcher):
|
|
36
|
+
"""
|
|
37
|
+
Wraps an HTML fetcher and a browser fetcher.
|
|
38
|
+
|
|
39
|
+
Contract: the underlying HTML fetcher must be able to return
|
|
40
|
+
`raw_html`, otherwise `_is_client_side_rendering` cannot detect
|
|
41
|
+
client-side-rendered pages reliably. VanillaSmartFetcher adds
|
|
42
|
+
`raw_html` to `include` whenever the caller supplies one.
|
|
43
|
+
|
|
44
|
+
`fetcher_kind` on the result reflects the fetcher that actually
|
|
45
|
+
produced the content ("http" or "browser"), not "composite".
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
html_fetcher: FetcherProtocol,
|
|
51
|
+
browser_fetcher: FetcherProtocol | None
|
|
52
|
+
):
|
|
53
|
+
self._html_fetcher = html_fetcher
|
|
54
|
+
self._browser_fetcher = browser_fetcher
|
|
55
|
+
|
|
56
|
+
async def fetch(
|
|
57
|
+
self,
|
|
58
|
+
url: str,
|
|
59
|
+
*,
|
|
60
|
+
include: tuple[str, ...] = DEFAULT_SECTIONS,
|
|
61
|
+
**kwargs: Any
|
|
62
|
+
) -> FetchResult:
|
|
63
|
+
# Note: We must return raw_html to check whether content is CSR
|
|
64
|
+
html_include = tuple(set(include + ("raw_html", "raw_text")))
|
|
65
|
+
response = await self._html_fetcher.fetch(url=url, include=html_include, **kwargs)
|
|
66
|
+
|
|
67
|
+
if response.status >= 400:
|
|
68
|
+
return self._build_response(response, include)
|
|
69
|
+
|
|
70
|
+
if self._browser_fetcher and self._is_client_side_rendering(response):
|
|
71
|
+
return await self._browser_fetcher.fetch(url=url, include=include, **kwargs)
|
|
72
|
+
|
|
73
|
+
return self._build_response(response, include)
|
|
74
|
+
|
|
75
|
+
async def close(self) -> None:
|
|
76
|
+
await self._html_fetcher.close()
|
|
77
|
+
if self._browser_fetcher:
|
|
78
|
+
await self._browser_fetcher.close()
|
|
79
|
+
|
|
80
|
+
def _build_response(
|
|
81
|
+
self,
|
|
82
|
+
response: FetchResult,
|
|
83
|
+
include: tuple[str, ...]
|
|
84
|
+
) -> FetchResult:
|
|
85
|
+
new_content = replace(
|
|
86
|
+
response.content,
|
|
87
|
+
raw_html=response.content.raw_html if "raw_html" in include else None,
|
|
88
|
+
raw_text=response.content.raw_text if "raw_text" in include else None
|
|
89
|
+
)
|
|
90
|
+
return replace(response, content=new_content)
|
|
91
|
+
|
|
92
|
+
def _is_client_side_rendering(self, response: FetchResult) -> bool:
|
|
93
|
+
|
|
94
|
+
html = response.content.raw_html
|
|
95
|
+
html_len = len(html)
|
|
96
|
+
if not html or html_len < 300:
|
|
97
|
+
logger.debug("CSR check: skip, html_len=%d < 300", html_len)
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
raw_text = response.content.raw_text
|
|
101
|
+
text_len = len(raw_text) if raw_text else 0
|
|
102
|
+
|
|
103
|
+
has_framework = any(m in html for m in FRAMEWORK_MARKERS)
|
|
104
|
+
has_ssr = any(m in html for m in SSR_MARKERS)
|
|
105
|
+
|
|
106
|
+
logger.debug(
|
|
107
|
+
"CSR check: html_len=%d text_len=%d framework=%s ssr=%s",
|
|
108
|
+
html_len, text_len, has_framework, has_ssr,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if has_framework and has_ssr:
|
|
112
|
+
logger.debug("CSR check: NOT CSR (framework + SSR marker)")
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
# No framework marker. Extremely thin text on a big page
|
|
116
|
+
if text_len < 2000 and len(html) > 10000:
|
|
117
|
+
logger.debug("CSR check: CSR (thin text on large html)")
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
# Last resort: explicit empty root
|
|
121
|
+
if text_len < 2000:
|
|
122
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
123
|
+
empty_root = self._has_empty_spa_root(soup)
|
|
124
|
+
logger.debug("CSR check: empty_root=%s", empty_root)
|
|
125
|
+
return empty_root
|
|
126
|
+
|
|
127
|
+
logger.debug("CSR check: NOT CSR (default)")
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
def _has_empty_spa_root(self, soup: BeautifulSoup) -> bool:
|
|
131
|
+
for elem in soup.find_all(id=True):
|
|
132
|
+
if elem.name in NON_CONTAINER_TAGS:
|
|
133
|
+
continue
|
|
134
|
+
if elem["id"].lower() in EMPTY_ROOT_IDS and not elem.get_text(strip=True):
|
|
135
|
+
return True
|
|
136
|
+
return False
|
|
137
|
+
|
fetta-0.2.0/src/fetta/browser.py
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
from contextlib import asynccontextmanager
|
|
2
|
-
|
|
3
|
-
from playwright.async_api import (
|
|
4
|
-
Browser,
|
|
5
|
-
Playwright,
|
|
6
|
-
async_playwright,
|
|
7
|
-
ViewportSize,
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class BrowserManager:
|
|
12
|
-
def __init__(self) -> None:
|
|
13
|
-
self._playwright: Playwright | None = None
|
|
14
|
-
self._browser: Browser | None = None
|
|
15
|
-
|
|
16
|
-
async def start(self) -> None:
|
|
17
|
-
self._playwright = await async_playwright().start()
|
|
18
|
-
self._browser = await self._playwright.chromium.launch(headless=True)
|
|
19
|
-
|
|
20
|
-
async def stop(self) -> None:
|
|
21
|
-
if self._browser is not None:
|
|
22
|
-
await self._browser.close()
|
|
23
|
-
self._browser = None
|
|
24
|
-
if self._playwright is not None:
|
|
25
|
-
await self._playwright.stop()
|
|
26
|
-
self._playwright = None
|
|
27
|
-
|
|
28
|
-
@asynccontextmanager
|
|
29
|
-
async def page(self):
|
|
30
|
-
if self._browser is None:
|
|
31
|
-
raise RuntimeError("BrowserManager is not started")
|
|
32
|
-
|
|
33
|
-
context = await self._browser.new_context(
|
|
34
|
-
user_agent=(
|
|
35
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
36
|
-
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
37
|
-
"Chrome/153.0.0.0 Safari/537.36"
|
|
38
|
-
),
|
|
39
|
-
viewport=ViewportSize(**{"width": 1920, "height": 1080}),
|
|
40
|
-
locale="en-GB",
|
|
41
|
-
extra_http_headers={
|
|
42
|
-
"Accept-Language": "en-GB,en;q=0.9,en-US;q=0.8,en;q=0.7",
|
|
43
|
-
},
|
|
44
|
-
)
|
|
45
|
-
page = await context.new_page()
|
|
46
|
-
try:
|
|
47
|
-
yield page
|
|
48
|
-
finally:
|
|
49
|
-
await context.close()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|