この記事はなに
facebook から自分の投稿などのデータを一括ダウンロードできる機能がいつのまにかアップグレードしていて、(1) 期間を指定して (2) JSON 形式でも、ダウンロードできるようになってました (以前は全期間対象に html 形式でしかダウンロードできなかった)。
で、意気揚々とダウンロードしたのはいいものの、マルチバイトコードのエンコーディングに悩まされました。ググってもよくわからず、どうにかこうにか自力で処理して MongoDB に突っ込むことができたので、それを記録したものです。
「こうやれば一発解決なのに」などの情報をいただければ幸い、という記事です。
あとは、未来の自分のためのメモとして。
実際のコード
npm
の mongodb
パッケージを使ったよ。
const fs = require('fs')
const mongodb = require('mongodb')
const MongoClient = mongodb.MongoClient
const client = new MongoClient('mongodb://localhost:27017')
client.connect((err) => {
const db = client.db('fbtk')
insertDocuments(db, () => {
client.close()
})
})
const insertDocuments = (db, callback) => {
const obj = ((s, r, u, v) => {
v = ((v) => {
u.forEach((c,i) => v[c] = {
u: parseInt(i/4), e: (8<=i&&i<12)*(i-8), l: (i%4)*4 })
return v
})({})
return JSON.parse(s.replace(r, (m, n11, n21, n22, n31, n32, offset, str) => {
return '\\u' + n11 + u[v[n21].e*4+v[n22].u] + u[v[n22].l+v[n31].e] + n32}))
})(require('fs').readFileSync('/var/tmp/your_posts.json', 'utf8'),
/\\u00[89a-f]([\da-f])\\u00([89ab])([\da-f])\\u00([89ab])([\da-f])/g,
'0123456789abcdef'.split(''))
db.collection('ownpost').insertMany(obj.status_updates, (err, result) => {
callback(result)
})
}
問題と解決
JavaScript で読むと文字化け
facebook からダウンロードした JSON の文字列データは、「UTF-8 でエンコーディングしたもの」のではなく、『「UTF-8 でエンコーディングしたもの」を ASCII 文字で表現できるよう "\u" エスケープしたもの』になっていました。
具体的には、
{ "description" : "Windows XP \u00e3\u0081\u00ae\u00e4\u00bb\u0095\u00e4\u00ba\u008b" }
のようになっていました。文字列は「Windows XP の仕事」を表しています。
最初は無邪気にこのまま MongoDB に突っ込んだのですが、文字化けしました。
JavaScript での Unicode エスケープ表現は、UTF-16 を「\uXXXX
」で表したものなので、U+306E である「の」は「\u306e
」という表現になるはずです。ですので、期待されるのは次のような文字列です。
{ "description" : "Windows XP \u306e\u4ed5\u4e8b" }
JavaScript で先の JSON をそのまま読み込むと、「の」に相当する「\u00e3\u0081\u00ae
」を 「U+00e3、U+0081、U+00ae」の 3 文字として解釈してしまいます。
「の」が「\u00e3\u0081\u00ae
」になっているのは、「の」を UTF-8 で表現すると「e381ae」になるから、でしょう。すなわち、2 バイト文字 (U+0800 〜 U+FFFF) の 2 進数表現が次の場合、
upper byte: vvvvxxxx
lower byte: yyyyzzzz
UTF-8 では、次の 3 バイトになります。
first byte: 1110vvvv
second byte: 10xxxxyy
third byte: 10yyzzzz
解決方法
簡単な方法がないかな、とググったのですが、すぐに見つからなかったので、自分でやってみました。
JSON.parse()
するときに、2 バイト文字 (U+0800 〜 U+FFFF) の UTF-8 表現である \u
エスケープされた 3 バイトの連続が現われたら、JavaScript の Unicode エスケープ表現に変換しちゃう、という方法です。先のコードで
JSON.parse(s.replace(r, (m, n11, n21, n22, n31, n32, offset, str) => {
return '\\u' + n11 + u[v[n21].e*4+v[n22].u] + u[v[n22].l+v[n31].e] + n32}))
のところがそれです。正規表現 r
は、2 バイト文字のところだけにマッチするようになっています。
あとは「16 進数 <-> 数値」変換が必要になりますが、まともにやるのがアレだったので、配列 u
や連想配列 v
を用意して基本的に表引きだけで済ますようにしました。配列 u
が数値から 16 進数表現文字への変換テーブル、連想配列 v
が 16 進数表現文字から対応する数値への変換テーブルになっています。
参考までに。
u = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' ]
v = { '0': { u: 0, e: 0, l: 0 },
'1': { u: 0, e: 0, l: 4 },
'2': { u: 0, e: 0, l: 8 },
'3': { u: 0, e: 0, l: 12 },
'4': { u: 1, e: 0, l: 0 },
'5': { u: 1, e: 0, l: 4 },
'6': { u: 1, e: 0, l: 8 },
'7': { u: 1, e: 0, l: 12 },
'8': { u: 2, e: 0, l: 0 },
'9': { u: 2, e: 1, l: 4 },
a : { u: 2, e: 2, l: 8 },
b : { u: 2, e: 3, l: 12 },
c : { u: 3, e: 0, l: 0 },
d : { u: 3, e: 0, l: 4 },
e : { u: 3, e: 0, l: 8 },
f : { u: 3, e: 0, l: 12 } }