前言
马上要到我们家小宝的生日了,思来想去没想好送什么东西,在上班摸鱼的时候无意间看到了三年前很火的《微信聊天记录统计报告》,这个东西很好玩,所以我决定搞一下。但是大体浏览了一下,发现事情没有想象的那么简单,所以借这次机会讲一整个过程记录下来,也是一次学习的机会啦。
参考文章:[https://zhuanlan.zhihu.com/p/589718049]、[https://www.itcode1024.com/196633/]
本教程使用的工具,请务必在开始前准备好:
逍遥模拟器[https://www.xyaz.cn/]
sqlcipher(加密数据库破解)[https://url21.ctfile.com/f/27727221-930256389-b825f4?p=3889](访问密码: 3889)
微信聊天记录导出EXCEL
这一步在参开文章说的比较全面,基本搬抄,总要将他导入导出的。
将聊天记录导入到电脑模拟器
将手机聊天记录迁移到电脑上模拟器里的微信,再将聊天记录文件从模拟器传输到电脑中,最终获得EnMicroMsg.db文件即可。
- 打开逍遥模拟器,将设置内中打开root权限。
- 在模拟器上安装微信,模拟器分辨率设置为手机形式的窄长型,注意不必着急登录,否则会把你正常手机上的微信踢下线。
- 进入你平时正常使用的手机上的微信,点击设置->聊天->聊天记录备份与迁移->迁移->迁移到手机/平板微信。
- 选择聊天记录的时间和内容。内容强烈建议选择”不含图片/视频/文件”,否则迁移过程可能会非常慢。时间以你想统计的年月日跨度为准,可以导入几年或者几个月的。尽量选取你认为有必要统计的好友的私聊记录,不要点击全选,否则会很慢很慢。
- 选择完成后点击迁移聊天记录,会出现如下的二维码,此时在电脑的模拟器上登录微信,用笔记本电脑自带的摄像头扫描该二维码即可开始同步。如果电脑没有摄像头,可以将该二维码截图后通过QQ、百度网盘、蓝牙等方式传输到电脑,之后在安卓模拟器里的微信点击扫描时选择实时截取屏幕的方式(如下图)扫取电脑上打开的二维码图片即可。
在这一步可能会遇到模拟器扫描二维码闪退问题,解决方法:[官网教程],我在这里用简单的方法说一下:
在桌面系统管理安装"谷歌安装器",在 设置->设备,将摄像头改为虚拟,就可以扫描电脑上的画面。
在这一步提示手机和电脑不在同一个wifi,可以在设置->网络->WIFI热点 改成与你手机连接的wifi同一个名字,前提是必须要模拟器的电脑和手机连接的是同一个网络。 - 同步完成后,打开模拟器里的文件管理器,在其 根目录/data/data/com.tencent.mm/MicroMsg/(一个32位字符串命名的文件夹中)中找到EnMicroMsg.db文件。这里的(32位字符串命名的文件夹)如果你只在模拟器上登陆过一个微信的话就只有一个,如果有两个这样命名的文件夹的话(如下图),那就每个都打开看看哪个文件夹中能找到EnMicroMsg.db。找到后将该db文件拷贝到电脑上。
关于如何从模拟器中复制文件到电脑文件夹中请参考:1.将文件选中,返回内部共享空间。2.打开Download文件夹,该文件夹是与电脑共享。3.点击左下角点,粘贴所选项,就可以就将内容粘贴在此。
参考教程:
如果找不到该文件或路径,请按照以下方法:1.点击左下角设置。2.点击常规设置3.将访问模式改成"超级用户访问模式",获取最高权限,再点击根目录就能找到上面路径。
聊天记录破解
这一步是对1.1中获得的EnMicroMsg.db文件进行破解,并且生成csv文件。聊天记录破解需要用到破解软件sqlcipher,和破解密码。
破解密码是手机的IMEI码和你微信的uin码直接拼接相连后,换算成32位小写的MD5的前七位。
手机IMEI码获取方式:在模拟器输入*#06#后自动出现,但现在的最新版本 IMEI (手机序列号)为固定值为1234567890ABCDEF,可以都试一下。模拟器的是861009457165503、手机的是1234567890ABCDEF,如果密码不正确就都试试。
微信的uin码获取方式:雷电模拟器中/data/data/com.tencent.mm/shared_prefs/ 找到文件auth_info_key_prefs.xml,再传输到电脑中用记事本打开,找到auth_uin,其中value后面跟着的就是微信uin码。
然后将手机IMEI码和微信uin码直接相连后,用换算工具换算成小写32位md5值,其前7位就是破解密码。
打开工具sqlcipher,打开我们导出的db文件,输入密码,就可以看到好多列表,依次点击File->Export->Table as CSV file,选择message表导出,一定要自己加上后缀.csv。导出就是excel。
到这一步就搞到了excel的数据了,自由发挥了,没有IT基础的,可以参考[参考教程]里的方法,如果有IT基础,java,js,python等都可以解析excel数据。可以自己DIY展示。
聊天记录数据分析(JavaScript)
我这里使用前端技术实现分析,做一个可视化的解析工具(python确实不太熟练 ==,如果你会使用python我还是推荐这个)。以下内容是给有代码基础的朋友阅读,如果是不懂代码的朋友碰巧看到了这篇文章,也可以直接下载我生成的exe文件,在电脑上安装使用,链接:[https://github.com/Aolcycle/wx-crecords]。
我把以下demo放到了github[https://github.com/Aolcycle/wx-crecords],需要的可以直接拉取。
开发思路
写其他语言的朋友可以看这里的思路,写法虽然不一样,但是思路是一样的。
- 上传CSV带客户端进行解析,获得解析后的数据(比如json)。
- 得到数据后筛选名称,比如我想统计李华的聊天记录,那我先把我跟他的聊天记录进行筛选。
- 筛选完成后获取可以统计内容,比如,我们聊天当中发的什么内容最多,我们最晚的一次聊天是到几点,我们说话聊天最多的一天是哪天之类的。
- 统计完成后生成模板数据,展示到模板上或者其他随便什么地方。
这里提供了两个表格,可以按照表内内容进行检索。
列名 | 内容 |
---|---|
msgId | 按所有消息时间顺序的唯一编号 |
type | 聊天内容类型 |
isSend | 标识消息是自己发送还是对方发送,1表示自己,0表示对方 |
createTime | 聊天时间 |
talker | 单聊的wxid或群聊编号"XXXX@chatroom" |
content | 聊天内容,单聊直接显示内容,群聊格式为“wxid:\n”内容 |
type值 | 表示内容 |
---|---|
1 | 文本内容 |
2 | 位置信息 |
3 | 图片及视频 |
34 | 语音消息 |
42 | 名片(公众号名片) |
43 | 图片及视频 |
47 | 表情包 |
48 | 定位信息 |
49 | 小程序链接 |
10000 | 撤回消息提醒(XXXX撤回了一条消息) |
1048625 | 照片 |
16777265 | 链接 |
285212721 | 文件 |
419430449 | 微信转账 |
436207665 | 微信红包 |
469762097 | 微信红包 |
·11879048186 | 位置共享 |
… | (还有未知type信息,待补充) |
使用的技术栈:
- electron
- nodejs
- TypeScript
- Vue3
- vite
- antdv
- xlsx-0.20.0
创建系统框架
这里就不造轮子了,快速构建框架,使用的是脚手架,本项目是基于nodejs框架搭建的,如果电脑没有nodejs请先到官网下载安装[https://nodejs.org/en]
-> nodejs -v //18.16.0
-> yarn create @quick-start/electron
name: wx-crecords
-> cd wx-crecords
-> yarn
-> yarn dev
UI框架方面,只要有个文件上传组件的框架都可以,比如element-ui等都可以,可以根据自己的熟练程度自由使用,由于之前我写过类似的,所以这里选择了antdv4.x。
antdv4.x文档地址
-> yarn add [email protected]
// 官网有按需引入和全局引入代码示例,我为了项目大小使用了按需引入,详细请见官网
// https://next.antdv.com/docs/vue/getting-started-cn
import { createApp } from 'vue';
import Antd from 'ant-design-vue';
import App from './App';
import 'ant-design-vue/dist/reset.css';
const app = createApp(App);
app.use(Antd).mount('#app');
还有用到的其他npm库,在这里一起引入了。
-> yarn add https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz //处理excel表格
编写导入CSV代码
将代码引入完成后,在App.vue页面开始编写代码(可以按照自己的代码规范来,我方便起见直接写在了App页面了),先实现上传excel文件进行解析。
<template>
<div>
<Upload
:file-list="fileList"
:max-count="1"
:custom-request="handleImport"
accept=".xls, .xlsx, .csv"
@remove="handleRemove"
>
<AButton> 选择文件 </AButton>
</Upload>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { Upload, Button } from 'ant-design-vue' // 按需导入
export default defineComponent({
name: 'App',
components: {
Upload,
AButton: Button
}
})
</script>
<script setup lang="ts">
import { ref } from 'vue'
import type { UploadProps } from 'ant-design-vue'
import { read, utils } from 'xlsx'
// 导入的文件列表
const fileList = ref<UploadProps['fileList']>([])
// 导入后文件的处理
const handleImport = (file): void => {
fileList.value = [...(fileList.value || []), file.file]
const reader: FileReader = new FileReader()
reader.readAsBinaryString(file.file)
reader.onload = (ev): void => {
if (ev.target) {
const res = ev.target.result
const worker = read(res, { type: 'binary' })
// 将返回的数据转换为json对象的数据
const jsonData = utils.sheet_to_json(worker.Sheets[worker.SheetNames[0]])
console.log(jsonData)
}
}
}
// 删除文件
const handleRemove: UploadProps['onRemove'] = (file) => {
if (fileList.value) {
const index = fileList.value.indexOf(file)
const newFileList = fileList.value.slice()
newFileList.splice(index, 1)
fileList.value = newFileList
}
}
</script>
<style lang="less"></style>
log日志打印一下,导入了excel中所有的数据,我这里是23万条数据,大约10秒中的样子。
在这里可能会遇到乱码问题,只需要使用excel软件打开csv,另存为xlsx格式,再打开上传就没问题了。
列表的展示和筛选
创建Table列表接受展示xlsx里的内容,后期好处理。
<ATable :data-source="dataSource" :columns="columns" :scroll="{ y: 550 }">
<template #emptyText> 暂无数据 </template>
</ATable>
// table列表数据
const dataSource = ref<[]>([])
interface ColumnsType {
title: string
key: string
align: string
ellipsis?: boolean
width?: number
dataIndex?: string
}
// 表格header
const columns = ref<ColumnsType[]>([
{
title: '聊天顺序编号',
key: 'msgId',
dataIndex: 'msgId',
width: 120,
align: 'center'
},
{
title: '聊天用户Id',
dataIndex: 'talker',
key: 'talker',
width: 120,
align: 'center'
},
{
title: '聊天时间',
dataIndex: 'createTime',
key: 'createTime',
width: 90,
align: 'center'
},
{
title: '聊天内容类型',
dataIndex: 'type',
key: 'type',
width: 90,
align: 'center'
},
{
title: '标识消息发送者(0自己1对方)',
dataIndex: 'isSend',
key: 'isSend',
width: 90,
align: 'center'
},
{
title: '聊天内容',
dataIndex: 'content',
key: 'content',
ellipsis: true,
width: 150,
align: 'left'
}
])
const handleImport = (file): void => {
//省略...
dataSource.value = jsonData
//省略...
})
编写统计数据代码和导出数据
我这里直接上代码了,因为统计代码这一块,大家可以自己编写,我这里就直接上我的demo源码了,也没有优化就只简单地罗列代码,我的建议还是有基础的同学自己优化。我的gitee上也会随时更新。
let closestTime = 0
let closestDate = ''
let closesContent = ''
let smallestDifference = 24 * 60 * 60 * 1000 // 初始化为一天的毫秒数
const targetTime = '04:00:00'
let isSend_1 = 0
let isSend_0 = 0
let type_10000 = 0
let wenzi = 0
let yuyin = 0
let biaoqing = 0
let earliestTimestamp = Number.MAX_SAFE_INTEGER
let earliestDate = ''
let earliestContent = ''
dataSource.value.forEach((item) => {
if (item.talker === 'wxid_cjsjytzw4wms22') {
dataList.push({
msgId: item.msgId,
talker: item.talker,
createTime: item.createTime,
type: item.type,
isSend: item.isSend,
content: item.content
})
if (item.__createTime__ < earliestTimestamp) {
earliestTimestamp = item.__createTime__
earliestContent = item.content
earliestDate = item.createTime
}
if (item.isSend == '0') {
isSend_0++
} else {
isSend_1++
}
if (item.type == '10000') {
type_10000++
}
if (item.type == '1') {
try {
wenzi = wenzi + (item.content.length ? item.content.length : 0)
} catch (e) {}
dataList_type_1.push({
msgId: item.msgId,
talker: item.talker,
createTime: item.createTime,
type: item.type,
isSend: item.isSend,
content: item.content
})
}
if (item.type == '34') {
yuyin++
}
if (item.type == '47') {
biaoqing++
}
const time = item.createTime.split(' ')[1] // 提取时间部分
const timeObj = dayjs()
.hour(parseInt(time.split(':')[0]))
.minute(parseInt(time.split(':')[1]))
.second(parseInt(time.split(':')[2]))
const targetTimeObj = dayjs()
.hour(parseInt(targetTime.split(':')[0]))
.minute(parseInt(targetTime.split(':')[1]))
.second(parseInt(targetTime.split(':')[2]))
let difference = Math.abs(timeObj.diff(targetTimeObj, 'millisecond'))
// 如果时间跨越午夜(00:00:00),需要考虑这一点
if (difference > 12 * 60 * 60 * 1000) {
difference = 24 * 60 * 60 * 1000 - difference
}
if (difference < smallestDifference) {
smallestDifference = difference
closestTime = time
closestDate = item.createTime
closesContent = item.content
}
}
})
console.log(
'我们一共聊了:',
dataList.length,
'条, 其中我发的消息有',
isSend_1,
'条, 你发的消息有',
isSend_0,
'条, 我发消息的占比是',
(isSend_1 / (isSend_1 + isSend_0)) * 100
)
console.log('撤回消息了:', type_10000, '条')
console.log('联系人最早一条信息的时间是:', earliestTimestamp, earliestDate)
console.log('联系人最早一条信息的内容是:', earliestContent)
const date1 = dayjs(dataList[0].createTime).format('YYYY-MM-DD')
console.log(
'最晚的聊天的时间是:',
closestTime,
'那天是:' + closestDate,
'内容是:',
closesContent
)
console.log('只看文字消息,我们一共聊了:' + wenzi + '字')
console.log('我们一共发了:' + yuyin + '条语音消息,表情包发了' + biaoqing + '条')
导出的话使用了方法:
const handleExport = (): void => {
const lists = ref<DataType[]>(dataList_type_1)
const titleArr = ['msgId', 'talker', 'createTime', 'type', 'isSend', 'content']
exportExcel(lists.value as [], '聊天记录', titleArr, 'sheet1')
}
import * as XLSX from 'xlsx'
/**
* 导出
* @param json 数据
* @param name 文件名
* @param titleArr 标题
* @param sheetName 表名
*/
export const exportExcel = (
json: [],
name: string,
titleArr: string[],
sheetName: string
): void => {
/* convert state to workbook */
const data: Array<string[]> = []
const keyArray: string[] = []
const getLength = (obj): number => {
let count = 1
for (const i in obj) {
// eslint-disable-next-line no-prototype-builtins
if (obj.hasOwnProperty(i)) {
count++
}
}
return count
}
if (!Array.isArray(titleArr)) {
titleArr = []
}
for (const key1 in json) {
// eslint-disable-next-line no-prototype-builtins
if (json.hasOwnProperty(key1)) {
const element: object = json[key1]
const rowDataArray: string[] = []
for (const key2 in element) {
// eslint-disable-next-line no-prototype-builtins
if (element.hasOwnProperty(key2)) {
const element2 = element[key2]
rowDataArray.push(element2)
if (keyArray.length < getLength(element)) {
keyArray.push(key2)
}
}
}
data.push(rowDataArray)
}
}
// keyArray为英文字段表头
data.splice(0, 0, titleArr)
const ws = XLSX.utils.aoa_to_sheet(data)
const wb = XLSX.utils.book_new()
// 此处隐藏英文字段表头
// var wsrows = [{ hidden: true }];
// ws['!rows'] = wsrows; // ws - worksheet
XLSX.utils.book_append_sheet(wb, ws, sheetName)
/* generate file and send to client */
XLSX.writeFile(wb, name + '.xlsx')
}
这里的导出可以自由编写导出内容,如果要使用以下的ROSTCM6,只需要导出“content”列就可以。
使用ROSTCM6汇总词汇
说实话我对这个东西的运用也不是很熟悉,网上有很多教程,大家可以直接baidu.com搜一下,我这里只做简单的使用。
1.我这里使用的是 功能性分析->词频分析,将我们导出的xlsx文件另存为txt文件,导入到待处理文件中,点击确定后就会生成字频统计了。不过我没搞懂排序应该怎么来,但是又没有太多时间,也就用肉眼统计了。如果大家又更好办法欢迎留言或提交到issues,一起讨论进步ROST+系列人文社科研究大数据计算工具.zip: [https://url21.ctfile.com/f/27727221-930256395-b892cf?p=3889] (访问密码: 3889)
2.使用词频,也是同理的,在功能性分析->词频分析。把“只输出排名前”,改为15000,获取的更准确,(我自己试倒是没什么差距,看个人使用吧)
制作H5页面
我这里使用的是 MAKA设计 当然,其他的一些H5设计软件或者网站都可以,根据自己喜好来。作为一个臭写代码的确实审美不在线(==),我做的效果确实不咋地,这里放出两个互联网上做的很好的H5,放在这里大家可以参考:
https://maka.im/mk-viewer-7/pcviewer/603283060/MPOA0SYBW603283060?platform_type=web
https://maka.im/pcviewer/603314886/QQI1RQLQW603314886
https://u603912349.viewer.maka.im/k/0HAFA3YUW603912349
制作过程就是拖拉拽,没什么好讲的,可以参考着来。