<audio id="audio" title="40丨SQLite：为什么微信用SQLite存储聊天记录？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/47/e1/47738cbc6adc70f6ac54bb1eb1113be1.mp3"></audio>

我在上一篇文章中讲了WebSQL，当我们在Chrome、Safari和Firefox等浏览器客户端中使用WebSQL时，会直接操作SQLite。实际上SQLite本身是一个嵌入式的开源数据库引擎，大小只有3M左右，可以将整个SQLite嵌入到应用中，而不用采用传统的客户端／服务器（Client/Server）的架构。这样做的好处就是非常轻便，在许多智能设备和应用中都可以使用SQLite，比如微信就采用了SQLite作为本地聊天记录的存储。

今天我们就来深入了解一下SQLite，今天的内容主要包括以下几方面：

1. SQLite是什么？它有哪些优点和不足？
1. 如何在Python中使用SQLite？
1. 如何编写SQL，通过SQLite查找微信的聊天记录？

## SQLite是什么

SQLite是在2000年发布的，到目前为止已经有19年了。一直采用C语言编写，采用C语言而非C++面向对象的方式，可以提升代码底层的执行效率。但SQLite也有一些优势与不足。

它的优势在于非常轻量级，存储数据非常高效，查询和操作数据简单方便。此外SQLite不需要安装和配置，有很好的迁移性，能够嵌入到很多应用程序中，与托管在服务器上的RDBMS相比，约束少易操作，可以有效减少服务器的压力。

不足在于SQLite常用于小到中型的数据存储，不适用高并发的情况。比如在微信本地可以使用SQLite，即使是几百M的数据文件，使用SQLite也可以很方便地查找数据和管理，但是微信本身的服务器就不能使用SQLite了，因为SQLite同一时间只允许一个写操作，吞吐量非常有限。

作为简化版的数据库，SQLite没有用户管理功能，在语法上也有一些自己的“方言”。比如在SQL中的SELECT语句，SQLite可以使用一个特殊的操作符来拼接两个列。在MySQL中会使用函数concat，而在SQLite、PostgreSQL、Oracle和Db2中使用||号，比如：SELECT `MesLocalID || Message FROM "Chat_1234"`。

这个语句代表的是从Chat_1234数据表中查询MesLocalID和Message字段并且将他们拼接起来。

但是在SQLite中不支持RIGHT JOIN，因此你需要将右外连接转换为左外连接，也就是LEFT JOIN，写成下面这样：

```
SELECT * FROM team LEFT JOIN player ON player.team_id = team.team_id

```

除此以外SQLite仅支持只读视图，也就是说，我们只能创建和读取视图，不能对它们的内容进行修改。

总的来说支持SQL标准的RDBMS语法都相似，只是不同的DBMS会有一些属于自己的“方言”，我们使用不同的DBMS的时候，需要注意。

## 在Python中使用SQLite

我之前介绍过如何在Python中使用MySQL，其中会使用到DB API规范（如下图所示）。基于DB API规范，我们可以对数据库进行连接、交互以及异常的处理。

<img src="https://static001.geekbang.org/resource/image/ef/a4/efd39186177ed0537e6e75dccaf3cba4.png" alt=""><br>
在Python中使用SQLite也会使用到DB API规范，与使用MySQL的交互方式一样，也会用到connection、cursor和exceptions。在Python中集成了SQLite3，直接加载相应的工具包就可以直接使用。下面我们就来看下如何在Python中使用SQLite。

在使用之前我们需要进行引用SQLite，使用：

```
import sqlite3

```

然后我们可以使用SQLite3创建数据库连接：

```
conn = sqlite3.connect(&quot;wucai.db&quot;)

```

这里我们连接的是wucai.db这个文件，如果没有这个文件存储，上面的调用会自动在相应的工程路径里进行创建，然后我们可以使用conn操作连接，通过会话连接conn来创建游标：

```
cur = conn.cursor()

```

通过这一步，我们得到了游标cur，然后可以使用execute()方法来执行各种DML，比如插入，删除，更新等，当然我们也可以进行SQL查询，用的同样是execute()方法。

比如我们想要创建heros数据表，以及相应的字段id、name、hp_max、mp_max、role_main，可以写成下面这样：

```
cur.execute(&quot;CREATE TABLE IF NOT EXISTS heros (id int primary key, name text, hp_max real, mp_max real, role_main text)&quot;)

```

在创建之后，我们可以使用execute()方法来添加一条数据：

```
cur.execute('insert into heros values(?, ?, ?, ?, ?)', (10000, '夏侯惇', 7350, 1746, '坦克'))

```

需要注意的是，一条一条插入数据太麻烦，我们也可以批量插入，这里会使用到executemany方法，这时我们传入的参数就是一个元组，比如：

```
cur.executemany('insert into heros values(?, ?, ?, ?, ?)', 
           ((10000, '夏侯惇', 7350, 1746, '坦克'),
            (10001, '钟无艳', 7000, 1760, '战士'),
          (10002, '张飞', 8341, 100, '坦克'),
          (10003, '牛魔', 8476, 1926, '坦克'),
          (10004, '吕布', 7344, 0, '战士')))

```

如果我们想要对heros数据表进行查询，同样使用execute执行SQL语句：

```
cur.execute(&quot;SELECT id, name, hp_max, mp_max, role_main FROM heros&quot;)

```

这时cur会指向查询结果集的第一个位置，如果我们想要获取数据有以下几种方法：

1. cur.fetchone()方法，获取一条记录；
1. cur.fetchmany(n) 方法，获取n条记录；
1. cur.fetchall()方法，获取全部数据行。

比如我想获取全部的结果集，可以写成这样：

```
result = cur.fetchall()

```

如果我们对事务操作完了，可以提交事务，使用`conn.commit()`即可。

同样，如果游标和数据库的连接都操作完了，可以对它们进行关闭：

```
cur.close()
conn.close()

```

上面这个过程的完整代码如下：

```
import sqlite3
# 创建数据库连接
conn = sqlite3.connect(&quot;wucai.db&quot;)
# 获取游标
cur = conn.cursor()
# 创建数据表
cur.execute(&quot;CREATE TABLE IF NOT EXISTS heros (id int primary key, name text, hp_max real, mp_max real, role_main text)&quot;)
# 插入英雄数据
cur.executemany('insert into heros values(?, ?, ?, ?, ?)', 
           ((10000, '夏侯惇', 7350, 1746, '坦克'),
            (10001, '钟无艳', 7000, 1760, '战士'),
          (10002, '张飞', 8341, 100, '坦克'),
          (10003, '牛魔', 8476, 1926, '坦克'),
          (10004, '吕布', 7344, 0, '战士')))
cur.execute(&quot;SELECT id, name, hp_max, mp_max, role_main FROM heros&quot;)
result = cur.fetchall()
print(result)
# 提交事务 
conn.commit()
# 关闭游标
cur.close()
# 关闭数据库连接
conn.close()

```

除了使用Python操作SQLite之外，在整个操作过程中，我们同样可以使用navicat数据库可视化工具来查看和管理SQLite。

<img src="https://static001.geekbang.org/resource/image/f2/fe/f2cb51591733d386239f843bd83c8afe.png" alt="">

## 通过SQLite查询微信的聊天记录

刚才我们提到很多应用都会集成SQLite作为客户端本地的数据库，这样就可以避免通过数据库服务器进行交互，减少服务器的压力。

如果你是iPhone手机，不妨跟着我执行以下的步骤，来查找下微信中的SQLite文件的位置吧。

第一步，使用iTunes备份iPhone；第二步，在电脑中查找备份文件。

当我们备份好数据之后，需要在本地找到备份的文件，如果是windows可以在C:\Users\XXXX\AppData\Roaming\Apple Computer\MobileSync\Backup 这个路径中找到备份文件夹。

第三步，查找Manifest.db。

在备份文件夹中会存在Manifest.db文件，这个文件定义了苹果系统中各种备份所在的文件位置。

第四步，查找MM.sqlite。

Manifest.db本身是SQLite数据文件，通过SQLite我们能看到文件中包含了Files数据表，这张表中有fileID、domain和relativePath等字段。

微信的聊天记录文件为MM.sqlite，我们可以直接通过SQL语句来查询下它的位置（也就是fileID）。

```
SELECT * FROM Files WHERE relativePath LIKE '%MM.sqlite'

```

<img src="https://static001.geekbang.org/resource/image/d1/52/d11e7857bbf1da3a6b4db9c34d561e52.png" alt="">

你能看到在我的微信备份中有2个MM.sqlite文件，这些都是微信的聊天记录。

第五步，分析找到的MM.sqlite。

这里我们需要在备份文件夹中查找相关的fileID，比如f71743874d7b858a01e3ddb933ce13a9a01f79aa。

找到这个文件后，我们可以复制一份，取名为weixin.db，这样就可以使用navicat对这个数据库进行可视化管理，如下图所示：

<img src="https://static001.geekbang.org/resource/image/64/27/6401a3e7bbcf7757b27233b777934327.png" alt=""><br>
微信会把你与每一个人的聊天记录都保存成一张数据表，在数据表中会有MesLocalID、Message、Status等相应的字段，它们分别代表在当前对话中的ID、聊天内容和聊天内容的状态）。

如果聊天对象很多的话，数据表也会有很多，如果想知道都有哪些聊天对象的数据表，可以使用：

```
SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'Chat\_%' escape '\'

```

这里需要说明的是sqlite_master是SQLite的系统表，数据表是只读的，里面保存了数据库中的数据表的名称。聊天记录的数据表都是以Chat_开头的，因为`（_）`属于特殊字符，在LIKE语句中会将`（_）`作为通配符。所以如果我们想要对开头为Chat_的文件名进行匹配，就需要用escape对这个特殊字符做转义。

## 总结

我今天讲了有关SQLite的内容。在使用SQLite的时候，需要注意SQLite有自己的方言，比如在进行表连接查询的时候不支持RIGHT JOIN，需要将其转换成LEFT JOIN等。同时，我们在使用execute()方法的时候，尽量采用带有参数的SQL语句，以免被SQL注入攻击。

学习完今天的内容后，不如试试用SQL查询来查找本地的聊天记录吧。

<img src="https://static001.geekbang.org/resource/image/b9/91/b932d2e3a31f908cf1de9b80f29e8891.png" alt=""><br>
最后留一道思考题吧。请你使用SQL查询对微信聊天记录中和“作业”相关的记录进行查找。不论是iPhone，还是Android手机都可以找到相应的SQLite文件，你可以使用Python对SQLite进行操作，并输出结果。

欢迎你在评论区写下你的答案，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。


