Python + tkinter 实战:小红书蒲公英KOL达人筛选数据整理工具的设计与实现

53 阅读12分钟

“一个独立开发者的工程化实践记录 —— 从筛选逻辑、界面架构,到日志体系与数据落盘。”


一、需求是怎么来的

小红书蒲公英是小红书官方的创作者商业合作平台,品牌方和 MCN 在上面筛选达人、看报价、看数据表现。它通常需要具备相应权限的企业资质账号才能开通。蒲公英页面

问题在于:它的数据都在页面上,但不给你导出。

做一次投放选型,可能要对比几十个达人。页面上你能看到报价、粉丝数、互动中位数、粉丝画像,但它们是散落在详情页里的,一页只能看一个。想做横向对比表,只能手动复制粘贴。

我需要的不是"抓到什么惊天数据",我需要的是一张能排序、能拉透视表、能留档的 CSV。

于是我写了这个工具。


二、先划清边界(这段放最前面)

工具本身决定不了用途,用的人决定。所以我把边界写在最前面,而不是丢在文末:

这个工具做什么

  • 面向已具备蒲公英访问权限的账号,辅助整理自己在平台内可见到的达人合作参考数据
  • 采集的是达人账号层面的汇总统计字段:报价、粉丝数、互动中位数、粉丝画像占比等,这些是平台面向投放方提供的决策参考数据
  • 输出 CSV,供自己在内部做投放选型、报价对比、数据留档

这个工具不做什么,也请不要拿它做

  • 不绕过平台权限、不破解风控、不提供任何规避访问限制的手段
  • 不采集普通用户的个人身份信息 —— 工具字段里没有、也不会有手机号、私信、评论用户列表这类内容
  • 不做高频、恶意或破坏性请求
  • 不把采集结果用于出售、传播他人信息或任何侵犯他人合法权益的行为

后面的技术内容,我也不会涉及任何接口地址、签名算法和风控细节。写这篇文章的目的是交流工程实现,不是提供可复制的采集方案。


三、整体设计

3.1 分层架构

┌──────────────────────────────────────────────┐
│  GUI 层 (tkinter)                             │
│  登录窗口 / 主窗口 / 日志滚动区 / 线程安全弹窗  │
├──────────────────────────────────────────────┤
│  调度层 (threading)                           │
│  主线程只负责界面,采集跑在 daemon 子线程       │
├──────────────────────────────────────────────┤
│  采集层 (requests)                            │
│  按条件请求 → JSON 解析 → 字段映射             │
├──────────────────────────────────────────────┤
│  输出层 (csv / logging)                       │
│  逐条 append CSV + 按天切分日志                │
└──────────────────────────────────────────────┘

软件只有两层用户可见入口:登录窗口和主窗口。登录成功后销毁登录窗口再创建主窗口,这样两套界面状态互不干扰。

3.2 两种采集方式

主界面上有一个下拉框控制采集方式,走两个完全不同的输入路径:

采集方式输入适用场景
按关键词采集笔记关键词 + 筛选条件 + 页码范围从零开始找达人,按内容领域筛选
按小红书号采集读取 博主列表.xlsx 中的「小红书号或昵称」列手上已有达人名单,批量补充详细数据

按小红书号采集 这个模式,本质是"给我一份名单,我把每个人的底细补齐"。实际用起来,这个是高频场景 —— 收到一份合作意向名单,第一件事就是先跑一遍看看报价和真实数据表现。


四、界面上那几个筛选条件怎么用

软件界面上的筛选项:筛选项

筛选项说明:

输入项说明
搜索关键词笔记层面的关键词,留空则不按关键词过滤
笔记类型不限 / 图文笔记为主 / 视频笔记为主
粉丝数量上下限区间,留两个 0 表示不限制
图文报价合作报价区间,留两个 0 表示不限制
搜索页范围从第几页到第几页,每页 20 个博主
采集间隔在 config_pub.json 里改 wait_sec,默认 2 秒

「粉丝数量 + 图文报价」这个组合是最有用的:筛掉粉丝量和报价不匹配的号。比如你想要 1w~10w 粉、报价 500 以内的腰部达人群,两个区间一叠加,范围立刻收窄。


五、输出的 34 个核心字段,拆开看

字段多不等于有用。我按业务含义分了四组,顺便说说哪几个是真能拿来决策的。

5.1 KOL 基础信息

字段说明
小红书昵称 / 小红书号昵称可能重复,小红书号才是唯一键,适合去重
地址达人所在地区
机构所属 MCN;无机构时标记「无机构」
账号类型一级内容标签,如「兴趣爱好」「美妆护肤」
获赞与收藏账号累计互动量级
账号评估平台给出的账号质量分级(异常 / 普通 / 优秀)
小红书链接 / 蒲公英链接回查用的直达入口

5.2 粉丝画像

字段说明
粉丝数
女性粉丝占比画像聚合值
年龄占比最多的例如「25-34居多, 占比37.6%」

⚠️ 这里要特别说明:这些是平台对达人粉丝群体给出的聚合统计值(百分比、分布),不是任何具体用户的个人信息。工具不采集、也无法采集到个体用户数据。这个区别很重要,别混为一谈。

5.3 报价与效率指标

字段说明
图文报价 / 视频报价合作参考价
预估阅读单价_图文报价 ÷ 预估阅读量,性价比的核心指标
预估 CPM(图文 / 视频)千次曝光成本预估
合作笔记数该达人已产生的商业合作笔记量级

报价不能直接横向比。 一个报价 3000 的号和一个报价 800 的号,谁更划算?看预估阅读单价。这一列是我做初筛时第一个排序的字段。

5.4 内容表现(两个维度就要分开看)

日常维度:

  • 图文3秒阅读
  • 日常_阅读中位数
  • 日常_互动中位数
  • 日常_阅读来源发现页占比
  • 日常_阅读来源搜索页占比

合作维度:

  • 合作_阅读中位数
  • 合作_互动中位数
  • 合作_阅读来源发现页占比
  • 合作_阅读来源搜索页占比

这里有两个非常实用的判断方法:

① 日常阅读中位数 vs 合作阅读中位数

如果两者接近,说明这个达人接了商单之后表现稳定,不会「一恰饭就掉量」。 如果合作数据明显低于日常,就要谨慎 —— 要么是商单内容质量下滑,要么是粉丝对广告接受度低。

这个差值,光看主页是看不出来的,必须两个数据放一起才成立。

② 搜索页占比 vs 发现页占比

  • 搜索页占比高 → 流量长尾属性强。笔记发出去之后还能持续被搜到,适合做种草沉淀、做品牌关键词占位。
  • 发现页占比高 → 推荐流爆量属性强。适合做短期曝光爆发、做活动造势。

同一个预算,投给两种达人的效果是不一样的。这个维度很多人筛达人的时候完全没看。

5.5 合作笔记阅读数(8 列)

合作笔记1阅读数 ~ 合作笔记8阅读数,按发布时间倒序取最近 8 篇。

中位数只能告诉你"平均水平",这 8 个数能告诉你稳定性。8 篇里如果有一两篇断崖式下跌,说明这个号流量波动大,单次投放的不确定性就高。

数据是拿来这样用的 —— 不是看一个数,是看一组数的分布。


六、几个实现上的坑和解法

6.1 tkinter 不是线程安全的

采集必须跑在子线程(否则界面直接卡死),但 tkinter 的控件和 messagebox 只能在主线程操作。子线程里直接调,Windows 上大概率报 RuntimeError: main thread is not in main loop 或者干脆假死。

解法是封装一层线程安全弹窗:

def _safe_showinfo(self, title, message):
    """线程安全弹窗:子线程通过 after 切回主线程执行。"""
    try:
        if threading.current_thread() is threading.main_thread():
            self.txt_msglist.bell()
            messagebox.showinfo(title, message)
        else:
            self.txt_msglist.after(
                0,
                lambda: (self.txt_msglist.bell(), messagebox.showinfo(title, message))
            )
    except Exception as e:
        self.logger.error(f'[_safe_showinfo] {e}')

关键在 widget.after(0, fn) —— 它把调用排进主线程的事件队列,是 tkinter 跨线程通信的标准姿势。同理还封装了 _safe_showerror 和 _safe_showwarning。

6.2 采集任务用 daemon 线程

class MyThread(threading.Thread):
    def __init__(self, func, *args):
        super().__init__()
        self.func = func
        self.args = args
        self.setDaemon(True)
        self.start()          # 构造即启动

    def run(self):
        self.func(*self.args)

setDaemon(True) 保证用户直接关掉主窗口时,采集线程不会挂着不放、导致进程退不干净。

6.3 日志:按天切分 + 保留 7 天

class Log_week():
    def get_logger(self):
        self.logger = logging.getLogger(__name__)
        formatter = '[%(asctime)s-%(filename)s][%(funcName)s-%(lineno)d]--%(message)s'
        self.logger.setLevel(logging.DEBUG)

        sh = logging.StreamHandler()
        log_formatter = logging.Formatter(formatter, datefmt='%Y-%m-%d %H:%M:%S')

        info_file_name = time.strftime("%Y-%m-%d") + '.log'
        case_dir = r'./logs/'
        info_handler = TimedRotatingFileHandler(
            filename=case_dir + info_file_name,
            when='MIDNIGHT',
            interval=1,
            backupCount=7,
            encoding='utf-8',
        )
        self.logger.addHandler(sh)
        sh.setFormatter(log_formatter)
        self.logger.addHandler(info_handler)
        info_handler.setFormatter(log_formatter)
        return self.logger

三个细节值得抄:

  1. 日志格式里带上 funcName 和 lineno。出问题直接定位到函数和行号,比到处 print 强太多。
  2. when='MIDNIGHT' + backupCount=7,每天零点切分,只留最近 7 天,不会把用户磁盘撑爆。
  3. 桌面软件一定要同时挂 StreamHandler(控制台)和文件 Handler。开发时看控制台,用户侧看文件。

6.4 数据落盘:逐条追加 + utf_8_sig

两个必须做对的细节:

① 不要等全部跑完才写文件。 每采到一条就 append 一次。中途断网、手动关掉、程序崩了,已经采到的数据都还在文件里。

② 编码一定用 utf_8_sig。

def init_csv(self, csv_header):
    """初始化csv文件"""
    with open(self.result_file, 'a+', encoding='utf_8_sig', newline='') as f:
        writer = csv.writer(f)
        writer.writerow(csv_header)

# 每采完一条就落盘
with open(self.result_file, 'a+', encoding='utf_8_sig', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(data_row)

utf_8_sig 是带 BOM 的 UTF-8。用普通 utf-8 写出来的 CSV,用户双击用 Excel 打开,Windows 版 Excel 会默认按 GBK 解读,满屏乱码 —— 用户会以为你的软件坏了。加个 BOM,Excel 就认了。这个坑我用一次乱码的代价学会了,之后所有 CSV 输出都固定 utf_8_sig。

6.5 请求节奏做成外部可配置

节奏不写死在代码里,放到 config_pub.json:

{
    "wait_sec": 2
}

每次请求前:

time.sleep(random.uniform(self.wait_sec, self.wait_sec + 1))

用随机区间而不是固定值,一是节奏更自然,二是能减少对目标服务的瞬时压力。默认 2 秒,用户按自己的任务规模往上调。

我对这件事的态度很明确:采集工具的第一原则是不给对方服务添麻烦。 频率克制一点,自己这边稳定性反而更好,被中途掐断的概率也更低。

读取配置时也做了校验,wait_sec < 1 直接拒绝并提示重新配置,避免用户手滑填 0 把间隔关掉。

6.6 配置文件的轻度隐藏

打包成 exe 之后,配置文件如果放在根目录,用户一眼就看到、也容易误删。一个小技巧是把它塞进一个看起来像第三方依赖的深层路径:

cfg_path = './node_modules/jsdom/lib/jsdom/living/xhr/'

配合打包时把 node_modules 一起复制过去,普通用户既不会误删、也不容易发现。这不能当安全手段用,只是降低误操作概率。真要防篡改,还得靠服务端校验。


七、用起来长什么样

  1. 打开软件,用账号密码登录

  2. 用内置的《cookie 小工具》完成 cookie 配置(自动写入 cookie.txt,不用手动复制粘贴)ck小工具

  3. 选择采集方式:按关键词 / 按小红书号

  4. 填写筛选条件

  5. 点「开始执行」,界面上的日志区会实时滚动进度

  6. 采集完成后,在软件目录拿 CSV,在 logs 目录看日志

软件主界面(目前已升级到v2.5版):软件界面


八、已知不足与后续计划

写技术文章如果不聊缺点,那就成软文了。列几个我自己清楚的问题:

问题说明计划
不做去重同一达人在多个关键词下会重复写入,需要自己在 Excel 里去重加一个「小红书号」维度的去重开关
单账号串行所有请求在同一个 cookie 下串行,速度受限于间隔设置多账号轮询不做 —— 会显著拉高账号风险,收益不划算
MacOS首次运行受 Gatekeeper 限制需要手动放行写更清楚的图文引导

九、更新日志

版本日期更新内容
v2.0—公开版本发布
v2.1—新增按昵称搜索
v2.2—新增字段:获赞与收藏
v2.32026-03-24新增 macOS 客户端
v2.4—新增字段:预估 CPM、蒲公英详情页链接
v2.52026-05-20新增《cookie 小工具》自动配置、成功/告警弹窗提醒、自定义采集间隔;优化设备绑定机制

十、参考资料

  • 项目仓库:github.com/mashukui/pgy_spider
  • ·
  • 演示视频:bilibili.com/video/BV1jJ4m1u7aj/
  • ·
  • 原创作者:马哥python说 —— 10年程序猿,专注Python开发

十一、合规声明

本文为个人项目的技术实践记录,仅用于技术交流与学习研究。文中不涉及任何接口地址、签名算法与风控细节,完整采集逻辑未公开。

工具仅供合法合规的数据分析、学习研究和自有业务场景使用。使用者应自行遵守目标平台的服务协议、隐私政策及所在地法律法规,不得用于高频恶意请求、未经授权采集或传播他人敏感信息、绕过平台权限与访问限制、以及任何侵犯他人合法权益的行为。

因使用者不当使用造成的风险与责任,由使用者自行承担。


@马哥python说 All rights reserved.