微信聊天记录报告制作教程

前言

马上要到我们家小宝的生日了,思来想去没想好送什么东西,在上班摸鱼的时候无意间看到了三年前很火的《微信聊天记录统计报告》,这个东西很好玩,所以我决定搞一下。但是大体浏览了一下,发现事情没有想象的那么简单,所以借这次机会讲一整个过程记录下来,也是一次学习的机会啦。

参考文章:[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文件即可。

  1. 打开逍遥模拟器,将设置内中打开root权限。
    微信截图_20230824142324.png
  2. 在模拟器上安装微信,模拟器分辨率设置为手机形式的窄长型,注意不必着急登录,否则会把你正常手机上的微信踢下线。
    QQ截图20230824142504.png
  3. 进入你平时正常使用的手机上的微信,点击设置->聊天->聊天记录备份与迁移->迁移->迁移到手机/平板微信。
    Screenshot_2023-08-24-14-22-14-744_com.tencent.mm.jpgScreenshot_2023-08-24-14-22-18-197_com.tencent.mm.jpgScreenshot_2023-08-24-14-22-20-240_com.tencent.mm.jpgScreenshot_2023-08-24-14-22-26-068_com.tencent.mm.jpgScreenshot_2023-08-24-14-22-28-361_com.tencent.mm.jpg
  4. 选择聊天记录的时间和内容。内容强烈建议选择”不含图片/视频/文件”,否则迁移过程可能会非常慢。时间以你想统计的年月日跨度为准,可以导入几年或者几个月的。尽量选取你认为有必要统计的好友的私聊记录,不要点击全选,否则会很慢很慢。
    Screenshot_2023-08-24-14-22-39-571_com.tencent.mm.jpg
  5. 选择完成后点击迁移聊天记录,会出现如下的二维码,此时在电脑的模拟器上登录微信,用笔记本电脑自带的摄像头扫描该二维码即可开始同步。如果电脑没有摄像头,可以将该二维码截图后通过QQ、百度网盘、蓝牙等方式传输到电脑,之后在安卓模拟器里的微信点击扫描时选择实时截取屏幕的方式(如下图)扫取电脑上打开的二维码图片即可。
    Screenshot_2023-08-24-14-22-52-191_com.tencent.mm.jpgScreenshot_2023-08-24-13-54-56-106_com.tencent.mm.jpg
    在这一步可能会遇到模拟器扫描二维码闪退问题,解决方法:[官网教程],我在这里用简单的方法说一下:
    在桌面系统管理安装"谷歌安装器",在 设置->设备,将摄像头改为虚拟,就可以扫描电脑上的画面。
    image.png
    image.png
    image.png
    image.png
    在这一步提示手机和电脑不在同一个wifi,可以在设置->网络->WIFI热点 改成与你手机连接的wifi同一个名字,前提是必须要模拟器的电脑和手机连接的是同一个网络。
    微信截图_20230824142524.png
  6. 同步完成后,打开模拟器里的文件管理器,在其 根目录/data/data/com.tencent.mm/MicroMsg/(一个32位字符串命名的文件夹中)中找到EnMicroMsg.db文件。这里的(32位字符串命名的文件夹)如果你只在模拟器上登陆过一个微信的话就只有一个,如果有两个这样命名的文件夹的话(如下图),那就每个都打开看看哪个文件夹中能找到EnMicroMsg.db。找到后将该db文件拷贝到电脑上。
    关于如何从模拟器中复制文件到电脑文件夹中请参考:1.将文件选中,返回内部共享空间。2.打开Download文件夹,该文件夹是与电脑共享。3.点击左下角点,粘贴所选项,就可以就将内容粘贴在此。
    image.pngimage.pngimage.pngimage.png
    image.png
    参考教程:

    如果找不到该文件或路径,请按照以下方法:1.点击左下角设置。2.点击常规设置3.将访问模式改成"超级用户访问模式",获取最高权限,再点击根目录就能找到上面路径。
    image.pngimage.pngimage.pngimage.pngimage.png

聊天记录破解

这一步是对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码。
image.pngimage.pngimage.png
然后将手机IMEI码和微信uin码直接相连后,用换算工具换算成小写32位md5值,其前7位就是破解密码。
image.png
打开工具sqlcipher,打开我们导出的db文件,输入密码,就可以看到好多列表,依次点击File->Export->Table as CSV file,选择message表导出,一定要自己加上后缀.csv。导出就是excel。
image.pngimage.pngimage.png
image.png
image.png
到这一步就搞到了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],需要的可以直接拉取。

开发思路

写其他语言的朋友可以看这里的思路,写法虽然不一样,但是思路是一样的。

  1. 上传CSV带客户端进行解析,获得解析后的数据(比如json)。
  2. 得到数据后筛选名称,比如我想统计李华的聊天记录,那我先把我跟他的聊天记录进行筛选。
  3. 筛选完成后获取可以统计内容,比如,我们聊天当中发的什么内容最多,我们最晚的一次聊天是到几点,我们说话聊天最多的一天是哪天之类的。
  4. 统计完成后生成模板数据,展示到模板上或者其他随便什么地方。

这里提供了两个表格,可以按照表内内容进行检索。

列名 内容
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 ant-design-vue@4.x
// 官网有按需引入和全局引入代码示例,我为了项目大小使用了按需引入,详细请见官网
// 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秒中的样子。
image.png
在这里可能会遇到乱码问题,只需要使用excel软件打开csv,另存为xlsx格式,再打开上传就没问题了。
image.png

列表的展示和筛选

创建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
  //省略...
})

image.png

编写统计数据代码和导出数据

我这里直接上代码了,因为统计代码这一块,大家可以自己编写,我这里就直接上我的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)

image.png
image.png
2.使用词频,也是同理的,在功能性分析->词频分析。把“只输出排名前”,改为15000,获取的更准确,(我自己试倒是没什么差距,看个人使用吧)
image.png

制作H5页面

我这里使用的是 MAKA设计 当然,其他的一些H5设计软件或者网站都可以,根据自己喜好来。作为一个臭写代码的确实审美不在线(==),我做的效果确实不咋地,这里放出两个互联网上做的很好的H5,放在这里大家可以参考:
image.pngimage.pngimage.png
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
制作过程就是拖拉拽,没什么好讲的,可以参考着来。

posted @ 2023-09-04 10:46  敖懿  阅读(1041)  评论(0)    收藏  举报