一、拾枝杂谈

我用 WorkBuddy 配合 Playwright 成功搭建了多平台数据矩阵,使用的核心技术栈就是 Python + Playwright + lark-cli(即飞书官方开源的命令行工具)。

一共七个平台,每个平台都有个性的问题,我都分别写了文章进行讲解。

只是我没想到,在最后一个平台——搜狐号的数据采集过程中,居然让我有点招架不住。

因为搜狐号派出了自己的“四大天王”——分别是“导航层级天王”,“日期选择天王”,“数据表格天王”,和 “日历弹窗天王”

今天这篇文章,我就带大家一起攻下这四大天王!


二、“导航层级天王”

搜狐号的导航非常复杂,要想查看自己发布的文章数据,是这样走的:“数据分析” -> “内容分析” -> “单篇” -> “图文”,还要选定日期来确定统计的文章。

但是最开始的 config.py 配置中,WorkBuddy 把搜狐号的入口URL选错了,当时的 config.py 里搜狐号的 home_url 和 creator_url 配的是"home_url": "https://mp.sohu.com/mpfe/v4/contentManagement/first/page"

所以脚本已启动就直接跳到了内容管理页,而不是数据分析页码。虽然我告诉了 WorkBuddy 正确的路径,但是脚本默认打开的起点就已经跑偏了,也就不可能读取正确的文章数据。

而且早期的脚本在“一通乱点”之后,没有进行检查,比如URL是否变化,页面上是否出现了“数据列表”或者类似的模块,playwright是否成功找到了 el-table 或 日期选择器。

相反,它直接就把当前页面的 HTML 抓下来解析了,然而既然当前页是内容管理页,那自然只能看到 “阅读 + 评论” 两项基础数据,与我们预期的完整数据大相径庭。

为了应对“导航层级天王”锐不可当的气势,我命令 WorkBuddy 做了两件事:

一是“单刀直入”,在 souhu.py 中代码写死,直接导航到数据分析页,不再依赖侧边栏点击 await self.page.goto("https://mp.sohu.com/mpfe/v4/data/analysis", ...)。相关代码片如下:

# Direct navigate to 数据分析 page (URL found from user manual nav)
print(f"[{self.name}] Direct navigate to data analysis page...")
try:
    await self.page.goto("https://mp.sohu.com/mpfe/v4/data/analysis",
                         wait_until="domcontentloaded", timeout=60000)
except Exception as e:
    print(f"[{self.name}] Direct nav error: {e}")

二是“步步为营”,先 goto 数据分析页,再点 tab,不再从内容管理页开始慢慢菜单导航。


三、“日期选择天王”

我先给大家还原一下“日期选择天王”是如何仗势欺人的。

我当时和 WorkBuddy 提出的需求是这样的:“进入搜狐号数据分析页后,把日期范围改成从 2026-07-15 到今天,这样能看到所有 GEO 系列的文章。”

但是脚本执行完,表格里只出来 1 条数据,而且日期范围看起来还是默认的最近几天,我让 WorkBuddy 排查后才发现,页面上有俩日期选择器!如下图所示:

可以看到,上面的选择器控制的是 图表数据,下面的选择器控制的是 表格数据

playwright 脚本最开始的写法是这样的:

date_selectors = [
    'input[placeholder*="日期"]',
    'input[placeholder*="开始"]',
    '.el-date-editor input',
    ...
]
for sel in date_selectors:
    el = self.page.locator(sel).first
    if await el.is_visible():
        await el.click()
        break

这么写问题出在哪儿?first 只拿到第一个可见的日期输入框, 也就是顶部图表区的那个; 填完顶部后,脚本没有意识到还有第二个选择器; 底部表格区的日期范围没被修改,仍然只显示默认 7 天内的文章, 所以只抓到 1 条数据。

为了应对“日期选择天王”的分身术,我让 WorkBuddy 使出了 “一石二鸟” 之法。不再只找“第一个”日期输入框,要找到全部 4 个输入框(顶部开始/结束 + 底部开始/结束),然后一口气全部填完。

验证思路如下:

# 找到全部 .el-range-input(两个日期范围选择器,共 4 个 input)
inputs = document.querySelectorAll('.el-range-input')

# 顶部图表区
inputs[0].value = "2026-07-15"
inputs[1].value = "2026-08-10"

# 底部数据列表区
inputs[2].value = "2026-07-15"
inputs[3].value = "2026-08-10"

# 触发 Vue/Element UI 的响应
inputs[0].dispatchEvent(new Event('input', {bubbles: true}));
inputs[0].dispatchEvent(new Event('change', {bubbles: true}));
# ... 对 4 个 input 都触发

然后填完后,按 Escape 键关闭日历弹窗,否则弹窗会挡住表格,影响读取数据;接着,再按 Enter 触发刷新,让表格真正按新日期范围重新请求数据。

由于搜狐号的 Element UI 日期选择器,输入框直接接受 2026-07-15 这种格式,所以 debug_sohu_date.py 里直接用了了项目约定的 ISO 格式。


四、“数据表格天王”

前两个天王被打倒后,页面终于能正确加载出数据列表了,但脚本一解析,返回的结果却是一塌糊涂!

document.querySelectorAll(‘table’) 找到了一些 <table> 标签,但是提取出来的 rows 要么是空的,要么只有表头,要么行内容对不上号。给人的感觉就像是页面上明明有表格,Playwright 却读不到数据。

WorkBuddy 的干活能力确实可以,一顿排查后它告诉我,根本原因是搜狐号后台用的是 Vue + Element UI。它的 组件渲染到 DOM 里之后,并不是我们平时写的那种简单结构:

<table>
  <thead><tr><th>标题</th><th>阅读</th>...</tr></thead>
  <tbody><tr><td>...</td></tr></tbody>
</table>

而是为了支持固定列、表头、虚拟滚动等效果,拆成了多张表和大量 div 嵌套,大概长下面这样:

<div class="el-table">
  <!-- 表头区域(单独一张 table) -->
  <div class="el-table__header-wrapper">
    <table class="el-table__header">...</table>
  </div>
  <!-- 表体区域(单独一张 table) -->
  <div class="el-table__body-wrapper">
    <table class="el-table__body">
      <tbody>
        <tr>
          <td>
            <div class="cell">
              <div class="label">
                文章标题
                <div>2026-07-20</div>
              </div>
            </div>
          </td>
          <td><div class="cell">123</div></td>
          <td><div class="cell">45</div></td>
          ...
        </tr>
      </tbody>
    </table>
  </div>
</div>

虽然能抓到 el-table__headerel-table__body 这两张表,但它按“第一张表的表头 + 第二张表的行”这种默认规则去拼,结果就乱了。

为了对付“数据表格天王”的障眼法,我和WorkBuddy商讨后决定采用“量体裁衣”政策,专门写了一个只针对 el-table 的解析器,核心逻辑在 sohu.py 的 _extract_articles_from_page() 里:

async def _extract_articles_from_page(self) -> list:
    return await self.page.evaluate(r"""
        () => {
            const out = [];
            const tbody = document.querySelector('.el-table__body tbody');
            if (!tbody) return out;
            const trs = tbody.querySelectorAll('tr');
            for (const tr of trs) {
                const labelEl = tr.querySelector('.label');
                if (!labelEl) continue;   // 过滤掉没有标题的行
                const fullText = (labelEl.innerText || '').trim();
                if (!fullText) continue;

                const lines = fullText.split('\n').map(s => s.trim()).filter(Boolean);
                const title = lines[0] || '';
                let dateStr = '';
                const dateMatch = fullText.match(/(\d{4}-\d{2}-\d{2})/);
                if (dateMatch) dateStr = dateMatch[1];

                const cells = tr.querySelectorAll('div.cell');
                const nums = [];
                for (let i = 1; i < cells.length - 1 && i < 7; i++) {
                    nums.push((cells[i].innerText || '').trim());
                }
                if (title && nums.length >= 6) {
                    out.push({
                        title: title,
                        publish_date: dateStr,
                        reads: nums[0],
                        visits: nums[1],
                        likes: nums[2],
                        comments: nums[3],
                        shares: nums[4],
                        votes: nums[5],
                    });
                }
            }
            return out;
        }
    """)

这个个性化方法实现了下面这些要点:

  1. 直接定位 .el-table__body tbody,而不是遍历所有 <table>;
  2. .label 判断有效数据行,没有 .label 的行直接跳过;
  3. .label 的文本里拆分标题和日期;
  4. div.cell 的顺序取 6 个数字字段,对应到 阅读数、访问数、点赞数、评论数、分享数、投票数。

三个问题全解决之后,才终于能稳定拿到搜狐号的数据。


五、“日历弹窗天王”

我没想到 “日期选择天王” 居然和 “日历弹窗天王” 狼狈为奸!

之前在解决“日期选择天王” 的时候,我们最终选择的修正方案是直接往日期输入框填数值。填好之后,脚本就直接去截图了,但是这时候日历弹窗还开着呢!

奇怪了,为啥弹窗还在遮挡呢?

这是因为 Element UI 的 el-date-picker 组件是这样的交互:

  1. 用户点击或聚焦日期输入框;
  2. 组件弹出一个日历面板,让用户点选日期;
  3. 如果用户点选或按 Enter,面板关闭;
  4. 如果用户按 Escape,面板也会关闭。

脚本用代码往输入框里填值,并没有真正完成“用户点选/关闭面板”这个动作, 所以日历面板一直悬在页面上,挡住了一大半内容。直观效果就是截图里一半画面被日历遮住了。

虽然 querySelector 读 DOM 不依赖视觉可见性,但如果弹窗层有 z-index 覆盖或焦点陷阱,某些操作会受影响。

修复方式也很简单,就是填好日期后,Playwright 模拟按一下 Escape 键关闭日历弹窗即可。如下所示:

# 1.填完 4 个日期输入框
inputs[0].value = "2026-07-15"
inputs[1].value = "2026-08-10"
inputs[2].value = "2026-07-15"
inputs[3].value = "2026-08-10"

# 2.触发 Element UI / Vue 的响应
for (const input of inputs) {
    input.dispatchEvent(new Event('input', {bubbles: true}));
    input.dispatchEvent(new Event('change', {bubbles: true}));
}

# 3.关键:按 Escape 关闭日历弹窗
await page.keyboard.press("Escape")

# 4.再按 Enter 触发表格刷新
await page.keyboard.press("Enter")

Δ总结

  • OK,以上就是本篇文章的全部内容了,感谢阅读
  • 这篇文章总结了up 在处理搜狐号数据统计时遇到的四个主要问题,包括导航复杂的问题,日期选择器不唯一的问题,数据表格隐式呈现的问题,以及日历弹窗遮挡的问题。我把它们比喻成了四大天王哈哈。
  • 关注迅高AI实验室,学习AI不迷路!

更多推荐