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

前言

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

参考文章:[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:47  敖懿  阅读(104)  评论(0编辑  收藏  举报