mirror of
https://github.com/NaiboWang/EasySpider.git
synced 2026-11-16 05:38:43 +08:00
173 lines
7.4 KiB
Python
173 lines
7.4 KiB
Python
"""Regressions for Unicode previews on legacy consoles (issue #487).
|
|
|
|
Run with the executor's Python dependencies installed:
|
|
python scripts/test_console_encoding.py
|
|
|
|
Load BrowserThread's real methods through AST to avoid the executor's startup
|
|
side effects and browser/OCR dependencies. HTML parsing uses the production
|
|
code with a controlled browser; persistence is adapted to the production CSV
|
|
writer rather than running BrowserThread.saveData's other filesystem writes.
|
|
"""
|
|
|
|
import ast
|
|
import contextlib
|
|
import csv
|
|
from datetime import datetime
|
|
import io
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
from urllib.parse import urljoin
|
|
|
|
from lxml import etree
|
|
|
|
EXECUTE_STAGE = Path(__file__).resolve().parents[1] / "ExecuteStage"
|
|
sys.path.insert(0, str(EXECUTE_STAGE))
|
|
import utils
|
|
|
|
|
|
def load_browser_methods():
|
|
path = EXECUTE_STAGE / "easyspider_executestage.py"
|
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
browser = next(node for node in tree.body
|
|
if isinstance(node, ast.ClassDef) and node.name == "BrowserThread")
|
|
methods = {"recordLog", "print_and_log", "getData", "executeNode"}
|
|
browser.bases = []
|
|
browser.body = [node for node in browser.body
|
|
if isinstance(node, ast.FunctionDef) and node.name in methods]
|
|
module = ast.fix_missing_locations(ast.Module(body=[browser], type_ignores=[]))
|
|
namespace = dict(vars(utils), datetime=datetime, etree=etree, time=time, urljoin=urljoin)
|
|
exec(compile(module, str(path), "exec"), namespace)
|
|
return namespace["BrowserThread"]
|
|
|
|
|
|
BrowserThread = load_browser_methods()
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def encoded_output(encoding):
|
|
raw = io.BytesIO()
|
|
stream = io.TextIOWrapper(raw, encoding=encoding, errors="strict", newline="")
|
|
try:
|
|
yield stream, raw
|
|
finally:
|
|
stream.close()
|
|
|
|
|
|
class ConsoleEncodingTests(unittest.TestCase):
|
|
def test_legacy_stream_escapes_once_and_preserves_print_formatting(self):
|
|
for encoding in ("gbk", "ascii"):
|
|
with self.subTest(encoding=encoding), encoded_output(encoding) as (stream, raw):
|
|
utils.safe_print("prefix", "价格€😀", 42, sep="|", end="!", file=stream, flush=True)
|
|
expected = "prefix|价格€😀|42!".encode(encoding, "backslashreplace")
|
|
self.assertEqual(raw.getvalue(), expected)
|
|
|
|
def test_unicode_streams_preserve_text_and_default_stdout(self):
|
|
with encoded_output("utf-8") as (stream, raw):
|
|
utils.safe_print("价格€😀", file=stream, flush=True)
|
|
self.assertEqual(raw.getvalue().decode("utf-8"), "价格€😀\n")
|
|
stream = io.StringIO()
|
|
with contextlib.redirect_stdout(stream):
|
|
utils.safe_print("价格€😀", "second", sep=None, end=None)
|
|
self.assertEqual(stream.getvalue(), "价格€😀 second\n")
|
|
|
|
def test_non_encoding_io_errors_propagate(self):
|
|
for error in (BrokenPipeError("closed pipe"), OSError("write failed")):
|
|
class BrokenStream:
|
|
encoding = "gbk"
|
|
|
|
def write(self, text):
|
|
raise error
|
|
|
|
with self.subTest(error=type(error).__name__):
|
|
with self.assertRaises(type(error)):
|
|
utils.safe_print("价格€😀", file=BrokenStream())
|
|
|
|
def test_flush_is_requested_only_when_enabled(self):
|
|
class FlushCountingStream(io.StringIO):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.flush_calls = 0
|
|
|
|
def flush(self):
|
|
self.flush_calls += 1
|
|
|
|
stream = FlushCountingStream()
|
|
utils.safe_print("first", file=stream)
|
|
self.assertEqual(stream.flush_calls, 0)
|
|
utils.safe_print("second", file=stream, flush=True)
|
|
self.assertEqual(stream.flush_calls, 1)
|
|
|
|
def test_row_preview_preserves_full_values_and_record_flags(self):
|
|
values = {"price": "€123456", "hidden": "😀secret"}
|
|
with encoded_output("gbk") as (stream, raw), contextlib.redirect_stdout(stream):
|
|
row = utils.new_line(values, 4, [True, False])
|
|
stream.flush()
|
|
self.assertEqual(raw.getvalue().decode("gbk"), "\\u20ac123 \n")
|
|
self.assertEqual(row, ["€123456", "😀secret"])
|
|
self.assertEqual(values, {"price": "€123456", "hidden": "😀secret"})
|
|
|
|
def test_print_and_log_preserves_unicode_in_log(self):
|
|
subject = BrowserThread()
|
|
subject.logs = io.StringIO()
|
|
with encoded_output("gbk") as (stream, raw), contextlib.redirect_stdout(stream):
|
|
subject.print_and_log("price", "€😀", sep="|", end="!")
|
|
self.assertEqual(raw.getvalue().decode("gbk"), "price|\\u20ac\\U0001f600!")
|
|
self.assertTrue(subject.logs.getvalue().endswith("|price|€😀!"))
|
|
subject.recordLog("€😀")
|
|
self.assertTrue(subject.logs.getvalue().endswith(" €😀\n"))
|
|
|
|
def test_extraction_node_saves_complete_unicode_rows_to_csv(self):
|
|
for encoding in ("gbk", "ascii", "utf-8"):
|
|
with self.subTest(encoding=encoding), tempfile.TemporaryDirectory() as directory:
|
|
subject = BrowserThread()
|
|
subject.shutdown_event = threading.Event()
|
|
subject.event = threading.Event()
|
|
subject.event.set()
|
|
subject.logs = io.StringIO()
|
|
subject.browser = SimpleNamespace(page_source="", iframe_env=False)
|
|
subject.outputParameters = {"price": ""}
|
|
subject.outputParametersRecord = [True]
|
|
subject.maxViewLength = 100
|
|
subject.OUTPUT = []
|
|
subject.dataNotFoundKeys = {"price": False}
|
|
subject.totalSteps = 0
|
|
subject.startSteps = 0
|
|
path = Path(directory) / "result.csv"
|
|
|
|
def save_data():
|
|
utils.write_to_csv(path, subject.OUTPUT, subject.outputParametersRecord)
|
|
subject.OUTPUT.clear()
|
|
|
|
subject.saveData = save_data
|
|
subject.procedure = [{
|
|
"title": "提取数据", "option": 3, "isInLoop": False,
|
|
"parameters": {
|
|
"clear": 0, "recordASField": 1, "newLine": True,
|
|
"waitElement": "", "wait": 0, "waitType": 0,
|
|
"params": [{
|
|
"name": "price", "optimizable": True, "iframe": False,
|
|
"relativeXPath": "/html/body/div", "relative": False,
|
|
"nodeType": 0, "contentType": 0, "default": "",
|
|
}],
|
|
},
|
|
}]
|
|
samples = ["Price€ 20", "价格😀 30"]
|
|
with encoded_output(encoding) as (stream, raw), contextlib.redirect_stdout(stream):
|
|
for value in samples:
|
|
subject.browser.page_source = f"<html><body><div>{value}</div></body></html>"
|
|
subject.executeNode(0)
|
|
stream.flush()
|
|
self.assertNotIn("Error executing node", raw.getvalue().decode(encoding))
|
|
self.assertEqual(subject.totalSteps, len(samples))
|
|
with path.open(encoding="utf-8-sig", newline="") as saved:
|
|
self.assertEqual(list(csv.reader(saved)), [[value] for value in samples])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|