iPhoneのメッセージアプリをMacに保存できないかと思って調べてみました。
iTunesのバックアップのファイルから取り出すことができるみたいですね。
~/Library/Application\ Support/MobileSync/Backup/[UDID]/3d/3d0d7e5fb2ce288813306e4d4636395e047a3d28
識別子(UDID)はそれぞれのiPhoneによって変わります。
3d0d
から始まる長いファイルの名前ですが、このファイルはiPhoneで使用されている時(バックアップされたファイルではなく)はsms.db
という名前のSQLiteのデータベースファイルになっているようです。
なので、SQLiteでアクセスしてメッセージを取り出してみます。
iTunesのバックアップで暗号化をする設定にしていたら、SQLiteでアクセスした際にError: file is encrypted or is not a database
のようなエラーとなりました。
なので、iTunesのバックアップで暗号化している場合は、一度、暗号化の設定をしていない状態でバックアップしてからファイルをコピーします。
コピーしたらまた暗号化の設定をしておきます。
table
最初にテーブルについて定義を確認しました。
_SqliteDatabaseProperties
column-name | type-name | column-constraint |
---|---|---|
key | TEXT | UNIQUE |
value | TEXT |
message
colmun-name | type-name | column-constraint |
---|---|---|
ROWID | INTEGER | PRIMARY KEY AUTOINCREMENT |
guid | TEXT | UNIQUE NOT NULL |
text | TEXT | DEFAULT 0 |
replace | INTEGER | |
service_center | TEXT | |
handle_id | INTEGER | DEFAULT 0 |
subject | TEXT | |
country | TEXT | |
attributedBody | BLOB | |
version | INTEGER | DEFAULT 0 |
type | INTEGER | DEFAULT 0 |
service | TEXT | |
account | TEXT | |
account_guid | TEXT | |
error | INTEGER | DEFAULT 0 |
date | INTEGER | |
date_read | INTEGER | |
date_delivered | INTEGER | |
is_delivered | INTEGER | DEFAULT 0 |
is_finished | INTEGER | DEFAULT 0 |
is_emote | INTEGER | DEFAULT 0 |
is_from_me | INTEGER | DEFAULT 0 |
is_empty | INTEGER | DEFAULT 0 |
is_delayed | INTEGER | DEFAULT 0 |
is_auto_reply | INTEGER | DEFAULT 0 |
is_prepared | INTEGER | DEFAULT 0 |
is_read | INTEGER | DEFAULT 0 |
is_system_message | INTEGER | DEFAULT 0 |
is_sent | INTEGER | DEFAULT 0 |
has_dd_results | INTEGER | DEFAULT 0 |
is_service_message | INTEGER | DEFAULT 0 |
is_forward | INTEGER | DEFAULT 0 |
was_downgraded | INTEGER | DEFAULT 0 |
is_archive | INTEGER | DEFAULT 0 |
cache_has_attachments | INTEGER | DEFAULT 0 |
cache_roomnames | INTEGER | DEFAULT 0 |
was_data_detected | INTEGER | DEFAULT 0 |
was_deduplicated | INTEGER | DEFAULT 0 |
is_audio_message | INTEGER | DEFAULT 0 |
is_played | INTEGER | DEFAULT 0 |
date_played | INTEGER | |
item_type | INTEGER | DEFAULT 0 |
other_handle | INTEGER | DEFAULT -1 |
group_title | TEXT | |
group_action_type | INTEGER | DEFAULT 0 |
share_status | INTEGER | |
share_direction | INTEGER | |
is_expirable | INTEGER | DEFAULT 0 |
expire_state | INTEGER | DEFAULT 0 |
message_action_type | INTEGER | DEFAULT 0 |
message_source | INTEGER | DEFAULT 0 |
associated_message_guid | STRING | DEFAULT NULL |
balloon_bundle_id | STRING | DEFAULT NULL |
payload_data | BLOB | |
associated_message_type | INTEGER | DEFAULT 0 |
expressive_send_style_id | STRING | DEFAULT NULL |
associated_message_range_location | INTEGER | DEFAULT 0 |
associated_message_range_length | INTEGER | DEFAULT 0 |
time_expressive_send_played | INTEGER | DEFAULT 0 |
message_summary_info | BLOB | DEFAULT NULL |
sqlite_sequence
colmun-name | type-name | column-constraint |
---|---|---|
name | ||
seq |
chat
colmun-name | type-name | column-constraint |
---|---|---|
ROWID | INTEGER | PRIMARY KEY AUTOINCREMENT |
guid | TEXT | UNIQUE NOT NULL |
style | INTEGER | |
state | INTEGER | |
account_id | TEXT | |
properties | BLOB | |
chat_identifier | TEXT | |
service_name | TEXT | |
room_name | TEXT | |
account_login | TEXT | |
is_archived | INTEGER | DEFAULT 0 |
last_addressed_handle | TEXT | |
display_name | TEXT | |
group_id | TEXT | |
is_filtered | INTEGER | DEFAULT 0 |
successful_query | INTEGER | DEFAULT 1 |
attachment
colmun-name | type-name | column-constraint |
---|---|---|
ROWID | INTEGER | PRIMARY KEY AUTOINCREMENT |
guid | TEXT | UNIQUE NOT NULL |
created_date | INTEGER | DEFAULT 0 |
start_date | INTEGER | DEFAULT 0 |
filename | TEXT | |
uti | TEXT | |
mime_type | TEXT | |
transfer_state | INTEGER | DEFAULT 0 |
is_outgoing | INTEGER | DEFAULT 0 |
user_info | BLOB | |
transfer_name | TEXT | |
total_bytes | INTEGER | DEFAULT -1 |
is_sticker | INTEGER | DEFAULT 0 |
sticker_user_info | BLOB | |
attribution_info | BLOB | |
hide_attachment | INTEGER | DEFAULT 0 |
handle
colmun-name | type-name | column-constraint |
---|---|---|
ROWID | INTEGER | PRIMARY KEY AUTOINCREMENT UNIQUE |
id | TEXT | NOT NULL, UNIQUE |
country | TEXT | |
service | TEXT | NOT NULL, UNIQUE |
uncanonicalized_id | TEXT |
message_attachment_join
colmun-name | type-name | column-constraint |
---|---|---|
message_id | INTEGER | REFERENCES message (ROWID) ON DELETE CASCADE, UNIQUE |
attachment_id | INTEGER | REFERENCES attachment (ROWID) ON DELETE CASCADE, UNIQUE |
chat_handle_join
colmun-name | type-name | column-constraint |
---|---|---|
chat_id | INTEGER | REFERENCES chat (ROWID) ON DELETE CASCADE, UNIQUE |
handle_id | INTEGER | REFERENCES handle (ROWID) ON DELETE CASCADE, UNIQUE |
chat_message_join
colmun-name | type-name | column-constraint |
---|---|---|
chat_id | INTEGER | REFERENCES chat (ROWID) ON DELETE CASCADE, PRIMARY KEY |
message_id | INTEGER | REFERENCES message (ROWID) ON DELETE CASCADE, PRIMARY KEY |
deleted_messages
colmun-name | type-name | column-constraint |
---|---|---|
ROWID | INTEGER | PRIMARY KEY AUTOINCREMENT UNIQUE |
guid | TEXT | NOT NULL |
message
テーブルにメッセージがありそうですね。
attachment
テーブルに添付ファイルの情報がありそうですが、実際の添付されたファイルは別のところにありそうで、今回はそこまで調べていません。
chat
テーブルはLINEでいうところのトークルームのような情報がありそうです。
handle
に相手のアドレスの情報があるようです。
取り出す
簡単に取り出すために次のようなコマンドとSQLにしてみました。
カレントディレクトリにsms.csv
という名前でファイルを作成します。
CSV的な結果になるようにしていますが、文字コードの問題があるので、Excelでは文字化けしてしまいます。
でも、UTF-8なので、絵文字はちゃんと表示されます。
$ sqlite3 3d0d7e5fb2ce288813306e4d4636395e047a3d28
sqlite> .output ./sms.csv
sqlite> .separator ,
sqlite> SELECT
'"' || T2.text || '"'
, T2.subject
, datetime(T2.date + 978307200, 'unixepoch', 'localtime') AS date
, CASE T2.is_from_me WHEN 0 THEN 'Received' WHEN 1 THEN 'Sent' ELSE 'Unknown' END AS is_from_me
, T3.id
FROM
chat_message_join T0
INNER JOIN
chat T1
ON
T0.chat_id = T1.ROWID
INNER JOIN
message T2
ON
T0.message_id = T2.ROWID
INNER JOIN
handle T3
ON
T2.handle_id = T3.ROWID
ORDER BY
T2.date DESC;
Node.jsとSQLite
Node.jsとSQLiteからアクセスしたソースも書いておきます。
こちらはExcelで開けるようにShift_JIS
に変換してみました。
その代わり絵文字は文字化けしてしまいます。
sqlite3とiconvのnpmパッケージを使ってみました。
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database('./3d0d7e5fb2ce288813306e4d4636395e047a3d28')
const Iconv = require('iconv').Iconv;
const sjis = new Iconv('UTF-8', 'Shift_JIS//TRANSLIT//IGNORE');
const fs = require('fs');
const filename = './sms.csv';
db.serialize(() => {
const sql = [];
sql.push('SELECT');
sql.push(' T2.text');
sql.push(' , T2.subject');
sql.push(' , datetime(T2.date + 978307200, \'unixepoch\', \'localtime\') AS date');
sql.push(' , CASE T2.is_from_me WHEN 0 THEN \'Received\' WHEN 1 THEN \'Sent\' ELSE \'Unknown\' END AS is_from_me');
sql.push(' , T3.id');
sql.push('FROM');
sql.push(' chat_message_join T0');
sql.push(' INNER JOIN');
sql.push(' chat T1');
sql.push(' ON');
sql.push(' T0.chat_id = T1.ROWID');
sql.push(' INNER JOIN');
sql.push(' message T2');
sql.push(' ON');
sql.push(' T0.message_id = T2.ROWID');
sql.push(' INNER JOIN');
sql.push(' handle T3');
sql.push(' ON');
sql.push(' T2.handle_id = T3.ROWID');
sql.push('ORDER BY');
sql.push(' T2.date DESC');
db.each(sql.join(' '), (err, row) => {
if (err) {
console.log(JSON.stringify(err));
return;
}
fs.appendFile(filename, sjis.convert(`${row.date}, ${row.id}, ${row.is_from_me}, ${row.subject}, \"${row.text}\"\r\n`));
});
});
db.close();