LoginSignup
3
1

More than 5 years have passed since last update.

facebook からダウンロードした自分のデータを MongoDB に突っ込んでみるテスト

Posted at

この記事はなに

facebook から自分の投稿などのデータを一括ダウンロードできる機能がいつのまにかアップグレードしていて、(1) 期間を指定して (2) JSON 形式でも、ダウンロードできるようになってました (以前は全期間対象に html 形式でしかダウンロードできなかった)。
で、意気揚々とダウンロードしたのはいいものの、マルチバイトコードのエンコーディングに悩まされました。ググってもよくわからず、どうにかこうにか自力で処理して MongoDB に突っ込むことができたので、それを記録したものです。
「こうやれば一発解決なのに」などの情報をいただければ幸い、という記事です。
あとは、未来の自分のためのメモとして。

実際のコード

npmmongodb パッケージを使ったよ。

dunk.js
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 } }
3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1