0. 前言
之所以想要提取微信的聊天记录并分析是因为也开始再学习python,但是单纯看看语法什么的又很无趣,无意间看到python可以进行微信聊天记录的分析,就自己尝试做了一下,感觉还是挺有意思的。
1.提取聊天记录数据库
我所用的是小米手机,所以提取聊天记录是主要是通过本地备份功能,其余手机也可参考下述博客,具体流程可参考:
提取微信数据库的主体流程都差不多,基本都是先进行备份,然后将备份文件复制到电脑进行解压,解压完成之后根据得到的数据库密码访问数据库。提取数据库的过程基本上都不会有什么问题,主要会出现的问题在于获取微信数据库密码。我开始使用的是手机IMEI + uin拼接取其32位MD5码前7位的方式,但是因为我手机上有两个IMEI码,尝试了各种组合获得的密码始终是错误的,所以就放弃采用这种方式,但是根据其他博客中的内容,这种方式也可以获取数据库密码。
我最终采用的是反序列化的方式获取数据库密码,即将微信com.tencent.mm\r\MicroMsg\systemInfo.cfg和com.tencent.mm\r\MicroMsg\CompatibleInfo.cfg这两个文件复制出来,通过参考代码获取数据库密码。开始直接在命令行通过javac编译并运行,如下所示
javac IMEI.java
java IMEI systemInfo.cfg CompatibleInfo.cfg
但是直接在命令行中运行会报 “错误: 找不到或无法加载主类 IMEI 原因: java.lang.ClassNotFoundException: IMEI”,但是将代码放在IDEA中可以运行并得到最终数据库密码,代码如下:
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.security.MessageDigest;
import java.util.HashMap;
public class IMEI {
public static void main(String[] args) {
// systemInfo.cfg存储路径,修改为电脑中存储位置
String systemInfo_path = "D:\\wechet-anayze\\systemInfo.cfg";
// compatibleInfo.cfg存储路径,修改为电脑中存储位置
String compatibleInfo_path1 = "D:\\wechet-anayze\\CompatibleInfo.cfg";
try {
ObjectInputStream in = new ObjectInputStream(new FileInputStream(
path));
Object DL = in.readObject();
HashMap hashWithOutFormat = (HashMap) DL;
ObjectInputStream in1 = new ObjectInputStream(new FileInputStream(
path1));
Object DJ = in1.readObject();
HashMap hashWithOutFormat1 = (HashMap) DJ;
String s = String.valueOf(hashWithOutFormat1.get(Integer
.valueOf(258))); // 取手机的IMEI
s = s + hashWithOutFormat.get(Integer.valueOf(1)); //合并到一个字符串
s = encode(s); // hash
System.out.println("The Key is : " + s.substring(0, 7));
in.close();
in1.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static String encode(String content) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(content.getBytes());
return getEncode32(digest);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static String getEncode32(MessageDigest digest) {
StringBuilder builder = new StringBuilder();
for (byte b : digest.digest()) {
builder.append(Integer.toHexString((b >> 4) & 0xf));
builder.append(Integer.toHexString(b & 0xf));
}
return builder.toString();
}
}
微信数据库及数据库密码获取完成之后就可以通过sqlcipher(官方下载地址)软件访问数据库中内容,查看具体的聊天信息等,我在后续处理中主要用到了message和contact表,message表中记录了所发送的所有消息信息,contact表中记录了所有的联系人信息,message表中主要使用到的内容如下:
message表主要内容
列名 | 内容 |
msgId | 按所有消息时间顺序的唯一编号 |
type | 聊天内容类型 |
isSend | 标识消息是自己发送还是对方发送,1表示自己,0表示对方 |
createTime | 聊天时间 |
talker | 单聊的wxid或群聊编号"XXXX@chatroom" |
content | 聊天内容,单聊直接显示内容,群聊格式为“wxid:\n”内容 |
因为也挺想知道和其他人的聊天类型,所以就也根据message表中的type值结合createTime查找具体的聊天信息,从而对type类型做了如下分类,
type类型信息
type值 | 表示内容 |
1 | 文本内容 |
2 | 位置信息 |
3 | 图片及视频 |
34 | 语音消息 |
42 | 名片(公众号名片) |
43 | 图片及视频 |
47 | 表情包 |
48 | 定位信息 |
49 | 小程序链接 |
10000 | 撤回消息提醒(XXXX撤回了一条消息) |
1048625 | 照片 |
16777265 | 链接 |
285212721 | 文件 |
419430449 | 微信转账 |
436207665 | 微信红包 |
469762097 | 微信红包 |
·11879048186 | 位置共享 |
… | (还有未知type信息,待补充) |
contact表主要内容
列名 | 含义 |
username | 微信id,格式是"wxid_xxxxxxxx"或者一看就是自己设置的 |
alias | 自己设置的那个可以通过查找加好友的微信名(和上面那个有的有区别有的为空) |
conRemark | 联系人备注名 |
nickname | 微信名片上的名字,公众号的名字 |
contactLabelIds | 联系人标签号 |
2.数据预处理
2.1 message表和contact表获取
获取到微信数据库及数据库密码后,可通过sqlciper软件查看数据库中内容,同时也可将所需的数据导出,可通过file/Export/Table as CSV file选项,选择需要导出的数据表,将之存储为CSV文件。
这里需要注意一下:
如果在python中直接读取导出的csv文件夹,python中会报错,大致是因为编码错误。其余博客中有提到使用excel打开导出的csv文件,然后以utf-8格式另存,最后读取另存后的文件,通过试验这种方法可以进行读取,但是因为csv文件用excel打开时超过16位的数字将会被强制使用科学计数法表示,所以message表中的createTime会被使用科学计数法表示,另存后的文件中creatTime时间精度会损失,所以我最终采用了一个简单粗暴的方法:用记事本打开csv文件,然后以utf-8的形式另存,在python中就可以正常读取了。(话说用记事本打开要比用excel打开快好多倍)
2.2 message表预处理
因为message表中和contact表中含有大量数据分析时不用的数据列,所以在数据预处理阶段中,只需要提取message表和contact表中我们所关注的一些数据。对于message表而言,我们只关注talker,createTime,type, isSend, content所表示的内容,也即聊天对象,聊天时间,消息类型,消息由谁发送,聊天内容。更近一步,对于聊天内容而言,我们只关注文本消息(文本消息可用于后续制作词云),对于其他类型的消息我们只需要统计出现的次数,而不必关注具体内容。所以在预处理过程中,同时需要将非文本类型消息内容置0,这样做的另一个好处是可以大大减少message表的数据量,就个人而言,将非文本消息置零后,message文件大小由134MB缩小到29M。
提取talker,createTime,type, isSend, content内容
"""
Create on 2020-11-21
@author: muxiaohe
"""
# 对message表中内容进行处理,只留下所需的type, isSend, createTime, talker, content内容,并将之存储于新的文件中
def get_needed_data(file_path, save_file_path):
message = read_file(file_path)
message = message[['type', 'isSend', 'createTime', 'talker', 'content']]
message.to_csv(save_file_path, encoding='utf_8_sig',header=True, index=False)
将非文本消息置零
"""
Create on 2020-11-21
@author: muxiaohe
"""
# 对message表中数据进行处理,,删除content中的无用数据,将type中非1值对应的content数据统统置为零
def data_clean(file_path, save_path):
message = pd.read_csv(file_path, sep=',', encoding='utf-8', low_memory=False)
print("源文件大小: ", message.shape)
message.loc[message.type != 1, 'content'] = 0
print("处理后文件大小: ", message.shape)
message.to_csv(save_path, encoding='utf_8_sig', header=True, index=False)
2.3 contact表预处理
与message表类似,contact表也需去除无关数据,contact表中我们只提取 usename, alias, conRemark, nickname信息,同时在处理时只保留了有备注的联系人,没有备注的联系人也没必要分析聊天记录。
"""
Create on 2020-11-21
@author: muxiaohe
"""
# 对contact表中数据进行预处理,获取所需数据,清除其余数据
def contact_pre_treatment(file_path, save_path):
contact_path = r"D:\wechet-anayze\recontact.txt"
contact_save_path = r"D:\wechet-anayze\pre-recontact.csv"
contact = pd.read_csv(contact_path, sep=',', encoding="utf-8", low_memory=False)
print("处理前文件大小: ", contact.shape)
contact = contact[['username', 'alias', 'conRemark', 'nickname']]
# 删除无用记录,只保留有备注的联系人
contact1 = contact.drop(contact[pd.isna(contact.conRemark)].index)
print("处理后文件大小: ", contact.shape)
contact1.to_csv(contact_save_path, encoding='utf_8_sig', header=True, index=False)
3.聊天记录分析
3.1 获取常用联系人聊天次数
在预处理的基础之上,获取每个联系人的聊天次数,并据此使用pyecharts绘制柱状图,使用pyecharts绘制时需特别注意:pyecharts传入数据时需要int数据,而从文件中读取赋值到list中的数值类型为int64,需要先使用int()函数进行转换,否则绘制出来的图形中data数据域将都为NaN。
"""
Create on 2020-11-21
@author: muxiaohe
"""
# 获取常用联系人聊天次数
def get_chat_nums(message_path, contact_path):
"""
:param message_path: 预处理完成后的message表存储路径
:param contact_path: 预处理完成后的contact表存储路径
:return:
"""
# message_path = r'D:\wechet-anayze\pre-message-2.txt'
# contact_path = r'D:\wechet-anayze\pre-recontact.csv'
message = pd.read_csv(message_path, sep=',', encoding='utf-8', low_memory=False)
contact = pd.read_csv(contact_path, sep=',', encoding='utf-8', low_memory=False)
# 提取出联系人列表中用户名和备注名称
contact = contact[['username', 'conRemark']]
# 将用户名提取出来
username = contact['username'].tolist()
print(type(username))
# 将用户名及备注名提取为一个字典
contact_dict = dict(zip(contact['username'], contact['conRemark']))
# 联系人及其聊天次数集合
contact_sum_message = {}
# 全部联系人聊天次数集合
sum_message = 0
# 联系人列表
uname_list = []
# 联系人列表对应的聊天次数列表
chat_num_list = []
# 遍历联系人列表,并逐一统计聊天次数
for uname in username:
# 根据微信id获取真实姓名,key为真实姓名
key = contact_dict.get(uname)
# 根据微信id统计聊天次数,value:聊天次数
value = (message['talker'] == uname).sum()
# 过滤聊天次数为0的联系人,只保留聊天次数不为0的联系人
if value != 0:
contact_sum_message[key] = value
sum_message += value
uname_list.append(key)
# 这里需特别注意:value值也即聊天的次数格式是int64,但是pyecharts中如果传入的是int64时,最终渲染出的html文件中会数据会丢失,
# 所以需转为int值(血泪教训)
chat_num_list.append(int(value))
# print(contact_sum_message)
print("总聊天次数: ", sum_message)
# 使用pyecharts绘制柱状图
c = (
Bar(init_opts=opts.InitOpts(width="1600px", height="600px", page_title="聊天次数统计"))
.add_xaxis(uname_list)
.add_yaxis(series_name="聊天次数", y_axis=chat_num_list, color='#FF6666')
.set_global_opts(
# 标题配置
title_opts=opts.TitleOpts(title="聊天次数统计"),
# X轴区域缩放配置项,可使用list同时配置多个配置项
datazoom_opts=[opts.DataZoomOpts(range_start=20, range_end=40), opts.DataZoomOpts(type_="inside")],
# 区域选择组件
brush_opts=opts.BrushOpts(),
# X坐标轴旋转
xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=-15)),
# 工具箱组件
toolbox_opts=opts.ToolboxOpts(),
# 图例配置
legend_opts=opts.LegendOpts(is_show=False),
)
.set_series_opts(
label_opts=opts.LabelOpts(is_show=False),
# 配置最大值最小值刻度线
markline_opts=opts.MarkLineOpts(
data=[
opts.MarkLineItem(type_="min", name="最小值"),
opts.MarkLineItem(type_="max", name="最大值"),
opts.MarkLineItem(type_="average", name="平均值"),
]
),
)
.render("chat_num_count.html")
)
3.2 获取聊天消息中不同类型消息占比
"""
Create on 2020-11-21
@author: muxiaohe
"""
import pandas as pd
from pyecharts import options as opts
from pyecharts.charts import Pie
# 获取各个消息聊天记录数量,并使用pycharts绘图
def get_message_type_frequency(file_path, wxid):
"""
:param file_path: 经过预处理后的message表存储位置
:param wxid: 待查询人的微信id
:return:
"""
# file_path = r'D:\wechet-anayze\pre-message-2.txt'
message = pd.read_csv(file_path, sep=',', encoding='utf-8', low_memory=False)
# wxid = ''
# 进行数据筛选,选择message表中与所需微信id一致的数据
message = message[message['talker'] == wxid]
# 根据消息类型统计每种类型的频次(索引为数字编码)
chat_type_count = message['type'].groupby(message['type']).size()
# 消息类型对应关系
message_type = {'1': '文本内容', "3": "图片及视频", "34": "语音消息", "42": "名片信息", "43": "图片及视频",
"47": "表情包", "48": "定位信息", "49": "小程序链接", "10000": "消息撤回提醒", "1048625": "网络照片",
"16777265": "链接信息", "419430449": "微信转账", "436207665": "红包", "469762097": "红包",
"-1879048186": "位置共享"}
# 集合对象,功能与chat_type_count相同,存储(聊天类型:频次)信息(索引为对应中文类型)
chat_type_count_dict = {}
# 根据消息类型代码
for key in chat_type_count.index:
if str(key) in message_type.keys():
print(message_type.get(str(key)))
chat_type_count_dict[message_type.get(str(key))] = chat_type_count[key]
else:
chat_type_count_dict[key] = chat_type_count[key]
print("结果集类型: ", type(chat_type_count_dict))
print(chat_type_count_dict)
x_data = []
y_data = []
for key in chat_type_count_dict:
temp = [str(key), chat_type_count_dict.get(key)]
x_data.append(str(key))
y_data.append(int(chat_type_count_dict.get(key)))
a1 = []
for z in zip(x_data, y_data):
a1.append(z)
pie = Pie(init_opts=opts.InitOpts(width="1600px", height="600px", page_title="消息类型统计"))
pie.add(
"",
data_pair=a1,
center=["35%", "60%"],
)
pie.set_global_opts(
title_opts=opts.TitleOpts(title="Pie-调整位置"),
legend_opts=opts.LegendOpts(pos_left="15%"),
)
pie.set_series_opts(label_opts=opts.LabelOpts(formatter="{b}:{c}"))
pie.render("message_type_count.html")
3.3 聊天记录词云
可选择具体的聊天对象,获取全部的聊天内容,代码中可以chinese_slice选择是否进行分词,如果不进行分词则将全部聊天内容拼接到一起,通过wordcloud模块生成词云,如果选择分词则会调用jiebe模块先进行分词,然后再通过wordcloud模块生成词云。
"""
Create on 2020-11-21
@author: muxiaohe
"""
import imageio
import jieba
import pandas as pd
import matplotlib.pyplot as plt
import wordcloud
# 根据聊天记录生成词云
def get_wordcloud(file_path, stopword_path, wxid, image_path):
"""
:param file_path: 预处理完成后的message表存储位置
:param stopword_path: 停用词文件存储位置
:param wxid: 待查询人微信id
:return:
"""
# 分词及词云处理
# message_path = r'D:/wechet-anayze/pre-message-2.txt'
# stopword_path = r'D:/wechet-anayze/stopword.txt'
# 文件读取
message = pd.read_csv(file_path, sep=',', encoding='utf-8', low_memory=False)
# 数据筛选,选择对应微信id的信息
message = message[message['talker'] == wxid]
# 提取聊天内容信息
content = message['content']
# 中文字型存储路径
font_path = r'C:\Windows\Fonts\MSYH.TTC'
# 是否选用分词
wordcut_flag = True
# 词云图片输出路径
image_out_name = 'word-heart.png'
# 读取停用词表
stopwords = [line.strip() for line in open(stopword_path, encoding='UTF-8').readlines()]
if image_out_name is None:
image_out_name = 'word-heart.png'
if wordcut_flag:
print("进行中文分词")
outstr = ""
text = ",".join(content)
text_list = jieba.lcut(text, cut_all=False)
for word in text_list:
if word not in stopwords:
if word != '\t' and '\n':
outstr += word
outstr += " "
# 如果想存储分词后结果,可取消下方注释
# savepath = r'D:/wechet-anayze/textlist.txt'
# fp = open(savepath, 'w', encoding='utf8', errors='ignore')
# fp.write(outstr)
# fp.close()
text = outstr
else:
print("不进行中文分词")
text = " ".join(content)
# 词云形状图片位置
mk = imageio.imread(image_path)
# 构建并配置词云对象w,注意要加scale参数,提高清晰度
w = wordcloud.WordCloud(width=1000,
height=700,
background_color='white',
font_path=font_path,
mask=mk,
scale=2,
stopwords=None,
contour_width=1,
contour_color='red')
# 将string变量传入w的generate()方法,给词云输入文字
w.generate(text)
# 展示图片
# 根据原始背景图片的色调进行上色
image_colors = wordcloud.ImageColorGenerator(mk)
plt.imshow(w.recolor(color_func=image_colors))
# 根据原始黑白色调进行上色
# plt.imshow(wc.recolor(color_func=grey_color_func, random_state=3), interpolation='bilinear') #生成黑白词云图
# 根据函数原始设置进行上色
# plt.imshow(wc)
# 隐藏图像坐标轴
plt.axis("off")
plt.show()
# 将词云图片导出到当前文件夹
w.to_file(image_out_name)