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

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

# 実際のコード

`npm` の `mongodb` パッケージを使ったよ。

```javascript: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" エスケープしたもの』になっていました。
具体的には、

```json
 { "description" : "Windows XP \u00e3\u0081\u00ae\u00e4\u00bb\u0095\u00e4\u00ba\u008b" }
```

のようになっていました。文字列は「Windows XP の仕事」を表しています。
最初は無邪気にこのまま MongoDB に突っ込んだのですが、文字化けしました。
JavaScript での Unicode エスケープ表現は、UTF-16 を「`\uXXXX`」で表したものなので、U+306E である「の」は「`\u306e`」という表現になるはずです。ですので、期待されるのは次のような文字列です。

```json
 { "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 エスケープ表現に変換しちゃう、という方法です。先のコードで

```javascript
    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 } }
```