项目文件夹

文件
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 行
37 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
#!/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()