mirror of
https://github.com/NaiboWang/EasySpider.git
synced 2026-11-03 23:24:46 +08:00
142 lines
5.5 KiB
Python
142 lines
5.5 KiB
Python
"""Regression tests for extraction preprocessing, without launching a browser.
|
|
|
|
Run with: python scripts/test_preprocess.py
|
|
|
|
The executor imports browser/OCR dependencies and initializes runtime directories
|
|
at module scope. Load its actual preprocess method and pure utility functions
|
|
from the source AST to exercise preprocessing with only the standard library.
|
|
These tests do not cover browser startup or crawling a live page.
|
|
"""
|
|
|
|
import ast
|
|
import copy
|
|
from enum import IntEnum, unique
|
|
from pathlib import Path
|
|
import re
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def load_preprocess():
|
|
namespace = {"IntEnum": IntEnum, "unique": unique, "re": re}
|
|
selections = (
|
|
("constants.py", ("GraphOption",)),
|
|
("utils.py", ("detect_optimizable", "lowercase_tags_in_xpath")),
|
|
("easyspider_executestage.py", ("preprocess",)),
|
|
)
|
|
for filename, names in selections:
|
|
path = ROOT / "ExecuteStage" / filename
|
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
nodes = tree.body
|
|
if filename == "easyspider_executestage.py":
|
|
nodes = next(node for node in nodes
|
|
if isinstance(node, ast.ClassDef)
|
|
and node.name == "BrowserThread").body
|
|
selected = [node for node in nodes if getattr(node, "name", None) in names]
|
|
if len(selected) != len(names):
|
|
raise RuntimeError(f"Could not find {names} in {path}")
|
|
exec(compile(ast.Module(body=selected, type_ignores=[]), str(path), "exec"),
|
|
namespace)
|
|
return namespace["preprocess"]
|
|
|
|
|
|
PREPROCESS = load_preprocess()
|
|
|
|
|
|
def field(name="title"):
|
|
return {
|
|
"name": name,
|
|
"relativeXPath": "/SPAN",
|
|
"nodeType": 1,
|
|
"contentType": 0,
|
|
"relative": True,
|
|
"default": "",
|
|
"beforeJS": "",
|
|
"afterJS": "",
|
|
"recordASField": 1,
|
|
}
|
|
|
|
|
|
class PreprocessTests(unittest.TestCase):
|
|
def preprocess(self, extraction, loop_type, loop_first, loop_overrides=None):
|
|
extract = {
|
|
"option": 3,
|
|
"sequence": [],
|
|
"parameters": copy.deepcopy(extraction),
|
|
}
|
|
loop = {
|
|
"option": 8,
|
|
"title": "Extract rows",
|
|
"sequence": [1 if loop_first else 0],
|
|
"parameters": {
|
|
"loopType": loop_type,
|
|
"xpath": "//LI",
|
|
"pathList": "//li[1]\n//li[2]",
|
|
**(loop_overrides or {}),
|
|
},
|
|
}
|
|
procedure = [loop, extract] if loop_first else [extract, loop]
|
|
worker = SimpleNamespace(procedure=procedure, task_version="0.6.3",
|
|
print_and_log=lambda *args: None)
|
|
PREPROCESS(worker)
|
|
return loop["parameters"], extract["parameters"]
|
|
|
|
def assert_empty_extraction(self, extraction):
|
|
for loop_type in (1, 2):
|
|
for loop_first in (True, False):
|
|
with self.subTest(loop_type=loop_type, loop_first=loop_first):
|
|
loop, extract = self.preprocess(extraction, loop_type, loop_first)
|
|
self.assertEqual(extract["params"], [])
|
|
self.assertEqual(extract["recordASField"], 0)
|
|
# An empty extraction must follow the normal no-output path;
|
|
# the quick path would append rows of stale field values.
|
|
self.assertFalse(loop["quickExtractable"])
|
|
self.assertNotIn("quickParams", loop)
|
|
|
|
def assert_populated_extraction(self, extraction, expected_name):
|
|
for loop_type in (1, 2):
|
|
for loop_first in (True, False):
|
|
with self.subTest(loop_type=loop_type, loop_first=loop_first):
|
|
loop, extract = self.preprocess(extraction, loop_type, loop_first)
|
|
self.assertTrue(loop["quickExtractable"])
|
|
self.assertEqual([p["name"] for p in extract["params"]],
|
|
[expected_name])
|
|
self.assertEqual([p["name"] for p in loop["quickParams"]],
|
|
[expected_name])
|
|
self.assertEqual(loop["baseXPath"], "//li" if loop_type == 1
|
|
else "//li[1]\n//li[2]")
|
|
|
|
def test_empty_current_params(self):
|
|
self.assert_empty_extraction({"params": []})
|
|
|
|
def test_empty_legacy_paras(self):
|
|
self.assert_empty_extraction({"paras": []})
|
|
|
|
def test_empty_current_params_override_legacy_fields(self):
|
|
self.assert_empty_extraction({"params": [], "paras": [field("old")]})
|
|
|
|
def test_populated_current_params(self):
|
|
self.assert_populated_extraction({"params": [field()]}, "title")
|
|
|
|
def test_populated_legacy_paras(self):
|
|
self.assert_populated_extraction({"paras": [field()]}, "title")
|
|
|
|
def test_current_params_override_legacy_fields(self):
|
|
self.assert_populated_extraction(
|
|
{"params": [field("current")], "paras": [field("old")]}, "current")
|
|
|
|
def test_existing_optimization_guards_remain_active(self):
|
|
for overrides in ({"iframe": True}, {"skipCount": 1}):
|
|
for loop_first in (True, False):
|
|
with self.subTest(overrides=overrides, loop_first=loop_first):
|
|
loop, _ = self.preprocess({"params": [field()]}, 1,
|
|
loop_first, overrides)
|
|
self.assertFalse(loop["quickExtractable"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|