scrapely-python 0.1.32__py3-none-any.whl → 0.1.34__py3-none-any.whl
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.
- scrapely/__init__.py +1 -1
- scrapely/actor.py +80 -5
- {scrapely_python-0.1.32.dist-info → scrapely_python-0.1.34.dist-info}/METADATA +1 -1
- {scrapely_python-0.1.32.dist-info → scrapely_python-0.1.34.dist-info}/RECORD +9 -9
- {scrapely_python-0.1.32.dist-info → scrapely_python-0.1.34.dist-info}/WHEEL +0 -0
- {scrapely_python-0.1.32.dist-info → scrapely_python-0.1.34.dist-info}/licenses/LICENSE +0 -0
- {scrapely_python-0.1.32.dist-info → scrapely_python-0.1.34.dist-info}/licenses/LICENSE-APACHE +0 -0
- {scrapely_python-0.1.32.dist-info → scrapely_python-0.1.34.dist-info}/licenses/NOTICE +0 -0
- {scrapely_python-0.1.32.dist-info → scrapely_python-0.1.34.dist-info}/top_level.txt +0 -0
scrapely/__init__.py
CHANGED
scrapely/actor.py
CHANGED
|
@@ -285,6 +285,53 @@ class _ActorMeta(type):
|
|
|
285
285
|
await cls._shutdown()
|
|
286
286
|
|
|
287
287
|
|
|
288
|
+
|
|
289
|
+
def _element_markup(obj: Any) -> Optional[str]:
|
|
290
|
+
"""The markup of a parsed XML/HTML element, or None when ``obj`` is not one.
|
|
291
|
+
|
|
292
|
+
Debug captures store the source an item was read from. Feed and page parsers hand over elements
|
|
293
|
+
(xml.etree / lxml elements and trees, BeautifulSoup tags, selectolax nodes, parsel / Scrapy
|
|
294
|
+
selectors); their ``str()`` is only a repr such as ``<Element 'change' at 0x...>``, which shows
|
|
295
|
+
nothing of the source. Optional parsers are recognised by module, never imported here.
|
|
296
|
+
"""
|
|
297
|
+
import xml.etree.ElementTree as ET
|
|
298
|
+
|
|
299
|
+
if isinstance(obj, ET.ElementTree):
|
|
300
|
+
root = obj.getroot()
|
|
301
|
+
return ET.tostring(root, encoding='unicode') if root is not None else ''
|
|
302
|
+
if isinstance(obj, ET.Element):
|
|
303
|
+
if obj.tail: # the text after the element belongs to its parent, as with lxml's with_tail=False
|
|
304
|
+
import copy
|
|
305
|
+
|
|
306
|
+
obj = copy.copy(obj)
|
|
307
|
+
obj.tail = None
|
|
308
|
+
return ET.tostring(obj, encoding='unicode')
|
|
309
|
+
modules = [getattr(c, '__module__', '') or '' for c in type(obj).__mro__]
|
|
310
|
+
if any(m == 'lxml' or m.startswith('lxml.') for m in modules):
|
|
311
|
+
from lxml import etree # type: ignore[import-not-found]
|
|
312
|
+
|
|
313
|
+
out = etree.tostring(obj, encoding='unicode', with_tail=False)
|
|
314
|
+
return out if isinstance(out, str) else None
|
|
315
|
+
if any(m == 'bs4' or m.startswith('bs4.') for m in modules):
|
|
316
|
+
return str(obj)
|
|
317
|
+
if any(m == 'selectolax' or m.startswith('selectolax.') for m in modules):
|
|
318
|
+
html = getattr(obj, 'html', None)
|
|
319
|
+
return html if isinstance(html, str) else None
|
|
320
|
+
if any(m == 'parsel' or m.startswith('parsel.') for m in modules) and callable(getattr(obj, 'get', None)):
|
|
321
|
+
out = obj.get()
|
|
322
|
+
return out if isinstance(out, str) else None
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _capture_json_default(obj: Any) -> Any:
|
|
327
|
+
"""json.dumps fallback for debug captures: elements as their markup, anything else as str()."""
|
|
328
|
+
try:
|
|
329
|
+
markup = _element_markup(obj)
|
|
330
|
+
except Exception: # an unusual node type must not lose the rest of the payload
|
|
331
|
+
markup = None
|
|
332
|
+
return markup if markup is not None else str(obj)
|
|
333
|
+
|
|
334
|
+
|
|
288
335
|
class Actor(metaclass=_ActorMeta):
|
|
289
336
|
"""Core class for building Actors on the Scrapely platform."""
|
|
290
337
|
|
|
@@ -925,7 +972,14 @@ class Actor(metaclass=_ActorMeta):
|
|
|
925
972
|
|
|
926
973
|
# Debug capture: raw source payloads for platform diagnostics. Only active when the platform
|
|
927
974
|
# sets SCRAPELY_DEBUG_CAPTURE=1 on its own diagnostic runs; a no-op in every other run.
|
|
975
|
+
# A payload up to _DEBUG_CAPTURE_MAX_BYTES characters is stored as it is; a larger one gzip-compressed
|
|
976
|
+
# (base64, at most _DEBUG_CAPTURE_MAX_BYTES characters again), keeping up to _DEBUG_CAPTURE_MAX_CHARS
|
|
977
|
+
# characters / _DEBUG_CAPTURE_MAX_ENCODED bytes of it. A plain cut at 256 KiB lost the part a check
|
|
978
|
+
# needed when a large API document starts with site navigation (the seller section of a 581 KB
|
|
979
|
+
# listing JSON sat at ~333 KB).
|
|
928
980
|
_DEBUG_CAPTURE_MAX_BYTES = 256 * 1024
|
|
981
|
+
_DEBUG_CAPTURE_MAX_CHARS = 8 * 1024 * 1024
|
|
982
|
+
_DEBUG_CAPTURE_MAX_ENCODED = 8 * 1024 * 1024
|
|
929
983
|
_DEBUG_CAPTURE_MAX_RECORDS = 500
|
|
930
984
|
_debug_capture_count = 0
|
|
931
985
|
|
|
@@ -967,15 +1021,36 @@ class Actor(metaclass=_ActorMeta):
|
|
|
967
1021
|
if cls._debug_capture_count >= cls._DEBUG_CAPTURE_MAX_RECORDS:
|
|
968
1022
|
return
|
|
969
1023
|
try:
|
|
970
|
-
|
|
971
|
-
if
|
|
972
|
-
text =
|
|
973
|
-
|
|
1024
|
+
markup = _element_markup(payload)
|
|
1025
|
+
if markup is not None:
|
|
1026
|
+
text = markup
|
|
1027
|
+
elif isinstance(payload, (bytes, bytearray)):
|
|
1028
|
+
text = bytes(payload).decode('utf-8', 'replace')
|
|
1029
|
+
elif isinstance(payload, str):
|
|
1030
|
+
text = payload
|
|
1031
|
+
else:
|
|
1032
|
+
text = json.dumps(payload, default=_capture_json_default, ensure_ascii=False, separators=(',', ':'))
|
|
1033
|
+
record = cls._debug_capture_record(str(key), text)
|
|
974
1034
|
cls._debug_capture_count += 1
|
|
975
|
-
await cls.set_value(cls.debug_capture_record_key(key),
|
|
1035
|
+
await cls.set_value(cls.debug_capture_record_key(key), record)
|
|
976
1036
|
except Exception as err: # never break the run
|
|
977
1037
|
logging.getLogger('scrapely').debug('debug_capture failed: %s', err)
|
|
978
1038
|
|
|
1039
|
+
@classmethod
|
|
1040
|
+
def _debug_capture_record(cls, key: str, text: str) -> Dict[str, Any]:
|
|
1041
|
+
"""The record for one capture: plain when small, else gzip-base64 (plain head as the last resort)."""
|
|
1042
|
+
limit = cls._DEBUG_CAPTURE_MAX_BYTES
|
|
1043
|
+
if len(text) <= limit:
|
|
1044
|
+
return {'key': key, 'payload': text}
|
|
1045
|
+
import gzip
|
|
1046
|
+
raw = text[: cls._DEBUG_CAPTURE_MAX_CHARS].encode('utf-8')
|
|
1047
|
+
kept = raw[: cls._DEBUG_CAPTURE_MAX_ENCODED]
|
|
1048
|
+
packed = base64.b64encode(gzip.compress(kept, 6)).decode('ascii')
|
|
1049
|
+
if len(packed) <= limit:
|
|
1050
|
+
truncated = len(text) > cls._DEBUG_CAPTURE_MAX_CHARS or len(kept) < len(raw)
|
|
1051
|
+
return {'key': key, 'payload': packed, 'encoding': 'gzip-base64', 'length': len(text), 'truncated': truncated}
|
|
1052
|
+
return {'key': key, 'payload': text[:limit], 'length': len(text), 'truncated': True}
|
|
1053
|
+
|
|
979
1054
|
@classmethod
|
|
980
1055
|
def _resolve_open_args(
|
|
981
1056
|
cls,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
scrapely/__init__.py,sha256=
|
|
1
|
+
scrapely/__init__.py,sha256=5lWyf7hHuDQXopBE8Koi2joSWNZWECE2DvPKu7CvkX0,9668
|
|
2
2
|
scrapely/_browser_locations.py,sha256=8QVVnkms2bDn3_LDYgYDR59MvFx8757l_JYnZ7xb3oI,145269
|
|
3
|
-
scrapely/actor.py,sha256=
|
|
3
|
+
scrapely/actor.py,sha256=DmDnwPSmqmdEq_HIBgW0JivKl-8FrUBhNKNGXYh6QGA,91407
|
|
4
4
|
scrapely/browser_configuration.py,sha256=jAZapNSHLFW05B96df4wNBtcyiiU9DA3ml0yTL1FwfA,5770
|
|
5
5
|
scrapely/cdp_retry.py,sha256=hdTJEAIKhApM7IOk1DrQEl8NoflSJSmqd4RbNJ2IsgI,8540
|
|
6
6
|
scrapely/charging.py,sha256=osi2Ys99IyIJwFj0eRtV_RshokPs4mA2CE7oftNbN58,95559
|
|
@@ -55,10 +55,10 @@ scrapely/storage/_file_system/_key_value_store_client.py,sha256=1C22qY-RWlARzMyW
|
|
|
55
55
|
scrapely/storage/_file_system/_storage_client.py,sha256=QHa3IULEb6AmgRFwpBqHD2OUTmtA_gl3lId-j6QdSFo,2027
|
|
56
56
|
scrapely/storage_clients/__init__.py,sha256=q2pC-Rwe-BfHl018OyN_P0EDSx4-hNdxbk3pOCLLd4U,618
|
|
57
57
|
scrapely/storages/__init__.py,sha256=kDNtd9VodjdHd4u7GzruJo0aB2HeiBN2_-HrkeqjmFg,139
|
|
58
|
-
scrapely_python-0.1.
|
|
59
|
-
scrapely_python-0.1.
|
|
60
|
-
scrapely_python-0.1.
|
|
61
|
-
scrapely_python-0.1.
|
|
62
|
-
scrapely_python-0.1.
|
|
63
|
-
scrapely_python-0.1.
|
|
64
|
-
scrapely_python-0.1.
|
|
58
|
+
scrapely_python-0.1.34.dist-info/licenses/LICENSE,sha256=pQGY0XwjIYh-a2HfawxHq1en53bXkFkDj2Q70jKWHr4,1088
|
|
59
|
+
scrapely_python-0.1.34.dist-info/licenses/LICENSE-APACHE,sha256=N8m0FzMq8i4UbTgXZvKEZJSD-27OF8pWZYS_atz4kTU,11355
|
|
60
|
+
scrapely_python-0.1.34.dist-info/licenses/NOTICE,sha256=1ltrzKk2ez7x2BPNpXeyvOlTty5AfABYDBlwNilpORk,788
|
|
61
|
+
scrapely_python-0.1.34.dist-info/METADATA,sha256=f84Jwc8hXM7jbHFipBYO1dlAojlHDxBpm1B8-BNmeYY,3582
|
|
62
|
+
scrapely_python-0.1.34.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
63
|
+
scrapely_python-0.1.34.dist-info/top_level.txt,sha256=T4B9UMrs4q99QSAFyD1zdXhl40KnLhBKqrDkPhOTsmY,9
|
|
64
|
+
scrapely_python-0.1.34.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.32.dist-info → scrapely_python-0.1.34.dist-info}/licenses/LICENSE-APACHE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|