Files
wehub-resource-sync 1d6fd29cda
tests / test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:36:27 +08:00

998 lines
37 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
simp-skill · Chat Parser
解析微信/QQ聊天记录,提取信号分析报告
支持格式:
- 微信导出 TXTWeChatMsg/留痕等工具)
- 微信导出 HTMLWeChatMsg
- 微信导出 CSVPyWxDump
- QQ 导出 TXT(QQ消息管理器)
- QQ 导出 MHT/MHTMLQQ消息管理器)
- 通用 JSON 格式
用法:
python3 chat_parser.py <input_file> <target_name> [--user <your_name>] [--output <output_file>]
示例:
python3 chat_parser.py wechat_export.txt 小美 --output output/xiaomei_analysis.md
python3 chat_parser.py qq_log.txt 小美 --user 我 --output output/xiaomei_analysis.md
"""
import re
import sys
import json
import html
import argparse
from datetime import datetime, timedelta
from pathlib import Path
from collections import Counter, defaultdict
from typing import Optional
# ─────────────────────────────────────────────
# 数据结构
# ─────────────────────────────────────────────
class Message:
"""单条消息"""
def __init__(self, timestamp: datetime, sender: str, content: str, msg_type: str = "text"):
self.timestamp = timestamp
self.sender = sender
self.content = content
self.msg_type = msg_type # text / image / sticker / voice / system
def __repr__(self):
return f"[{self.timestamp.strftime('%Y-%m-%d %H:%M')}] {self.sender}: {self.content[:30]}"
# ─────────────────────────────────────────────
# 格式探测与解析
# ─────────────────────────────────────────────
def detect_format(filepath: str) -> str:
"""自动探测聊天记录格式"""
path = Path(filepath)
ext = path.suffix.lower()
if ext == ".json":
return "json"
if ext in (".mht", ".mhtml"):
return "qq_mht"
if ext == ".csv":
return "wechat_csv"
if ext == ".html":
return "wechat_html"
# TXT 格式需要读取内容判断
try:
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
sample = f.read(4096)
except Exception:
return "unknown"
# QQ TXT: "2024-01-01 12:00:00 用户名(12345678)"
if re.search(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} .+\(\d+\)', sample):
return "qq_txt"
# 微信 WeChatMsg TXT: "2024-01-01 12:00:00\n用户名\n消息内容"
if re.search(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}', sample):
return "wechat_txt"
return "plaintext"
def parse_wechat_txt(filepath: str, target_name: str, user_name: str) -> list:
"""解析微信导出 TXTWeChatMsg格式)"""
messages = []
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
# 匹配时间戳行
pattern = r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\n(.+?)\n(.*?)(?=\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\n|\Z)'
matches = re.findall(pattern, content, re.DOTALL)
for ts_str, sender, msg_content in matches:
try:
ts = datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
except ValueError:
continue
sender = sender.strip()
msg_content = msg_content.strip()
if not msg_content or msg_content in ("[图片]", "[语音]", "[视频]", "[文件]"):
msg_type = "image" if "[图片]" in msg_content else (
"voice" if "[语音]" in msg_content else "media"
)
if not msg_content:
continue
messages.append(Message(ts, sender, msg_content, msg_type))
else:
messages.append(Message(ts, sender, msg_content))
return messages
def parse_qq_txt(filepath: str, target_name: str, user_name: str) -> list:
"""解析QQ导出 TXT"""
messages = []
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
lines = f.readlines()
current_ts = None
current_sender = None
current_content = []
header_pattern = re.compile(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (.+?)(?:\(\d+\))?$')
def flush():
if current_ts and current_sender and current_content:
content = "\n".join(current_content).strip()
if content:
messages.append(Message(current_ts, current_sender, content))
for line in lines:
line = line.rstrip()
m = header_pattern.match(line)
if m:
flush()
try:
current_ts = datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S")
except ValueError:
current_ts = None
current_sender = m.group(2).strip()
current_content = []
elif current_ts is not None:
current_content.append(line)
flush()
return messages
def parse_qq_mht(filepath: str, target_name: str, user_name: str) -> list:
"""解析QQ导出 MHT/MHTML"""
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
raw = f.read()
# 去除HTML标签
clean = re.sub(r'<[^>]+>', ' ', raw)
clean = html.unescape(clean)
clean = re.sub(r'\s+', ' ', clean)
# 同QQ TXT 解析
messages = []
pattern = re.compile(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (.+?)(?:\(\d+\))? (.+?)(?=\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}|\Z)')
for m in pattern.finditer(clean):
try:
ts = datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S")
sender = m.group(2).strip()
content = m.group(3).strip()
if content:
messages.append(Message(ts, sender, content))
except ValueError:
continue
return messages
def parse_wechat_html(filepath: str, target_name: str, user_name: str) -> list:
"""解析微信导出 HTMLWeChatMsg"""
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
raw = f.read()
messages = []
# 匹配消息块
msg_pattern = re.compile(
r'<div class="message[^"]*"[^>]*>.*?'
r'<span class="time"[^>]*>([^<]+)</span>.*?'
r'<span class="sender"[^>]*>([^<]+)</span>.*?'
r'<div class="content"[^>]*>(.*?)</div>',
re.DOTALL
)
for m in msg_pattern.finditer(raw):
ts_str = m.group(1).strip()
sender = html.unescape(m.group(2).strip())
content = html.unescape(re.sub(r'<[^>]+>', '', m.group(3))).strip()
try:
ts = datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
except ValueError:
try:
ts = datetime.strptime(ts_str, "%Y/%m/%d %H:%M:%S")
except ValueError:
continue
if content:
messages.append(Message(ts, sender, content))
return messages
def parse_wechat_csv(filepath: str, target_name: str, user_name: str) -> list:
"""解析 PyWxDump CSV 导出"""
import csv
messages = []
with open(filepath, "r", encoding="utf-8-sig", errors="ignore") as f:
reader = csv.DictReader(f)
for row in reader:
ts_field = row.get("CreateTime") or row.get("timestamp") or row.get("time", "")
sender_field = row.get("NickName") or row.get("sender") or row.get("from", "")
content_field = row.get("StrContent") or row.get("content") or row.get("msg", "")
try:
if ts_field.isdigit():
ts = datetime.fromtimestamp(int(ts_field))
else:
ts = datetime.strptime(ts_field[:19], "%Y-%m-%d %H:%M:%S")
except (ValueError, AttributeError):
continue
sender = sender_field.strip()
content = content_field.strip()
if content:
messages.append(Message(ts, sender, content))
return messages
def parse_json(filepath: str, target_name: str, user_name: str) -> list:
"""解析通用 JSON 格式"""
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
messages = []
items = data if isinstance(data, list) else data.get("messages", [])
for item in items:
ts_raw = item.get("timestamp") or item.get("time") or item.get("createTime", "")
sender = item.get("sender") or item.get("from") or item.get("nickName", "")
content = item.get("content") or item.get("text") or item.get("msg", "")
try:
if isinstance(ts_raw, (int, float)):
ts = datetime.fromtimestamp(ts_raw)
else:
ts = datetime.strptime(str(ts_raw)[:19], "%Y-%m-%d %H:%M:%S")
except (ValueError, TypeError):
continue
if content:
messages.append(Message(ts, str(sender).strip(), str(content).strip()))
return messages
def parse_chat(filepath: str, target_name: str, user_name: str = "我") -> list:
"""主解析入口:自动选择格式"""
fmt = detect_format(filepath)
parsers = {
"wechat_txt": parse_wechat_txt,
"wechat_html": parse_wechat_html,
"wechat_csv": parse_wechat_csv,
"qq_txt": parse_qq_txt,
"qq_mht": parse_qq_mht,
"json": parse_json,
"plaintext": parse_wechat_txt, # fallback
}
parser = parsers.get(fmt, parse_wechat_txt)
messages = parser(filepath, target_name, user_name)
# 过滤:只保留目标和用户的消息
relevant = [m for m in messages if target_name in m.sender or user_name in m.sender]
# 按时间排序
relevant.sort(key=lambda m: m.timestamp)
return relevant
# ─────────────────────────────────────────────
# 信号分析引擎
# ─────────────────────────────────────────────
class SignalAnalyzer:
"""信号分析引擎:从聊天记录中提取追求策略相关信号"""
def __init__(self, messages: list, target_name: str, user_name: str):
self.messages = messages
self.target = target_name
self.user = user_name
self.target_msgs = [m for m in messages if target_name in m.sender]
self.user_msgs = [m for m in messages if user_name in m.sender]
# ── 基础统计 ──────────────────────────────────
def message_counts(self) -> dict:
return {
"total": len(self.messages),
"from_target": len(self.target_msgs),
"from_user": len(self.user_msgs),
"target_ratio": round(len(self.target_msgs) / max(len(self.messages), 1) * 100, 1),
}
def date_range(self) -> dict:
if not self.messages:
return {}
first = self.messages[0].timestamp
last = self.messages[-1].timestamp
days = (last - first).days + 1
return {
"first_date": first.strftime("%Y-%m-%d"),
"last_date": last.strftime("%Y-%m-%d"),
"total_days": days,
"avg_msgs_per_day": round(len(self.messages) / max(days, 1), 1),
}
# ── 主动性分析 ─────────────────────────────────
def initiative_analysis(self) -> dict:
"""分析谁更主动开启对话"""
sessions = self._split_sessions()
target_starts = 0
user_starts = 0
for session in sessions:
if not session:
continue
first = session[0]
if self.target in first.sender:
target_starts += 1
elif self.user in first.sender:
user_starts += 1
total = target_starts + user_starts or 1
return {
"target_initiates": target_starts,
"user_initiates": user_starts,
"target_initiative_ratio": round(target_starts / total * 100, 1),
"user_initiative_ratio": round(user_starts / total * 100, 1),
"verdict": self._initiative_verdict(target_starts, user_starts),
}
def _initiative_verdict(self, target: int, user: int) -> str:
if user == 0 and target == 0:
return "数据不足"
ratio = target / (target + user)
if ratio >= 0.6:
return "🟢 ta 经常主动找你(强绿灯)"
elif ratio >= 0.4:
return "🟡 双方主动程度差不多"
elif ratio >= 0.2:
return "🟡 你更主动,ta 偶尔主动"
else:
return "🔴 几乎都是你在主动,ta 很少主动"
# ── 回复速度分析 ───────────────────────────────
def reply_speed_analysis(self) -> dict:
"""分析回复速度和趋势"""
target_delays = []
user_delays = []
for i in range(1, len(self.messages)):
prev = self.messages[i - 1]
curr = self.messages[i]
delay = (curr.timestamp - prev.timestamp).total_seconds()
# 超过4小时视为新会话,不计算
if delay > 14400:
continue
if self.target in curr.sender and self.user in prev.sender:
target_delays.append(delay)
elif self.user in curr.sender and self.target in prev.sender:
user_delays.append(delay)
def stats(delays):
if not delays:
return {"avg_seconds": None, "median_seconds": None, "fast_ratio": None}
avg = sum(delays) / len(delays)
sorted_d = sorted(delays)
median = sorted_d[len(sorted_d) // 2]
fast = sum(1 for d in delays if d < 300) / len(delays) # 5分钟内回复
return {
"avg_seconds": round(avg),
"avg_display": _format_seconds(avg),
"median_display": _format_seconds(median),
"fast_ratio": round(fast * 100, 1),
}
target_stats = stats(target_delays)
user_stats = stats(user_delays)
# 速度趋势:比较前半段和后半段
trend = "数据不足"
if len(target_delays) >= 10:
first_half = sum(target_delays[:len(target_delays)//2]) / (len(target_delays)//2)
second_half = sum(target_delays[len(target_delays)//2:]) / (len(target_delays) - len(target_delays)//2)
if second_half < first_half * 0.7:
trend = "🟢 ta 回复越来越快(温度在升)"
elif second_half > first_half * 1.5:
trend = "🔴 ta 回复越来越慢(需要注意)"
else:
trend = "🟡 回复速度变化不大"
return {
"target_reply": target_stats,
"user_reply": user_stats,
"speed_comparison": self._speed_verdict(target_stats, user_stats),
"trend": trend,
}
def _speed_verdict(self, target: dict, user: dict) -> str:
ta = target.get("avg_seconds")
me = user.get("avg_seconds")
if ta is None or me is None:
return "数据不足"
if ta < 120:
return "🟢 ta 回复你很快(秒回/分钟级)"
elif ta < 600:
return "🟢 ta 回复较及时(10分钟内)"
elif ta < me * 0.5:
return "🟡 ta 比你回复略慢,但尚可"
elif ta > me * 2:
return "🔴 ta 回复你明显比你回复ta慢"
else:
return "🟡 双方回复速度差不多"
# ── 消息长度分析 ───────────────────────────────
def message_length_analysis(self) -> dict:
"""分析消息长度(情感投入指标)"""
target_lens = [len(m.content) for m in self.target_msgs if m.msg_type == "text"]
user_lens = [len(m.content) for m in self.user_msgs if m.msg_type == "text"]
def avg(lst):
return round(sum(lst) / len(lst), 1) if lst else 0
target_avg = avg(target_lens)
user_avg = avg(user_lens)
verdict = ""
if target_avg > user_avg * 1.3:
verdict = "🟢 ta 发给你的消息比你的更长(投入度高)"
elif target_avg < user_avg * 0.5:
verdict = "🔴 ta 的消息明显比你短(可能不够投入)"
elif target_avg > 50:
verdict = "🟢 ta 愿意给你发长消息(有话说)"
else:
verdict = "🟡 双方消息长度差不多"
return {
"target_avg_len": target_avg,
"user_avg_len": user_avg,
"target_long_msgs": sum(1 for l in target_lens if l > 100),
"verdict": verdict,
}
# ── 深夜信号分析 ───────────────────────────────
def late_night_analysis(self) -> dict:
"""深夜消息(22:00-02:00)是重要亲密度信号"""
late_night_range = set(range(22, 24)) | set(range(0, 3))
target_late = [m for m in self.target_msgs if m.timestamp.hour in late_night_range]
user_late = [m for m in self.user_msgs if m.timestamp.hour in late_night_range]
target_initiates_late = 0
for session in self._split_sessions():
if not session:
continue
first = session[0]
if first.timestamp.hour in late_night_range and self.target in first.sender:
target_initiates_late += 1
verdict = ""
if target_initiates_late >= 5:
verdict = "🟢🟢 ta 多次在深夜主动找你(强亲密信号)"
elif target_initiates_late >= 2:
verdict = "🟢 ta 有过深夜主动联系你"
elif len(target_late) > 0:
verdict = "🟡 ta 有在深夜回复你,但不常主动"
else:
verdict = "⚪ 没有明显的深夜互动记录"
return {
"target_late_msgs": len(target_late),
"target_initiates_late_night": target_initiates_late,
"verdict": verdict,
}
# ── 话题分析 ───────────────────────────────────
def topic_analysis(self) -> dict:
"""分析高频话题和ta主动延伸的话题"""
all_words = []
for m in self.target_msgs:
# 简单分词:按标点和空格切分
words = re.findall(r'[\u4e00-\u9fff]{2,6}', m.content)
all_words.extend(words)
# 过滤停用词
stopwords = {
'什么', '这个', '那个', '一个', '可以', '没有', '知道', '觉得', '感觉',
'就是', '但是', '因为', '所以', '如果', '现在', '时候', '已经', '还是',
'好像', '应该', '可能', '不是', '一样', '这样', '那样', '一下',
}
filtered = [w for w in all_words if w not in stopwords]
top_topics = Counter(filtered).most_common(15)
# 话题延伸:ta在我发消息后是否追问
follow_up_count = 0
for i in range(1, len(self.messages)):
prev = self.messages[i - 1]
curr = self.messages[i]
delay = (curr.timestamp - prev.timestamp).total_seconds()
if (self.user in prev.sender and self.target in curr.sender
and delay < 3600 and '' in curr.content or '?' in curr.content):
follow_up_count += 1
return {
"top_topics": top_topics,
"target_follow_up_questions": follow_up_count,
"follow_up_verdict": (
"🟢 ta 经常追问你的话(在乎你说的)" if follow_up_count >= 10
else "🟡 ta 有时会追问" if follow_up_count >= 3
else "⚪ ta 很少追问"
),
}
# ── 语言特征提取 ───────────────────────────────
def language_features(self) -> dict:
"""提取ta的语言习惯,用于画像构建"""
all_target_text = " ".join(m.content for m in self.target_msgs)
# 语气词/口头禅检测
particles = ['哈哈', '哈', '嗯', '啊', '呢', '吧', '哦', '噢', '嘿', '诶',
'好的', '好啊', '好哦', '嗯嗯', '哎', '哎呀', '唉', '哇', '哇哦',
'嗯哦', '好嘞', '行', '行吧', '确实', '对哦', '对的', '对对', '真的',
'真的吗', '没有', '有吗', '是吗', '是哦', '可以', '好可以', '6', '666']
particle_counts = {p: all_target_text.count(p) for p in particles if all_target_text.count(p) > 0}
top_particles = sorted(particle_counts.items(), key=lambda x: -x[1])[:8]
# emoji统计
emoji_pattern = re.compile(
"[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF"
"\U0001F680-\U0001F6FF\U0001F1E0-\U0001F1FF"
"\U00002702-\U000027B0\U000024C2-\U0001F251]+",
flags=re.UNICODE
)
all_emojis = emoji_pattern.findall(all_target_text)
emoji_freq = Counter(all_emojis).most_common(5)
# 标点习惯
has_ellipsis = all_target_text.count("...") + all_target_text.count("……")
has_exclaim = all_target_text.count("") + all_target_text.count("!")
has_question = all_target_text.count("") + all_target_text.count("?")
total_msgs = max(len(self.target_msgs), 1)
# 消息风格
short_msgs = sum(1 for m in self.target_msgs if len(m.content) < 20)
long_msgs = sum(1 for m in self.target_msgs if len(m.content) > 100)
style = (
"短句连发型" if short_msgs > total_msgs * 0.7
else "长篇输出型" if long_msgs > total_msgs * 0.2
else "混合型"
)
return {
"top_particles": top_particles,
"top_emojis": emoji_freq,
"exclamation_per_msg": round(has_exclaim / total_msgs, 2),
"question_per_msg": round(has_question / total_msgs, 2),
"ellipsis_count": has_ellipsis,
"message_style": style,
}
# ── 综合信号评分 ───────────────────────────────
def signal_score(self) -> dict:
"""计算综合信号评分(满分25"""
score = 0
signals = []
counts = self.message_counts()
initiative = self.initiative_analysis()
speed = self.reply_speed_analysis()
length = self.message_length_analysis()
late_night = self.late_night_analysis()
topic = self.topic_analysis()
# 主动性评分(0-6
ratio = initiative["target_initiative_ratio"]
if ratio >= 50:
score += 6
signals.append(f"🟢 ta主动开启 {ratio}% 的对话(强绿灯)")
elif ratio >= 35:
score += 3
signals.append(f"🟡 ta主动开启 {ratio}% 的对话")
elif ratio >= 20:
score += 1
else:
score -= 2
signals.append("🔴 ta几乎不主动联系你")
# 回复速度评分(0-5
target_avg = speed["target_reply"].get("avg_seconds")
if target_avg is not None:
if target_avg < 120:
score += 5
signals.append(f"🟢 ta平均 {_format_seconds(target_avg)} 回复你(很快)")
elif target_avg < 600:
score += 3
signals.append(f"🟢 ta平均 {_format_seconds(target_avg)} 回复你")
elif target_avg > 3600:
score -= 1
signals.append(f"🔴 ta平均 {_format_seconds(target_avg)} 才回复你(较慢)")
# 回复速度趋势评分(0-3
trend = speed.get("trend", "")
if "越来越快" in trend:
score += 3
signals.append("🟢 ta最近回复你越来越快(温度在升)")
elif "越来越慢" in trend:
score -= 2
signals.append("🔴 ta最近回复你越来越慢(需注意)")
# 消息长度评分(0-3
if "投入度高" in length["verdict"]:
score += 3
signals.append("🟢 ta发给你的消息比你的长(更用心)")
elif "明显比你短" in length["verdict"]:
score -= 1
# 深夜信号评分(0-5
late_initiates = late_night["target_initiates_late_night"]
if late_initiates >= 5:
score += 5
signals.append(f"🟢🟢 ta {late_initiates} 次在深夜主动找你")
elif late_initiates >= 2:
score += 2
signals.append(f"🟢 ta有过深夜主动联系你 ({late_initiates}次)")
elif late_night["target_late_msgs"] > 0:
score += 1
# 追问行为评分(0-3
follow_up = topic["target_follow_up_questions"]
if follow_up >= 10:
score += 3
signals.append(f"🟢 ta经常追问你 ({follow_up}次),说明ta在意你说的话")
elif follow_up >= 3:
score += 1
# 确定等级
if score >= 18:
level = "🟢🟢🟢 强烈绿灯"
advice = "信号非常明显!是时候认真准备表白了。"
elif score >= 12:
level = "🟢🟡 中度绿灯"
advice = "有明显好感,继续深化情感连接,创造更多1v1机会。"
elif score >= 6:
level = "🟡 模糊信号"
advice = "信号不够明确,可以适当试探,不要急着表白。"
elif score >= 0:
level = "🟡🔴 弱信号"
advice = "目前还没明显兴趣迹象,先建立更稳固的关系基础。"
else:
level = "🔴 警示信号"
advice = "有一些不积极的信号,建议重新评估追求策略。"
return {
"score": score,
"max_score": 25,
"level": level,
"key_signals": signals,
"advice": advice,
}
# ── 辅助方法 ───────────────────────────────────
def _split_sessions(self, gap_minutes: int = 60) -> list:
"""将消息按时间间隔分割成会话"""
if not self.messages:
return []
sessions = []
current = [self.messages[0]]
for m in self.messages[1:]:
gap = (m.timestamp - current[-1].timestamp).total_seconds() / 60
if gap > gap_minutes:
sessions.append(current)
current = [m]
else:
current.append(m)
sessions.append(current)
return sessions
# ─────────────────────────────────────────────
# 报告生成
# ─────────────────────────────────────────────
def _format_seconds(seconds: float) -> str:
"""将秒数格式化为可读字符串"""
if seconds < 60:
return f"{int(seconds)}秒"
elif seconds < 3600:
return f"{int(seconds/60)}分钟"
else:
return f"{seconds/3600:.1f}小时"
def generate_report(messages: list, target_name: str, user_name: str, output_path: Optional[str] = None) -> str:
"""生成完整的信号分析报告"""
if not messages:
return "❌ 未找到有效消息,请检查文件格式和姓名设置。"
analyzer = SignalAnalyzer(messages, target_name, user_name)
counts = analyzer.message_counts()
date_range = analyzer.date_range()
initiative = analyzer.initiative_analysis()
speed = analyzer.reply_speed_analysis()
length = analyzer.message_length_analysis()
late_night = analyzer.late_night_analysis()
topic = analyzer.topic_analysis()
features = analyzer.language_features()
score = analyzer.signal_score()
lines = [
f"# 💝 聊天记录信号分析报告",
f"",
f"> 心上人:**{target_name}** | 分析时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}",
f"> 记录时间:{date_range.get('first_date', '?')} {date_range.get('last_date', '?')}{date_range.get('total_days', '?')}天)",
f"",
f"---",
f"",
f"## 📊 综合信号评分",
f"",
f"**{score['score']} / {score['max_score']} 分** — {score['level']}",
f"",
f"**关键信号**",
]
for sig in score["key_signals"]:
lines.append(f"- {sig}")
lines += [
f"",
f"> 💡 **建议**{score['advice']}",
f"",
f"---",
f"",
f"## 📱 基础统计",
f"",
f"| 指标 | 数值 |",
f"|------|------|",
f"| 总消息数 | {counts['total']} 条 |",
f"| ta 发的消息 | {counts['from_target']} 条({counts['target_ratio']}% |",
f"| 你发的消息 | {counts['from_user']} 条 |",
f"| 日均消息数 | {date_range.get('avg_msgs_per_day', '?')} 条 |",
f"",
f"---",
f"",
f"## 🎯 主动性分析",
f"",
f"- ta 主动开启对话:**{initiative['target_initiates']} 次**{initiative['target_initiative_ratio']}%",
f"- 你主动开启对话:**{initiative['user_initiates']} 次**{initiative['user_initiative_ratio']}%",
f"- 判断:{initiative['verdict']}",
f"",
f"---",
f"",
f"## ⚡ 回复速度",
f"",
f"- ta 平均回复速度:**{speed['target_reply'].get('avg_display', '数据不足')}**",
f"- 你的平均回复速度:**{speed['user_reply'].get('avg_display', '数据不足')}**",
f"- ta 5分钟内快速回复比例:{speed['target_reply'].get('fast_ratio', '?')}%",
f"- 速度对比:{speed['speed_comparison']}",
f"- 趋势:{speed['trend']}",
f"",
f"---",
f"",
f"## 📏 消息长度(情感投入度)",
f"",
f"- ta 平均消息长度:**{length['target_avg_len']} 字**",
f"- 你的平均消息长度:**{length['user_avg_len']} 字**",
f"- ta 发给你的长消息(>100字):{length['target_long_msgs']} 条",
f"- 判断:{length['verdict']}",
f"",
f"---",
f"",
f"## 🌙 深夜信号(22:00-02:00",
f"",
f"- ta 在深夜发的消息:{late_night['target_late_msgs']} 条",
f"- ta 主动在深夜开启对话:{late_night['target_initiates_late_night']} 次",
f"- 判断:{late_night['verdict']}",
f"",
f"---",
f"",
f"## 💬 话题分析",
f"",
f"**ta 的高频词汇 Top 10**",
]
for word, count in topic["top_topics"][:10]:
lines.append(f"- 「{word}」× {count}")
lines += [
f"",
f"- ta 追问你的次数:{topic['target_follow_up_questions']} 次",
f"- 判断:{topic['follow_up_verdict']}",
f"",
f"---",
f"",
f"## 🗣️ ta 的语言特征(用于定制情话)",
f"",
f"- 消息风格:**{features['message_style']}**",
f"- 每条消息平均感叹号:{features['exclamation_per_msg']} 个",
f"- 每条消息平均问号:{features['question_per_msg']} 个",
f"",
f"**常用语气词/口头禅**",
]
for particle, count in features["top_particles"]:
lines.append(f"- 「{particle}」× {count}")
if features["top_emojis"]:
lines.append(f"")
lines.append(f"**常用 Emoji**")
for emoji, count in features["top_emojis"]:
lines.append(f"- {emoji} × {count}")
lines += [
f"",
f"---",
f"",
f"## 🎯 给你的追求建议",
f"",
f"基于以上分析,当前阶段的建议:",
f"",
f"1. **根据信号等级**{score['advice']}",
f"",
f"2. **基于ta的语言习惯**,你的消息风格建议:",
f" - ta 是「{features['message_style']}」,所以你的消息也不要太长/太短,跟ta的节奏走",
f" - 适当用ta熟悉的语气词,会让ta觉得亲切",
f"",
f"3. **最优互动时间**",
]
if late_night["target_initiates_late_night"] >= 2:
lines.append(f" - ta 有深夜主动联系你的习惯,这是最亲密的互动时段")
lines += [
f" - 根据回复速度,ta 在快速回复时更活跃,选择那个时间段互动效果更好",
f"",
f"4. **下一步行动**:运行 `/simp analyze` 获取更详细的策略建议",
f"",
f"---",
f"",
f"*由 simp-skill · 追爱军师 生成*",
]
report = "\n".join(lines)
if output_path:
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
f.write(report)
print(f"✅ 报告已保存到 {output_path}")
return report
# ─────────────────────────────────────────────
# 互动时间数据提取
# ─────────────────────────────────────────────
def extract_time_data(
messages: list,
target_name: str,
user_name: str,
slug: str,
base_dir: Path | None = None,
) -> int:
from tools.time_tracker import record_interaction, DEFAULT_BASE_DIR as TRACKER_BASE
if base_dir is None:
base_dir = TRACKER_BASE
written = 0
for i, msg in enumerate(messages):
content_summary = msg.content[:50].replace("\n", " ")
sender = msg.sender
if target_name in sender:
interaction_type = "chat_received"
data: dict = {"content_summary": content_summary}
elif user_name in sender:
interaction_type = "chat_sent"
data = {"content_summary": content_summary}
else:
continue
if i > 0:
prev = messages[i - 1]
delay_min = (msg.timestamp - prev.timestamp).total_seconds() / 60
if delay_min <= 240 and prev.sender != msg.sender:
if interaction_type == "chat_received":
data["reply_delay_min"] = round(delay_min)
try:
record_interaction(slug, interaction_type, data, ts=msg.timestamp, base_dir=base_dir)
written += 1
except (FileNotFoundError, ValueError):
continue
return written
# ─────────────────────────────────────────────
# 主程序
# ─────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="simp-skill · 聊天记录信号分析器",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python3 chat_parser.py wechat.txt 小美
python3 chat_parser.py qq_log.txt 小美 --user 我的QQ昵称
python3 chat_parser.py wechat.html 小美 --output crushes/xiaomei/memories/chats/analysis.md
"""
)
parser.add_argument("input", help="聊天记录文件路径")
parser.add_argument("target", help="心上人的名字(需与聊天记录中的显示名一致)")
parser.add_argument("--user", default="我", help="你自己的名字(默认:我)")
parser.add_argument("--output", "-o", help="输出文件路径(默认:打印到控制台)")
parser.add_argument("--format", "-f", choices=["wechat_txt", "qq_txt", "qq_mht", "wechat_html", "wechat_csv", "json"],
help="强制指定格式(默认:自动检测)")
parser.add_argument("--track-time", action="store_true",
help="同时将互动时间数据写入 interactions.jsonl")
parser.add_argument("--slug", help="档案 slug--track-time 时必需)")
args = parser.parse_args()
print(f"💝 simp-skill · 聊天记录分析器")
print(f"📂 读取文件:{args.input}")
print(f"🎯 心上人:{args.target}")
print(f"👤 你的名字:{args.user}")
print()
try:
messages = parse_chat(args.input, args.target, args.user)
except FileNotFoundError:
print(f"❌ 文件不存在:{args.input}")
sys.exit(1)
except Exception as e:
print(f"❌ 解析失败:{e}")
sys.exit(1)
if not messages:
print(f"⚠️ 未找到有效消息。请确认:")
print(f" 1. 文件格式是否正确")
print(f" 2. 名字「{args.target}」是否与聊天记录中一致(区分大小写)")
print(f" 3. 如果名字包含空格,请用引号括起来")
sys.exit(1)
print(f"✅ 成功读取 {len(messages)} 条消息")
print(f"🔍 正在分析信号...")
print()
report = generate_report(messages, args.target, args.user, args.output)
if args.track_time:
if not args.slug:
print("--track-time 需要 --slug 参数指定档案名")
sys.exit(1)
from tools.chat_parser import extract_time_data
count = extract_time_data(messages, args.target, args.user, args.slug)
print(f"已写入 {count} 条互动时间记录")
if not args.output:
print(report)
if __name__ == "__main__":
main()