微信聊天记录提取及分析(wordcloud+pyecharts)

0. 前言

​ 之所以想要提取微信的聊天记录并分析是因为也开始再学习python,但是单纯看看语法什么的又很无趣,无意间看到python可以进行微信聊天记录的分析,就自己尝试做了一下,感觉还是挺有意思的。

1.提取聊天记录数据库

​ 我所用的是小米手机,所以提取聊天记录是主要是通过本地备份功能,其余手机也可参考下述博客,具体流程可参考:

微信聊天记录导出(2020新版)

安卓\电脑微信聊天记录导出表格

微信聊天记录数据提取并分析

利用python做微信聊天记录词云分析

提取微信数据库的主体流程都差不多,基本都是先进行备份,然后将备份文件复制到电脑进行解压,解压完成之后根据得到的数据库密码访问数据库。提取数据库的过程基本上都不会有什么问题,主要会出现的问题在于获取微信数据库密码。我开始使用的是手机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(官方下载地址)软件访问数据库中内容,查看具体的聊天信息等,我在后续处理中主要用到了messagecontact表,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)

有兴趣的话可以参考我的GitHub地址,里边有所需的全部文件及代码,有帮到的请给star呀~

微信聊天记录提取及分析

    原文作者:muzhicihe
    原文地址: https://blog.csdn.net/muzhicihe/article/details/109902849
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞