Skip to content

Python 爬虫实战:构建社媒热点笔记与评论采集系统

前言

在社交媒体数据分析中,如何稳定、高效地获取热点内容及其用户反馈一直是个技术难点。某书(XHS)作为典型的 SPA(单页应用),其前端更新频繁,反爬机制复杂。

本文将面向工程师,详细介绍一套 社媒数据采集系统 的技术架构与核心实现。该系统采用 Playwright 浏览器自动化 + 网络请求拦截 的混合策略,实现了对热点笔记与评论的稳定抓取、智能过滤及自动化推送。

总体架构

为了兼顾灵活性与产品化体验,系统采用了分层架构设计:

  • 抓取层:基于 Playwright,负责浏览器交互、页面渲染与网络事件监听。
  • 登录与会话层:管理 Cookies 轮换、登录态保持与上下文复用。
  • 数据层:基于 SQLite 的轻量级存储,负责数据去重、持久化与索引优化。
  • 推送层:将清洗后的客源数据批量推送到下游业务系统。
  • UI 层:基于 PyQt6 构建桌面端界面,通过多线程实现非阻塞交互。

核心技术选型

为什么选择 Playwright?

相较于传统的 Requests/BeautifulSoup 纯协议抓取,我们优先选择了 Playwright,主要基于以下考量:

  1. 真实渲染:能够完美处理动态加载的 JS 内容,对抗前端混淆。
  2. 网络拦截:可以直接监听并解析浏览器发出的 API 请求(Response Interception),获取结构化的 JSON 数据,避免了复杂的 HTML 解析。
  3. 上下文复用:支持持久化 BrowserContext,大幅减少冷启动与重复登录的开销。

核心功能实现

1. 热点笔记抓取:混合模式

系统支持“关键字搜索”与“发现页推荐”两种模式。核心逻辑在于利用 Playwright 加载页面,同时监听后台 API 数据的返回。

我们通过监听 window.__INITIAL_STATE__ 或特定的 API 路由来获取数据,而不是单纯依赖 DOM 解析。

python
async def start_crawling(self, keywords):
    # 启动浏览器上下文
    context = await self.browser.new_context(storage_state="auth.json")
    page = await context.new_page()
    
    # 监听网络响应,直接截获 JSON 数据
    page.on("response", self.handle_api_response)
    
    # 访问搜索页
    await page.goto(f"https://www.example.com/search_result?keyword={keywords}")
    
    # 模拟人类滚动行为,触发懒加载
    await self.auto_scroll(page)

def handle_api_response(self, response):
    """拦截搜索接口与详情页接口"""
    if "/api/sns/web/v1/search/notes" in response.url:
        data = response.json()
        self.save_notes(data['data']['items'])

这种方式既利用了浏览器的渲染能力通过反爬检测,又直接获取了干净的 JSON 数据。

2. 评论深度抓取:攻克“展开”难题

评论抓取是本项目的难点,尤其是多级子评论的展开。在 评论抓取模块 中,我们实现了一套智能的展开逻辑:

  1. 监听 API:监听 /api/sns/web/v1/comment/ 相关接口。
  2. 自动展开:识别页面中的“展开”、“查看更多回复”按钮,并模拟点击。
  3. 父子关联:在内存中构建评论树,将子评论挂载到对应的父评论下。
python
async def expand_replies(self, page):
    """智能识别并点击展开按钮"""
    # 定位所有可见的展开按钮
    # 包含:展开 x 条回复、查看更多等文案
    expand_buttons = page.locator("text=/展开|查看更多|更多回复/")
    
    count = await expand_buttons.count()
    for i in range(count):
        btn = expand_buttons.nth(i)
        if await btn.is_visible():
            # 滚动到元素位置,模拟真实点击
            await btn.scroll_into_view_if_needed()
            await btn.click()
            # 随机延时,避免触发频控
            await page.wait_for_timeout(random.randint(500, 1500))

3. 数据存储与去重

使用 SQLite 作为本地数据库,设计了高效的表结构以支持高频写入与去重。

数据存储模块 中,我们使用了 INSERT OR REPLACE 语法来确保数据的幂等性(即重复抓取同一篇笔记时,自动更新而非报错)。

python
CREATE TABLE IF NOT EXISTS notes (
    note_id TEXT PRIMARY KEY,
    title TEXT,
    user_id TEXT,
    likes INTEGER,
    comments INTEGER,
    collected INTEGER,
    share INTEGER,
    hot_score REAL,  -- 自定义热度评分
    crawl_time INTEGER
);

-- 针对查询频繁的字段建立索引
CREATE INDEX IF NOT EXISTS idx_notes_time ON notes(crawl_time);
CREATE INDEX IF NOT EXISTS idx_notes_score ON notes(hot_score DESC);

4. 反爬与稳定性策略

为了保证工具的长期稳定运行,我们在 爬虫主逻辑 中集成了多重防护机制:

  • 登录态检测:在操作前检查登录元素,若失效则暂停任务并弹窗提示。
  • 人类行为模拟
    • 随机延时:点击与滚动之间插入高斯分布的随机等待时间。
    • 鼠标轨迹:避免机械式的瞬间跳转。
  • 僵尸进程清理:在 初始化阶段,会自动扫描并清理上次异常退出遗留的浏览器进程,防止内存泄漏。

UI 与工程化

为了让非技术人员(如运营团队)也能使用,我们基于 PyQt6 开发了桌面客户端,并实现了任务的线程化管理。同时,通过 PyInstallerInno Setup,我们将复杂的 Python 环境打包为标准的 Windows 安装程序 (.exe),实现了“开箱即用”。

UI 线程管理 模块中,抓取任务运行在独立的 QThread 中,通过信号(Signal)与槽(Slot)机制将日志和进度实时更新到 UI,确保界面不会因阻塞操作而卡死。

python
class CrawlThread(QThread):
    log_signal = pyqtSignal(str)
    progress_signal = pyqtSignal(int)

    def run(self):
        # 实例化爬虫核心
        crawler = XHSCrawler(config=self.config)
        # 注入回调函数,实现实时日志
        crawler.set_logger(self.log_signal.emit)
        crawler.start()

总结

本系统通过 浏览器真实渲染 + API 网络拦截 + HTML 兜底 的三重策略,有效地解决了 SPA 网页数据抓取的稳定性问题。

  • 对于数据:通过 SQLite 本地化存储与标准化清洗,建立了可靠的数据资产。
  • 对于效率:自动化的客源过滤与推送,替代了人工手动搜寻,大幅提升了运营效率。
  • 对于扩展:模块化的设计使得后续接入新的分析规则或推送渠道变得十分简单。

最后更新: