From cc1109628f53843569e2499269d1b917b1194a8a Mon Sep 17 00:00:00 2001 From: wangshaoqing Date: Tue, 26 May 2026 15:54:44 +0800 Subject: [PATCH] fix: scroll recommendation feed container --- Douyin.py | 47 ++++++++++++++++++++++++++++++++++++++++++++--- test_douyin.py | 36 ++++++++++++++++++++++++++++-------- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/Douyin.py b/Douyin.py index 542105c..b7d77e9 100644 --- a/Douyin.py +++ b/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: - 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") time.sleep(plan.down_wait) 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") time.sleep(plan.reverse_wait) 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: print(f"[INFO] 继续停留 {plan.settle_wait:.1f}s") diff --git a/test_douyin.py b/test_douyin.py index 99ecba9..15458b0 100644 --- a/test_douyin.py +++ b/test_douyin.py @@ -68,6 +68,18 @@ class FakeScrollPage: 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): def test_module_can_import_without_optional_runtime_dependencies(self) -> None: module = importlib.import_module("Douyin") @@ -184,16 +196,24 @@ class DouyinModuleTests(unittest.TestCase): ) with mock.patch.object(module.time, "sleep") as mocked_sleep: module.run_human_scroll_sequence(page, plan) - self.assertEqual( - page.scripts, - [ - "window.scrollBy(0, 500);", - "window.scrollBy(0, -120);", - "window.scrollBy(0, 240);", - ], - ) + self.assertIn("window.scrollBy(0, 500);", page.scripts) + self.assertIn("window.scrollBy(0, -120);", page.scripts) + self.assertIn("window.scrollBy(0, 240);", page.scripts) 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: module = importlib.import_module("Douyin") connection = mock.MagicMock()