引言
YouTube 作为全球最大的视频平台,其评论区蕴藏着大量用户反馈和舆情数据。然而 YouTube 官方 API 有严格的配额限制,每天仅提供 10,000 单位的配额,对于大规模数据采集需求来说远远不够。
本文将详细介绍如何利用 Python 构建一个完整的、可直接交付用户的 YouTube 评论采集工具,涵盖 GUI 界面、数据流水线、软件保护与授权管理等关键技术点。
技术架构总览
┌─────────────────────────────────────────────────┐
│ TKinter GUI │
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ 登录窗口 │ │ 参数配置 │ │ 实时日志输出 │ │
│ └──────────┘ └──────────┘ └───────────────┘ │
├─────────────────────────────────────────────────┤
│ 业务逻辑层 │
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ 用户认证 │ │ 评论爬取 │ │ 数据转换导出 │ │
│ └──────────┘ └──────────┘ └───────────────┘ │
├─────────────────────────────────────────────────┤
│ 数据存储层 │
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ MySQL │ │ JSON │ │ CSV / XLSX │ │
│ │ (远程认证) │ │ (临时存储) │ │ (最终输出) │ │
│ └──────────┘ └──────────┘ └───────────────┘ │
└─────────────────────────────────────────────────┘
软件界面概览

一、GUI 层:TKinter 双窗口设计
整个应用采用双窗口模式 :登录窗口 → 主工作窗口,两者通过 root_login.destroy() 和 create_spider_root() 实现平滑切换。
1.1 登录窗口
相关代码:
python
def create_login_root():
root_login = tk.Tk()
root_login.title('爬油管评论软件v3.3')
root_login.minsize(width=400, height=300)
# 记住上次登录的用户信息
if os.path.exists(cfg_path + 'userinfo.txt'):
with open(cfg_path + 'userinfo.txt', 'r') as f:
userinfos = f.readlines()
entry_username.insert(0, str(userinfos[0]).strip())
entry_password.insert(0, str(userinfos[1]).strip())
这里有几个设计细节值得注意:
- 凭据持久化 :将用户名和密码明文写入本地
userinfo.txt,下次启动时自动填充,减少用户重复输入。虽然从安全角度看明文存储不够严谨,但对于面向非技术用户的桌面工具,这是一个务实的取舍。 - 配置文件隐藏 :配置路径选择在
node_modules/jsdom/lib/jsdom/living/xhr/下,利用 node_modules 目录"不会被普通用户轻易翻看"的特点来做简单隐藏。
1.2 主工作窗口
主窗口使用 ttk.LabelFrame 进行分区布局,核心控件包括:
| 控件 | 类型 | 用途 |
|---|---|---|
comment_num |
tk.Spinbox |
爬取数量,-1 表示全量 |
order_type |
ttk.Combobox |
排序方式(按日期/按热门) |
txt_msglist |
tk.Text + Scrollbar |
实时滚动日志 |
1.3 多线程防止 UI 冻结
爬取任务是 IO 密集型操作,如果在主线程中执行会导致界面卡死。这里自定义了 MyThread 类:
python
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) 确保主窗口关闭时爬取线程不会成为僵尸进程。线程间的 UI 更新通过共享的 txt_msglist Text 控件完成,使用 delete + insert 的方式刷新日志内容。
二、爬虫核心:第三方库的使用与优化
2.1 基础调用
python
from youtube_comment_downloader import YoutubeCommentDownloader, SORT_BY_POPULAR, SORT_BY_RECENT
downloader = YoutubeCommentDownloader()
comments = downloader.get_comments_from_url(
'https://www.youtube.com/watch?v={}'.format(video_id),
sort_by=SORT_BY_RECENT # 或 SORT_BY_POPULAR
)
youtube_comment_downloader 是一个轻量级的第三方库,它模拟浏览器请求直接从 YouTube 页面抓取评论,不需要 YouTube Data API key,也因此不受API配额限制。这是选择它而非官方API的核心原因。
2.2 数量控制与惰性求值
python
from itertools import islice
if self.comment_num != '-1':
comment_num2 = int(self.comment_num)
comments = islice(comments, comment_num2) # 取出 TOP N
这里使用了 itertools.islice 而非直接切片。因为 get_comments_from_url 返回的是生成器 ,数据按需加载。islice 在达到指定数量后直接停止迭代,不会浪费流量拉取多余的评论,这对于网络请求来说至关重要------每一条额外的评论都是一次 HTTP 往返。
2.3 逐条落盘策略
python
for comment in comments:
with open('./jsons/{}.json'.format(video_id), 'a+', encoding='utf-8') as f:
f.write(json.dumps(comment, ensure_ascii=False))
f.write('\n')
关键设计决策:边爬边写 。如果将评论全部收集到内存中再一次写入,一旦程序中途崩溃,之前爬取的数据将全部丢失。逐条追加写入的方式实现了断点续传的效果------即使程序异常退出,已写入的 JSON 行也不会丢失。
这里每条评论写成一行 JSON(JSONL 格式),便于后续用 pd.read_json(lines=True) 快速读取。
三、数据流水线:JSON → CSV → XLSX
3.1 JSONL 到 DataFrame
python
df = pd.read_json('./jsons/{}.json'.format(video_id), lines=True)
df.drop(['photo', 'heart', 'reply'], axis=1, inplace=True)
if 'paid' in df.columns:
df.drop(['paid'], axis=1, inplace=True)
原始评论数据中包含了 photo(头像)、heart(点赞)、reply(回复内容)等字段,这些对大多数分析场景无用,而且 reply 是嵌套结构,直接导出到二维表格会引发格式问题,因此提前剔除。
特别注意 paid 字段的处理:它只在某些特定评论中出现(如付费置顶评论),如果直接 drop 可能导致 KeyError,所以使用 if 'paid' in df.columns 先做判断。
3.2 时间戳转换的坑
python
def trans_time(self, v_timestamp):
"""10位时间戳转换为时间字符串"""
v_timestamp = int(str(v_timestamp)[:10])
timeArray = time.localtime(v_timestamp)
otherStyleTime = time.strftime("%Y-%m-%d %H:%M:%S", timeArray)
return otherStyleTime
df['time2'] = df['time_parsed'].apply(
lambda x: self.trans_time(x) if pd.notna(x) else x
)
这里有两个细节值得展开:
- 为什么取前10位? YouTube 返回的时间戳可能是毫秒级(13位)也可能是秒级(10位),
[:10]统一截断为 10 位秒级时间戳。 - 为什么要判断
pd.notna(x)? 某些评论的time_parsed字段可能为NaN,此时int(str(x)[:10])会抛出ValueError: invalid literal for int() with base 10: 'nan'。v3.3 版本正是修复了这个 bug。
3.3 追加模式写入 CSV
python
if os.path.exists(self.result_file1):
header = False
else:
header = True
df.to_csv(self.result_file1, index=False, mode='a+', encoding='utf_8_sig', header=header)
多个视频的评论写入同一个 CSV 文件时,只在第一次写入时写表头 ,后续写入仅追加数据行。utf_8_sig 编码确保 Excel 打开时中文不乱码。
3.4 CSV → XLSX 最终输出
python
df2 = pd.read_csv(self.result_file1)
df2['replies'].fillna(0, inplace=True)
df2.to_excel(self.result_file2, index=False)
将 replies(回复数)字段的空值填充为 0,使数据在 Excel 中展示更整洁。
四、软件保护与授权
授权逻辑采用三层验证:
- 用户凭证验证:用户名 + 密码匹配
- 有效期验证 :当前时间在
end_time之前 - 机器码绑定:首次登录时绑定 CPU 序列号,后续登录验证机器码一致性,防止账号共享
获取 CPU 序列号的方式跨平台兼容:
- Windows:通过
wmi库读取Win32_Processor.ProcessorId - macOS:通过
system_profiler SPHardwareDataType命令提取 Serial Number
五、日志系统:按天滚动
python
info_handler = TimedRotatingFileHandler(
filename=case_dir + info_file_name,
when='MIDNIGHT',
interval=1,
backupCount=7,
encoding='utf-8'
)
使用 TimedRotatingFileHandler 而非 RotatingFileHandler,以时间为维度而非文件大小来切割日志。配置为每天午夜滚动一次,保留最近 7 天的日志。这样做的好处是出问题时可以快速定位到当天的日志文件,不会因为文件过大而难以排查。
总结
本文完整介绍了YouTube评论采集工具的工程实现,涵盖了从 TKinter GUI 到数据流水线再到软件保护的各个环节。核心的技术选型思路是用轻量级爬虫库替代官方 API,从而绕开配额限制实现大规模采集。代码虽然体量不大(不到 500 行),但包含了桌面应用开发中常见的关键模式:多线程调度、流式数据处理、跨平台兼容、以及商业软件的授权管理,希望对有类似需求的开发者有所启发。
更多交流或疑问,可前往 GitHub仓库Issue区 进行讨论。
作者声明:本文涉及的完整代码和数据仅供技术学习交流,请遵守目标网站的使用条款和相关法律法规。