fix: scroll recommendation feed container
This commit is contained in:
parent
d0f6c5e5ab
commit
cc1109628f
47
Douyin.py
47
Douyin.py
@ -404,17 +404,58 @@ def create_human_scroll_plan(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_scroll_step(page: Any, distance: int) -> bool:
|
||||||
|
script = f"""
|
||||||
|
const distance = {distance};
|
||||||
|
function findMainScrollContainer() {{
|
||||||
|
const preferredSelectors = ['.tKqwmYAX', '.route-scroll-container', '.semi-tabs-content'];
|
||||||
|
for (const selector of preferredSelectors) {{
|
||||||
|
const el = document.querySelector(selector);
|
||||||
|
if (el && el.scrollHeight > el.clientHeight + 20) {{
|
||||||
|
return el;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
const candidates = Array.from(document.querySelectorAll('*'))
|
||||||
|
.filter((el) => {{
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return rect.width > 300
|
||||||
|
&& rect.height > 200
|
||||||
|
&& el.scrollHeight > el.clientHeight + 20;
|
||||||
|
}})
|
||||||
|
.sort((a, b) => {{
|
||||||
|
const areaA = a.getBoundingClientRect().width * a.getBoundingClientRect().height;
|
||||||
|
const areaB = b.getBoundingClientRect().width * b.getBoundingClientRect().height;
|
||||||
|
return areaB - areaA;
|
||||||
|
}});
|
||||||
|
|
||||||
|
return candidates[0] || null;
|
||||||
|
}}
|
||||||
|
|
||||||
|
const scrollTarget = findMainScrollContainer();
|
||||||
|
if (scrollTarget) {{
|
||||||
|
scrollTarget.scrollBy(0, distance);
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
return false;
|
||||||
|
"""
|
||||||
|
scrolled_container = bool(page.run_js(script))
|
||||||
|
if not scrolled_container:
|
||||||
|
page.run_js(f"window.scrollBy(0, {distance});")
|
||||||
|
return scrolled_container
|
||||||
|
|
||||||
|
|
||||||
def run_human_scroll_sequence(page: Any, plan: HumanScrollPlan) -> None:
|
def run_human_scroll_sequence(page: Any, plan: HumanScrollPlan) -> None:
|
||||||
page.run_js(f"window.scrollBy(0, {plan.down_distance});")
|
run_scroll_step(page, plan.down_distance)
|
||||||
print(f"[INFO] 向下滚动 {plan.down_distance}px,停留 {plan.down_wait:.1f}s")
|
print(f"[INFO] 向下滚动 {plan.down_distance}px,停留 {plan.down_wait:.1f}s")
|
||||||
time.sleep(plan.down_wait)
|
time.sleep(plan.down_wait)
|
||||||
|
|
||||||
if plan.reverse_distance > 0:
|
if plan.reverse_distance > 0:
|
||||||
page.run_js(f"window.scrollBy(0, -{plan.reverse_distance});")
|
run_scroll_step(page, -plan.reverse_distance)
|
||||||
print(f"[INFO] 小幅回滚 {plan.reverse_distance}px,停留 {plan.reverse_wait:.1f}s")
|
print(f"[INFO] 小幅回滚 {plan.reverse_distance}px,停留 {plan.reverse_wait:.1f}s")
|
||||||
time.sleep(plan.reverse_wait)
|
time.sleep(plan.reverse_wait)
|
||||||
forward_distance = plan.reverse_distance * 2
|
forward_distance = plan.reverse_distance * 2
|
||||||
page.run_js(f"window.scrollBy(0, {forward_distance});")
|
run_scroll_step(page, forward_distance)
|
||||||
|
|
||||||
if plan.settle_wait > 0:
|
if plan.settle_wait > 0:
|
||||||
print(f"[INFO] 继续停留 {plan.settle_wait:.1f}s")
|
print(f"[INFO] 继续停留 {plan.settle_wait:.1f}s")
|
||||||
|
|||||||
@ -68,6 +68,18 @@ class FakeScrollPage:
|
|||||||
self.scripts.append(script)
|
self.scripts.append(script)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeContainerScrollPage:
|
||||||
|
def __init__(self, container_found=True):
|
||||||
|
self.container_found = container_found
|
||||||
|
self.scripts = []
|
||||||
|
|
||||||
|
def run_js(self, script):
|
||||||
|
self.scripts.append(script)
|
||||||
|
if "findMainScrollContainer" in script:
|
||||||
|
return self.container_found
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class DouyinModuleTests(unittest.TestCase):
|
class DouyinModuleTests(unittest.TestCase):
|
||||||
def test_module_can_import_without_optional_runtime_dependencies(self) -> None:
|
def test_module_can_import_without_optional_runtime_dependencies(self) -> None:
|
||||||
module = importlib.import_module("Douyin")
|
module = importlib.import_module("Douyin")
|
||||||
@ -184,16 +196,24 @@ class DouyinModuleTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
with mock.patch.object(module.time, "sleep") as mocked_sleep:
|
with mock.patch.object(module.time, "sleep") as mocked_sleep:
|
||||||
module.run_human_scroll_sequence(page, plan)
|
module.run_human_scroll_sequence(page, plan)
|
||||||
self.assertEqual(
|
self.assertIn("window.scrollBy(0, 500);", page.scripts)
|
||||||
page.scripts,
|
self.assertIn("window.scrollBy(0, -120);", page.scripts)
|
||||||
[
|
self.assertIn("window.scrollBy(0, 240);", page.scripts)
|
||||||
"window.scrollBy(0, 500);",
|
|
||||||
"window.scrollBy(0, -120);",
|
|
||||||
"window.scrollBy(0, 240);",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
mocked_sleep.assert_has_calls([mock.call(2.5), mock.call(1.0), mock.call(3.0)])
|
mocked_sleep.assert_has_calls([mock.call(2.5), mock.call(1.0), mock.call(3.0)])
|
||||||
|
|
||||||
|
def test_run_scroll_step_prefers_main_scroll_container(self) -> None:
|
||||||
|
module = importlib.import_module("Douyin")
|
||||||
|
page = FakeContainerScrollPage(container_found=True)
|
||||||
|
self.assertTrue(module.run_scroll_step(page, 500))
|
||||||
|
self.assertIn("const distance = 500;", page.scripts[-1])
|
||||||
|
self.assertIn("scrollTarget.scrollBy(0, distance);", page.scripts[-1])
|
||||||
|
|
||||||
|
def test_run_scroll_step_falls_back_to_window_when_container_is_missing(self) -> None:
|
||||||
|
module = importlib.import_module("Douyin")
|
||||||
|
page = FakeContainerScrollPage(container_found=False)
|
||||||
|
self.assertFalse(module.run_scroll_step(page, 500))
|
||||||
|
self.assertEqual(page.scripts[-1], "window.scrollBy(0, 500);")
|
||||||
|
|
||||||
def test_ensure_browser_debug_port_ready_accepts_open_port(self) -> None:
|
def test_ensure_browser_debug_port_ready_accepts_open_port(self) -> None:
|
||||||
module = importlib.import_module("Douyin")
|
module = importlib.import_module("Douyin")
|
||||||
connection = mock.MagicMock()
|
connection = mock.MagicMock()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user