Initial commit: 微信联系人祝福管理系统

This commit is contained in:
xiaji
2026-02-26 16:55:40 +08:00
commit 21c03e5bd0
23 changed files with 4272 additions and 0 deletions

40
.gitignore vendored Normal file
View File

@@ -0,0 +1,40 @@
# 核心数据文件 - 不上传
contacts.db
contacts_data.json
ocr_progress.json
ocr_result.txt
# 截图和图片数据 - 不上传
*.png
scroll/
scroll_complete/
scroll_full/
Snipaste/
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
.venv/
ENV/
# IDE
.idea/
.vscode/
*.swp
*.swo
# 系统文件
.DS_Store
Thumbs.db
# 日志
*.log
# 临时文件
*.tmp
*.temp

60
README.md Normal file
View File

@@ -0,0 +1,60 @@
# 微信联系人祝福管理
一个用于管理微信联系人并发送节日祝福的 Web 应用。
## 功能特性
- 联系人管理:添加、编辑、删除联系人
- 分类标签:支持同事、好友、同学、老师、亲戚、客户、供应商等分类
- 自定义内容:为每个联系人设置自定义备注
- 祝福语管理:编辑和保存个性化祝福语
- 批量操作:批量选择、批量删除
- 搜索筛选:按姓名或分类快速查找
## 技术栈
- **后端**: Python Flask
- **前端**: HTML + Bootstrap 5 + JavaScript
- **数据库**: SQLite
## 快速开始
### 安装依赖
```bash
pip install flask
```
### 启动服务
```bash
python app.py
```
### 访问页面
打开浏览器访问: http://localhost:5000/static/contacts_manager.html
## 项目结构
```
├── app.py # Flask 后端 API
├── init_db.py # 数据库初始化脚本
├── static/
│ └── contacts_manager.html # 前端页面
└── contacts.db # SQLite 数据库(不包含在仓库中)
```
## API 接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/contacts | 获取联系人列表 |
| POST | /api/contacts | 创建联系人 |
| PUT | /api/contacts/:id | 更新联系人 |
| DELETE | /api/contacts/:id | 删除联系人 |
| GET | /api/stats | 获取统计数据 |
## 许可证
MIT License

28
add_column.py Normal file
View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
"""添加 custom_content 和 search_name 字段"""
import sqlite3
DB_PATH = r"D:\夏骥\微信研究\contacts.db"
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# 检查字段是否已存在
cursor.execute("PRAGMA table_info(contacts)")
columns = [col[1] for col in cursor.fetchall()]
if 'custom_content' not in columns:
cursor.execute('ALTER TABLE contacts ADD COLUMN custom_content TEXT DEFAULT ""')
print("已添加 custom_content 字段")
else:
print("custom_content 字段已存在")
if 'search_name' not in columns:
cursor.execute('ALTER TABLE contacts ADD COLUMN search_name TEXT DEFAULT ""')
print("已添加 search_name 字段")
else:
print("search_name 字段已存在")
conn.commit()
conn.close()
print("完成")

249
app.py Normal file
View File

@@ -0,0 +1,249 @@
# -*- coding: utf-8 -*-
"""
Flask后端API - 联系人管理
"""
from flask import Flask, jsonify, request, send_from_directory
import sqlite3
import os
app = Flask(__name__, static_folder='static', static_url_path='/static')
# 数据库文件
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'contacts.db')
# 前端页面路由
@app.route('/')
def index():
return send_from_directory('static', 'contacts_manager.html')
def get_db():
"""获取数据库连接"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def row_to_dict(row):
"""将sqlite Row转换为字典"""
return dict(row) if row else None
@app.route('/api/contacts', methods=['GET'])
def get_contacts():
"""获取所有联系人"""
conn = get_db()
cursor = conn.cursor()
# 获取查询参数
category = request.args.get('category')
search = request.args.get('search')
page = int(request.args.get('page', 1))
page_size = int(request.args.get('page_size', 20))
# 先获取总数
count_sql = "SELECT COUNT(*) FROM contacts WHERE 1=1"
count_params = []
if category:
# 支持多标签筛选:查找包含该标签的记录
count_sql += " AND (category = ? OR category LIKE ? OR category LIKE ? OR category LIKE ?)"
count_params.extend([
category, # 完全匹配: "同事"
f"{category},%", # 开头: "同事,好友"
f"%,{category}", # 结尾: "好友,同事"
f"%,{category},%" # 中间: "好友,同事,亲戚"
])
if search:
count_sql += " AND (name LIKE ? OR search_name LIKE ?)"
count_params.extend([f"%{search}%", f"%{search}%"])
cursor.execute(count_sql, count_params)
total = cursor.fetchone()[0]
# 获取分页数据
sql = "SELECT * FROM contacts WHERE 1=1"
params = []
if category:
sql += " AND (category = ? OR category LIKE ? OR category LIKE ? OR category LIKE ?)"
params.extend([
category,
f"{category},%",
f"%,{category}",
f"%,{category},%"
])
if search:
sql += " AND (name LIKE ? OR search_name LIKE ?)"
params.extend([f"%{search}%", f"%{search}%"])
sql += " ORDER BY name LIMIT ? OFFSET ?"
params.extend([page_size, (page - 1) * page_size])
cursor.execute(sql, params)
rows = cursor.fetchall()
conn.close()
contacts = []
for row in rows:
c = row_to_dict(row)
c['selected'] = bool(c.get('selected', 0))
contacts.append(c)
return jsonify({
'contacts': contacts,
'total': total,
'page': page,
'page_size': page_size,
'total_pages': (total + page_size - 1) // page_size
})
@app.route('/api/contacts/<int:contact_id>', methods=['GET'])
def get_contact(contact_id):
"""获取单个联系人"""
conn = get_db()
cursor = conn.cursor()
cursor.execute("SELECT * FROM contacts WHERE id = ?", (contact_id,))
row = cursor.fetchone()
conn.close()
if row:
c = row_to_dict(row)
c['selected'] = bool(c.get('selected', 0))
return jsonify(c)
return jsonify({'error': 'Not found'}), 404
@app.route('/api/contacts', methods=['POST'])
def create_contact():
"""创建联系人"""
data = request.json
conn = get_db()
cursor = conn.cursor()
# 如果没有指定 custom_content默认使用 category 的值
custom_content = data.get('custom_content', '')
if not custom_content:
custom_content = data.get('category', '')
cursor.execute('''
INSERT INTO contacts (name, search_name, category, custom_content, blessing, selected)
VALUES (?, ?, ?, ?, ?, ?)
''', (
data.get('name', ''),
data.get('search_name', data.get('name', '')),
data.get('category', ''),
custom_content,
data.get('blessing', '马年新春快乐!愿您在新的一年里,事业腾飞,马到成功!'),
1 if data.get('selected') else 0
))
contact_id = cursor.lastrowid
conn.commit()
conn.close()
return jsonify({'id': contact_id, 'message': '创建成功'})
@app.route('/api/contacts/<int:contact_id>', methods=['PUT'])
def update_contact(contact_id):
"""更新联系人"""
data = request.json
conn = get_db()
cursor = conn.cursor()
# 构建更新语句
updates = []
params = []
if 'name' in data:
updates.append("name = ?")
params.append(data['name'])
if 'search_name' in data:
updates.append("search_name = ?")
params.append(data['search_name'])
if 'category' in data:
updates.append("category = ?")
params.append(data['category'])
if 'custom_content' in data:
updates.append("custom_content = ?")
params.append(data['custom_content'])
if 'blessing' in data:
updates.append("blessing = ?")
params.append(data['blessing'])
if 'selected' in data:
updates.append("selected = ?")
params.append(1 if data['selected'] else 0)
if updates:
updates.append("updated_at = CURRENT_TIMESTAMP")
params.append(contact_id)
sql = f"UPDATE contacts SET {', '.join(updates)} WHERE id = ?"
cursor.execute(sql, params)
conn.commit()
conn.close()
return jsonify({'message': '更新成功'})
@app.route('/api/contacts/<int:contact_id>', methods=['DELETE'])
def delete_contact(contact_id):
"""删除联系人"""
conn = get_db()
cursor = conn.cursor()
cursor.execute("DELETE FROM contacts WHERE id = ?", (contact_id,))
conn.commit()
conn.close()
return jsonify({'message': '删除成功'})
@app.route('/api/stats', methods=['GET'])
def get_stats():
"""获取统计数据"""
conn = get_db()
cursor = conn.cursor()
# 总数
cursor.execute("SELECT COUNT(*) FROM contacts")
total = cursor.fetchone()[0]
# 已选择数
cursor.execute("SELECT COUNT(*) FROM contacts WHERE selected = 1")
selected = cursor.fetchone()[0]
# 分类统计
cursor.execute("""
SELECT category, COUNT(*) as count
FROM contacts
GROUP BY category
ORDER BY count DESC
""")
categories = [{'category': row[0] or '未分类', 'count': row[1]} for row in cursor.fetchall()]
conn.close()
return jsonify({
'total': total,
'selected': selected,
'categories': categories
})
if __name__ == '__main__':
print("=" * 50)
print("联系人管理API服务")
print("=" * 50)
print("启动服务: http://localhost:5000")
print("前端页面: http://localhost:5000/static/contacts_manager.html")
print("=" * 50)
app.run(debug=True, host='0.0.0.0', port=5000)

142
batch_ocr.py Normal file
View File

@@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
"""
对已截图的图片进行OCR识别并生成前端可用的JSON数据
"""
import os
import requests
import base64
from PIL import Image
import glob
import json
import re
def ocr_image(image_path):
"""OCR识别单张图片"""
with open(image_path, 'rb') as f:
image_base64 = base64.b64encode(f.read()).decode('utf-8')
url = "http://localhost:11434/api/chat"
payload = {
"model": "glm-ocr",
"messages": [{
"role": "user",
"content": """识别图片中的所有联系人名称。要求:
1. 只输出联系人名称,每行一个
2. 忽略分组标题如星号、字母A-Z等
3. 忽略数字统计
4. 不要添加任何其他内容""",
"images": [image_base64]
}],
"stream": False
}
try:
response = requests.post(url, json=payload, timeout=60)
return response.json().get('message', {}).get('content', '')
except Exception as e:
print(f"OCR失败: {e}")
return ""
def is_valid_contact(line):
"""判断是否是有效的联系人"""
line = line.strip()
if not line:
return False
invalid = ["公众号", "服务号", "企业微信联系人", "我的企业", "联系人",
"星标朋友", "新的朋友", "群聊", "标签", "仅聊天", "设备"]
if line in invalid:
return False
if len(line) == 1 and line.isalpha():
return False
if line.startswith(">") or line.startswith("!"):
return False
# 过滤JSON格式的内容
if line.startswith('"') or line.startswith('{') or line.startswith('[') or line.startswith('```'):
return False
if line.startswith('"') and ':' in line:
return False
return True
def clean_contact_name(name):
"""清理联系人名称"""
# 移除引号
name = name.strip('"\'')
# 移除末尾的标点
name = name.rstrip(',,。::')
return name.strip()
def main():
print("=" * 60)
print("批量OCR识别截图")
print("=" * 60)
# 获取所有截图
scroll_dir = r"D:\夏骥\微信研究\scroll"
screenshots = sorted(glob.glob(os.path.join(scroll_dir, "*.png")))
print(f"找到 {len(screenshots)} 张截图")
all_contacts = set()
for i, path in enumerate(screenshots):
print(f"\n[{i+1}/{len(screenshots)}] {os.path.basename(path)}")
result = ocr_image(path)
new_count = 0
for line in result.strip().split('\n'):
line = line.strip()
if is_valid_contact(line):
cleaned = clean_contact_name(line)
if cleaned and cleaned not in all_contacts:
new_count += 1
print(f" + {cleaned}")
all_contacts.add(cleaned)
print(f" 本轮新增 {new_count},累计 {len(all_contacts)}")
# 生成JSON数据供前端使用
contacts_json = []
for idx, name in enumerate(sorted(all_contacts, key=lambda x: (not x[0].isalpha() if x else True, x.lower() if x and x[0].isalpha() else x))):
if name: # 确保名称非空
contacts_json.append({
"id": idx + 1,
"name": name,
"category": "",
"blessing": "马年新春快乐!愿您在新的一年里,事业腾飞,马到成功!",
"selected": False
})
# 保存为JSON文件供前端导入
json_file = r"D:\夏骥\微信研究\contacts_data.json"
with open(json_file, 'w', encoding='utf-8') as f:
json.dump(contacts_json, f, ensure_ascii=False, indent=2)
print(f"\nJSON数据已保存: {json_file}")
# 保存纯文本结果
print("\n" + "=" * 60)
print("保存结果...")
result_file = r"D:\夏骥\微信研究\ocr_result.txt"
with open(result_file, 'w', encoding='utf-8') as f:
f.write(f"微信通讯录OCR识别结果\n")
f.write(f"共截图 {len(screenshots)}\n")
f.write(f"共识别 {len(all_contacts)} 个联系人\n")
f.write("=" * 60 + "\n\n")
for c in sorted(all_contacts, key=lambda x: (not x[0].isalpha() if x else True, x.lower() if x and x[0].isalpha() else x)):
if c:
f.write(f"{c}\n")
print(f"结果已保存: {result_file}")
print(f"共识别到 {len(all_contacts)} 个不重复联系人")
if __name__ == '__main__':
main()

188
batch_ocr_complete.py Normal file
View File

@@ -0,0 +1,188 @@
# -*- coding: utf-8 -*-
"""
对完整截图进行OCR识别并与数据库对比去重
"""
import os
import sqlite3
import requests
import base64
from PIL import Image
import glob
import json
def ocr_image(image_path):
"""OCR识别单张图片"""
with open(image_path, 'rb') as f:
image_base64 = base64.b64encode(f.read()).decode('utf-8')
url = "http://localhost:11434/api/chat"
payload = {
"model": "glm-ocr",
"messages": [{
"role": "user",
"content": """识别图片中的所有联系人名称。要求:
1. 只输出联系人名称,每行一个
2. 忽略分组标题如星号、字母A-Z等
3. 忽略数字统计
4. 不要添加任何其他内容""",
"images": [image_base64]
}],
"stream": False
}
try:
response = requests.post(url, json=payload, timeout=60)
return response.json().get('message', {}).get('content', '')
except Exception as e:
print(f"OCR失败: {e}")
return ""
def is_valid_contact(line):
"""判断是否是有效的联系人"""
line = line.strip()
if not line or len(line) < 2:
return False
invalid = ["公众号", "服务号", "企业微信联系人", "我的企业", "联系人",
"星标朋友", "新的朋友", "群聊", "标签", "仅聊天", "设备"]
if line in invalid:
return False
if len(line) == 1 and line.isalpha():
return False
if line.startswith(">") or line.startswith("!"):
return False
if line.startswith('"') or line.startswith('{') or line.startswith('[') or line.startswith('```'):
return False
if line.startswith('"') and ':' in line:
return False
return True
def clean_contact_name(name):
"""清理联系人名称"""
name = name.strip('"\'')
name = name.rstrip(',,。::')
return name.strip()
def get_existing_contacts():
"""从数据库获取已存在的联系人"""
conn = sqlite3.connect(r'D:\夏骥\微信研究\contacts.db')
cursor = conn.cursor()
cursor.execute('SELECT name FROM contacts')
existing = set(row[0] for row in cursor.fetchall())
conn.close()
return existing
def add_new_contacts(new_contacts):
"""将新联系人添加到数据库"""
if not new_contacts:
return 0
conn = sqlite3.connect(r'D:\夏骥\微信研究\contacts.db')
cursor = conn.cursor()
# 获取当前最大ID
cursor.execute('SELECT MAX(id) FROM contacts')
max_id = cursor.fetchone()[0] or 0
added = 0
for idx, name in enumerate(new_contacts, start=max_id + 1):
cursor.execute('''
INSERT INTO contacts (id, name, category, blessing, selected)
VALUES (?, ?, ?, ?, ?)
''', (idx, name, '', '马年新春快乐!愿您在新的一年里,事业腾飞,马到成功!', False))
added += 1
conn.commit()
conn.close()
return added
def main():
print("=" * 60)
print("批量OCR识别并去重入库")
print("=" * 60)
# 获取截图目录
scroll_dir = r"D:\夏骥\微信研究\scroll_complete"
if not os.path.exists(scroll_dir):
print(f"目录不存在: {scroll_dir}")
return
screenshots = sorted(glob.glob(os.path.join(scroll_dir, "*.png")))
if not screenshots:
print("未找到截图文件!")
return
print(f"找到 {len(screenshots)} 张截图")
# 获取已存在的联系人
existing_contacts = get_existing_contacts()
print(f"数据库中已有 {len(existing_contacts)} 个联系人")
all_new_contacts = set()
skipped_count = 0
for i, path in enumerate(screenshots):
print(f"\n[{i+1}/{len(screenshots)}] {os.path.basename(path)}")
result = ocr_image(path)
new_in_this = 0
for line in result.strip().split('\n'):
line = line.strip()
if is_valid_contact(line):
cleaned = clean_contact_name(line)
if cleaned and len(cleaned) >= 2:
if cleaned in existing_contacts:
skipped_count += 1
print(f" - {cleaned} (已存在,跳过)")
elif cleaned not in all_new_contacts:
new_in_this += 1
print(f" + {cleaned} (新)")
all_new_contacts.add(cleaned)
print(f" 本轮新增 {new_in_this},累计新发现 {len(all_new_contacts)},跳过 {skipped_count}")
print(f"\n{'='*60}")
print(f"OCR完成")
print(f"发现新联系人: {len(all_new_contacts)}")
print(f"跳过已存在: {skipped_count}")
# 入库
if all_new_contacts:
added = add_new_contacts(sorted(all_new_contacts, key=lambda x: (not x[0].isalpha() if x else True, x.lower() if x and x[0].isalpha() else x)))
print(f"成功入库: {added}")
# 更新JSON文件
conn = sqlite3.connect(r'D:\夏骥\微信研究\contacts.db')
cursor = conn.cursor()
cursor.execute('SELECT * FROM contacts ORDER BY id')
all_contacts = []
for row in cursor.fetchall():
all_contacts.append({
"id": row[0],
"name": row[1],
"category": row[2],
"blessing": row[3],
"selected": bool(row[4])
})
conn.close()
json_file = r"D:\夏骥\微信研究\contacts_data.json"
with open(json_file, 'w', encoding='utf-8') as f:
json.dump(all_contacts, f, ensure_ascii=False, indent=2)
print(f"JSON数据已更新: {json_file}")
print(f"数据库总联系人: {len(all_contacts)}")
if __name__ == '__main__':
main()

185
batch_ocr_fast.py Normal file
View File

@@ -0,0 +1,185 @@
# -*- coding: utf-8 -*-
"""
快速OCR识别 - 每5张截图识别一次
"""
import os
import sqlite3
import requests
import base64
from PIL import Image
import glob
import json
import time
def ocr_image(image_path):
"""OCR识别单张图片"""
with open(image_path, 'rb') as f:
image_base64 = base64.b64encode(f.read()).decode('utf-8')
url = "http://localhost:11434/api/chat"
payload = {
"model": "glm-ocr",
"messages": [{
"role": "user",
"content": """识别图片中的所有联系人名称。要求:
1. 只输出联系人名称,每行一个
2. 忽略分组标题如星号、字母A-Z等
3. 忽略数字统计
4. 不要添加任何其他内容""",
"images": [image_base64]
}],
"stream": False
}
try:
response = requests.post(url, json=payload, timeout=60)
return response.json().get('message', {}).get('content', '')
except Exception as e:
print(f"OCR失败: {e}")
return ""
def is_valid_contact(line):
"""判断是否是有效的联系人"""
line = line.strip()
if not line or len(line) < 2:
return False
invalid = ["公众号", "服务号", "企业微信联系人", "我的企业", "联系人",
"星标朋友", "新的朋友", "群聊", "标签", "仅聊天", "设备"]
if line in invalid:
return False
if len(line) == 1 and line.isalpha():
return False
if line.startswith(">") or line.startswith("!"):
return False
return True
def clean_contact_name(name):
"""清理联系人名称"""
name = name.strip('"\'')
name = name.rstrip(',,。::')
return name.strip()
def get_existing_contacts():
"""从数据库获取已存在的联系人"""
conn = sqlite3.connect(r'D:\夏骥\微信研究\contacts.db')
cursor = conn.cursor()
cursor.execute('SELECT name FROM contacts')
existing = set(row[0] for row in cursor.fetchall())
conn.close()
return existing
def add_new_contacts(new_contacts):
"""将新联系人添加到数据库"""
if not new_contacts:
return 0
conn = sqlite3.connect(r'D:\夏骥\微信研究\contacts.db')
cursor = conn.cursor()
# 获取当前最大ID
cursor.execute('SELECT MAX(id) FROM contacts')
max_id = cursor.fetchone()[0] or 0
added = 0
for idx, name in enumerate(new_contacts, start=max_id + 1):
cursor.execute('''
INSERT INTO contacts (id, name, category, blessing, selected)
VALUES (?, ?, ?, ?, ?)
''', (idx, name, '', '马年新春快乐!愿您在新的一年里,事业腾飞,马到成功!', False))
added += 1
conn.commit()
conn.close()
return added
def main():
print("=" * 60)
print("快速OCR识别 - 采样模式")
print("=" * 60)
# 获取截图目录
scroll_dir = r"D:\夏骥\微信研究\scroll_complete"
screenshots = sorted(glob.glob(os.path.join(scroll_dir, "*.png")))
if not screenshots:
print("未找到截图文件!")
return
print(f"找到 {len(screenshots)} 张截图")
# 获取已存在的联系人
existing_contacts = get_existing_contacts()
print(f"数据库中已有 {len(existing_contacts)} 个联系人")
# 每5张截图识别一次
step = 5
all_new_contacts = set()
skipped_count = 0
for i in range(0, len(screenshots), step):
batch = screenshots[i:i+step]
print(f"\n[{i+1}/{len(screenshots)}] 处理批次 {i//step + 1}")
# 只识别批次的第一张
path = batch[0]
result = ocr_image(path)
new_in_this = 0
for line in result.strip().split('\n'):
line = line.strip()
if is_valid_contact(line):
cleaned = clean_contact_name(line)
if cleaned and len(cleaned) >= 2:
if cleaned in existing_contacts:
skipped_count += 1
elif cleaned not in all_new_contacts:
new_in_this += 1
all_new_contacts.add(cleaned)
print(f" + {cleaned}")
print(f" 本轮新增 {new_in_this},累计新发现 {len(all_new_contacts)}")
print(f"\n{'='*60}")
print(f"OCR完成")
print(f"发现新联系人: {len(all_new_contacts)}")
print(f"跳过已存在: {skipped_count}")
# 入库
if all_new_contacts:
added = add_new_contacts(sorted(all_new_contacts, key=lambda x: (not x[0].isalpha() if x else True, x.lower() if x and x[0].isalpha() else x)))
print(f"成功入库: {added}")
# 更新JSON文件
conn = sqlite3.connect(r'D:\夏骥\微信研究\contacts.db')
cursor = conn.cursor()
cursor.execute('SELECT * FROM contacts ORDER BY id')
all_contacts = []
for row in cursor.fetchall():
all_contacts.append({
"id": row[0],
"name": row[1],
"category": row[2],
"blessing": row[3],
"selected": bool(row[4])
})
conn.close()
json_file = r"D:\夏骥\微信研究\contacts_data.json"
with open(json_file, 'w', encoding='utf-8') as f:
json.dump(all_contacts, f, ensure_ascii=False, indent=2)
print(f"JSON数据已更新: {json_file}")
print(f"数据库总联系人: {len(all_contacts)}")
if __name__ == '__main__':
main()

144
batch_ocr_full.py Normal file
View File

@@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
"""
对完整截图进行OCR识别
"""
import os
import requests
import base64
from PIL import Image
import glob
import json
def ocr_image(image_path):
"""OCR识别单张图片"""
with open(image_path, 'rb') as f:
image_base64 = base64.b64encode(f.read()).decode('utf-8')
url = "http://localhost:11434/api/chat"
payload = {
"model": "glm-ocr",
"messages": [{
"role": "user",
"content": """识别图片中的所有联系人名称。要求:
1. 只输出联系人名称,每行一个
2. 忽略分组标题如星号、字母A-Z等
3. 忽略数字统计
4. 不要添加任何其他内容""",
"images": [image_base64]
}],
"stream": False
}
try:
response = requests.post(url, json=payload, timeout=60)
return response.json().get('message', {}).get('content', '')
except Exception as e:
print(f"OCR失败: {e}")
return ""
def is_valid_contact(line):
"""判断是否是有效的联系人"""
line = line.strip()
if not line or len(line) < 2:
return False
invalid = ["公众号", "服务号", "企业微信联系人", "我的企业", "联系人",
"星标朋友", "新的朋友", "群聊", "标签", "仅聊天", "设备"]
if line in invalid:
return False
if len(line) == 1 and line.isalpha():
return False
if line.startswith(">") or line.startswith("!"):
return False
if line.startswith('"') or line.startswith('{') or line.startswith('[') or line.startswith('```'):
return False
if line.startswith('"') and ':' in line:
return False
return True
def clean_contact_name(name):
"""清理联系人名称"""
name = name.strip('"\'')
name = name.rstrip(',,。::')
return name.strip()
def main():
print("=" * 60)
print("批量OCR识别完整截图")
print("=" * 60)
# 获取截图目录
scroll_dir = r"D:\夏骥\微信研究\scroll_full"
if not os.path.exists(scroll_dir):
print(f"目录不存在: {scroll_dir}")
print("请先运行 scroll_full_contacts.py 进行截图")
return
screenshots = sorted(glob.glob(os.path.join(scroll_dir, "*.png")))
if not screenshots:
print("未找到截图文件!")
return
print(f"找到 {len(screenshots)} 张截图")
all_contacts = set()
for i, path in enumerate(screenshots):
print(f"\n[{i+1}/{len(screenshots)}] {os.path.basename(path)}")
result = ocr_image(path)
new_count = 0
for line in result.strip().split('\n'):
line = line.strip()
if is_valid_contact(line):
cleaned = clean_contact_name(line)
if cleaned and len(cleaned) >= 2 and cleaned not in all_contacts:
new_count += 1
print(f" + {cleaned}")
all_contacts.add(cleaned)
print(f" 本轮新增 {new_count},累计 {len(all_contacts)}")
# 生成JSON数据
contacts_json = []
for idx, name in enumerate(sorted(all_contacts, key=lambda x: (not x[0].isalpha() if x else True, x.lower() if x and x[0].isalpha() else x))):
if name and len(name) >= 2:
contacts_json.append({
"id": idx + 1,
"name": name,
"category": "",
"blessing": "马年新春快乐!愿您在新的一年里,事业腾飞,马到成功!",
"selected": False
})
# 保存JSON文件
json_file = r"D:\夏骥\微信研究\contacts_data.json"
with open(json_file, 'w', encoding='utf-8') as f:
json.dump(contacts_json, f, ensure_ascii=False, indent=2)
print(f"\nJSON数据已保存: {json_file}")
# 保存纯文本结果
result_file = r"D:\夏骥\微信研究\ocr_result_full.txt"
with open(result_file, 'w', encoding='utf-8') as f:
f.write(f"微信通讯录OCR识别结果完整\n")
f.write(f"共截图 {len(screenshots)}\n")
f.write(f"共识别 {len(contacts_json)} 个联系人\n")
f.write("=" * 60 + "\n\n")
for c in contacts_json:
f.write(f"{c['name']}\n")
print(f"结果已保存: {result_file}")
print(f"\n共识别到 {len(contacts_json)} 个不重复联系人")
if __name__ == '__main__':
main()

160
batch_ocr_parallel.py Normal file
View File

@@ -0,0 +1,160 @@
# -*- coding: utf-8 -*-
"""
并行OCR识别 - 多进程加速
"""
import os
import sqlite3
import requests
import base64
from PIL import Image
import glob
import json
from multiprocessing import Pool, Manager
import time
def ocr_image(args):
"""OCR识别单张图片"""
image_path, idx, total = args
try:
with open(image_path, 'rb') as f:
image_base64 = base64.b64encode(f.read()).decode('utf-8')
url = "http://localhost:11434/api/chat"
payload = {
"model": "glm-ocr",
"messages": [{
"role": "user",
"content": """识别图片中的所有联系人名称。要求:
1. 只输出联系人名称,每行一个
2. 忽略分组标题如星号、字母A-Z等
3. 忽略数字统计
4. 不要添加任何其他内容""",
"images": [image_base64]
}],
"stream": False
}
response = requests.post(url, json=payload, timeout=60)
result = response.json().get('message', {}).get('content', '')
contacts = []
for line in result.strip().split('\n'):
line = line.strip()
if line and len(line) >= 2 and len(line) < 50:
# 简单过滤
if not any(x in line for x in ['公众号', '服务号', '企业微信', '联系人', '星标朋友', '新的朋友']):
contacts.append(line.strip('"\'').rstrip(',,。::'))
print(f"[{idx+1}/{total}] {os.path.basename(image_path)}: 发现 {len(contacts)} 个联系人")
return contacts
except Exception as e:
print(f"[{idx+1}/{total}] {os.path.basename(image_path)}: 失败 - {e}")
return []
def get_existing_contacts():
"""从数据库获取已存在的联系人"""
conn = sqlite3.connect(r'D:\夏骥\微信研究\contacts.db')
cursor = conn.cursor()
cursor.execute('SELECT name FROM contacts')
existing = set(row[0] for row in cursor.fetchall())
conn.close()
return existing
def add_new_contacts(new_contacts):
"""将新联系人添加到数据库"""
if not new_contacts:
return 0
conn = sqlite3.connect(r'D:\夏骥\微信研究\contacts.db')
cursor = conn.cursor()
# 获取当前最大ID
cursor.execute('SELECT MAX(id) FROM contacts')
max_id = cursor.fetchone()[0] or 0
added = 0
for idx, name in enumerate(new_contacts, start=max_id + 1):
cursor.execute('''
INSERT INTO contacts (id, name, category, blessing, selected)
VALUES (?, ?, ?, ?, ?)
''', (idx, name, '', '马年新春快乐!愿您在新的一年里,事业腾飞,马到成功!', False))
added += 1
conn.commit()
conn.close()
return added
def main():
print("=" * 60)
print("并行OCR识别")
print("=" * 60)
# 获取截图目录
scroll_dir = r"D:\夏骥\微信研究\scroll_complete"
screenshots = sorted(glob.glob(os.path.join(scroll_dir, "*.png")))
if not screenshots:
print("未找到截图文件!")
return
print(f"找到 {len(screenshots)} 张截图")
# 获取已存在的联系人
existing_contacts = get_existing_contacts()
print(f"数据库中已有 {len(existing_contacts)} 个联系人")
# 准备参数
args_list = [(path, i, len(screenshots)) for i, path in enumerate(screenshots)]
# 并行处理 - 使用4个进程
print("\n开始并行OCR识别...")
all_contacts = set()
with Pool(processes=4) as pool:
results = pool.map(ocr_image, args_list)
# 收集结果
for contacts in results:
for name in contacts:
if name and len(name) >= 2 and name not in existing_contacts:
all_contacts.add(name)
print(f"\n{'='*60}")
print(f"OCR完成")
print(f"发现新联系人: {len(all_contacts)}")
# 入库
if all_contacts:
added = add_new_contacts(sorted(all_contacts, key=lambda x: (not x[0].isalpha() if x else True, x.lower() if x and x[0].isalpha() else x)))
print(f"成功入库: {added}")
# 更新JSON文件
conn = sqlite3.connect(r'D:\夏骥\微信研究\contacts.db')
cursor = conn.cursor()
cursor.execute('SELECT * FROM contacts ORDER BY id')
all_db_contacts = []
for row in cursor.fetchall():
all_db_contacts.append({
"id": row[0],
"name": row[1],
"category": row[2],
"blessing": row[3],
"selected": bool(row[4])
})
conn.close()
json_file = r"D:\夏骥\微信研究\contacts_data.json"
with open(json_file, 'w', encoding='utf-8') as f:
json.dump(all_db_contacts, f, ensure_ascii=False, indent=2)
print(f"JSON数据已更新: {json_file}")
print(f"数据库总联系人: {len(all_db_contacts)}")
if __name__ == '__main__':
main()

231
batch_ocr_resume.py Normal file
View File

@@ -0,0 +1,231 @@
# -*- coding: utf-8 -*-
"""
断点续传OCR识别
"""
import os
import sqlite3
import requests
import base64
from PIL import Image
import glob
import json
import time
def ocr_image(image_path):
"""OCR识别单张图片"""
with open(image_path, 'rb') as f:
image_base64 = base64.b64encode(f.read()).decode('utf-8')
url = "http://localhost:11434/api/chat"
payload = {
"model": "glm-ocr",
"messages": [{
"role": "user",
"content": """识别图片中的所有联系人名称。要求:
1. 只输出联系人名称,每行一个
2. 忽略分组标题如星号、字母A-Z等
3. 忽略数字统计
4. 不要添加任何其他内容""",
"images": [image_base64]
}],
"stream": False
}
try:
response = requests.post(url, json=payload, timeout=60)
return response.json().get('message', {}).get('content', '')
except Exception as e:
print(f"OCR失败: {e}")
return ""
def is_valid_contact(line):
"""判断是否是有效的联系人"""
line = line.strip()
if not line or len(line) < 2:
return False
invalid = ["公众号", "服务号", "企业微信联系人", "我的企业", "联系人",
"星标朋友", "新的朋友", "群聊", "标签", "仅聊天", "设备"]
if line in invalid:
return False
if len(line) == 1 and line.isalpha():
return False
if line.startswith(">") or line.startswith("!"):
return False
return True
def clean_contact_name(name):
"""清理联系人名称"""
name = name.strip('"\'')
name = name.rstrip(',,。::')
return name.strip()
def get_existing_contacts():
"""从数据库获取已存在的联系人"""
conn = sqlite3.connect(r'D:\夏骥\微信研究\contacts.db')
cursor = conn.cursor()
cursor.execute('SELECT name FROM contacts')
existing = set(row[0] for row in cursor.fetchall())
conn.close()
return existing
def add_new_contacts(new_contacts):
"""将新联系人添加到数据库"""
if not new_contacts:
return 0
conn = sqlite3.connect(r'D:\夏骥\微信研究\contacts.db')
cursor = conn.cursor()
# 获取当前最大ID
cursor.execute('SELECT MAX(id) FROM contacts')
max_id = cursor.fetchone()[0] or 0
added = 0
for idx, name in enumerate(new_contacts, start=max_id + 1):
cursor.execute('''
INSERT INTO contacts (id, name, category, blessing, selected)
VALUES (?, ?, ?, ?, ?)
''', (idx, name, '', '马年新春快乐!愿您在新的一年里,事业腾飞,马到成功!', False))
added += 1
conn.commit()
conn.close()
return added
def save_progress(progress):
"""保存进度"""
with open(r'D:\夏骥\微信研究\ocr_progress.json', 'w', encoding='utf-8') as f:
json.dump(progress, f, ensure_ascii=False, indent=2)
def load_progress():
"""加载进度"""
try:
with open(r'D:\夏骥\微信研究\ocr_progress.json', 'r', encoding='utf-8') as f:
return json.load(f)
except:
return {"processed": 0, "total": 0, "new_contacts": []}
def main():
print("=" * 60)
print("断点续传OCR识别")
print("=" * 60)
# 获取截图目录
scroll_dir = r"D:\夏骥\微信研究\scroll_complete"
screenshots = sorted(glob.glob(os.path.join(scroll_dir, "*.png")))
if not screenshots:
print("未找到截图文件!")
return
print(f"找到 {len(screenshots)} 张截图")
# 加载进度
progress = load_progress()
processed = progress.get("processed", 0)
all_new_contacts = set(progress.get("new_contacts", []))
print(f"已处理: {processed} 张,已发现新联系人: {len(all_new_contacts)}")
# 获取已存在的联系人
existing_contacts = get_existing_contacts()
print(f"数据库中已有 {len(existing_contacts)} 个联系人")
skipped_count = 0
batch_size = 50 # 每50张保存一次进度
start_time = time.time()
for i, path in enumerate(screenshots[processed:], start=processed):
current_time = time.time()
elapsed = current_time - start_time
avg_time = elapsed / (i - processed + 1) if i > processed else 0
remaining = avg_time * (len(screenshots) - i - 1)
percent = (i + 1) / len(screenshots) * 100
print(f"\n[{i+1}/{len(screenshots)}] {percent:.1f}% | 预计剩余: {remaining/60:.1f}分钟")
print(f" 文件: {os.path.basename(path)}")
result = ocr_image(path)
new_in_this = 0
for line in result.strip().split('\n'):
line = line.strip()
if is_valid_contact(line):
cleaned = clean_contact_name(line)
if cleaned and len(cleaned) >= 2:
if cleaned in existing_contacts:
skipped_count += 1
elif cleaned not in all_new_contacts:
new_in_this += 1
all_new_contacts.add(cleaned)
print(f" ✓ 新: {cleaned}")
print(f" 本轮: +{new_in_this} | 累计新: {len(all_new_contacts)} | 跳过: {skipped_count}")
# 保存进度
progress["processed"] = i + 1
progress["total"] = len(screenshots)
progress["new_contacts"] = list(all_new_contacts)
if (i + 1) % batch_size == 0:
save_progress(progress)
print(f" 💾 进度已保存 ({i+1}/{len(screenshots)})")
# 每10张显示汇总
if (i + 1) % 10 == 0:
print(f"\n{'='*60}")
print(f"📊 进度汇总: {i+1}/{len(screenshots)} ({percent:.1f}%)")
print(f"⏱️ 已用时间: {elapsed/60:.1f}分钟 | 预计剩余: {remaining/60:.1f}分钟")
print(f"👤 新联系人: {len(all_new_contacts)} | 跳过: {skipped_count}")
print(f"{'='*60}\n")
# 最终保存
save_progress(progress)
print(f"\n{'='*60}")
print(f"OCR完成")
print(f"发现新联系人: {len(all_new_contacts)}")
print(f"跳过已存在: {skipped_count}")
# 入库
if all_new_contacts:
added = add_new_contacts(sorted(all_new_contacts, key=lambda x: (not x[0].isalpha() if x else True, x.lower() if x and x[0].isalpha() else x)))
print(f"成功入库: {added}")
# 更新JSON文件
conn = sqlite3.connect(r'D:\夏骥\微信研究\contacts.db')
cursor = conn.cursor()
cursor.execute('SELECT * FROM contacts ORDER BY id')
all_contacts = []
for row in cursor.fetchall():
all_contacts.append({
"id": row[0],
"name": row[1],
"category": row[2],
"blessing": row[3],
"selected": bool(row[4])
})
conn.close()
json_file = r"D:\夏骥\微信研究\contacts_data.json"
with open(json_file, 'w', encoding='utf-8') as f:
json.dump(all_contacts, f, ensure_ascii=False, indent=2)
print(f"JSON数据已更新: {json_file}")
print(f"数据库总联系人: {len(all_contacts)}")
if __name__ == '__main__':
main()

734
contacts_manager.html Normal file
View File

@@ -0,0 +1,734 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>微信联系人新年祝福管理</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container-main {
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 30px;
}
.header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #eee;
}
.header h1 {
color: #333;
font-weight: bold;
}
.header .subtitle {
color: #666;
margin-top: 10px;
}
.stats-bar {
display: flex;
justify-content: center;
gap: 30px;
margin-bottom: 20px;
}
.stat-item {
text-align: center;
padding: 15px 25px;
background: #f8f9fa;
border-radius: 10px;
}
.stat-number {
font-size: 28px;
font-weight: bold;
color: #667eea;
}
.stat-label {
color: #666;
font-size: 14px;
}
.toolbar {
margin-bottom: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.contact-table {
width: 100%;
}
.contact-row {
display: grid;
grid-template-columns: 40px 1fr 120px 1fr 60px;
gap: 15px;
padding: 12px 15px;
border-bottom: 1px solid #eee;
align-items: center;
transition: background 0.2s;
}
.contact-row:hover {
background: #f8f9fa;
}
.contact-row.header-row {
background: #667eea;
color: white;
font-weight: bold;
border-radius: 8px;
}
.contact-name {
cursor: pointer;
color: #333;
font-weight: 500;
}
.contact-name:hover {
color: #667eea;
text-decoration: underline;
}
.category-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.category-badge.同事 { background: #e3f2fd; color: #1976d2; }
.category-badge.好友 { background: #e8f5e9; color: #388e3c; }
.category-badge.老师 { background: #fff3e0; color: #f57c00; }
.category-badge.亲戚 { background: #fce4ec; color: #c2185b; }
.category-badge.客户 { background: #f3e5f5; color: #7b1fa2; }
.category-badge.其他 { background: #f5f5f5; color: #616161; }
.category-badge:hover {
transform: scale(1.05);
}
.blessing-text {
color: #666;
cursor: pointer;
font-style: italic;
}
.blessing-text:hover {
color: #667eea;
}
.action-btn {
padding: 5px 15px;
border-radius: 5px;
border: none;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.btn-send {
background: #667eea;
color: white;
}
.btn-send:hover {
background: #5a6fd6;
}
.btn-select-all {
background: #28a745;
color: white;
}
.btn-export {
background: #17a2b8;
color: white;
}
.modal-content {
border-radius: 15px;
}
.modal-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 15px 15px 0 0;
}
.modal-header .btn-close {
filter: brightness(0) invert(1);
}
.category-select {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.category-option {
padding: 8px 20px;
border: 2px solid #ddd;
border-radius: 20px;
cursor: pointer;
transition: all 0.2s;
}
.category-option:hover {
border-color: #667eea;
}
.category-option.selected {
background: #667eea;
color: white;
border-color: #667eea;
}
.filter-tabs {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.filter-tab {
padding: 6px 15px;
border-radius: 20px;
background: #f0f0f0;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.filter-tab:hover, .filter-tab.active {
background: #667eea;
color: white;
}
.search-box {
margin-bottom: 15px;
}
.checkbox-wrapper {
display: flex;
align-items: center;
justify-content: center;
}
.checkbox-wrapper input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.empty-state {
text-align: center;
padding: 50px;
color: #999;
}
.pagination {
display: flex;
justify-content: center;
gap: 5px;
margin-top: 20px;
}
.page-btn {
padding: 8px 15px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 5px;
}
.page-btn:hover, .page-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
</style>
</head>
<body>
<div class="container container-main">
<div class="header">
<h1>🧧 微信联系人新年祝福管理</h1>
<div class="subtitle">马年新春,为您的联系人送上一份温暖的祝福</div>
</div>
<div class="stats-bar">
<div class="stat-item">
<div class="stat-number" id="totalCount">0</div>
<div class="stat-label">总联系人数</div>
</div>
<div class="stat-item">
<div class="stat-number" id="selectedCount">0</div>
<div class="stat-label">已选择发送</div>
</div>
<div class="stat-item">
<div class="stat-number" id="categorizedCount">0</div>
<div class="stat-label">已分类</div>
</div>
</div>
<div class="toolbar">
<button class="action-btn btn-select-all" onclick="toggleSelectAll()">全选/取消</button>
<button class="action-btn btn-send" onclick="sendBlessings()">发送祝福</button>
<button class="action-btn btn-export" onclick="exportData()">导出数据</button>
<button class="action-btn" style="background:#6c757d;color:white" onclick="importData()">导入数据</button>
</div>
<div class="search-box">
<input type="text" class="form-control" id="searchInput" placeholder="搜索联系人..." oninput="filterContacts()">
</div>
<div class="filter-tabs">
<div class="filter-tab active" onclick="filterByCategory('all')">全部</div>
<div class="filter-tab" onclick="filterByCategory('同事')">同事</div>
<div class="filter-tab" onclick="filterByCategory('好友')">好友</div>
<div class="filter-tab" onclick="filterByCategory('老师')">老师</div>
<div class="filter-tab" onclick="filterByCategory('亲戚')">亲戚</div>
<div class="filter-tab" onclick="filterByCategory('客户')">客户</div>
<div class="filter-tab" onclick="filterByCategory('其他')">其他</div>
<div class="filter-tab" onclick="filterByCategory('未分类')">未分类</div>
</div>
<div id="contactList">
<!-- 联系人列表将在这里动态生成 -->
</div>
<div class="pagination" id="pagination">
<!-- 分页将在这里动态生成 -->
</div>
</div>
<!-- 编辑联系人名称模态框 -->
<div class="modal fade" id="editNameModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑联系人名称</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="editContactId">
<div class="mb-3">
<label class="form-label">联系人名称</label>
<input type="text" class="form-control" id="editNameInput">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveName()">保存</button>
</div>
</div>
</div>
</div>
<!-- 设置分类模态框 -->
<div class="modal fade" id="categoryModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">设置分类</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="categoryContactId">
<div class="category-select" id="categorySelect">
<div class="category-option" data-category="同事" onclick="selectCategory(this)">同事</div>
<div class="category-option" data-category="好友" onclick="selectCategory(this)">好友</div>
<div class="category-option" data-category="老师" onclick="selectCategory(this)">老师</div>
<div class="category-option" data-category="亲戚" onclick="selectCategory(this)">亲戚</div>
<div class="category-option" data-category="客户" onclick="selectCategory(this)">客户</div>
<div class="category-option" data-category="其他" onclick="selectCategory(this)">其他</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveCategory()">保存</button>
</div>
</div>
</div>
</div>
<!-- 编辑祝福语模态框 -->
<div class="modal fade" id="blessingModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑祝福语</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="blessingContactId">
<div class="mb-3">
<label class="form-label">选择祝福语模板</label>
<select class="form-select" id="blessingTemplate" onchange="applyBlessingTemplate()">
<option value="">自定义</option>
<option value="马年新春快乐!愿您在新的一年里,事业腾飞,马到成功!">马年祝福1</option>
<option value="新春佳节,祝您马年大吉,万事如意,阖家幸福!">马年祝福2</option>
<option value="马年到来,愿您龙马精神,身体健康,财源广进!">马年祝福3</option>
<option value="新春快乐!祝您马年行大运,心想事成!">马年祝福4</option>
<option value="新年好!愿您马年一切顺利,步步高升!">马年祝福5</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">祝福语内容</label>
<textarea class="form-control" id="blessingInput" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveBlessing()">保存</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 数据存储
let contacts = [];
let currentPage = 1;
const pageSize = 20;
let currentFilter = 'all';
// 初始化
document.addEventListener('DOMContentLoaded', function() {
loadData();
});
// 从LocalStorage加载数据
function loadData() {
const saved = localStorage.getItem('wechatContacts');
if (saved) {
contacts = JSON.parse(saved);
renderContacts();
updateStats();
} else {
// 如果没有保存的数据尝试加载JSON文件
loadFromJsonFile();
}
}
// 从JSON文件加载
async function loadFromJsonFile() {
try {
const response = await fetch('contacts_data.json');
if (response.ok) {
contacts = await response.json();
saveData();
renderContacts();
updateStats();
alert('已加载联系人数据!');
} else {
// 使用示例数据
loadSampleData();
}
} catch (e) {
// 文件不存在或加载失败,使用示例数据
loadSampleData();
}
}
// 加载示例数据
function loadSampleData() {
const sampleNames = [
"深信服刘峻委", "深信服-刘艳彬", "深信服-刘英杰", "深信服马杰",
"沈阳营业部陈亮", "深圳分公司-姜沣原", "深圳通晓鹏", "神州数码刘月华"
];
contacts = sampleNames.map((name, index) => ({
id: index + 1,
name: name,
category: '',
blessing: '马年新春快乐!愿您在新的一年里,事业腾飞,马到成功!',
selected: false
}));
saveData();
renderContacts();
updateStats();
}
// 保存数据到LocalStorage
function saveData() {
localStorage.setItem('wechatContacts', JSON.stringify(contacts));
updateStats();
}
// 渲染联系人列表
function renderContacts() {
const listContainer = document.getElementById('contactList');
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
// 过滤
let filtered = contacts.filter(c => {
const matchSearch = c.name.toLowerCase().includes(searchTerm);
const matchCategory = currentFilter === 'all' ||
(currentFilter === '未分类' && !c.category) ||
c.category === currentFilter;
return matchSearch && matchCategory;
});
// 分页
const totalPages = Math.ceil(filtered.length / pageSize);
const start = (currentPage - 1) * pageSize;
const pageData = filtered.slice(start, start + pageSize);
if (pageData.length === 0) {
listContainer.innerHTML = '<div class="empty-state">暂无联系人数据</div>';
document.getElementById('pagination').innerHTML = '';
return;
}
// 表头
let html = `
<div class="contact-row header-row">
<div class="checkbox-wrapper">
<input type="checkbox" onchange="togglePageSelect(this.checked)">
</div>
<div>联系人名称</div>
<div>分类</div>
<div>祝福语</div>
<div>发送</div>
</div>
`;
// 数据行
pageData.forEach(contact => {
html += `
<div class="contact-row" data-id="${contact.id}">
<div class="checkbox-wrapper">
<input type="checkbox" ${contact.selected ? 'checked' : ''}
onchange="toggleContact(${contact.id}, this.checked)">
</div>
<div class="contact-name" onclick="editName(${contact.id})">${contact.name}</div>
<div>
<span class="category-badge ${contact.category || '其他'}"
onclick="editCategory(${contact.id})">
${contact.category || '点击分类'}
</span>
</div>
<div class="blessing-text" onclick="editBlessing(${contact.id})">
${contact.blessing || '点击设置祝福语'}
</div>
<div>
<input type="checkbox" class="send-checkbox"
${contact.selected ? 'checked' : ''}
onchange="toggleContact(${contact.id}, this.checked)">
</div>
</div>
`;
});
listContainer.innerHTML = html;
// 分页
renderPagination(totalPages);
}
// 渲染分页
function renderPagination(totalPages) {
const pagination = document.getElementById('pagination');
if (totalPages <= 1) {
pagination.innerHTML = '';
return;
}
let html = '';
for (let i = 1; i <= totalPages; i++) {
html += `<button class="page-btn ${i === currentPage ? 'active' : ''}"
onclick="goToPage(${i})">${i}</button>`;
}
pagination.innerHTML = html;
}
// 跳转页面
function goToPage(page) {
currentPage = page;
renderContacts();
}
// 更新统计
function updateStats() {
document.getElementById('totalCount').textContent = contacts.length;
document.getElementById('selectedCount').textContent = contacts.filter(c => c.selected).length;
document.getElementById('categorizedCount').textContent = contacts.filter(c => c.category).length;
}
// 切换联系人选择
function toggleContact(id, checked) {
const contact = contacts.find(c => c.id === id);
if (contact) {
contact.selected = checked;
saveData();
renderContacts();
}
}
// 全选/取消
function toggleSelectAll() {
const allSelected = contacts.every(c => c.selected);
contacts.forEach(c => c.selected = !allSelected);
saveData();
renderContacts();
}
// 当前页全选
function togglePageSelect(checked) {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
let filtered = contacts.filter(c => {
const matchSearch = c.name.toLowerCase().includes(searchTerm);
const matchCategory = currentFilter === 'all' ||
(currentFilter === '未分类' && !c.category) ||
c.category === currentFilter;
return matchSearch && matchCategory;
});
const start = (currentPage - 1) * pageSize;
const pageIds = filtered.slice(start, start + pageSize).map(c => c.id);
contacts.filter(c => pageIds.includes(c.id)).forEach(c => c.selected = checked);
saveData();
renderContacts();
}
// 搜索过滤
function filterContacts() {
currentPage = 1;
renderContacts();
}
// 分类过滤
function filterByCategory(category) {
currentFilter = category;
currentPage = 1;
document.querySelectorAll('.filter-tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
renderContacts();
}
// 编辑名称
function editName(id) {
const contact = contacts.find(c => c.id === id);
if (contact) {
document.getElementById('editContactId').value = id;
document.getElementById('editNameInput').value = contact.name;
new bootstrap.Modal(document.getElementById('editNameModal')).show();
}
}
// 保存名称
function saveName() {
const id = parseInt(document.getElementById('editContactId').value);
const newName = document.getElementById('editNameInput').value.trim();
if (newName) {
const contact = contacts.find(c => c.id === id);
if (contact) {
contact.name = newName;
saveData();
renderContacts();
}
}
bootstrap.Modal.getInstance(document.getElementById('editNameModal')).hide();
}
// 编辑分类
function editCategory(id) {
const contact = contacts.find(c => c.id === id);
if (contact) {
document.getElementById('categoryContactId').value = id;
document.querySelectorAll('.category-option').forEach(opt => {
opt.classList.remove('selected');
if (opt.dataset.category === contact.category) {
opt.classList.add('selected');
}
});
new bootstrap.Modal(document.getElementById('categoryModal')).show();
}
}
// 选择分类
function selectCategory(element) {
document.querySelectorAll('.category-option').forEach(opt => {
opt.classList.remove('selected');
});
element.classList.add('selected');
}
// 保存分类
function saveCategory() {
const id = parseInt(document.getElementById('categoryContactId').value);
const selected = document.querySelector('.category-option.selected');
if (selected) {
const contact = contacts.find(c => c.id === id);
if (contact) {
contact.category = selected.dataset.category;
saveData();
renderContacts();
}
}
bootstrap.Modal.getInstance(document.getElementById('categoryModal')).hide();
}
// 编辑祝福语
function editBlessing(id) {
const contact = contacts.find(c => c.id === id);
if (contact) {
document.getElementById('blessingContactId').value = id;
document.getElementById('blessingInput').value = contact.blessing || '';
document.getElementById('blessingTemplate').value = '';
new bootstrap.Modal(document.getElementById('blessingModal')).show();
}
}
// 应用祝福语模板
function applyBlessingTemplate() {
const template = document.getElementById('blessingTemplate').value;
if (template) {
document.getElementById('blessingInput').value = template;
}
}
// 保存祝福语
function saveBlessing() {
const id = parseInt(document.getElementById('blessingContactId').value);
const blessing = document.getElementById('blessingInput').value.trim();
const contact = contacts.find(c => c.id === id);
if (contact) {
contact.blessing = blessing;
saveData();
renderContacts();
}
bootstrap.Modal.getInstance(document.getElementById('blessingModal')).hide();
}
// 发送祝福(模拟)
function sendBlessings() {
const selected = contacts.filter(c => c.selected);
if (selected.length === 0) {
alert('请先选择要发送祝福的联系人!');
return;
}
let message = `即将向 ${selected.length} 位联系人发送祝福:\n\n`;
selected.forEach(c => {
message += `${c.name}: ${c.blessing}\n`;
});
message += '\n\n此为模拟功能实际发送需要在微信中操作';
alert(message);
}
// 导出数据
function exportData() {
const dataStr = JSON.stringify(contacts, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'wechat_contacts_backup.json';
a.click();
URL.revokeObjectURL(url);
}
// 导入数据
function importData() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = function(e) {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = function(e) {
try {
contacts = JSON.parse(e.target.result);
saveData();
renderContacts();
alert('导入成功!');
} catch (err) {
alert('导入失败:文件格式错误');
}
};
reader.readAsText(file);
};
input.click();
}
</script>
</body>
</html>

55
dedupe_contacts.py Normal file
View File

@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
"""
根据 search_name 去重,只保留一条记录
"""
import sqlite3
DB_PATH = r"D:\夏骥\微信研究\contacts.db"
def main():
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# 1. 查找重复的 search_name
cursor.execute('''
SELECT search_name, COUNT(*) as cnt
FROM contacts
WHERE search_name != ''
GROUP BY search_name
HAVING COUNT(*) > 1
ORDER BY cnt DESC
''')
duplicates = cursor.fetchall()
print(f'发现 {len(duplicates)} 个重复的搜索姓名')
# 2. 对每个重复的 search_name只保留 id 最小的一条
deleted_count = 0
for search_name, cnt in duplicates:
# 获取该 search_name 的所有 id
cursor.execute('''
SELECT id FROM contacts
WHERE search_name = ?
ORDER BY id
''', (search_name,))
ids = [row[0] for row in cursor.fetchall()]
# 保留第一个,删除其他的
keep_id = ids[0]
delete_ids = ids[1:]
if delete_ids:
placeholders = ','.join('?' * len(delete_ids))
cursor.execute(f'DELETE FROM contacts WHERE id IN ({placeholders})', delete_ids)
deleted_count += len(delete_ids)
print(f' "{search_name}": 保留 id={keep_id}, 删除 {len(delete_ids)}')
conn.commit()
conn.close()
print(f'\n共删除 {deleted_count} 条重复记录')
if __name__ == '__main__':
main()

98
init_db.py Normal file
View File

@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
"""
初始化SQLite数据库并导入联系人数据
"""
import sqlite3
import json
import os
# 数据库文件
DB_PATH = r"D:\夏骥\微信研究\contacts.db"
# JSON数据文件
JSON_PATH = r"D:\夏骥\微信研究\contacts_data.json"
def init_database():
"""初始化数据库"""
# 删除旧数据库(如果存在)
if os.path.exists(DB_PATH):
os.remove(DB_PATH)
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# 创建联系人表
cursor.execute('''
CREATE TABLE contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
category TEXT DEFAULT '',
blessing TEXT DEFAULT '马年新春快乐!愿您在新的一年里,事业腾飞,马到成功!',
selected INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
return conn
def import_from_json(conn):
"""从JSON导入联系人"""
cursor = conn.cursor()
# 读取JSON数据
with open(JSON_PATH, 'r', encoding='utf-8') as f:
contacts = json.load(f)
# 导入联系人
for contact in contacts:
cursor.execute('''
INSERT INTO contacts (name, category, blessing, selected)
VALUES (?, ?, ?, ?)
''', (
contact.get('name', ''),
contact.get('category', ''),
contact.get('blessing', '马年新春快乐!愿您在新的一年里,事业腾飞,马到成功!'),
1 if contact.get('selected', False) else 0
))
conn.commit()
print(f"已导入 {len(contacts)} 个联系人到数据库")
def main():
print("=" * 50)
print("初始化数据库")
print("=" * 50)
# 初始化数据库
conn = init_database()
print("数据库表已创建")
# 导入数据
if os.path.exists(JSON_PATH):
import_from_json(conn)
else:
print(f"JSON文件不存在: {JSON_PATH}")
# 验证数据
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM contacts")
count = cursor.fetchone()[0]
print(f"\n数据库中共有 {count} 个联系人")
# 显示示例数据
cursor.execute("SELECT id, name, category, blessing, selected FROM contacts LIMIT 5")
print("\n示例数据:")
for row in cursor.fetchall():
print(f" ID:{row[0]} 姓名:{row[1]} 分类:{row[2]} 祝福语:{row[3][:20]}...")
conn.close()
print(f"\n数据库文件: {DB_PATH}")
if __name__ == '__main__':
main()

224
ocr_wechat_contacts.py Normal file
View File

@@ -0,0 +1,224 @@
# -*- coding: utf-8 -*-
"""
使用OCR识别微信通讯录 - 两阶段处理
第一阶段:快速滚动截图直到到底
第二阶段批量OCR识别所有截图
"""
import uiautomation as auto
import time
import requests
import base64
import os
from PIL import Image
def capture_wechat_window():
"""截取微信窗口"""
wechat_window = auto.WindowControl(searchDepth=1, Name='微信')
if not wechat_window.Exists(3, 1):
print("未找到微信窗口!请确保微信已打开并登录。")
return None, None
print(f"找到微信窗口: {wechat_window.Name}")
screenshot_path = r"D:\夏骥\微信研究\wechat_screenshot.png"
wechat_window.CaptureToImage(screenshot_path)
return screenshot_path, wechat_window
def capture_contact_region(wechat_window, index):
"""截取通讯录区域"""
rect = wechat_window.BoundingRectangle
x_offset = 70
y_offset = 130
width = 280
height = rect.height() - 160
screenshot_path = f"D:\\夏骥\\微信研究\\scroll\\region_{index:03d}.png"
try:
bitmap = wechat_window.ToBitmap(x=x_offset, y=y_offset, width=width, height=height)
bitmap.ToFile(screenshot_path)
return screenshot_path
except Exception as e:
print(f"截图失败: {e}")
return None
def get_image_hash(image_path):
"""计算图片哈希"""
try:
img = Image.open(image_path)
img = img.resize((16, 16), Image.Resampling.LANCZOS)
img = img.convert('L')
pixels = list(img.get_flattened_data())
avg = sum(pixels) / len(pixels)
return ''.join(['1' if p > avg else '0' for p in pixels])
except:
return None
def images_similarity(hash1, hash2):
"""计算相似度"""
if not hash1 or not hash2:
return 0
diff = sum(c1 != c2 for c1, c2 in zip(hash1, hash2))
return 1 - diff / len(hash1)
def scroll_down(wechat_window):
"""向下滚动"""
try:
rect = wechat_window.BoundingRectangle
center_x = rect.left + 200
center_y = rect.top + 400
auto.SetCursorPos(center_x, center_y)
auto.Click(center_x, center_y)
time.sleep(0.2)
auto.WheelDown(wheelTimes=3)
time.sleep(0.3)
return True
except:
return False
def ocr_image(image_path):
"""OCR识别单张图片"""
with open(image_path, 'rb') as f:
image_base64 = base64.b64encode(f.read()).decode('utf-8')
url = "http://localhost:11434/api/chat"
payload = {
"model": "glm-ocr",
"messages": [{
"role": "user",
"content": """识别图片中的所有联系人名称。要求:
1. 只输出联系人名称,每行一个
2. 忽略分组标题如星号、字母A-Z等
3. 忽略数字统计
4. 不要添加任何其他内容""",
"images": [image_base64]
}],
"stream": False
}
try:
response = requests.post(url, json=payload, timeout=60)
return response.json().get('message', {}).get('content', '')
except Exception as e:
print(f"OCR失败: {e}")
return ""
def is_valid_contact(line):
"""判断是否是有效的联系人"""
line = line.strip()
if not line:
return False
# 过滤分组标题
invalid = ["公众号", "服务号", "企业微信联系人", "我的企业", "联系人",
"星标朋友", "新的朋友", "群聊", "标签", "仅聊天", "设备"]
if line in invalid:
return False
if len(line) == 1 and line.isalpha():
return False
if line.startswith(">") or line.startswith("!"):
return False
return True
def main():
print("=" * 60)
print("微信通讯录OCR识别 - 两阶段处理")
print("=" * 60)
# 创建截图目录
scroll_dir = r"D:\夏骥\微信研究\scroll"
os.makedirs(scroll_dir, exist_ok=True)
# 清理旧截图
for f in os.listdir(scroll_dir):
if f.endswith('.png'):
os.remove(os.path.join(scroll_dir, f))
# ===== 第一阶段:快速滚动截图 =====
print("\n[阶段1] 滚动截图中...")
_, wechat_window = capture_wechat_window()
if not wechat_window:
return
screenshots = []
last_hash = None
no_change = 0
max_screenshots = 100
for i in range(max_screenshots):
path = capture_contact_region(wechat_window, i)
if path:
screenshots.append(path)
print(f" 截图 {i+1}: {path}")
# 检测是否到底
current_hash = get_image_hash(path)
if last_hash:
sim = images_similarity(last_hash, current_hash)
if sim > 0.95:
no_change += 1
if no_change >= 2:
print(f"\n 检测到到底,共截图 {len(screenshots)}")
break
else:
no_change = 0
last_hash = current_hash
scroll_down(wechat_window)
print(f"\n[阶段1完成] 共截图 {len(screenshots)}")
# ===== 第二阶段批量OCR =====
print("\n[阶段2] OCR识别中...")
all_contacts = set()
for i, path in enumerate(screenshots):
print(f" OCR {i+1}/{len(screenshots)}: ", end="", flush=True)
result = ocr_image(path)
new_count = 0
for line in result.strip().split('\n'):
line = line.strip()
if is_valid_contact(line):
if line not in all_contacts:
new_count += 1
all_contacts.add(line)
print(f"新增 {new_count} 个,累计 {len(all_contacts)}")
# ===== 保存结果 =====
print("\n[保存结果]")
sorted_contacts = sorted(all_contacts, key=lambda x: (not x[0].isalpha(), x.lower() if x[0].isalpha() else x))
result_file = r"D:\夏骥\微信研究\ocr_result.txt"
with open(result_file, 'w', encoding='utf-8') as f:
f.write(f"微信通讯录OCR识别结果\n")
f.write(f"共截图 {len(screenshots)}\n")
f.write(f"共识别 {len(all_contacts)} 个联系人\n")
f.write("=" * 60 + "\n\n")
for c in sorted_contacts:
f.write(f"{c}\n")
print(f"结果已保存: {result_file}")
print(f"\n共识别到 {len(all_contacts)} 个联系人")
if __name__ == '__main__':
main()

356
print_wechat_contacts.py Normal file
View File

@@ -0,0 +1,356 @@
# -*- coding: utf-8 -*-
"""
打印PC微信通讯录的所有子控件
需要以管理员权限运行Python
"""
import uiautomation as auto
import time
import ctypes
from ctypes import wintypes, POINTER, byref, c_int, c_ulong, c_void_p
import comtypes.client
def print_control_tree(control, depth=0, max_depth=20, file=None):
"""递归打印控件树"""
if depth > max_depth:
return
# 获取控件信息
control_type = control.ControlTypeName
class_name = control.ClassName or ""
name = control.Name or ""
automation_id = control.AutomationId or ""
rect = control.BoundingRectangle
# 缩进
indent = " " * depth
# 打印控件信息
line = f"{indent}ControlType: {control_type} ClassName: {class_name} Name: {name} AutomationId: {automation_id} Rect: {rect} Depth: {depth}"
print(line)
if file:
file.write(line + "\n")
# 递归打印子控件
try:
children = control.GetChildren()
for child in children:
print_control_tree(child, depth + 1, max_depth, file)
except Exception as e:
error_line = f"{indent} [Error getting children: {e}]"
print(error_line)
if file:
file.write(error_line + "\n")
def print_control_tree_raw_comtypes(uiAutomation, element, depth=0, max_depth=20, file=None):
"""使用comtypes直接访问IUIAutomation接口"""
if depth > max_depth:
return
try:
# 获取控件属性
name = element.CurrentName or ""
class_name = element.CurrentClassName or ""
automation_id = element.CurrentAutomationId or ""
control_type_id = element.CurrentControlType
localized_control_type = element.CurrentLocalizedControlType or ""
# 获取BoundingRectangle
try:
rect = element.CurrentBoundingRectangle
rect_str = f"({rect.left},{rect.top},{rect.right},{rect.bottom})"
except:
rect_str = "N/A"
indent = " " * depth
line = f"{indent}ControlType: {localized_control_type} ClassName: {class_name} Name: {name} AutomationId: {automation_id} Rect: {rect_str} Depth: {depth}"
print(line)
if file:
file.write(line + "\n")
# 使用RawViewWalker遍历子元素
tree_walker = uiAutomation.RawViewWalker
child = tree_walker.GetFirstChildElement(element)
while child:
print_control_tree_raw_comtypes(uiAutomation, child, depth + 1, max_depth, file)
# 获取下一个兄弟元素
child = tree_walker.GetNextSiblingElement(child)
except Exception as e:
indent = " " * depth
error_line = f"{indent}[Error: {e}]"
print(error_line)
if file:
file.write(error_line + "\n")
def enum_windows_callback(hwnd, results):
"""枚举窗口回调函数"""
import win32gui
import win32process
if win32gui.IsWindowVisible(hwnd):
class_name = win32gui.GetClassName(hwnd)
title = win32gui.GetWindowText(hwnd)
_, pid = win32process.GetWindowThreadProcessId(hwnd)
# 检查是否是微信相关窗口
try:
import subprocess
result = subprocess.run(['tasklist', '/FI', f'PID eq {pid}', '/NH'],
capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW)
if 'WeChat' in result.stdout or 'wechat' in result.stdout.lower():
results.append((hwnd, class_name, title, pid))
except:
pass
return True
def print_win32_children(hwnd, depth=0, max_depth=20, file=None):
"""使用Win32 API枚举子窗口"""
import win32gui
if depth > max_depth:
return
class_name = win32gui.GetClassName(hwnd)
title = win32gui.GetWindowText(hwnd)
rect = win32gui.GetWindowRect(hwnd)
indent = " " * depth
line = f"{indent}HWND: 0x{hwnd:X} ClassName: {class_name} Title: {title} Rect: {rect} Depth: {depth}"
print(line)
if file:
file.write(line + "\n")
# 枚举子窗口
def child_callback(child_hwnd, _):
print_win32_children(child_hwnd, depth + 1, max_depth, file)
return True
try:
win32gui.EnumChildWindows(hwnd, child_callback, None)
except:
pass
def print_accessible_tree(hwnd, depth=0, max_depth=20, file=None):
"""使用IAccessible接口枚举控件"""
import ctypes
from ctypes import POINTER, byref
from ctypes import windll, oledll
if depth > max_depth:
return
try:
import comtypes.client
from comtypes import IUnknown
# 获取IAccessible接口
accessible = oledll.oleacc.AccessibleObjectFromWindow(
hwnd,
0xFFFFFFFC, # OBJID_CLIENT
comtypes.IUnknown._iid_,
byref(ctypes.POINTER(comtypes.IUnknown)())
)
# 尝试获取更多信息
indent = " " * depth
line = f"{indent}HWND: 0x{hwnd:X} [Accessible Object Available]"
print(line)
if file:
file.write(line + "\n")
except Exception as e:
indent = " " * depth
line = f"{indent}HWND: 0x{hwnd:X} [No IAccessible: {e}]"
print(line)
if file:
file.write(line + "\n")
def main():
print("=" * 80)
print("打印PC微信通讯录的所有子控件")
print("=" * 80)
print()
# 设置全局搜索超时时间
auto.SetGlobalSearchTimeout(10)
# 获取桌面根控件
root = auto.GetRootControl()
print("桌面根控件:", root)
print()
# 查找微信主窗口 - 尝试多种方式
print("正在查找微信窗口...")
wechat_window = None
# 方式1: 通过ClassName查找 (旧版微信)
wechat_window = auto.WindowControl(searchDepth=1, ClassName='WeChatMainWndForPC')
if wechat_window.Exists(2, 1):
print(f"方式1找到微信窗口: {wechat_window.Name}, ClassName: {wechat_window.ClassName}")
else:
# 方式2: 通过Name查找
wechat_window = auto.WindowControl(searchDepth=1, Name='微信')
if wechat_window.Exists(2, 1):
print(f"方式2找到微信窗口: {wechat_window.Name}, ClassName: {wechat_window.ClassName}")
else:
# 方式3: 通过进程名查找 (新版微信 WeChatAppEx.exe)
print("尝试通过进程查找微信窗口...")
for window in root.GetChildren():
try:
process_id = window.ProcessId
import subprocess
result = subprocess.run(['tasklist', '/FI', f'PID eq {process_id}', '/NH'],
capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW)
if 'WeChat' in result.stdout or 'wechat' in result.stdout.lower():
wechat_window = window
print(f"方式3找到微信窗口: {window.Name}, ClassName: {window.ClassName}, PID: {process_id}")
break
except Exception as e:
continue
if wechat_window is None or not wechat_window.Exists(1, 1):
print("\n未找到微信窗口!请确保:")
print("1. 微信已打开并登录")
print("2. 以管理员权限运行此脚本")
print("\n尝试打印所有顶级窗口以帮助调试...")
print("=" * 80)
for window in root.GetChildren():
try:
print(f"Window: Name='{window.Name}' ClassName='{window.ClassName}' PID={window.ProcessId}")
except:
pass
return
print()
# 打印微信窗口的基本信息
print("=" * 80)
print("微信主窗口信息:")
print("=" * 80)
print(f"Name: {wechat_window.Name}")
print(f"ClassName: {wechat_window.ClassName}")
print(f"AutomationId: {wechat_window.AutomationId}")
print(f"ControlType: {wechat_window.ControlTypeName}")
print(f"ProcessId: {wechat_window.ProcessId}")
print(f"Handle: {wechat_window.NativeWindowHandle}")
print()
# 输出文件
output_file = r"D:\夏骥\微信研究\wechat_controls_output.txt"
# 首先尝试标准方法
print("=" * 80)
print("方式1: 使用标准UIAutomation遍历控件...")
print("=" * 80)
with open(output_file, 'w', encoding='utf-8') as f:
f.write("=" * 80 + "\n")
f.write("微信窗口控件树 - 标准UIAutomation\n")
f.write("=" * 80 + "\n\n")
f.write(f"微信窗口: Name={wechat_window.Name}, ClassName={wechat_window.ClassName}\n\n")
print_control_tree(wechat_window, depth=0, max_depth=20, file=f)
print()
print(f"标准方法输出已保存到: {output_file}")
print()
# 尝试使用RawViewWalker (可以获取更多控件)
print("=" * 80)
print("方式2: 使用RawViewWalker遍历控件 (包括原始控件)...")
print("=" * 80)
output_file_raw = r"D:\夏骥\微信研究\wechat_controls_raw.txt"
try:
# 加载UIAutomation类型库
comtypes.client.GetModule('UIAutomationCore.dll')
from comtypes.gen.UIAutomationClient import IUIAutomation, CUIAutomation
# 创建IUIAutomation实例
uiAutomation = comtypes.CoCreateInstance(CUIAutomation._reg_clsid_, interface=IUIAutomation)
# 获取RootElement
root_element = uiAutomation.GetRootElement()
# 找到微信窗口元素
condition = uiAutomation.CreatePropertyCondition(30005, "微信") # UIA_NamePropertyId = 30005
# 查找微信窗口 (TreeScope_Children = 2)
wechat_element = root_element.FindFirst(2, condition)
if wechat_element:
with open(output_file_raw, 'w', encoding='utf-8') as f:
f.write("=" * 80 + "\n")
f.write("微信窗口控件树 - RawViewWalker\n")
f.write("=" * 80 + "\n\n")
print_control_tree_raw_comtypes(uiAutomation, wechat_element, depth=0, max_depth=20, file=f)
print(f"\nRawViewWalker输出已保存到: {output_file_raw}")
else:
print("使用RawViewWalker未找到微信窗口元素")
except Exception as e:
print(f"RawViewWalker方法出错: {e}")
import traceback
traceback.print_exc()
print()
# 尝试使用Win32 API枚举窗口
print("=" * 80)
print("方式3: 使用Win32 API枚举所有子窗口...")
print("=" * 80)
try:
import win32gui
import win32process
hwnd = wechat_window.NativeWindowHandle
output_file_win32 = r"D:\夏骥\微信研究\wechat_controls_win32.txt"
with open(output_file_win32, 'w', encoding='utf-8') as f:
f.write("=" * 80 + "\n")
f.write("微信窗口控件树 - Win32 API\n")
f.write("=" * 80 + "\n\n")
print_win32_children(hwnd, depth=0, max_depth=20, file=f)
print(f"\nWin32 API输出已保存到: {output_file_win32}")
except ImportError:
print("未安装pywin32跳过Win32 API方式")
print("可以通过 'pip install pywin32' 安装")
except Exception as e:
print(f"Win32 API方法出错: {e}")
import traceback
traceback.print_exc()
print()
print("=" * 80)
print("完成!")
print("=" * 80)
print()
print("说明:")
print("新版微信(WeChatAppEx.exe)使用CEF/Chromium渲染UI")
print("其内部控件不通过Windows原生控件实现")
print("因此UIAutomation和Win32 API都无法直接访问其内部控件。")
print()
print("如需获取微信控件信息,可以尝试:")
print("1. 使用Microsoft Accessibility Insights工具")
print("2. 使用Chrome DevTools Protocol (如果微信支持)")
print("3. 使用OCR或图像识别技术")
if __name__ == '__main__':
main()

156
scroll_complete.py Normal file
View File

@@ -0,0 +1,156 @@
# -*- coding: utf-8 -*-
"""
完整截取微信通讯录 - 小步滚动确保不遗漏
"""
import uiautomation as auto
import time
import os
from PIL import Image
def find_wechat_window():
"""查找微信窗口"""
wechat_window = auto.WindowControl(searchDepth=1, Name='微信')
if wechat_window.Exists(3, 1):
return wechat_window
return None
def capture_contact_region(wechat_window, index, save_dir):
"""截取通讯录区域"""
rect = wechat_window.BoundingRectangle
# 通讯录列表区域
x_offset = 70
y_offset = 130
width = 280
height = rect.height() - 160
screenshot_path = os.path.join(save_dir, f"region_{index:04d}.png")
try:
bitmap = wechat_window.ToBitmap(x=x_offset, y=y_offset, width=width, height=height)
bitmap.ToFile(screenshot_path)
return screenshot_path
except Exception as e:
print(f"截图失败: {e}")
return None
def get_image_hash(image_path):
"""计算图片感知哈希"""
try:
img = Image.open(image_path)
img = img.resize((32, 32), Image.Resampling.LANCZOS)
img = img.convert('L')
pixels = list(img.getdata())
avg = sum(pixels) / len(pixels)
return ''.join(['1' if p > avg else '0' for p in pixels])
except Exception as e:
print(f"哈希计算失败: {e}")
return None
def images_similarity(hash1, hash2):
"""计算图片相似度"""
if not hash1 or not hash2:
return 0
diff = sum(c1 != c2 for c1, c2 in zip(hash1, hash2))
return 1 - diff / len(hash1)
def scroll_down(wechat_window, wheel_times=1):
"""向下滚动 - 小幅度滚动"""
try:
rect = wechat_window.BoundingRectangle
center_x = rect.left + 200
center_y = rect.top + 400
auto.SetCursorPos(center_x, center_y)
auto.Click(center_x, center_y)
time.sleep(0.15)
auto.WheelDown(wheelTimes=wheel_times)
time.sleep(0.25)
return True
except Exception as e:
print(f"滚动失败: {e}")
return False
def main():
print("=" * 60)
print("完整截取微信通讯录 - 小步滚动版本")
print("=" * 60)
# 创建新的截图目录
save_dir = r"D:\夏骥\微信研究\scroll_complete"
# 清空或创建目录
if os.path.exists(save_dir):
import shutil
shutil.rmtree(save_dir)
os.makedirs(save_dir)
print(f"截图保存目录: {save_dir}")
# 查找微信窗口
print("\n查找微信窗口...")
wechat_window = find_wechat_window()
if not wechat_window:
print("未找到微信窗口!请确保微信已打开。")
return
print(f"找到微信窗口: {wechat_window.Name}")
# 提示用户准备
print("\n" + "!" * 60)
print("请确保微信通讯录界面已打开,并滚动到最顶部!")
print("按字母顺序从A开始显示联系人列表")
print("!" * 60)
time.sleep(3)
# 开始截图
print("\n开始截图...")
screenshots = []
last_hash = None
no_change_count = 0
max_screenshots = 2000 # 最大截图数
consecutive_same = 3 # 连续相同截图数判定到底
for i in range(max_screenshots):
path = capture_contact_region(wechat_window, i, save_dir)
if not path:
print(f"截图 {i} 失败,跳过")
continue
screenshots.append(path)
print(f" 截图 {i + 1}: {os.path.basename(path)}")
# 检测是否到底(图片相似度)
current_hash = get_image_hash(path)
if last_hash:
sim = images_similarity(last_hash, current_hash)
if sim > 0.98: # 非常相似
no_change_count += 1
print(f" -> 相似度 {sim:.2%},连续相同 {no_change_count}")
if no_change_count >= consecutive_same:
print(f"\n检测到底部!连续 {consecutive_same} 张截图相同")
break
else:
no_change_count = 0
last_hash = current_hash
# 小幅度滚动每次滚动1格
scroll_down(wechat_window, wheel_times=1)
print("\n" + "=" * 60)
print(f"截图完成!共 {len(screenshots)}")
print(f"保存目录: {save_dir}")
print("=" * 60)
# 提示下一步
print("\n下一步:运行 batch_ocr_complete.py 进行OCR识别")
if __name__ == '__main__':
main()

167
scroll_full_contacts.py Normal file
View File

@@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
"""
完整抓取微信通讯录 - 从头开始滚动到底
"""
import uiautomation as auto
import time
import os
from PIL import Image
def find_wechat_window():
"""查找微信窗口"""
wechat_window = auto.WindowControl(searchDepth=1, Name='微信')
if wechat_window.Exists(3, 1):
return wechat_window
return None
def capture_contact_region(wechat_window, index, save_dir):
"""截取通讯录区域"""
rect = wechat_window.BoundingRectangle
x_offset = 70
y_offset = 130
width = 280
height = rect.height() - 160
screenshot_path = os.path.join(save_dir, f"region_{index:03d}.png")
try:
bitmap = wechat_window.ToBitmap(x=x_offset, y=y_offset, width=width, height=height)
bitmap.ToFile(screenshot_path)
return screenshot_path
except Exception as e:
print(f"截图失败: {e}")
return None
def get_image_hash(image_path):
"""计算图片哈希"""
try:
img = Image.open(image_path)
img = img.resize((16, 16), Image.Resampling.LANCZOS)
img = img.convert('L')
pixels = list(img.get_flattened_data())
avg = sum(pixels) / len(pixels)
return ''.join(['1' if p > avg else '0' for p in pixels])
except:
return None
def images_similarity(hash1, hash2):
"""计算相似度"""
if not hash1 or not hash2:
return 0
diff = sum(c1 != c2 for c1, c2 in zip(hash1, hash2))
return 1 - diff / len(hash1)
def scroll_to_top(wechat_window):
"""滚动到顶部"""
print("滚动到顶部...")
rect = wechat_window.BoundingRectangle
center_x = rect.left + 200
center_y = rect.top + 400
# 点击获取焦点
auto.SetCursorPos(center_x, center_y)
auto.Click(center_x, center_y)
time.sleep(0.3)
# 多次向上滚动确保到顶部
for _ in range(20):
auto.SendKeys('{Home}')
time.sleep(0.2)
# 额外滚动几次PageUp
for _ in range(5):
auto.SendKeys('{PageUp}')
time.sleep(0.3)
print("已滚动到顶部")
time.sleep(1)
def scroll_down(wechat_window):
"""向下滚动一页"""
try:
rect = wechat_window.BoundingRectangle
center_x = rect.left + 200
center_y = rect.top + 400
auto.SetCursorPos(center_x, center_y)
auto.Click(center_x, center_y)
time.sleep(0.2)
auto.WheelDown(wheelTimes=3)
time.sleep(0.4)
return True
except:
return False
def main():
print("=" * 60)
print("完整抓取微信通讯录")
print("=" * 60)
# 创建新的截图目录
save_dir = r"D:\夏骥\微信研究\scroll_full"
os.makedirs(save_dir, exist_ok=True)
# 清理旧截图
for f in os.listdir(save_dir):
if f.endswith('.png'):
os.remove(os.path.join(save_dir, f))
# 查找微信窗口
print("\n查找微信窗口...")
wechat_window = find_wechat_window()
if not wechat_window:
print("未找到微信窗口!")
return
print(f"找到微信窗口: {wechat_window.Name}")
# 滚动到顶部
print("\n请确保微信通讯录界面已打开...")
time.sleep(2)
scroll_to_top(wechat_window)
# 开始截图
print("\n开始滚动截图...")
screenshots = []
last_hash = None
no_change = 0
max_screenshots = 150
for i in range(max_screenshots):
path = capture_contact_region(wechat_window, i, save_dir)
if path:
screenshots.append(path)
print(f" 截图 {i+1}")
# 检测是否到底
current_hash = get_image_hash(path)
if last_hash:
sim = images_similarity(last_hash, current_hash)
if sim > 0.95:
no_change += 1
if no_change >= 2:
print(f"\n检测到到底,共截图 {len(screenshots)}")
break
else:
no_change = 0
last_hash = current_hash
scroll_down(wechat_window)
print(f"\n完成!共截图 {len(screenshots)}")
print(f"截图保存目录: {save_dir}")
# 提示运行OCR
print("\n接下来请运行 batch_ocr_full.py 进行OCR识别")
if __name__ == '__main__':
main()

143
scroll_full_continue.py Normal file
View File

@@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
"""
继续截取微信通讯录 - 从当前位置继续滚动到底
"""
import uiautomation as auto
import time
import os
from PIL import Image
def find_wechat_window():
"""查找微信窗口"""
wechat_window = auto.WindowControl(searchDepth=1, Name='微信')
if wechat_window.Exists(3, 1):
return wechat_window
return None
def capture_contact_region(wechat_window, index, save_dir):
"""截取通讯录区域"""
rect = wechat_window.BoundingRectangle
x_offset = 70
y_offset = 130
width = 280
height = rect.height() - 160
screenshot_path = os.path.join(save_dir, f"region_{index:03d}.png")
try:
bitmap = wechat_window.ToBitmap(x=x_offset, y=y_offset, width=width, height=height)
bitmap.ToFile(screenshot_path)
return screenshot_path
except Exception as e:
print(f"截图失败: {e}")
return None
def get_image_hash(image_path):
"""计算图片哈希"""
try:
img = Image.open(image_path)
img = img.resize((16, 16), Image.Resampling.LANCZOS)
img = img.convert('L')
pixels = list(img.get_flattened_data())
avg = sum(pixels) / len(pixels)
return ''.join(['1' if p > avg else '0' for p in pixels])
except:
return None
def images_similarity(hash1, hash2):
"""计算相似度"""
if not hash1 or not hash2:
return 0
diff = sum(c1 != c2 for c1, c2 in zip(hash1, hash2))
return 1 - diff / len(hash1)
def scroll_down(wechat_window):
"""向下滚动一页"""
try:
rect = wechat_window.BoundingRectangle
center_x = rect.left + 200
center_y = rect.top + 400
auto.SetCursorPos(center_x, center_y)
auto.Click(center_x, center_y)
time.sleep(0.2)
auto.WheelDown(wheelTimes=3)
time.sleep(0.4)
return True
except:
return False
def main():
print("=" * 60)
print("继续截取微信通讯录")
print("=" * 60)
# 使用已有的截图目录
save_dir = r"D:\夏骥\微信研究\scroll_full"
# 获取已有截图数量
existing_files = [f for f in os.listdir(save_dir) if f.endswith('.png')]
start_index = len(existing_files)
print(f"已有 {start_index} 张截图")
print(f"将从第 {start_index + 1} 张开始继续...")
# 查找微信窗口
print("\n查找微信窗口...")
wechat_window = find_wechat_window()
if not wechat_window:
print("未找到微信窗口!")
return
print(f"找到微信窗口: {wechat_window.Name}")
# 提示用户
print("\n请确保微信通讯录界面已打开并滚动到之前的位置...")
time.sleep(3)
# 开始截图
print("\n开始继续截图...")
screenshots = []
last_hash = None
no_change = 0
max_screenshots = 100
for i in range(max_screenshots):
actual_index = start_index + i
path = capture_contact_region(wechat_window, actual_index, save_dir)
if path:
screenshots.append(path)
print(f" 截图 {actual_index + 1}")
# 检测是否到底
current_hash = get_image_hash(path)
if last_hash:
sim = images_similarity(last_hash, current_hash)
if sim > 0.95:
no_change += 1
if no_change >= 2:
print(f"\n检测到到底,共新增截图 {len(screenshots)}")
break
else:
no_change = 0
last_hash = current_hash
scroll_down(wechat_window)
print(f"\n完成!新增 {len(screenshots)} 张截图")
print(f"总截图数: {start_index + len(screenshots)}")
print(f"截图保存目录: {save_dir}")
# 提示运行OCR
print("\n接下来请运行 batch_ocr_full.py 进行OCR识别")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,842 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>微信联系人祝福管理</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container-main {
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 30px;
}
.header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #eee;
}
.stats-bar {
display: flex;
justify-content: center;
gap: 30px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.stat-item {
text-align: center;
padding: 15px 25px;
background: #f8f9fa;
border-radius: 10px;
}
.stat-number {
font-size: 28px;
font-weight: bold;
color: #667eea;
}
.search-bar {
margin-bottom: 20px;
}
.filter-bar {
margin-bottom: 20px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.filter-btn {
padding: 6px 16px;
border-radius: 20px;
font-size: 13px;
}
.contact-table {
font-size: 14px;
}
.contact-table th {
background: #f8f9fa;
font-weight: 600;
white-space: nowrap;
}
.contact-name {
cursor: pointer;
color: #0d6efd;
font-weight: 500;
}
.contact-name:hover {
text-decoration: underline;
}
.category-badge {
cursor: pointer;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
display: inline-block;
background: #e9ecef;
color: #6c757d;
border: 1px solid transparent;
transition: all 0.15s;
margin: 2px;
}
.category-badge:hover {
transform: scale(1.05);
}
.category-badge.active {
color: #fff;
font-weight: 500;
}
.category-badge.同事.active { background: #0dcaf0; border-color: #0dcaf0; }
.category-badge.好友.active { background: #198754; border-color: #198754; }
.category-badge.同学.active { background: #17a2b8; border-color: #17a2b8; }
.category-badge.老师.active { background: #ffc107; border-color: #ffc107; color: #212529; }
.category-badge.亲戚.active { background: #dc3545; border-color: #dc3545; }
.category-badge.客户.active { background: #6f42c1; border-color: #6f42c1; }
.category-badge.供应商.active { background: #20c997; border-color: #20c997; }
.category-badge.其他.active { background: #fd7e14; border-color: #fd7e14; }
.category-tags {
display: flex;
flex-wrap: wrap;
gap: 2px;
}
.send-badge {
cursor: pointer;
padding: 4px 12px;
border-radius: 15px;
font-size: 12px;
display: inline-block;
transition: all 0.15s;
border: 2px solid #dee2e6;
background: #fff;
color: #6c757d;
}
.send-badge:hover {
transform: scale(1.05);
}
.send-badge.active {
background: #198754;
border-color: #198754;
color: #fff;
font-weight: 500;
}
.blessing-text {
cursor: pointer;
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
color: #198754;
}
.blessing-text:hover {
text-decoration: underline;
}
.pagination-container {
margin-top: 20px;
justify-content: center;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
}
/* Modal styles */
.modal-body input, .modal-body textarea, .modal-body select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
}
.modal-body textarea {
resize: vertical;
min-height: 80px;
}
</style>
</head>
<body>
<div class="container-main">
<div class="header">
<h1><i class="bi bi-wechat"></i> 微信联系人祝福管理</h1>
<p class="subtitle">点击姓名/分类/祝福语可编辑,勾选发送选择框</p>
</div>
<!-- 统计栏 -->
<div class="stats-bar">
<div class="stat-item">
<div class="stat-number" id="totalCount">0</div>
<div>总联系人</div>
</div>
<div class="stat-item">
<div class="stat-number" id="selectedCount">0</div>
<div>已选择</div>
</div>
</div>
<!-- 新增联系人表单 -->
<div class="add-contact-form mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-person-plus"></i> 新增联系人</span>
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#addContactCollapse">
<i class="bi bi-chevron-down"></i>
</button>
</div>
<div class="collapse show" id="addContactCollapse">
<div class="card-body">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label">姓名 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="newNameInput" placeholder="联系人姓名">
</div>
<div class="col-md-4">
<label class="form-label">搜索姓名</label>
<input type="text" class="form-control" id="newSearchNameInput" placeholder="用于搜索的简化姓名(可选)">
</div>
<div class="col-md-2">
<label class="form-label">分类</label>
<select class="form-select" id="newCategorySelect">
<option value="">未分类</option>
<option value="同事">同事</option>
<option value="好友">好友</option>
<option value="同学">同学</option>
<option value="老师">老师</option>
<option value="亲戚">亲戚</option>
<option value="客户">客户</option>
<option value="供应商">供应商</option>
<option value="其他">其他</option>
</select>
</div>
<div class="col-md-2">
<button class="btn btn-primary w-100" onclick="addContact()">
<i class="bi bi-plus-lg"></i> 添加
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 搜索栏 -->
<div class="search-bar">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="searchInput" placeholder="搜索联系人...">
<button class="btn btn-primary" onclick="loadContacts()">搜索</button>
<select class="form-select" style="max-width: 120px;" id="pageSizeSelect" onchange="changePageSize()">
<option value="20">20条/页</option>
<option value="50">50条/页</option>
<option value="100" selected>100条/页</option>
</select>
</div>
</div>
<!-- 分类筛选 -->
<div class="filter-bar">
<button class="btn btn-outline-primary filter-btn active" data-category="" onclick="setFilter('')">全部</button>
<button class="btn btn-outline-primary filter-btn" data-category="同事" onclick="setFilter('同事')">同事</button>
<button class="btn btn-outline-primary filter-btn" data-category="好友" onclick="setFilter('好友')">好友</button>
<button class="btn btn-outline-primary filter-btn" data-category="同学" onclick="setFilter('同学')">同学</button>
<button class="btn btn-outline-primary filter-btn" data-category="老师" onclick="setFilter('老师')">老师</button>
<button class="btn btn-outline-primary filter-btn" data-category="亲戚" onclick="setFilter('亲戚')">亲戚</button>
<button class="btn btn-outline-primary filter-btn" data-category="客户" onclick="setFilter('客户')">客户</button>
<button class="btn btn-outline-primary filter-btn" data-category="供应商" onclick="setFilter('供应商')">供应商</button>
<button class="btn btn-outline-primary filter-btn" data-category="其他" onclick="setFilter('其他')">其他</button>
<button class="btn btn-danger ms-3" id="batchDeleteBtn" onclick="batchDelete()" style="display:none;">
<i class="bi bi-trash"></i> 批量删除 (<span id="selectedCount2">0</span>)
</button>
</div>
<!-- 表格 -->
<div class="table-responsive">
<table class="table table-hover contact-table">
<thead>
<tr>
<th style="width: 50px;"><input type="checkbox" id="selectAll" onchange="toggleSelectAll()"></th>
<th style="width: 50px;">ID</th>
<th style="min-width: 150px;">姓名</th>
<th style="min-width: 120px;">搜索姓名</th>
<th style="min-width: 100px;">分类</th>
<th style="min-width: 100px;">自定义内容</th>
<th>祝福语</th>
<th style="width: 60px;">发送</th>
<th style="width: 50px;">操作</th>
</tr>
</thead>
<tbody id="contactTable">
<tr>
<td colspan="9" class="loading">加载中...</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<nav>
<ul class="pagination pagination-container" id="pagination"></ul>
</nav>
</div>
<!-- 编辑姓名 Modal -->
<div class="modal fade" id="nameModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑姓名</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="editNameId">
<div class="mb-3">
<label class="form-label">姓名</label>
<input type="text" id="editNameInput">
</div>
<div class="mb-3">
<label class="form-label">搜索姓名</label>
<input type="text" id="editSearchNameInput" placeholder="用于搜索的简化姓名">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveName()">保存</button>
</div>
</div>
</div>
</div>
<!-- 编辑祝福语 Modal -->
<div class="modal fade" id="blessingModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑祝福语</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="editBlessingId">
<div class="mb-3">
<label class="form-label">祝福语</label>
<textarea id="editBlessingInput" rows="4">马年新春快乐!愿您在新的一年里,事业腾飞,马到成功!</textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveBlessing()">保存</button>
</div>
</div>
</div>
</div>
<!-- 编辑自定义内容 Modal -->
<div class="modal fade" id="customContentModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑自定义内容</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="editCustomContentId">
<div class="mb-3">
<label class="form-label">自定义内容</label>
<input type="text" id="editCustomContentInput" placeholder="输入自定义内容,默认为分类值">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveCustomContent()">保存</button>
</div>
</div>
</div>
</div>
<!-- Toast 提示 -->
<div class="toast-container">
<div class="toast" id="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-body" id="toastBody"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const API_BASE = '/api';
let currentPage = 1;
let pageSize = 100;
let currentFilter = '';
let contacts = [];
// 页面加载
document.addEventListener('DOMContentLoaded', function() {
loadContacts();
updateStats();
});
// 搜索回车事件
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
loadContacts(1);
}
});
let totalPages = 1;
let totalCount = 0;
// 加载联系人
async function loadContacts(page = 1) {
currentPage = page;
const search = document.getElementById('searchInput').value;
let url = `${API_BASE}/contacts?page=${page}&page_size=${pageSize}`;
if (currentFilter) url += `&category=${encodeURIComponent(currentFilter)}`;
if (search) url += `&search=${encodeURIComponent(search)}`;
try {
const response = await fetch(url);
const data = await response.json();
contacts = data.contacts;
totalPages = data.total_pages;
totalCount = data.total;
renderTable();
renderPagination();
} catch (error) {
console.error('加载失败:', error);
showToast('加载失败,请检查后端服务是否运行', 'danger');
}
}
// 渲染表格
function renderTable() {
const tbody = document.getElementById('contactTable');
const allCategories = ['同事', '好友', '同学', '老师', '亲戚', '客户', '供应商', '其他'];
if (contacts.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" class="text-center py-4">暂无数据</td></tr>';
updateBatchDeleteBtn();
return;
}
tbody.innerHTML = contacts.map(c => {
// 解析当前分类(支持逗号分隔的多标签)
const currentCats = (c.category || '').split(',').map(s => s.trim()).filter(s => s);
// 生成分类标签
const catTags = allCategories.map(cat => {
const isActive = currentCats.includes(cat);
return `<span class="category-badge ${cat} ${isActive ? 'active' : ''}"
onclick="toggleCategory(${c.id}, '${cat}')">${cat}</span>`;
}).join('');
// 自定义内容:如果没有则默认显示分类
const customContent = c.custom_content || c.category || '';
// 发送状态
const isSelected = c.selected;
const sendBadge = `<span class="send-badge ${isSelected ? 'active' : ''}"
onclick="toggleSend(${c.id})">${isSelected ? '✓ 发送' : '发送'}</span>`;
return `
<tr>
<td><input type="checkbox" class="contact-checkbox" data-id="${c.id}" ${c.selected ? 'checked' : ''} onchange="toggleSelect(${c.id}); updateBatchDeleteBtn()"></td>
<td>${c.id}</td>
<td><span class="contact-name" onclick="openNameModal(${c.id}, '${escapeHtml(c.name)}', '${escapeHtml(c.search_name || '')}')">${escapeHtml(c.name)}</span></td>
<td><span class="contact-name" onclick="openNameModal(${c.id}, '${escapeHtml(c.name)}', '${escapeHtml(c.search_name || '')}')">${escapeHtml(c.search_name || '')}</span></td>
<td><div class="category-tags">${catTags}</div></td>
<td><span class="contact-name" onclick="openCustomContentModal(${c.id}, '${escapeHtml(customContent)}')">${escapeHtml(customContent)}</span></td>
<td><span class="blessing-text" onclick="openBlessingModal(${c.id}, '${escapeHtml(c.blessing)}')" title="${escapeHtml(c.blessing)}">${escapeHtml(c.blessing)}</span></td>
<td>${sendBadge}</td>
<td>
<button class="btn btn-sm btn-danger" onclick="deleteContact(${c.id})"><i class="bi bi-trash"></i></button>
</td>
</tr>
`;
}).join('');
updateBatchDeleteBtn();
}
// 渲染分页
function renderPagination() {
const pagination = document.getElementById('pagination');
if (totalPages <= 1) {
pagination.innerHTML = '';
return;
}
let html = `
<li class="page-item ${currentPage <= 1 ? 'disabled' : ''}">
<a class="page-link" href="javascript:void(0)" onclick="loadContacts(${currentPage - 1})">上一页</a>
</li>
`;
// 显示页码
for (let i = Math.max(1, currentPage - 2); i <= Math.min(totalPages, currentPage + 2); i++) {
html += `
<li class="page-item ${i === currentPage ? 'active' : ''}">
<a class="page-link" href="javascript:void(0)" onclick="loadContacts(${i})">${i}</a>
</li>
`;
}
html += `
<li class="page-item ${currentPage >= totalPages ? 'disabled' : ''}">
<a class="page-link" href="javascript:void(0)" onclick="loadContacts(${currentPage + 1})">下一页</a>
</li>
`;
pagination.innerHTML = html;
}
// 设置筛选
function setFilter(category) {
currentFilter = category;
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.category === category) {
btn.classList.add('active');
}
});
loadContacts(1);
}
// 改变每页显示数量
function changePageSize() {
pageSize = parseInt(document.getElementById('pageSizeSelect').value);
loadContacts(1);
}
// 新增联系人
async function addContact() {
const name = document.getElementById('newNameInput').value.trim();
const searchName = document.getElementById('newSearchNameInput').value.trim();
const category = document.getElementById('newCategorySelect').value;
if (!name) {
showToast('请输入联系人姓名', 'warning');
document.getElementById('newNameInput').focus();
return;
}
try {
const response = await fetch(`${API_BASE}/contacts`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
name: name,
search_name: searchName || name,
category: category,
blessing: '马年新春快乐!愿您在新的一年里,事业腾飞,马到成功!',
selected: false
})
});
if (response.ok) {
// 清空表单
document.getElementById('newNameInput').value = '';
document.getElementById('newSearchNameInput').value = '';
document.getElementById('newCategorySelect').value = '';
showToast('添加成功', 'success');
loadContacts(1);
updateStats();
} else {
showToast('添加失败', 'danger');
}
} catch (error) {
showToast('添加失败,请检查后端服务', 'danger');
}
}
// 回车快捷添加
document.getElementById('newNameInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
addContact();
}
});
document.getElementById('newSearchNameInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
addContact();
}
});
// 更新统计
async function updateStats() {
try {
const response = await fetch(`${API_BASE}/stats`);
const stats = await response.json();
document.getElementById('totalCount').textContent = stats.total;
document.getElementById('selectedCount').textContent = stats.selected;
} catch (error) {
console.error('统计加载失败:', error);
}
}
// 全选/取消全选
function toggleSelectAll() {
const selectAll = document.getElementById('selectAll').checked;
document.querySelectorAll('.contact-checkbox').forEach(cb => {
cb.checked = selectAll;
const id = parseInt(cb.dataset.id);
updateSelect(id, selectAll);
});
updateStats();
updateBatchDeleteBtn();
}
// 单个选择
async function toggleSelect(id) {
const checkbox = document.querySelector(`.contact-checkbox[data-id="${id}"]`);
await updateSelect(id, checkbox.checked);
updateStats();
}
// 更新选择状态
async function updateSelect(id, selected) {
try {
await fetch(`${API_BASE}/contacts/${id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({selected: selected})
});
} catch (error) {
console.error('更新失败:', error);
}
}
// 打开编辑姓名Modal
function openNameModal(id, name, searchName) {
document.getElementById('editNameId').value = id;
document.getElementById('editNameInput').value = name;
document.getElementById('editSearchNameInput').value = searchName || '';
new bootstrap.Modal(document.getElementById('nameModal')).show();
}
// 保存姓名
async function saveName() {
const id = document.getElementById('editNameId').value;
const name = document.getElementById('editNameInput').value.trim();
const searchName = document.getElementById('editSearchNameInput').value.trim();
if (!name) {
showToast('姓名不能为空', 'warning');
return;
}
try {
await fetch(`${API_BASE}/contacts/${id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: name, search_name: searchName})
});
bootstrap.Modal.getInstance(document.getElementById('nameModal')).hide();
showToast('保存成功', 'success');
loadContacts(currentPage);
} catch (error) {
showToast('保存失败', 'danger');
}
}
// 快速切换分类标签
async function toggleCategory(id, category) {
// 找到当前联系人
const contact = contacts.find(c => c.id === id);
if (!contact) return;
// 解析当前分类
let currentCats = (contact.category || '').split(',').map(s => s.trim()).filter(s => s);
// 切换分类
const index = currentCats.indexOf(category);
if (index > -1) {
currentCats.splice(index, 1); // 移除
} else {
currentCats.push(category); // 添加
}
const newCategory = currentCats.join(',');
try {
await fetch(`${API_BASE}/contacts/${id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({category: newCategory})
});
// 更新本地数据
contact.category = newCategory;
// 重新渲染表格
renderTable();
showToast('分类已更新', 'success');
updateStats();
} catch (error) {
showToast('更新失败', 'danger');
}
}
// 快速切换发送状态
async function toggleSend(id) {
const contact = contacts.find(c => c.id === id);
if (!contact) return;
const newSelected = !contact.selected;
try {
await fetch(`${API_BASE}/contacts/${id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({selected: newSelected})
});
contact.selected = newSelected;
renderTable();
showToast(newSelected ? '已标记发送' : '已取消发送', 'success');
updateStats();
} catch (error) {
showToast('更新失败', 'danger');
}
}
// 打开编辑祝福语Modal
function openBlessingModal(id, blessing) {
document.getElementById('editBlessingId').value = id;
document.getElementById('editBlessingInput').value = blessing;
new bootstrap.Modal(document.getElementById('blessingModal')).show();
}
// 保存祝福语
async function saveBlessing() {
const id = document.getElementById('editBlessingId').value;
const blessing = document.getElementById('editBlessingInput').value.trim();
if (!blessing) {
showToast('祝福语不能为空', 'warning');
return;
}
try {
await fetch(`${API_BASE}/contacts/${id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({blessing: blessing})
});
bootstrap.Modal.getInstance(document.getElementById('blessingModal')).hide();
showToast('保存成功', 'success');
loadContacts(currentPage);
} catch (error) {
showToast('保存失败', 'danger');
}
}
// 打开编辑自定义内容Modal
function openCustomContentModal(id, customContent) {
document.getElementById('editCustomContentId').value = id;
document.getElementById('editCustomContentInput').value = customContent;
new bootstrap.Modal(document.getElementById('customContentModal')).show();
}
// 保存自定义内容
async function saveCustomContent() {
const id = document.getElementById('editCustomContentId').value;
const customContent = document.getElementById('editCustomContentInput').value.trim();
try {
await fetch(`${API_BASE}/contacts/${id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({custom_content: customContent})
});
bootstrap.Modal.getInstance(document.getElementById('customContentModal')).hide();
showToast('保存成功', 'success');
loadContacts(currentPage);
} catch (error) {
showToast('保存失败', 'danger');
}
}
// 删除联系人
async function deleteContact(id) {
if (!confirm('确定要删除这个联系人吗?')) return;
try {
await fetch(`${API_BASE}/contacts/${id}`, {method: 'DELETE'});
showToast('删除成功', 'success');
loadContacts(currentPage);
updateStats();
} catch (error) {
showToast('删除失败', 'danger');
}
}
// 批量删除
async function batchDelete() {
const checkedIds = [];
document.querySelectorAll('.contact-checkbox:checked').forEach(cb => {
checkedIds.push(parseInt(cb.dataset.id));
});
if (checkedIds.length === 0) {
showToast('请先选择要删除的联系人', 'warning');
return;
}
if (!confirm(`确定要删除选中的 ${checkedIds.length} 个联系人吗?`)) return;
try {
let success = 0;
for (const id of checkedIds) {
await fetch(`${API_BASE}/contacts/${id}`, {method: 'DELETE'});
success++;
}
showToast(`成功删除 ${success} 个联系人`, 'success');
loadContacts(currentPage);
updateStats();
updateBatchDeleteBtn();
} catch (error) {
showToast('删除失败', 'danger');
}
}
// 更新批量删除按钮状态
function updateBatchDeleteBtn() {
const checkedCount = document.querySelectorAll('.contact-checkbox:checked').length;
const btn = document.getElementById('batchDeleteBtn');
const countSpan = document.getElementById('selectedCount2');
if (checkedCount > 0) {
btn.style.display = 'inline-block';
countSpan.textContent = checkedCount;
} else {
btn.style.display = 'none';
}
}
// 显示Toast提示
function showToast(message, type = 'info') {
const toast = document.getElementById('toast');
const toastBody = document.getElementById('toastBody');
toastBody.textContent = message;
toast.className = `toast show bg-${type} text-white`;
setTimeout(() => {
toast.className = 'toast';
}, 3000);
}
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>

50
update_search_name.py Normal file
View File

@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
"""
清理 search_name 字段:移除特殊符号、英文单词、数字、表情符号
"""
import sqlite3
import re
DB_PATH = r"D:\夏骥\微信研究\contacts.db"
def clean_search_name(name):
"""清理搜索姓名:只保留中文"""
if not name:
return ''
# 只保留中文字符
cleaned = re.sub(r'[^\u4e00-\u9fa5]', '', name)
return cleaned.strip()
def main():
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# 更新所有记录
cursor.execute('SELECT id, search_name FROM contacts')
rows = cursor.fetchall()
updated = 0
for row in rows:
id_, search_name = row
cleaned = clean_search_name(search_name)
cursor.execute('UPDATE contacts SET search_name = ? WHERE id = ?', (cleaned, id_))
updated += 1
conn.commit()
print(f'已更新 {updated} 条记录')
# 显示示例
cursor.execute('SELECT id, name, search_name FROM contacts LIMIT 20')
print('\n示例数据:')
for row in cursor.fetchall():
print(f' {row[0]}: name="{row[1]}" -> search_name="{row[2]}"')
conn.close()
print('\n完成!')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,8 @@
================================================================================
微信窗口控件树 - 标准UIAutomation
================================================================================
微信窗口: Name=微信, ClassName=Qt51514QWindowIcon
ControlType: WindowControl ClassName: Qt51514QWindowIcon Name: 微信 AutomationId: Rect: (-3,15,1044,1033)[1047x1018] Depth: 0
ControlType: PaneControl ClassName: MMUIRenderSubWindowHW Name: MMUIRenderSubWindowHW AutomationId: Rect: (5,15,1036,1025)[1031x1010] Depth: 1

6
wechat_controls_raw.txt Normal file
View File

@@ -0,0 +1,6 @@
================================================================================
微信窗口控件树 - RawViewWalker
================================================================================
ControlType: 窗口 ClassName: Qt51514QWindowIcon Name: 微信 AutomationId: Rect: (-3,15,1044,1033) Depth: 0
ControlType: 窗格 ClassName: MMUIRenderSubWindowHW Name: MMUIRenderSubWindowHW AutomationId: Rect: (5,15,1036,1025) Depth: 1

View File

@@ -0,0 +1,6 @@
================================================================================
微信窗口控件树 - Win32 API
================================================================================
HWND: 0x401BE ClassName: Qt51514QWindowIcon Title: 微信 Rect: (-3, 15, 1044, 1033) Depth: 0
HWND: 0x1109FE ClassName: MMUIRenderSubWindowHW Title: MMUIRenderSubWindowHW Rect: (5, 15, 1036, 1025) Depth: 1