refactor(fetchers): Less duplicated code and better handling for unstable websites
- Websites that never finish loading their requests won't crash the code now if you used `network_idle` with them - Fixed a typo in `load_dom` in DynamicSession's async_fetch - Removed dead code
This commit is contained in:
@@ -2,17 +2,27 @@ from time import time
|
|||||||
from asyncio import sleep as asyncio_sleep, Lock
|
from asyncio import sleep as asyncio_sleep, Lock
|
||||||
|
|
||||||
from camoufox import DefaultAddons
|
from camoufox import DefaultAddons
|
||||||
from playwright.sync_api import BrowserContext, Playwright
|
from playwright.sync_api import (
|
||||||
from playwright.async_api import (
|
Page,
|
||||||
BrowserContext as AsyncBrowserContext,
|
Frame,
|
||||||
Playwright as AsyncPlaywright,
|
BrowserContext,
|
||||||
|
Playwright,
|
||||||
|
Response as SyncPlaywrightResponse,
|
||||||
)
|
)
|
||||||
|
from playwright.async_api import (
|
||||||
|
Page as AsyncPage,
|
||||||
|
Frame as AsyncFrame,
|
||||||
|
Playwright as AsyncPlaywright,
|
||||||
|
Response as AsyncPlaywrightResponse,
|
||||||
|
BrowserContext as AsyncBrowserContext,
|
||||||
|
)
|
||||||
|
from playwright._impl._errors import Error as PlaywrightError
|
||||||
from camoufox.pkgman import installed_verstr as camoufox_version
|
from camoufox.pkgman import installed_verstr as camoufox_version
|
||||||
from camoufox.utils import launch_options as generate_launch_options
|
from camoufox.utils import launch_options as generate_launch_options
|
||||||
|
|
||||||
from ._page import PageInfo, PagePool
|
from ._page import PageInfo, PagePool
|
||||||
from scrapling.parser import Selector
|
from scrapling.parser import Selector
|
||||||
from scrapling.core._types import Any, cast, Dict, Optional, TYPE_CHECKING
|
from scrapling.core._types import Any, cast, Dict, List, Optional, Callable, TYPE_CHECKING
|
||||||
from scrapling.engines.toolbelt.fingerprints import get_os_name
|
from scrapling.engines.toolbelt.fingerprints import get_os_name
|
||||||
from ._validators import validate, PlaywrightConfig, CamoufoxConfig
|
from ._validators import validate, PlaywrightConfig, CamoufoxConfig
|
||||||
from ._config_tools import _compiled_stealth_scripts, _launch_kwargs, _context_kwargs
|
from ._config_tools import _compiled_stealth_scripts, _launch_kwargs, _context_kwargs
|
||||||
@@ -26,10 +36,35 @@ class SyncSession:
|
|||||||
self.max_pages = max_pages
|
self.max_pages = max_pages
|
||||||
self.page_pool = PagePool(max_pages)
|
self.page_pool = PagePool(max_pages)
|
||||||
self._max_wait_for_page = 60
|
self._max_wait_for_page = 60
|
||||||
self.playwright: Optional[Playwright] = None
|
self.playwright: Playwright | Any = None
|
||||||
self.context: Optional[BrowserContext] = None
|
self.context: BrowserContext | Any = None
|
||||||
self._closed = False
|
self._closed = False
|
||||||
|
|
||||||
|
def __create__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self): # pragma: no cover
|
||||||
|
"""Close all resources"""
|
||||||
|
if self._closed:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.context:
|
||||||
|
self.context.close()
|
||||||
|
self.context = None
|
||||||
|
|
||||||
|
if self.playwright:
|
||||||
|
self.playwright.stop()
|
||||||
|
self.playwright = None # pyright: ignore
|
||||||
|
|
||||||
|
self._closed = True
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.__create__()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.close()
|
||||||
|
|
||||||
def _get_page(
|
def _get_page(
|
||||||
self,
|
self,
|
||||||
timeout: int | float,
|
timeout: int | float,
|
||||||
@@ -65,17 +100,76 @@ class SyncSession:
|
|||||||
"max_pages": self.max_pages,
|
"max_pages": self.max_pages,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _wait_for_networkidle(page: Page | Frame, timeout: Optional[int] = None):
|
||||||
|
"""Wait for the page to become idle (no network activity) even if there are never-ending requests."""
|
||||||
|
try:
|
||||||
|
page.wait_for_load_state("networkidle", timeout=timeout)
|
||||||
|
except PlaywrightError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _wait_for_page_stability(self, page: Page | Frame, load_dom: bool, network_idle: bool):
|
||||||
|
page.wait_for_load_state(state="load")
|
||||||
|
if load_dom:
|
||||||
|
page.wait_for_load_state(state="domcontentloaded")
|
||||||
|
if network_idle:
|
||||||
|
self._wait_for_networkidle(page)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_response_handler(page_info: PageInfo, response_container: List) -> Callable:
|
||||||
|
"""Create a response handler that captures the final navigation response.
|
||||||
|
|
||||||
|
:param page_info: The PageInfo object containing the page
|
||||||
|
:param response_container: A list to store the final response (mutable container)
|
||||||
|
:return: A callback function for page.on("response", ...)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def handle_response(finished_response: SyncPlaywrightResponse):
|
||||||
|
if (
|
||||||
|
finished_response.request.resource_type == "document"
|
||||||
|
and finished_response.request.is_navigation_request()
|
||||||
|
and finished_response.request.frame == page_info.page.main_frame
|
||||||
|
):
|
||||||
|
response_container[0] = finished_response
|
||||||
|
|
||||||
|
return handle_response
|
||||||
|
|
||||||
|
|
||||||
class AsyncSession:
|
class AsyncSession:
|
||||||
def __init__(self, max_pages: int = 1):
|
def __init__(self, max_pages: int = 1):
|
||||||
self.max_pages = max_pages
|
self.max_pages = max_pages
|
||||||
self.page_pool = PagePool(max_pages)
|
self.page_pool = PagePool(max_pages)
|
||||||
self._max_wait_for_page = 60
|
self._max_wait_for_page = 60
|
||||||
self.playwright: Optional[AsyncPlaywright] = None
|
self.playwright: AsyncPlaywright | Any = None
|
||||||
self.context: Optional[AsyncBrowserContext] = None
|
self.context: AsyncBrowserContext | Any = None
|
||||||
self._closed = False
|
self._closed = False
|
||||||
self._lock = Lock()
|
self._lock = Lock()
|
||||||
|
|
||||||
|
async def __create__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""Close all resources"""
|
||||||
|
if self._closed: # pragma: no cover
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.context:
|
||||||
|
await self.context.close()
|
||||||
|
self.context = None # pyright: ignore
|
||||||
|
|
||||||
|
if self.playwright:
|
||||||
|
await self.playwright.stop()
|
||||||
|
self.playwright = None # pyright: ignore
|
||||||
|
|
||||||
|
self._closed = True
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
await self.__create__()
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
await self.close()
|
||||||
|
|
||||||
async def _get_page(
|
async def _get_page(
|
||||||
self,
|
self,
|
||||||
timeout: int | float,
|
timeout: int | float,
|
||||||
@@ -122,6 +216,40 @@ class AsyncSession:
|
|||||||
"max_pages": self.max_pages,
|
"max_pages": self.max_pages,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _wait_for_networkidle(page: AsyncPage | AsyncFrame, timeout: Optional[int] = None):
|
||||||
|
"""Wait for the page to become idle (no network activity) even if there are never-ending requests."""
|
||||||
|
try:
|
||||||
|
await page.wait_for_load_state("networkidle", timeout=timeout)
|
||||||
|
except PlaywrightError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def _wait_for_page_stability(self, page: AsyncPage | AsyncFrame, load_dom: bool, network_idle: bool):
|
||||||
|
await page.wait_for_load_state(state="load")
|
||||||
|
if load_dom:
|
||||||
|
await page.wait_for_load_state(state="domcontentloaded")
|
||||||
|
if network_idle:
|
||||||
|
await self._wait_for_networkidle(page)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_response_handler(page_info: PageInfo, response_container: List) -> Callable:
|
||||||
|
"""Create an async response handler that captures the final navigation response.
|
||||||
|
|
||||||
|
:param page_info: The PageInfo object containing the page
|
||||||
|
:param response_container: A list to store the final response (mutable container)
|
||||||
|
:return: A callback function for page.on("response", ...)
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def handle_response(finished_response: AsyncPlaywrightResponse):
|
||||||
|
if (
|
||||||
|
finished_response.request.resource_type == "document"
|
||||||
|
and finished_response.request.is_navigation_request()
|
||||||
|
and finished_response.request.frame == page_info.page.main_frame
|
||||||
|
):
|
||||||
|
response_container[0] = finished_response
|
||||||
|
|
||||||
|
return handle_response
|
||||||
|
|
||||||
|
|
||||||
class DynamicSessionMixin:
|
class DynamicSessionMixin:
|
||||||
def __validate__(self, **params):
|
def __validate__(self, **params):
|
||||||
|
|||||||
@@ -2,18 +2,16 @@ from random import randint
|
|||||||
from re import compile as re_compile
|
from re import compile as re_compile
|
||||||
|
|
||||||
from playwright.sync_api import (
|
from playwright.sync_api import (
|
||||||
Response as SyncPlaywrightResponse,
|
|
||||||
sync_playwright,
|
|
||||||
Locator,
|
|
||||||
Page,
|
Page,
|
||||||
|
Locator,
|
||||||
|
sync_playwright,
|
||||||
)
|
)
|
||||||
from playwright.async_api import (
|
from playwright.async_api import (
|
||||||
async_playwright,
|
async_playwright,
|
||||||
Response as AsyncPlaywrightResponse,
|
|
||||||
BrowserContext as AsyncBrowserContext,
|
|
||||||
Playwright as AsyncPlaywright,
|
|
||||||
Locator as AsyncLocator,
|
|
||||||
Page as async_Page,
|
Page as async_Page,
|
||||||
|
Locator as AsyncLocator,
|
||||||
|
Playwright as AsyncPlaywright,
|
||||||
|
BrowserContext as AsyncBrowserContext,
|
||||||
)
|
)
|
||||||
from playwright._impl._errors import Error as PlaywrightError
|
from playwright._impl._errors import Error as PlaywrightError
|
||||||
|
|
||||||
@@ -184,38 +182,13 @@ class StealthySession(StealthySessionMixin, SyncSession):
|
|||||||
if self.cookies: # pragma: no cover
|
if self.cookies: # pragma: no cover
|
||||||
self.context.add_cookies(self.cookies)
|
self.context.add_cookies(self.cookies)
|
||||||
|
|
||||||
def __enter__(self): # pragma: no cover
|
|
||||||
self.__create__()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def close(self): # pragma: no cover
|
|
||||||
"""Close all resources"""
|
|
||||||
if self._closed: # pragma: no cover
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.context:
|
|
||||||
self.context.close()
|
|
||||||
self.context = None
|
|
||||||
|
|
||||||
if self.playwright:
|
|
||||||
self.playwright.stop()
|
|
||||||
self.playwright = None
|
|
||||||
|
|
||||||
self._closed = True
|
|
||||||
|
|
||||||
def _solve_cloudflare(self, page: Page) -> None: # pragma: no cover
|
def _solve_cloudflare(self, page: Page) -> None: # pragma: no cover
|
||||||
"""Solve the cloudflare challenge displayed on the playwright page passed
|
"""Solve the cloudflare challenge displayed on the playwright page passed
|
||||||
|
|
||||||
:param page: The targeted page
|
:param page: The targeted page
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
try:
|
self._wait_for_networkidle(page, timeout=5000)
|
||||||
page.wait_for_load_state("networkidle", timeout=5000)
|
|
||||||
except PlaywrightError:
|
|
||||||
pass
|
|
||||||
challenge_type = self._detect_cloudflare(ResponseFactory._get_page_content(page))
|
challenge_type = self._detect_cloudflare(ResponseFactory._get_page_content(page))
|
||||||
if not challenge_type:
|
if not challenge_type:
|
||||||
log.error("No Cloudflare challenge found.")
|
log.error("No Cloudflare challenge found.")
|
||||||
@@ -241,8 +214,7 @@ class StealthySession(StealthySessionMixin, SyncSession):
|
|||||||
outer_box = {}
|
outer_box = {}
|
||||||
iframe = page.frame(url=__CF_PATTERN__)
|
iframe = page.frame(url=__CF_PATTERN__)
|
||||||
if iframe is not None:
|
if iframe is not None:
|
||||||
iframe.wait_for_load_state(state="domcontentloaded")
|
self._wait_for_page_stability(iframe, True, True)
|
||||||
iframe.wait_for_load_state("networkidle")
|
|
||||||
|
|
||||||
if challenge_type != "embedded":
|
if challenge_type != "embedded":
|
||||||
while not iframe.frame_element().is_visible():
|
while not iframe.frame_element().is_visible():
|
||||||
@@ -258,7 +230,7 @@ class StealthySession(StealthySessionMixin, SyncSession):
|
|||||||
|
|
||||||
# Move the mouse to the center of the window, then press and hold the left mouse button
|
# Move the mouse to the center of the window, then press and hold the left mouse button
|
||||||
page.mouse.click(captcha_x, captcha_y, delay=60, button="left")
|
page.mouse.click(captcha_x, captcha_y, delay=60, button="left")
|
||||||
page.wait_for_load_state("networkidle")
|
self._wait_for_networkidle(page)
|
||||||
if iframe is not None:
|
if iframe is not None:
|
||||||
# Wait for the frame to be removed from the page
|
# Wait for the frame to be removed from the page
|
||||||
while iframe in page.frames:
|
while iframe in page.frames:
|
||||||
@@ -266,8 +238,7 @@ class StealthySession(StealthySessionMixin, SyncSession):
|
|||||||
if challenge_type != "embedded":
|
if challenge_type != "embedded":
|
||||||
page.locator(box_selector).last.wait_for(state="detached")
|
page.locator(box_selector).last.wait_for(state="detached")
|
||||||
page.locator(".zone-name-title").wait_for(state="hidden")
|
page.locator(".zone-name-title").wait_for(state="hidden")
|
||||||
page.wait_for_load_state(state="load")
|
self._wait_for_page_stability(page, True, False)
|
||||||
page.wait_for_load_state(state="domcontentloaded")
|
|
||||||
|
|
||||||
log.info("Cloudflare captcha is solved")
|
log.info("Cloudflare captcha is solved")
|
||||||
return
|
return
|
||||||
@@ -328,32 +299,19 @@ class StealthySession(StealthySessionMixin, SyncSession):
|
|||||||
if self._closed: # pragma: no cover
|
if self._closed: # pragma: no cover
|
||||||
raise RuntimeError("Context manager has been closed")
|
raise RuntimeError("Context manager has been closed")
|
||||||
|
|
||||||
final_response = None
|
|
||||||
referer = (
|
referer = (
|
||||||
generate_convincing_referer(url) if (params.google_search and "referer" not in self._headers_keys) else None
|
generate_convincing_referer(url) if (params.google_search and "referer" not in self._headers_keys) else None
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_response(finished_response: SyncPlaywrightResponse):
|
|
||||||
nonlocal final_response
|
|
||||||
if (
|
|
||||||
finished_response.request.resource_type == "document"
|
|
||||||
and finished_response.request.is_navigation_request()
|
|
||||||
and finished_response.request.frame == page_info.page.main_frame
|
|
||||||
):
|
|
||||||
final_response = finished_response
|
|
||||||
|
|
||||||
page_info = self._get_page(params.timeout, params.extra_headers, params.disable_resources)
|
page_info = self._get_page(params.timeout, params.extra_headers, params.disable_resources)
|
||||||
page_info.mark_busy(url=url)
|
final_response = [None]
|
||||||
|
handle_response = self._create_response_handler(page_info, final_response)
|
||||||
|
|
||||||
try: # pragma: no cover
|
try: # pragma: no cover
|
||||||
# Navigate to URL and wait for a specified state
|
# Navigate to URL and wait for a specified state
|
||||||
page_info.page.on("response", handle_response)
|
page_info.page.on("response", handle_response)
|
||||||
first_response = page_info.page.goto(url, referer=referer)
|
first_response = page_info.page.goto(url, referer=referer)
|
||||||
if params.load_dom:
|
self._wait_for_page_stability(page_info.page, params.load_dom, params.network_idle)
|
||||||
page_info.page.wait_for_load_state(state="domcontentloaded")
|
|
||||||
|
|
||||||
if params.network_idle:
|
|
||||||
page_info.page.wait_for_load_state("networkidle")
|
|
||||||
|
|
||||||
if not first_response:
|
if not first_response:
|
||||||
raise RuntimeError(f"Failed to get response for {url}")
|
raise RuntimeError(f"Failed to get response for {url}")
|
||||||
@@ -361,11 +319,7 @@ class StealthySession(StealthySessionMixin, SyncSession):
|
|||||||
if params.solve_cloudflare:
|
if params.solve_cloudflare:
|
||||||
self._solve_cloudflare(page_info.page)
|
self._solve_cloudflare(page_info.page)
|
||||||
# Make sure the page is fully loaded after the captcha
|
# Make sure the page is fully loaded after the captcha
|
||||||
page_info.page.wait_for_load_state(state="load")
|
self._wait_for_page_stability(page_info.page, params.load_dom, params.network_idle)
|
||||||
if params.load_dom:
|
|
||||||
page_info.page.wait_for_load_state(state="domcontentloaded")
|
|
||||||
if params.network_idle:
|
|
||||||
page_info.page.wait_for_load_state("networkidle")
|
|
||||||
|
|
||||||
if params.page_action:
|
if params.page_action:
|
||||||
try:
|
try:
|
||||||
@@ -378,17 +332,13 @@ class StealthySession(StealthySessionMixin, SyncSession):
|
|||||||
waiter: Locator = page_info.page.locator(params.wait_selector)
|
waiter: Locator = page_info.page.locator(params.wait_selector)
|
||||||
waiter.first.wait_for(state=params.wait_selector_state)
|
waiter.first.wait_for(state=params.wait_selector_state)
|
||||||
# Wait again after waiting for the selector, helpful with protections like Cloudflare
|
# Wait again after waiting for the selector, helpful with protections like Cloudflare
|
||||||
page_info.page.wait_for_load_state(state="load")
|
self._wait_for_page_stability(page_info.page, params.load_dom, params.network_idle)
|
||||||
if params.load_dom:
|
|
||||||
page_info.page.wait_for_load_state(state="domcontentloaded")
|
|
||||||
if params.network_idle:
|
|
||||||
page_info.page.wait_for_load_state("networkidle")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Error waiting for selector {params.wait_selector}: {e}")
|
log.error(f"Error waiting for selector {params.wait_selector}: {e}")
|
||||||
|
|
||||||
page_info.page.wait_for_timeout(params.wait)
|
page_info.page.wait_for_timeout(params.wait)
|
||||||
response = ResponseFactory.from_playwright_response(
|
response = ResponseFactory.from_playwright_response(
|
||||||
page_info.page, first_response, final_response, params.selector_config, bool(params.page_action)
|
page_info.page, first_response, final_response[0], params.selector_config, bool(params.page_action)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Close the page to free up resources
|
# Close the page to free up resources
|
||||||
@@ -513,38 +463,13 @@ class AsyncStealthySession(StealthySessionMixin, AsyncSession):
|
|||||||
if self.cookies:
|
if self.cookies:
|
||||||
await self.context.add_cookies(self.cookies) # pyright: ignore [reportArgumentType]
|
await self.context.add_cookies(self.cookies) # pyright: ignore [reportArgumentType]
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def _solve_cloudflare(self, page: async_Page): # pragma: no cover
|
||||||
await self.__create__()
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
await self.close()
|
|
||||||
|
|
||||||
async def close(self):
|
|
||||||
"""Close all resources"""
|
|
||||||
if self._closed: # pragma: no cover
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.context:
|
|
||||||
await self.context.close()
|
|
||||||
self.context = None # pyright: ignore
|
|
||||||
|
|
||||||
if self.playwright:
|
|
||||||
await self.playwright.stop()
|
|
||||||
self.playwright = None # pyright: ignore
|
|
||||||
|
|
||||||
self._closed = True
|
|
||||||
|
|
||||||
async def _solve_cloudflare(self, page: async_Page):
|
|
||||||
"""Solve the cloudflare challenge displayed on the playwright page passed. The async version
|
"""Solve the cloudflare challenge displayed on the playwright page passed. The async version
|
||||||
|
|
||||||
:param page: The async targeted page
|
:param page: The async targeted page
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
try:
|
await self._wait_for_networkidle(page, timeout=5000)
|
||||||
await page.wait_for_load_state("networkidle", timeout=5000)
|
|
||||||
except PlaywrightError:
|
|
||||||
pass
|
|
||||||
challenge_type = self._detect_cloudflare(await ResponseFactory._get_async_page_content(page))
|
challenge_type = self._detect_cloudflare(await ResponseFactory._get_async_page_content(page))
|
||||||
if not challenge_type:
|
if not challenge_type:
|
||||||
log.error("No Cloudflare challenge found.")
|
log.error("No Cloudflare challenge found.")
|
||||||
@@ -570,8 +495,7 @@ class AsyncStealthySession(StealthySessionMixin, AsyncSession):
|
|||||||
outer_box = {}
|
outer_box = {}
|
||||||
iframe = page.frame(url=__CF_PATTERN__)
|
iframe = page.frame(url=__CF_PATTERN__)
|
||||||
if iframe is not None:
|
if iframe is not None:
|
||||||
await iframe.wait_for_load_state(state="domcontentloaded")
|
await self._wait_for_page_stability(iframe, True, True)
|
||||||
await iframe.wait_for_load_state("networkidle")
|
|
||||||
|
|
||||||
if challenge_type != "embedded":
|
if challenge_type != "embedded":
|
||||||
while not await (await iframe.frame_element()).is_visible():
|
while not await (await iframe.frame_element()).is_visible():
|
||||||
@@ -587,7 +511,7 @@ class AsyncStealthySession(StealthySessionMixin, AsyncSession):
|
|||||||
|
|
||||||
# Move the mouse to the center of the window, then press and hold the left mouse button
|
# Move the mouse to the center of the window, then press and hold the left mouse button
|
||||||
await page.mouse.click(captcha_x, captcha_y, delay=60, button="left")
|
await page.mouse.click(captcha_x, captcha_y, delay=60, button="left")
|
||||||
await page.wait_for_load_state("networkidle")
|
await self._wait_for_networkidle(page)
|
||||||
if iframe is not None:
|
if iframe is not None:
|
||||||
# Wait for the frame to be removed from the page
|
# Wait for the frame to be removed from the page
|
||||||
while iframe in page.frames:
|
while iframe in page.frames:
|
||||||
@@ -595,8 +519,7 @@ class AsyncStealthySession(StealthySessionMixin, AsyncSession):
|
|||||||
if challenge_type != "embedded":
|
if challenge_type != "embedded":
|
||||||
await page.locator(box_selector).wait_for(state="detached")
|
await page.locator(box_selector).wait_for(state="detached")
|
||||||
await page.locator(".zone-name-title").wait_for(state="hidden")
|
await page.locator(".zone-name-title").wait_for(state="hidden")
|
||||||
await page.wait_for_load_state(state="load")
|
await self._wait_for_page_stability(page, True, False)
|
||||||
await page.wait_for_load_state(state="domcontentloaded")
|
|
||||||
|
|
||||||
log.info("Cloudflare captcha is solved")
|
log.info("Cloudflare captcha is solved")
|
||||||
return
|
return
|
||||||
@@ -657,22 +580,13 @@ class AsyncStealthySession(StealthySessionMixin, AsyncSession):
|
|||||||
if self._closed: # pragma: no cover
|
if self._closed: # pragma: no cover
|
||||||
raise RuntimeError("Context manager has been closed")
|
raise RuntimeError("Context manager has been closed")
|
||||||
|
|
||||||
final_response = None
|
|
||||||
referer = (
|
referer = (
|
||||||
generate_convincing_referer(url) if (params.google_search and "referer" not in self._headers_keys) else None
|
generate_convincing_referer(url) if (params.google_search and "referer" not in self._headers_keys) else None
|
||||||
)
|
)
|
||||||
|
|
||||||
async def handle_response(finished_response: AsyncPlaywrightResponse):
|
|
||||||
nonlocal final_response
|
|
||||||
if (
|
|
||||||
finished_response.request.resource_type == "document"
|
|
||||||
and finished_response.request.is_navigation_request()
|
|
||||||
and finished_response.request.frame == page_info.page.main_frame
|
|
||||||
):
|
|
||||||
final_response = finished_response
|
|
||||||
|
|
||||||
page_info = await self._get_page(params.timeout, params.extra_headers, params.disable_resources)
|
page_info = await self._get_page(params.timeout, params.extra_headers, params.disable_resources)
|
||||||
page_info.mark_busy(url=url)
|
final_response = [None]
|
||||||
|
handle_response = self._create_response_handler(page_info, final_response)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
if not isinstance(page_info.page, async_Page):
|
if not isinstance(page_info.page, async_Page):
|
||||||
@@ -682,11 +596,7 @@ class AsyncStealthySession(StealthySessionMixin, AsyncSession):
|
|||||||
# Navigate to URL and wait for a specified state
|
# Navigate to URL and wait for a specified state
|
||||||
page_info.page.on("response", handle_response)
|
page_info.page.on("response", handle_response)
|
||||||
first_response = await page_info.page.goto(url, referer=referer)
|
first_response = await page_info.page.goto(url, referer=referer)
|
||||||
if params.load_dom:
|
await self._wait_for_page_stability(page_info.page, params.load_dom, params.network_idle)
|
||||||
await page_info.page.wait_for_load_state(state="domcontentloaded")
|
|
||||||
|
|
||||||
if params.network_idle:
|
|
||||||
await page_info.page.wait_for_load_state("networkidle")
|
|
||||||
|
|
||||||
if not first_response:
|
if not first_response:
|
||||||
raise RuntimeError(f"Failed to get response for {url}")
|
raise RuntimeError(f"Failed to get response for {url}")
|
||||||
@@ -694,11 +604,7 @@ class AsyncStealthySession(StealthySessionMixin, AsyncSession):
|
|||||||
if params.solve_cloudflare:
|
if params.solve_cloudflare:
|
||||||
await self._solve_cloudflare(page_info.page)
|
await self._solve_cloudflare(page_info.page)
|
||||||
# Make sure the page is fully loaded after the captcha
|
# Make sure the page is fully loaded after the captcha
|
||||||
await page_info.page.wait_for_load_state(state="load")
|
await self._wait_for_page_stability(page_info.page, params.load_dom, params.network_idle)
|
||||||
if params.load_dom:
|
|
||||||
await page_info.page.wait_for_load_state(state="domcontentloaded")
|
|
||||||
if params.network_idle:
|
|
||||||
await page_info.page.wait_for_load_state("networkidle")
|
|
||||||
|
|
||||||
if params.page_action:
|
if params.page_action:
|
||||||
try:
|
try:
|
||||||
@@ -711,11 +617,7 @@ class AsyncStealthySession(StealthySessionMixin, AsyncSession):
|
|||||||
waiter: AsyncLocator = page_info.page.locator(params.wait_selector)
|
waiter: AsyncLocator = page_info.page.locator(params.wait_selector)
|
||||||
await waiter.first.wait_for(state=params.wait_selector_state)
|
await waiter.first.wait_for(state=params.wait_selector_state)
|
||||||
# Wait again after waiting for the selector, helpful with protections like Cloudflare
|
# Wait again after waiting for the selector, helpful with protections like Cloudflare
|
||||||
await page_info.page.wait_for_load_state(state="load")
|
await self._wait_for_page_stability(page_info.page, params.load_dom, params.network_idle)
|
||||||
if params.load_dom:
|
|
||||||
await page_info.page.wait_for_load_state(state="domcontentloaded")
|
|
||||||
if params.network_idle:
|
|
||||||
await page_info.page.wait_for_load_state("networkidle")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Error waiting for selector {params.wait_selector}: {e}")
|
log.error(f"Error waiting for selector {params.wait_selector}: {e}")
|
||||||
|
|
||||||
@@ -723,7 +625,7 @@ class AsyncStealthySession(StealthySessionMixin, AsyncSession):
|
|||||||
|
|
||||||
# Create response object
|
# Create response object
|
||||||
response = await ResponseFactory.from_async_playwright_response(
|
response = await ResponseFactory.from_async_playwright_response(
|
||||||
page_info.page, first_response, final_response, params.selector_config, bool(params.page_action)
|
page_info.page, first_response, final_response[0], params.selector_config, bool(params.page_action)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Close the page to free up resources
|
# Close the page to free up resources
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
from playwright.sync_api import (
|
from playwright.sync_api import (
|
||||||
Response as SyncPlaywrightResponse,
|
|
||||||
sync_playwright,
|
|
||||||
Playwright,
|
|
||||||
Locator,
|
Locator,
|
||||||
|
Playwright,
|
||||||
|
sync_playwright,
|
||||||
)
|
)
|
||||||
from playwright.async_api import (
|
from playwright.async_api import (
|
||||||
async_playwright,
|
async_playwright,
|
||||||
Response as AsyncPlaywrightResponse,
|
|
||||||
BrowserContext as AsyncBrowserContext,
|
|
||||||
Playwright as AsyncPlaywright,
|
|
||||||
Locator as AsyncLocator,
|
Locator as AsyncLocator,
|
||||||
Page as async_Page,
|
Playwright as AsyncPlaywright,
|
||||||
|
BrowserContext as AsyncBrowserContext,
|
||||||
)
|
)
|
||||||
from patchright.sync_api import sync_playwright as sync_patchright
|
from patchright.sync_api import sync_playwright as sync_patchright
|
||||||
from patchright.async_api import async_playwright as async_patchright
|
from patchright.async_api import async_playwright as async_patchright
|
||||||
@@ -178,28 +175,6 @@ class DynamicSession(DynamicSessionMixin, SyncSession):
|
|||||||
if self.cookies: # pragma: no cover
|
if self.cookies: # pragma: no cover
|
||||||
self.context.add_cookies(self.cookies)
|
self.context.add_cookies(self.cookies)
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
self.__create__()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def close(self): # pragma: no cover
|
|
||||||
"""Close all resources"""
|
|
||||||
if self._closed:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.context:
|
|
||||||
self.context.close()
|
|
||||||
self.context = None
|
|
||||||
|
|
||||||
if self.playwright:
|
|
||||||
self.playwright.stop()
|
|
||||||
self.playwright = None # pyright: ignore
|
|
||||||
|
|
||||||
self._closed = True
|
|
||||||
|
|
||||||
def fetch(
|
def fetch(
|
||||||
self,
|
self,
|
||||||
url: str,
|
url: str,
|
||||||
@@ -253,32 +228,19 @@ class DynamicSession(DynamicSessionMixin, SyncSession):
|
|||||||
if self._closed: # pragma: no cover
|
if self._closed: # pragma: no cover
|
||||||
raise RuntimeError("Context manager has been closed")
|
raise RuntimeError("Context manager has been closed")
|
||||||
|
|
||||||
final_response = None
|
|
||||||
referer = (
|
referer = (
|
||||||
generate_convincing_referer(url) if (params.google_search and "referer" not in self._headers_keys) else None
|
generate_convincing_referer(url) if (params.google_search and "referer" not in self._headers_keys) else None
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_response(finished_response: SyncPlaywrightResponse):
|
|
||||||
nonlocal final_response
|
|
||||||
if (
|
|
||||||
finished_response.request.resource_type == "document"
|
|
||||||
and finished_response.request.is_navigation_request()
|
|
||||||
and finished_response.request.frame == page_info.page.main_frame
|
|
||||||
):
|
|
||||||
final_response = finished_response
|
|
||||||
|
|
||||||
page_info = self._get_page(params.timeout, params.extra_headers, params.disable_resources)
|
page_info = self._get_page(params.timeout, params.extra_headers, params.disable_resources)
|
||||||
page_info.mark_busy(url=url)
|
final_response = [None]
|
||||||
|
handle_response = self._create_response_handler(page_info, final_response)
|
||||||
|
|
||||||
try: # pragma: no cover
|
try: # pragma: no cover
|
||||||
# Navigate to URL and wait for a specified state
|
# Navigate to URL and wait for a specified state
|
||||||
page_info.page.on("response", handle_response)
|
page_info.page.on("response", handle_response)
|
||||||
first_response = page_info.page.goto(url, referer=referer)
|
first_response = page_info.page.goto(url, referer=referer)
|
||||||
if params.load_dom:
|
self._wait_for_page_stability(page_info.page, params.load_dom, params.network_idle)
|
||||||
page_info.page.wait_for_load_state(state="domcontentloaded")
|
|
||||||
|
|
||||||
if params.network_idle:
|
|
||||||
page_info.page.wait_for_load_state("networkidle")
|
|
||||||
|
|
||||||
if not first_response:
|
if not first_response:
|
||||||
raise RuntimeError(f"Failed to get response for {url}")
|
raise RuntimeError(f"Failed to get response for {url}")
|
||||||
@@ -294,11 +256,7 @@ class DynamicSession(DynamicSessionMixin, SyncSession):
|
|||||||
waiter: Locator = page_info.page.locator(params.wait_selector)
|
waiter: Locator = page_info.page.locator(params.wait_selector)
|
||||||
waiter.first.wait_for(state=params.wait_selector_state)
|
waiter.first.wait_for(state=params.wait_selector_state)
|
||||||
# Wait again after waiting for the selector, helpful with protections like Cloudflare
|
# Wait again after waiting for the selector, helpful with protections like Cloudflare
|
||||||
page_info.page.wait_for_load_state(state="load")
|
self._wait_for_page_stability(page_info.page, params.load_dom, params.network_idle)
|
||||||
if params.load_dom:
|
|
||||||
page_info.page.wait_for_load_state(state="domcontentloaded")
|
|
||||||
if params.network_idle:
|
|
||||||
page_info.page.wait_for_load_state("networkidle")
|
|
||||||
except Exception as e: # pragma: no cover
|
except Exception as e: # pragma: no cover
|
||||||
log.error(f"Error waiting for selector {params.wait_selector}: {e}")
|
log.error(f"Error waiting for selector {params.wait_selector}: {e}")
|
||||||
|
|
||||||
@@ -306,7 +264,7 @@ class DynamicSession(DynamicSessionMixin, SyncSession):
|
|||||||
|
|
||||||
# Create response object
|
# Create response object
|
||||||
response = ResponseFactory.from_playwright_response(
|
response = ResponseFactory.from_playwright_response(
|
||||||
page_info.page, first_response, final_response, params.selector_config, bool(params.page_action)
|
page_info.page, first_response, final_response[0], params.selector_config, bool(params.page_action)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Close the page to free up resources
|
# Close the page to free up resources
|
||||||
@@ -431,28 +389,6 @@ class AsyncDynamicSession(DynamicSessionMixin, AsyncSession):
|
|||||||
if self.cookies:
|
if self.cookies:
|
||||||
await self.context.add_cookies(self.cookies) # pyright: ignore
|
await self.context.add_cookies(self.cookies) # pyright: ignore
|
||||||
|
|
||||||
async def __aenter__(self):
|
|
||||||
await self.__create__()
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
await self.close()
|
|
||||||
|
|
||||||
async def close(self):
|
|
||||||
"""Close all resources"""
|
|
||||||
if self._closed: # pragma: no cover
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.context:
|
|
||||||
await self.context.close()
|
|
||||||
self.context = None # pyright: ignore
|
|
||||||
|
|
||||||
if self.playwright:
|
|
||||||
await self.playwright.stop()
|
|
||||||
self.playwright = None # pyright: ignore
|
|
||||||
|
|
||||||
self._closed = True
|
|
||||||
|
|
||||||
async def fetch(
|
async def fetch(
|
||||||
self,
|
self,
|
||||||
url: str,
|
url: str,
|
||||||
@@ -506,24 +442,17 @@ class AsyncDynamicSession(DynamicSessionMixin, AsyncSession):
|
|||||||
if self._closed: # pragma: no cover
|
if self._closed: # pragma: no cover
|
||||||
raise RuntimeError("Context manager has been closed")
|
raise RuntimeError("Context manager has been closed")
|
||||||
|
|
||||||
final_response = None
|
|
||||||
referer = (
|
referer = (
|
||||||
generate_convincing_referer(url) if (params.google_search and "referer" not in self._headers_keys) else None
|
generate_convincing_referer(url) if (params.google_search and "referer" not in self._headers_keys) else None
|
||||||
)
|
)
|
||||||
|
|
||||||
async def handle_response(finished_response: AsyncPlaywrightResponse):
|
|
||||||
nonlocal final_response
|
|
||||||
if (
|
|
||||||
finished_response.request.resource_type == "document"
|
|
||||||
and finished_response.request.is_navigation_request()
|
|
||||||
and finished_response.request.frame == page_info.page.main_frame
|
|
||||||
):
|
|
||||||
final_response = finished_response
|
|
||||||
|
|
||||||
page_info = await self._get_page(params.timeout, params.extra_headers, params.disable_resources)
|
page_info = await self._get_page(params.timeout, params.extra_headers, params.disable_resources)
|
||||||
page_info.mark_busy(url=url)
|
final_response = [None]
|
||||||
|
handle_response = self._create_response_handler(page_info, final_response)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from playwright.async_api import Page as async_Page
|
||||||
|
|
||||||
if not isinstance(page_info.page, async_Page):
|
if not isinstance(page_info.page, async_Page):
|
||||||
raise TypeError
|
raise TypeError
|
||||||
|
|
||||||
@@ -531,11 +460,7 @@ class AsyncDynamicSession(DynamicSessionMixin, AsyncSession):
|
|||||||
# Navigate to URL and wait for a specified state
|
# Navigate to URL and wait for a specified state
|
||||||
page_info.page.on("response", handle_response)
|
page_info.page.on("response", handle_response)
|
||||||
first_response = await page_info.page.goto(url, referer=referer)
|
first_response = await page_info.page.goto(url, referer=referer)
|
||||||
if self.load_dom:
|
await self._wait_for_page_stability(page_info.page, params.load_dom, params.network_idle)
|
||||||
await page_info.page.wait_for_load_state(state="domcontentloaded")
|
|
||||||
|
|
||||||
if params.network_idle:
|
|
||||||
await page_info.page.wait_for_load_state("networkidle")
|
|
||||||
|
|
||||||
if not first_response:
|
if not first_response:
|
||||||
raise RuntimeError(f"Failed to get response for {url}")
|
raise RuntimeError(f"Failed to get response for {url}")
|
||||||
@@ -551,11 +476,7 @@ class AsyncDynamicSession(DynamicSessionMixin, AsyncSession):
|
|||||||
waiter: AsyncLocator = page_info.page.locator(params.wait_selector)
|
waiter: AsyncLocator = page_info.page.locator(params.wait_selector)
|
||||||
await waiter.first.wait_for(state=params.wait_selector_state)
|
await waiter.first.wait_for(state=params.wait_selector_state)
|
||||||
# Wait again after waiting for the selector, helpful with protections like Cloudflare
|
# Wait again after waiting for the selector, helpful with protections like Cloudflare
|
||||||
await page_info.page.wait_for_load_state(state="load")
|
await self._wait_for_page_stability(page_info.page, params.load_dom, params.network_idle)
|
||||||
if self.load_dom:
|
|
||||||
await page_info.page.wait_for_load_state(state="domcontentloaded")
|
|
||||||
if params.network_idle:
|
|
||||||
await page_info.page.wait_for_load_state("networkidle")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Error waiting for selector {params.wait_selector}: {e}")
|
log.error(f"Error waiting for selector {params.wait_selector}: {e}")
|
||||||
|
|
||||||
@@ -563,7 +484,7 @@ class AsyncDynamicSession(DynamicSessionMixin, AsyncSession):
|
|||||||
|
|
||||||
# Create response object
|
# Create response object
|
||||||
response = await ResponseFactory.from_async_playwright_response(
|
response = await ResponseFactory.from_async_playwright_response(
|
||||||
page_info.page, first_response, final_response, params.selector_config, bool(params.page_action)
|
page_info.page, first_response, final_response[0], params.selector_config, bool(params.page_action)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Close the page to free up resources
|
# Close the page to free up resources
|
||||||
|
|||||||
@@ -209,15 +209,3 @@ class StatusText:
|
|||||||
def get(cls, status_code: int) -> str:
|
def get(cls, status_code: int) -> str:
|
||||||
"""Get the phrase for a given HTTP status code."""
|
"""Get the phrase for a given HTTP status code."""
|
||||||
return cls._phrases.get(status_code, "Unknown Status Code")
|
return cls._phrases.get(status_code, "Unknown Status Code")
|
||||||
|
|
||||||
|
|
||||||
def get_variable_name(var: Any) -> Optional[str]:
|
|
||||||
"""Get the name of a variable using global and local scopes.
|
|
||||||
:param var: The variable to find the name for
|
|
||||||
:return: The name of the variable if found, None otherwise
|
|
||||||
"""
|
|
||||||
for scope in [globals(), locals()]:
|
|
||||||
for name, value in scope.items():
|
|
||||||
if value is var:
|
|
||||||
return name
|
|
||||||
return None
|
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ from platform import system as platform_system
|
|||||||
|
|
||||||
from tldextract import extract
|
from tldextract import extract
|
||||||
from browserforge.headers import Browser, HeaderGenerator
|
from browserforge.headers import Browser, HeaderGenerator
|
||||||
|
from browserforge.headers.generator import SUPPORTED_OPERATING_SYSTEMS
|
||||||
|
|
||||||
from scrapling.core._types import Dict, Literal
|
from scrapling.core._types import Dict, Literal, Tuple
|
||||||
|
|
||||||
__OS_NAME__ = platform_system()
|
__OS_NAME__ = platform_system()
|
||||||
OSName = Literal["linux", "macos", "windows"]
|
OSName = Literal["linux", "macos", "windows"]
|
||||||
@@ -29,12 +30,12 @@ def generate_convincing_referer(url: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
@lru_cache(1, typed=True)
|
@lru_cache(1, typed=True)
|
||||||
def get_os_name() -> OSName | None:
|
def get_os_name() -> OSName | Tuple:
|
||||||
"""Get the current OS name in the same format needed for browserforge, if the OS is Unknown, return None so browserforge uses all.
|
"""Get the current OS name in the same format needed for browserforge, if the OS is Unknown, return None so browserforge uses all.
|
||||||
|
|
||||||
:return: Current OS name or `None` otherwise
|
:return: Current OS name or `None` otherwise
|
||||||
"""
|
"""
|
||||||
match __OS_NAME__:
|
match __OS_NAME__: # pragma: no cover
|
||||||
case "Linux":
|
case "Linux":
|
||||||
return "linux"
|
return "linux"
|
||||||
case "Darwin":
|
case "Darwin":
|
||||||
@@ -42,7 +43,7 @@ def get_os_name() -> OSName | None:
|
|||||||
case "Windows":
|
case "Windows":
|
||||||
return "windows"
|
return "windows"
|
||||||
case _:
|
case _:
|
||||||
return None
|
return SUPPORTED_OPERATING_SYSTEMS
|
||||||
|
|
||||||
|
|
||||||
def generate_headers(browser_mode: bool = False) -> Dict:
|
def generate_headers(browser_mode: bool = False) -> Dict:
|
||||||
@@ -63,10 +64,7 @@ def generate_headers(browser_mode: bool = False) -> Dict:
|
|||||||
Browser(name="edge", min_version=130),
|
Browser(name="edge", min_version=130),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
if os_name:
|
return HeaderGenerator(browser=browsers, os=os_name, device="desktop").generate()
|
||||||
return HeaderGenerator(browser=browsers, os=os_name, device="desktop").generate()
|
|
||||||
else:
|
|
||||||
return HeaderGenerator(browser=browsers, device="desktop").generate()
|
|
||||||
|
|
||||||
|
|
||||||
__default_useragent__ = generate_headers(browser_mode=False).get("User-Agent")
|
__default_useragent__ = generate_headers(browser_mode=False).get("User-Agent")
|
||||||
|
|||||||
Reference in New Issue
Block a user