Pydoll 完整教程:动态页面查询、分页与结果采集

用 Pydoll 3 打开动态列表、点击标签筛选、逐页读取关联字段,保存 CSV 与诊断截图,并用 URL 和内容等待避免重复读取旧页面。

亿牛云技术团队2026年10月28日5 分钟阅读

在动态页面里,点击完成、网络空闲和结果更新是三个不同状态。只在每次点击后睡两秒,任务可能在快页面浪费时间,在慢页面读到旧数据。

本文用 Pydoll 3 读取 Quotes to Scrape 的动态列表,选择 life 标签,再处理最多两页结果。每条名言保留作者和来源,输出 CSV;失败时尝试保存当前页面截图,帮助定位卡在哪一步。

准备输入地址

先在浏览器找到下列页面并确认内容,再从地址栏复制完整地址。服务或仓库地址从相应账户设置、官方项目的接入说明或克隆菜单取得。不要从示例输出中反推真实地址。

  • QUOTES_LIST_URL:Quotes to Scrape JavaScript 列表页地址,路径为 /js/。

在执行后续命令的同一个 Bash/WSL 终端中设置:

export QUOTES_LIST_URL='<paste-the-actual-quotes-list-url>'

下文 JSON 或任务参数中的 <变量名> 是待替换标记,提交前填写刚复制的实际值;JSON 不会自动展开环境变量。结果示例中的 example.16yun.cn 只表示来源字段的形状,不是本文部署的演示站。

安装与浏览器准备

使用 Python 3.12,并确保已经安装 Chrome 或 Edge。Pydoll 不为你下载浏览器,也不需要 WebDriver。安装:

python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install pydoll-python==3.0.0

本文采用 v3 的导入和元素 API。不要把旧版本中同名属性、其他自动化库的等待接口直接混用。基础用法见 Pydoll 官方仓库。

定位与等待先建立明确含义

本例使用 CSS 确定目标:.quote 是重复对象,内部 .text 与 .author 构成一条数据。标签链接的 href 表示目标筛选;点击后先等待 URL 到达目标,再确认列表出现。

这种方法适用于当前站点的完整导航。若另一站点点击后 URL 不变,应等待实际结果标记或列表内容更新,不能继续用 URL 变化判断成功。

完整代码

创建 collect_quotes.py:

import asyncio
import os
import csv
from pathlib import Path
from urllib.parse import urljoin
from pydoll import Chrome
 
async def main():
    out = Path("pydoll-output")
    out.mkdir(exist_ok=True)
    rows, seen = [], set()
    async with Chrome() as browser:
        tab = await browser.start()
        try:
            await tab.go_to(os.environ["QUOTES_LIST_URL"])
            link = await tab.query('a.tag[href="/tag/life/page/1/"]', timeout=15)
            target = urljoin(await tab.current_url(), link.get_attribute("href"))
            await link.click()
            await tab.wait_for_url(target, timeout=20)
 
            for page_number in range(1, 3):
                await tab.query(".quote", timeout=15)
                cards = await tab.query(".quote", find_all=True)
                source_url = await tab.current_url()
                for card in cards:
                    text = (await (await card.query(".text")).text()).strip()
                    author = (await (await card.query(".author")).text()).strip()
                    if not text or not author:
                        raise ValueError("名言或作者缺失")
                    key = (text, author)
                    if key not in seen:
                        seen.add(key)
                        rows.append({
                            "text": text, "author": author,
                            "source_url": source_url, "page_number": page_number,
                        })
                if page_number == 2:
                    break
                next_link = await tab.query("li.next a", timeout=2, raise_exc=False)
                if next_link is None:
                    break
                next_url = urljoin(source_url, next_link.get_attribute("href"))
                if next_url == source_url:
                    raise ValueError("下一页仍指向当前页")
                await next_link.click()
                await tab.wait_for_url(next_url, timeout=20)
            await tab.take_screenshot(str(out / "last-page.png"), beyond_viewport=True)
        except Exception:
            try:
                await tab.take_screenshot(str(out / "failure.png"), beyond_viewport=True)
            except Exception as shot_error:
                print("截图未保存:", type(shot_error).__name__)
            raise
 
    if not rows:
        raise ValueError("没有取得任何目标记录")
    with (out / "quotes.csv").open("w", encoding="utf-8-sig", newline="") as stream:
        writer = csv.DictWriter(stream, fieldnames=["text", "author", "source_url", "page_number"])
        writer.writeheader()
        writer.writerows(rows)
    print({"unique_quotes": len(rows)})
 
asyncio.run(main())

运行:

python collect_quotes.py

async with 负责当前程序启动的浏览器生命周期。截图动作与采集失败分开处理,截图失败不会把原来的页面错误改写成成功。

get_attribute 在本文版本中读取元素属性,不加 await;text 是异步方法,需要 await。接口形式相似不代表它们有相同调用方式。

怎样核对 CSV

打开 pydoll-output/quotes.csv,检查来源路径是否为 life 标签分页,并抽查正文与作者是否属于同一个 .quote。不要分别抓全页正文数组和作者数组后按位置拼接。

预期数据结构是“文本、作者、来源、页号”,具体条数由当时页面决定。最多两页并不是两页一定都存在;当没有 next 时正常停止,但仍需核对页面是否确实已经到末页。

去重键是本例的文本与作者组合。它适合这个练习任务,不是通用商品键。迁移商品数据时使用 SKU 或明确业务编号,避免相同展示名称被误合并。

当前脚本只有完全走完主线才写最终 CSV。失败后不要把目录里上一次的 quotes.csv 当成新一轮结果;正式使用时为批次增加独立输出目录,或者在调用方记录运行状态。

如何迁移到自己的动态查询页

先修改进入地址与筛选元素,确认动作确实改变数据;再修改重复对象与内部字段,最后处理分页终止条件。需要输入搜索词时,用该版本元素输入接口填写,并等待与你业务结果相关的标记,而非只调用 network idle。

代理配置只在网络条件确实需要时加入。先用直连建立选择器与字段基线,再检查代理或账号条件的差异,这样错误不会同时来自四个配置来源。

如果列表是虚拟化滚动,DOM 里可能只有当前窗口的少量项。本文按常规分页处理,不适合直接将当前 DOM 数量当作整份无限列表总量。

常见问题与处理

启动提示没有浏览器。 安装系统浏览器或明确配置其可执行路径。反复安装 Python 包不会自动补齐浏览器。

标签找不到。 检查当前 URL、动态列表是否加载,以及该标签链接是否仍存在。若更换筛选词,同步修改 href 与任务预期。

点击后页面仍未更新。 查看目标地址是否变化;URL 不变的应用应增加内容更新判断。不要只延长固定睡眠。

第二页重复第一页。 比较保存的 source_url、next href 与实际正文,检查下一页是否指向当前页。只靠去重把重复行删掉,会隐藏漏页原因。

属性读取报错。 区分同步 get_attribute 与异步 text/query,核对依赖版本,不通过在所有地方增删 await 猜测。

错误截图也失败。 原始异常仍应保留;浏览器已失联或页面被关闭时,不应要求截图一定可得。根据阶段检查启动、导航与元素操作。

CSV 中文或标点异常。 先检查内存文本与文件回读,再检查表格软件导入编码,不把编码问题当成选择器错误。

动态列表的关键是把每次动作与数据更新证据连接起来。浏览器能点按钮之后,还要证明读到的是对应筛选条件和页面的新结果。

需要企业代理方案?

我们可根据目标站点、并发规模与稳定性目标提供定制方案。