Chat Analyzer:本地化聊天记录分析工具实战
1. 项目概述从杂乱聊天记录到可行动洞察的完整闭环你有没有盯着手机里那个动辄上万条消息的群聊发过呆凌晨三点发的“收到”周末刷屏的“哈哈哈”还有那些被反复转发、最后连自己都忘了源头的段子和链接——这些碎片化的文字、表情、时间戳每天都在我们指尖流淌却像沙子一样从指缝漏走留不下任何痕迹。直到某天我翻出三年前一个技术群的 WhatsApp 导出文件里面密密麻麻全是“这个 bug 怎么复现”“求个 demo”“已修复上线了”但当我真正想回溯那次关键的技术决策过程时却发现除了模糊的记忆什么也抓不住。这让我意识到我们不是缺乏数据而是缺乏把数据变成信息、再把信息变成判断力的工具。Chat Analyzer 就是在这种“数据过剩、洞察匮乏”的切肤之痛中诞生的。它不是一个花哨的 SaaS 产品也不是一个需要注册登录的云端服务而是一个完全本地化、一次上传、全程离线、会话结束即销毁的轻量级分析工具。它的核心价值非常朴素把你在 WhatsApp、Telegram 这类平台导出的原始文本或 JSON 文件用几秒钟时间变成一张张能直接读出故事的图表、一份份能快速定位关键对话的摘要、甚至是一张标记着所有成员分享过地理位置的地图。它不追求大模型的炫技而是死磕“真实场景下的可用性”——比如当老板问“上个月大家对新 API 文档的反馈集中在哪些点”你不用再手动翻两百页聊天记录而是打开 Analyzer输入“API 文档”“404”“报错”三秒后就能看到带时间轴的趋势图和高频词云当你要准备一场线上分享想快速了解听众最常讨论的技术栈它能立刻告诉你谁是“React 发言王”谁是“Python 问题终结者”以及他们各自最爱用的三个表情符号背后的情绪倾向。这个项目最打动我的地方在于它彻底放弃了“技术正确性”的执念转而拥抱“用户直觉”。它不强制你理解 TF-IDF 的数学推导但会让你一眼看出“为什么张三的‘’和李四的‘’在语义上根本不是一回事”它不堆砌 LLM 的参数配置但能让你用自然语言提问“帮我总结上周五下午关于数据库迁移的全部讨论”然后给出一段精准得不像 AI 写的摘要。它解决的不是“能不能做”的问题而是“愿不愿意用”的问题。所以如果你正被海量的私域沟通数据淹没又苦于找不到一个既专业又不折腾的入口那么接下来的内容就是我踩过所有坑、试过所有框架、最终沉淀下来的、可直接复刻的完整实践手册。2. 整体架构与设计思路为什么是 Streamlit而不是 Flask 或 Dash2.1 框架选型一场关于“开发效率”与“交付体验”的权衡在动手写第一行代码之前我花了整整两周时间在 Flask、Dash 和 Streamlit 之间反复横跳。当时的想法很天真既然要做一个数据应用那肯定得用最“正统”的 Web 框架。于是 Flask 成了第一个试验品。我搭好了基础路由写好了文件上传接口也用 Plotly 渲染出了第一张折线图。但问题很快来了为了让前端能实时响应用户选择的“按月/按周/按日”粒度我不得不写一堆 JavaScript 去监听下拉框变化再通过 AJAX 调用后端 API最后用 DOM 操作去替换图表容器。整个过程就像在乐高积木上硬焊电路板——功能是实现了但每加一个交互代码复杂度就指数级上升。更致命的是当我把原型给一位非技术背景的产品同事看时她盯着那个需要手动刷新才能更新的页面只说了一句“这玩意儿我敢把它发给老板吗”Dash 的尝试则走向了另一个极端。它的声明式语法确实优雅dcc.Graph(figurefig)一行代码就能出图组件联动也比 Flask 简洁得多。但代价是巨大的学习成本和调试黑洞。Dash 的回调Callback机制要求你必须严格遵循“输入-输出-状态”的依赖链一旦某个组件的状态没有被正确声明整个页面就会静默崩溃而错误日志往往只显示“Callback failed”具体哪一行、哪个变量出问题全靠猜。我曾为一个简单的日期范围筛选器卡了三天最后发现只是因为date_picker_range组件的start_date和end_date默认值类型不一致一个传的是字符串一个传的是 datetime 对象。这种“优雅”背后的脆弱性对于一个目标用户是普通人的工具来说是不可接受的。直到我遇见 Streamlit才真正理解了什么叫“为数据科学家而生”。它的哲学极其简单粗暴把你的 Python 脚本当成一个网页每一行st.write()、st.plotly_chart()都是页面上的一个元素而st.button()、st.selectbox()这些小部件本质上就是给脚本加了“交互式输入”。它没有路由没有回调没有前后端分离的概念。你写的不是“服务器代码”而是一份“会动的 Jupyter Notebook”。当我第一次用st.file_uploader()上传一个 WhatsApp 文件然后紧接着用st.dataframe(df.head())把解析后的 DataFrame 直接打出来时那种“所见即所得”的流畅感是 Flask 和 Dash 永远无法提供的。它把 80% 的工程精力从“如何让页面动起来”转移到了“如何让数据说话”上。这正是 Chat Analyzer 这类工具最需要的底层支撑——一个能让开发者心无旁骛地打磨分析逻辑而不是和框架斗智斗勇的环境。2.2 架构分层数据流如何从“一坨乱码”变成“一张地图”Chat Analyzer 的内部结构可以清晰地划分为三层接入层、处理层、呈现层。这个分层不是为了炫技而是为了应对现实世界中最棘手的问题数据源的千奇百怪。接入层The Inlet这是整个系统的“嘴”。它只做一件事无差别地吞下所有你能塞进来的东西。WhatsApp 导出的是.txtTelegram 是.html或.jsonSignal 可能是加密的 SQLite 数据库。Streamlit 的st.file_uploader在这里扮演了“万能插头”的角色。它不关心你传的是什么格式只负责把二进制流交到下一层。这个设计的关键在于“延迟解析”——文件上传后系统不会立刻尝试解码而是先存入一个临时内存缓冲区等待用户在界面上明确选择“这是 WhatsApp 数据”还是“这是 Telegram 数据”然后再调用对应的解析器。这避免了因格式误判导致的整个应用崩溃也给了用户最大的容错空间。处理层The Engine这是整个系统的“心脏”也是我投入最多心血的地方。它的核心任务是执行“数据归一化”Normalization。想象一下WhatsApp 的导出文件里一条消息长这样[12/03/2023, 14:22:15] Alon Cohen: Hey, did you see the new PR?而 Telegram 的 JSON 里同一条消息可能是{ id: 12345, type: message, date: 2023-03-12T14:22:15, from: Alon Cohen, text: Hey, did you see the new PR? }两者的信息维度时间、人、内容是相同的但组织方式天差地别。归一化引擎要做的就是把所有这些“方言”翻译成统一的“普通话”——一个标准的 Pandas DataFrame其列名固定为[timestamp, username, message, is_media]。这个过程绝不是简单的字段映射。比如WhatsApp 的时间格式[DD/MM/YYYY, HH:MM:SS]需要pd.to_datetime()的特定format参数而 Telegram 的 ISO 标准时间则可以直接解析。更麻烦的是WhatsApp 的文本里充满了Media omitted这样的占位符它们不是真正的文字但会影响后续的词频统计。因此归一化函数里必须包含一个is_media布尔列用于在后续所有分析中自动过滤掉这些“幽灵消息”。这个看似简单的 DataFrame是所有高级分析得以展开的唯一基石。没有它后面的“用户活跃度”、“表情关联度”、“对话聚类”全都是空中楼阁。呈现层The Face这是整个系统的“脸”也是用户唯一能直接感知的部分。Streamlit 的魔力在这里体现得淋漓尽致。它把传统的“HTML/CSS/JS”三件套压缩成了st.sidebar、st.tabs、st.columns这几个语义化极强的容器。st.sidebar不仅是侧边栏更是整个应用的“控制中枢”——所有全局过滤器如时间范围、用户列表都放在这里一旦改变下方所有图表和表格都会自动重绘。st.tabs则完美对应了文章目录里的 A/B/C/D 四个章节让用户可以在不同分析视角间无缝切换而无需刷新页面。最精妙的是st.columns它让我能实现“所见即所得”的探索式分析。比如在“聊天探索器”里左边是st.dataframe(filtered_df)显示匹配的原始消息右边是st.plotly_chart(generate_activity_overtime(...))显示这些消息的时间趋势。用户输入一个关键词两边内容同步更新这种即时反馈带来的掌控感是任何静态报告都无法比拟的。这个三层架构保证了 Chat Analyzer 的可扩展性。未来如果要支持微信我只需要在接入层增加一个parse_wechat()函数在处理层将其输出喂给同一个归一化引擎呈现层完全不需要改动。它也保证了可维护性。当某个分析模块比如地理定位出了 Bug我只需要聚焦在Section D: Geographics对应的代码块里调试而不用担心牵一发而动全身。这是一种工程师的克制也是一种对用户时间的尊重。3. 核心模块深度解析从代码到洞见的每一个细节3.1 数据归一化让千差万别的聊天记录说出同一种语言数据归一化是 Chat Analyzer 的“地基工程”其质量直接决定了上层所有分析的可信度。很多人以为这只是个简单的“格式转换”实则不然。它是一场与时间格式、编码混乱、文本噪声的持久战。下面我将拆解 WhatsApp 归一化函数的每一行代码告诉你为什么这么写以及不这么写会掉进哪些坑。def normalize_whatsapp_data(chat_export): # write data locally tempdir tempfile.mkdtemp(prefixchat_exports) with open(os.path.join(tempdir, chat_export.txt), modewb) as f: f.write(chat_export.read())这段代码的第一步是把用户上传的二进制流写入一个临时文件。你可能会问为什么不直接用StringIO在内存里操作答案是WhatsAppParser 库只认物理文件路径。这是一个典型的“库限制倒逼架构设计”的案例。tempfile.mkdtemp()创建的临时目录其生命周期与当前 Streamlit 会话绑定会话结束后自动清理确保了数据的绝对安全。prefixchat_exports则是为了方便调试时在系统临时目录里快速定位文件。# parse whatsapp txt file parser WhatsAppParser(os.path.join(tempdir, chat_export.txt)) parser.parse_file()这里调用了chatminer库的WhatsAppParser。选择这个库而非自己手写正则是经过深思熟虑的。WhatsApp 的导出格式虽然看起来简单但存在大量边缘情况多行消息换行符、带方括号的特殊字符、不同地区的日期格式美式 MM/DD/YYYY vs 英式 DD/MM/YYYY。chatminer是一个经过大量真实数据验证的成熟库它内置了针对这些情况的健壮处理逻辑。自己写一个“够用”的解析器可能只要几十行代码但要写一个“永远不出错”的解析器可能需要几百行且维护成本极高。在工具类产品开发中“站在巨人肩膀上”不是偷懒而是对用户负责。# add metadata df parser.parsed_messages.get_df(as_pandasTrue).rename(columns{author: username}) df[date] df[timestamp].dt.date df[hour] df[timestamp].dt.floor(h).dt.strftime(%H:%M) df[week] df[timestamp].dt.to_period(W).dt.start_time df[month] df[timestamp].to_numpy().astype(datetime64[M]) df[day_name] df[timestamp].dt.day_name() df[is_media] df[message].str.contains(Media omitted) df[text_length] df[message].str.split().map(len)这才是归一化的精髓所在——元数据的智能衍生。df[date]和df[hour]是为了后续按天/按小时聚合做准备df[week]和df[month]使用了 Pandas 的to_period它能自动处理跨年、跨月的边界问题比手动strftime(%Y-%m)更可靠df[day_name]直接给出 “Monday”、“Tuesday”省去了后续用字典映射的麻烦df[is_media]是一个布尔开关它让“用户发言次数”和“用户发送消息数”这两个指标有了本质区别——前者反映参与度后者反映信息量df[text_length]计算的是单词数str.split().map(len)而非字符数因为一个长 URL 或一串乱码字符数可能上千但实际信息量为零。这个细节直接决定了“谁是话痨”这个结论的准确性。提示df[timestamp].to_numpy().astype(datetime64[M])这行代码是我在实践中踩过的一个大坑。最初我用df[timestamp].dt.to_period(M)结果发现当数据跨越 2023 年 12 月和 2024 年 1 月时to_period(M)会生成2023-12和2024-01两个 Period 对象而它们在groupby时无法直接比较大小。改用datetime64[M]后它们变成了标准的 numpy 时间戳排序和聚合毫无压力。这再次印证了一个真理在数据科学里类型即契约。3.2 用户级分析为什么“最常用表情”和“最相关表情”是两回事在群聊分析中表情符号Emoji是最具表现力的非文本元素。但如何解读它们却是个大学问。很多初学者会直接统计每个用户的 Emoji 频次然后取 Top 3。这就像只看一个人说了多少句话却不管他说的是“你好”还是“救命”。Chat Analyzer 提供了两种截然不同的分析范式它们分别回答了两个根本性问题。3.2.1 最常用表情Most Frequent Emoji量化“行为习惯”这个分析的目标非常直接谁最依赖哪个表情来表达情绪它的实现逻辑堪称教科书级别的简洁df[emojis_list] df[message].map(emoji.distinct_emoji_list) emoji_df df[df[emojis_list].apply(len) 0].groupby(username, as_indexFalse).agg({emojis_list: sum}) bow_df pd.DataFrame(X.toarray(), columnsemojies, indexemoji_df[username]) top_freq_emoji pd.DataFrame(bow_df.idxmax(axis1)).reset_index().rename(columns{0: Most Frequent Emoji})核心在于emoji.distinct_emoji_list()这个函数。它能精准识别出一条消息里的所有独立 Emoji比如Hello ! How are you? 会被解析为[, , ]而不会把和!错误地连在一起。groupby(username).agg({emojis_list: sum})则是神来之笔——它把每个用户的所有 Emoji 列表“拼接”成一个超长列表为后续的词频统计铺平了道路。bow_df.idxmax(axis1)直接给出了每行即每个用户中数值最大的列名也就是该用户使用最多的 Emoji。这个方法的价值在于它揭示了个体的行为惯性。比如数据显示用户“Alice”最常用的表情是而用户“Bob”最常用的是。这背后可能意味着Alice 是一个习惯性给予快速确认和鼓励的人她的沟通风格偏向高效、积极而 Bob 则更倾向于用幽默化解尴尬他的存在本身就在调节群聊的氛围。这是一种基于行为模式的、温和的“人格画像”。3.2.2 最相关表情Most Associated Emoji挖掘“语义独特性”如果说“最常用”是描述“频率”那么“最相关”就是在定义“身份”。它的核心思想是一个真正能代表某个人的 Emoji不应该是在全群都泛滥的而应该是在他/她身上出现得特别多但在别人身上却很少见的。这正是 TF-IDFTerm Frequency-Inverse Document Frequency思想的完美迁移。在传统 NLP 中TF-IDF 用于衡量一个词在某篇文档中的重要性TF词频越高说明这个词在本文中越重要IDF逆文档频率越高说明这个词在整个语料库中越稀有因此越能区分这篇文档。我们将这个思想迁移到用户-Emoji 关系上TFUser-Term Frequency某个 Emoji 在某个用户的所有消息中出现的次数。IDFInverse Group Frequency这个 Emoji 在整个群聊中出现的总次数的倒数。一个 Emoji 如果被 90% 的人都在用它的 IDF 就极低意味着它缺乏区分度。ClassTfidfTransformer正是为此而生。它不再把整个群聊看作一个“文档”而是把每个用户看作一个独立的“类别”Class然后计算每个 Emoji 在该类别中的“类内 TF-IDF 权重”。权重最高的 Emoji就是该用户最具“语义指纹”意义的标志。ctfidf ClassTfidfTransformer(reduce_frequent_wordsTrue).fit_transform(X).toarray() words_per_class_min {user_name: [emojies[index] for index in ctfidf[label].argsort()[-1:]] for label, user_name in zip(emoji_df.username.index, emoji_df.username)}reduce_frequent_wordsTrue这个参数至关重要。它会自动降低那些在全群范围内高频出现的 Emoji如,,❤️的权重从而把舞台让给那些“小众但精准”的符号。例如数据分析显示用户 “Charlie” 的“最相关表情”是柱状图而用户 “Diana” 的是放大镜。这几乎可以断定Charlie 是群里负责数据汇报和可视化的人而 Diana 则是那个总在深挖问题根源、追问细节的“侦探”。这种洞察是单纯的频次统计永远无法企及的。注意ClassTfidfTransformer来自bertopic库它并非为 Emoji 分析而生但其数学原理的普适性让它成为了这个场景下的“意外之喜”。这提醒我们在数据科学中不要被问题的表象所束缚要敢于把不同领域的工具嫁接到新的土壤上。3.3 文本分析从“大海捞针”到“精准定位”的对话聚类聊天记录的文本分析最大的挑战不是技术而是定义什么是“一段有意义的对话”。在现实中群聊的节奏是跳跃的前一秒还在激烈讨论“数据库索引优化”下一秒就有人发了个“猫猫表情包”再下一秒又切回了“CI/CD 流水线配置”。如果强行把所有消息按时间顺序排成一长串任何 NLP 模型都会迷失方向。Chat Analyzer 采用了一种基于时间间隔Time Gap的朴素但极其有效的聚类策略。其核心假设是如果两条消息之间的时间间隔超过了某个阈值那么它们大概率不属于同一轮对话。这个阈值不是拍脑袋决定的而是通过数据驱动的方式找到的。df st.session_state[data].sort_values(timestamp) df df.join(df[[timestamp]].shift(-1), lsuffix, rsuffix_prev) df[time_diff_minutes] ((df[timestamp_prev] - df[timestamp]).dt.seconds / 60) sns.ecdfplot(df[time_diff_minutes]) plt.axhline(0.90, colorr)这段代码绘制的是“消息时间间隔”的经验累积分布函数ECDF。它告诉我们90% 的相邻消息其间隔时间都小于 22 分钟。这个数字就是我们的“对话分割阈值”。选择 90% 分位数是一种稳健的工程实践——它足够大能包容绝大多数正常的对话间隙比如大家去吃午饭、开会、睡觉又足够小能有效切开那些明显属于不同话题的长间隔。确定了阈值聚类就变得异常简单threshold 0.9 df[conversation_id] (df[time_diff_minutes] df[time_diff_minutes].quantile(threshold)).astype(int).cumsum() df.loc[(df[time_diff_minutes] df[time_diff_minutes].quantile(threshold)), conversation_id] - 1cumsum()是 Pandas 的“累加计数器”它会为每一个满足time_diff_minutes 22的行生成一个新的、递增的 ID。- 1则是为了让第一个 conversation_id 从 0 开始符合程序员的习惯。最终每一条消息都被打上了conversation_id标签整个聊天记录就被切割成了一个个语义相对完整的“对话单元”。这个聚类结果的可视化是验证其有效性的黄金标准def viz_conversations(df, start_period, end_period): sample_df df.query(ftimestamp {start_period} and timestamp {end_period}) plt.figure(figsize(20,2)) ax plt.gca() ax.plot(sample_df[timestamp], np.ones(len(sample_df)), .b, alpha0.4, markersize10) for conv_id, conv_df in sample_df.groupby(conversation_id): if len(conv_df) 3: start mdates.date2num(conv_df[timestamp].min()) end mdates.date2num(conv_df[timestamp].max()) ax.add_patch(Rectangle([start,0.75], end - start, 0.8, colorb, alpha0.2)) else: ax.plot(conv_df[timestamp], np.ones(len(conv_df)), .r, markersize10) plt.title(Conversations Timeline)这张图里蓝色的矩形块代表被识别出的、长度大于 3 条消息的“有效对话”红色的点则代表那些孤立的、或只有 1-2 条的“噪音”。当你看到一个长达 48 小时的蓝色矩形块里面包含了从“问题提出”、“方案讨论”、“代码审查”到“最终确认”的全过程你就知道这个基于时间的朴素算法已经成功捕获了群聊中最珍贵的“知识结晶”。4. 实操全流程从零开始搭建你的专属 Chat Analyzer4.1 环境准备与依赖安装五分钟搞定所有前置条件搭建 Chat Analyzer 的第一步永远不是写代码而是构建一个干净、隔离、可复现的运行环境。我强烈建议你放弃pip install全局安装的方式因为它会在你的系统 Python 环境里留下难以清理的“痕迹”并可能与其他项目产生冲突。取而代之的是使用venv创建一个专属的虚拟环境。以下是我在 macOS 和 Ubuntu 上亲测有效的完整流程Windows 用户只需将source命令换成venv\Scripts\activate.bat即可。# 1. 创建一个项目文件夹并进入它 mkdir chat-analyzer cd chat-analyzer # 2. 创建虚拟环境Python 3.8 是硬性要求 python3 -m venv venv # 3. 激活虚拟环境这一步至关重要所有后续安装都将在其中进行 source venv/bin/activate # 4. 升级 pip 到最新版避免旧版本的兼容性问题 pip install --upgrade pip # 5. 安装核心依赖注意这里没有安装 transformers 和 torch因为它们体积巨大我们稍后按需安装 pip install streamlit pandas plotly seaborn matplotlib folium streamlit-folium emoji scikit-learn现在你的环境已经具备了运行 Chat Analyzer 前端和基础分析逻辑的所有能力。但请注意transformers用于对话摘要和torchPyTorchtransformers的底层依赖是两个“巨无霸”库它们的下载和安装可能耗时数分钟且对网络稳定性要求很高。因此我推荐采用“按需加载”的策略# 6. 可选仅在你需要使用“对话摘要”功能时再安装它们 pip install transformers torch --index-url https://download.pytorch.org/whl/cpu--index-url参数指定了 PyTorch 的 CPU 版本下载源这可以避免在没有 GPU 的机器上pip自动尝试下载庞大的 CUDA 版本而导致失败。如果你的机器有 NVIDIA GPU可以将cpu替换为cu118对应 CUDA 11.8。实操心得在venv激活状态下你可以随时用which python和which pip命令来确认你正在使用的是否是虚拟环境内的解释器和包管理器。如果输出路径里包含venv/bin/那就说明一切正常。这是防止“环境污染”的第一道防火墙。4.2 核心代码编写一份可直接运行的app.py现在让我们把前面所有讨论过的架构、模块和逻辑汇聚成一份完整的、可直接运行的 Streamlit 应用脚本。请将以下代码保存为项目根目录下的app.py文件。这份代码经过了极致的精简和注释每一个st.调用都对应着 UI 上的一个可见元素你可以清晰地看到数据流是如何从上传、解析、分析最终呈现在用户眼前的。import streamlit as st import pandas as pd import plotly.express as px import seaborn as sns import matplotlib.pyplot as plt import matplotlib.dates as mdates from matplotlib.patches import Rectangle import numpy as np import tempfile import os import emoji from chatminer.chatparsers import WhatsAppParser from sklearn.feature_extraction.text import CountVectorizer from bertopic.vectorizers._ctfidf import ClassTfidfTransformer import re import folium from streamlit_folium import st_folium # 设置页面标题和图标 st.set_page_config( page_titleChat Analyzer, page_icon, layoutwide ) # 初始化 session state用于在不同页面间共享数据 if data not in st.session_state: st.session_state[data] None # 1. 页面顶部横幅 st.title( Chat Analyzer — From Raw Chats To Data Insights) st.markdown( *Upload your WhatsApp or Telegram chat export to unlock hidden patterns, trends, and insights.* ) # 2. 数据上传与归一化模块 st.header( Step 1: Upload Normalize Your Chat Data) uploaded_file st.file_uploader(Choose a chat export file (.txt for WhatsApp, .json/.html for Telegram), type[txt, json, html]) if uploaded_file is not None: # 根据文件扩展名选择不同的解析逻辑 file_extension uploaded_file.name.split(.)[-1].lower() if file_extension txt: # WhatsApp 处理逻辑 try: tempdir tempfile.mkdtemp(prefixchat_exports) with open(os.path.join(tempdir, chat_export.txt), modewb) as f: f.write(uploaded_file.read()) parser WhatsAppParser(os.path.join(tempdir, chat_export.txt)) parser.parse_file() df parser.parsed_messages.get_df(as_pandasTrue).rename(columns{author: username}) df[date] df[timestamp].dt.date df[hour] df[timestamp].dt.floor(h).dt.strftime(%H:%M) df[week] df[timestamp].dt.to_period(W).dt.start_time df[month] df[timestamp].to_numpy().astype(datetime64[M]) df[day_name] df[timestamp].dt.day_name() df[is_media] df[message].str.contains(Media omitted) df[text_length] df[message].str.split().map(len) st.session_state[data] df st.success(f✅ Successfully parsed {len(df)} messages from WhatsApp!) st.dataframe(df.head(5), hide_indexTrue) except Exception as e: st.error(f❌ Failed to parse WhatsApp file: {e}) elif file_extension in [json, html]: # 这里是 Telegram 的伪代码占位符实际项目中你需要集成 telegram-export-parser 等库 st.warning(⚠️ Telegram parsing is a placeholder. Please implement your own logic or use a library like telegram-export-parser.) st.session_state[data] pd.DataFrame({ timestamp: pd.date_range(2023-01-01, periods10, freqH), username: [Alice, Bob, Charlie] * 3 [Alice], message: [Hello!, Hi there!, How are you?, Fine, thanks!, What about you?, Busy with work., Same here., Let\s meet later., Sounds good!, See you!], is_media: [False] * 10, text_length: [1, 2, 3, 2, 3, 3, 2, 3, 2, 2] }) else: st.error(❌ Unsupported file format. Please upload a .txt, .json, or .html file.) # 3. 基础统计分析模块 if st.session_state[data] is not None: df st.session_state[data] st.header( Section A: Basic Statistics) # 创建侧边栏过滤器 with st.sidebar: st.subheader( Global Filters) min_date, max_date df[date].min(), df[date].max() date_range st.date_input(Select Date Range, value[min_date, max_date], min_valuemin_date, max_valuemax_date) if len(date_range) 2: filtered_df df[(df[date] date_range[0]) (df[date] date_range[1])] else: filtered_df df # 活跃度趋势图 st.subheader( Overall Chat Activity Over Time) unit st.radio(Count by:, (Messages, Users), horizontalTrue) granularity st.radio(Granularity:, (month, week, date), horizontalTrue) def generate_activity_overtime(df, unit, granularity): unit_dict {Messages: count, Users: nunique} agg_df df.groupby(granularity, as_indexFalse).agg({username: unit_dict[unit]}).reset_index() agg_df agg_df.rename(columns{username: f#{unit}, index: granularity.capitalize()}) fig px.line(agg_df, xgranularity.capitalize(), yf#{unit}) fig[data][0][line][color] #24d366 fig.update_layout(paper_bgcolorrgba(18,32,43), plot_bgcolorrgba(18,32,43), hovermodex, title_textfOverall Chat Activity Over Time ({unit})) return fig st.plotly_chart(generate_activity_overtime(filtered_df, unit, granularity)) # 4. 用户级分析模块 st.header( Section B: User-Level Analysis) if st.session_state[data] is not None: df st.session_state[data] # ... (此处省略具体的 Emoji 分析代码逻辑与前文一致) st.info( This section analyzes user behavior through message count, word count, and emoji usage patterns.) # 5. 地理位置分析模块 st.header( Section D: Geographics) if st.session_state[data] is not None: df st.session_state[data] LOCATIONS_PATTERN r(https:\/\/maps\.google\.com\/\?q-?\d\.\d,-?\d\.\d) locations_df df[df[message].str.contains(r^(?.*maps.google.com)(?.*q))] if not locations_df.empty: try: # 提取经纬度 locations_df[lat_lon] locations_df[message].str.extract(LOCATIONS_PATTERN)[0].str.split().str[1] locations_df[[lat, lon]] locations_df[lat_lon].str.split(,, expandTrue) locations_df[lat] locations_df[lat].astype(float) locations_df[lon] locations_df[lon].astype(float) # 创建 Folium 地图 center_lat locations_df[lat].mean() center_lon locations_df[lon].mean() m folium.Map(location[center_lat, center_lon], zoom_start2, tilescartodbpositron) for idx, row in locations_df.iterrows(): folium.Marker( location[row[lat], row[lon]], popupf{row[username]}br{row[date]}, tooltiprow[username] ).add_to(m) st_folium(m, width700, height500) st.success(f✅ Found and plotted {len(locations_df)} locations!) except Exception as e: st.error(f❌ Failed to parse locations: {e}) else: st.info(ℹ