WebアプリのDBからjsonデータを取得⇒Wordpress取り込み用にデータ変換⇒一括で記事登録したのでその時の様子を書く

# 今回やったこと

## やったことサマリ

もともと運営していた全国の絶景を集めたWebアプリがあって、そこのデータベースから情報を抜いてきて、いい感じに編集して、Wordpressの記事を一括登録(300件くらい)したよ！

誰か同じことを考えている方に役に立つかもしれないからQiitaにしておいたよ！

こんな感じです。

## 背景

私はもともと、[絶景マップ](http://tasokori.net/webapps/zekkei-map/)というWebアプリを持っていました。

これは、旅好きの私が実際にその場所を訪れて「ここは絶景やな！」と思ったポイントを登録していき、地図一覧や写真一覧でオススメスポットが見れるよ、という主に私向けの弱小アプリでした。こんな感じです↓
![zekkei-map1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145371/2e984f98-b447-757c-612f-eaf7382d4fa5.png)
なんですけど、まあこんな弱小アプリでは集客できるわけでもなく(自分向けですしね)、大したPVはありませんでした。

このアプリ自体の行く末としては、今のまま弱小でPV無くてもよかったんですが、実はこの度[バイクツーリング特化のブログ](https://love-moto-touring.info)を作成しまして、こちらでバイクツーリングにオススメな絶景スポットを紹介し始めました。

既にせっかくWebアプリの方にデータが溜まっているのに、また新たに一からWordpressでブログ記事を書いていくのは辛すぎる(Webアプリの方に300カ所程度の場所情報が詰まっているので...)ので、今回Webアプリのデータベースに溜まっている情報をもとに、Wordpressの記事(ドラフトレベル)を一括で作成して楽してやろう！と思い立ち、2時間程で変換ツール作成及びWordpressへのインポートが正常終了したことを確認したので、Qiitaに残してみようかなと思いました。そんな感じです。

# DBからjsonデータ取得⇒変換⇒Wordpressに一括記事登録の全貌を書いていく

## 1. DBからjsonデータ取得(5分)

もともとのWebアプリは、レンタルサーバ(ロリポップ)のphpMyAdminにログインし、GUIからダウンロードしてきました。継続的にデータを連携する必要があれば、ここもphpにしておくべきですが、今回はこの1回きりですので、普通にダウンロードします。

まずは「エクスポート」をクリック
![lolipop-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145371/b1354165-9846-628b-6854-0abe6d905ac1.png)

エクスポートタブで、「フォーマット」をJSONにして「実行」押下でOKです。
![lolipop-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145371/19969696-924d-faf2-1912-59bb30885dc6.png)

ここで1点注意で、phpMyAdminからJSONとしてダウンロードすると、先頭にコメントが付いてきます。これをそのままjavascriptとかでimportするとエラーになるので(JSONはコメントNGなんで)、事前に消しておきます。
![jsondata-1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145371/b1248fac-fdd8-7296-c740-ce9745c01cf8.png)
↑こんな感じで色々コメントが付いていました。消します。

これでJSONデータの準備は完了です。とっても簡単です！

###一応、テーブルの構造を書いておく(なんの参考にもならないけど)

一応、今ダウンロードしたDBのテーブルの構造について書いておきます。正直これは何のタメにもならないですが、話がちょっと分かりやすくなるかなと。(後のソース見た時とか)

今回、Webアプリ入力は色々とテーブルがあれど、Wordpressの記事にするために使用するのはわずか2テーブルでして、それが下記のとおり。
![lolipop3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145371/f6dafbc5-e2f8-62ce-7141-7866d704bd97.png)


####MHM_M_POINT_DATA(絶景の場所情報メインを司るテーブル)

|物理名  |意味  |
|---|---|
|id |ID |
|name |場所の名称 |
|lat |緯度 |
|lng |経度 |
|prefecture |都道府県名 |
|zip_no |郵便番号 |
|address |住所 |
|caption |その場所に対するコメント(長文) |
|favorite |お気に入り度(今回不使用) |
|accessibility |アクセスのかんたんさ(今回不使用) |
|crowdness_ave |混雑度(今回不使用) |
|place_type |場所の種別(高原とか建築物とかそういう感じ)(今回不使用) |
|gmap_by_latlng |不使用(今回不使用) |
|image_url_top |サムネイル画像のURL(今回不使用) |
|is_private |プレイべート扱いにする時に使うフラグ(今回不使用) |
|create_datetime |作成日(今回不使用) |
|update_datetime |更新日(今回不使用) |

####MHM_M_PICTURES(絶景の場所情報に紐づく画像を保持しておくテーブル)

|物理名  |意味  |
|---|---|
|id |ID |
|seq |連番 |
|image_url |画像のURL |
|image_url_thumb |画像のサムネイルURL |
|comment |画像へのコメント |
|visit_date |撮影日 |
|month |撮影した月 |
|timing_of_month |上旬/中旬/下旬の区分 |
|author |撮影者 |
|recomend |推奨度 |
|is_private |プライベート画像の場合ON |
|create_datetime |作成日 |
|update_datetime |更新日 |

## 2. 変換(Javascriptで変換ツール作成)(2時間)

さて、データソースを無事取得できたので、今度はそのデータをWordpressに取り込める形に変換していきます。

今回wordpressへ一括記事取り込みに使用したプラグインは「Really Simple CSV Importer」で、このプラグインでの記事取り込みは、下記のようなcsvを作れば良いとのこと。

>"post_id","post_name","post_author","post_date","post_type","post_status","post_title","post_content","post_category","post_tags","custom_field"

|物理名  |意味  |
|---|---|
|post_id |記事のID |
|post_name |記事名前(スラッグ？) |
|post_author |投稿者 |
|post_date |公開日(draftだったら要らないと思う) |
|post_type |投稿タイプ(投稿なら"post"でok) |
|post_status |"draft"か"publish"をセット |
|post_title |タイトル |
|post_content |本文 |
|post_category |カテゴリ名 |
|post_tags |タグ |
|custom_field| カスタムフィールド？用途不明... |

というわけで、この手順2. のターゲットとしては、この形式を満たすcsvを作成することです。

これには様々すぎる方法があるのでアレですが、今回は私の好きなjsでやっていきます。

### 作った変換ツールのソース

早速作ったソースがこちら↓ htmlファイル1つとjsファイル1つになっています

#### html

```html:index.html
<html>

<head>

    <meta charset="UTF-8">

    <title>DBからダウンロードしたjsonデータからWordpress取り込み用のcsvを作る</title>

    <style>
    .button-wrapper{
        text-align: center;
    }
    #convert-exec {
        width: 80%;
        text-align: center;
        display: inline-block;
        padding: 0.5em 1em;
        text-decoration: none;
        background: #668ad8;
        color: #FFF;
        border-bottom: solid 4px #627295;
        border-radius: 3px;
    }
    #convert-exec:active {
        -webkit-transform: translateY(4px);
        transform: translateY(4px);
        border-bottom: none;
    }
    </style>

    <script src="https://code.jquery.com/jquery-3.4.1.js" integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU=" crossorigin="anonymous"></script>

    <!-- 変換メインロジック -->
    <script src="js/JsonWpcsvConverter.js"></script>

</head>

<body>
    <div class="button-wrapper">
        <a id="convert-exec" href="#">実行</a>
    </div>

    <script> 

    $(document).ready(function(){

        // 実行ボタンにイベントをアタッチ
        $("#convert-exec").on("click", async function(){

            const jwc = new JsonWpcsvConverter()

            // csvデータに変換する
            const output = await jwc.convert()

            // csvをダウンロードする
            downloadCsv(output)
        })

        /**
         * csv文字列からcsvを生成してダウンロードする
         * @param {Array<Object>} arr 
         */
        function downloadCsv(csvStr){
            const bom     = new Uint8Array([0xEF, 0xBB, 0xBF])
            const blob    = new Blob([bom, csvStr], {'type': 'text/csv'})
            const url     = window.URL || window.webkitURL
            const blobURL = url.createObjectURL(blob)

            let a      = document.createElement('a')
            a.download = decodeURI("mycsv.csv")
            a.href     = blobURL
            a.type     = 'text/csv'

            a.click()
        }
    })
        
    </script>

</body>

</html>

```

htmlの方はホントシンプルで、ボタン1つ、ボタンへのイベントリスナセット、csvダウンロードロジックのみ記述してます。

####js

メインの変換ロジックを担当しているのが下記↓

```javascript:JsonWpcsvConverter.js
class JsonWpcsvConverter{

    /**
     * 変換ロジック実行
     */
    async convert(){

        // jsonを取得(テーブル1)
        const pointJsonData = await $.ajax({
            url: "data/MHM_M_POINT_DATA.json",
            dataType: "json",
        })

        // jsonを取得(テーブル2)
        const pictureJsonData = await $.ajax({
            url: "data/MHM_M_PICTURES.json",
            dataType: "json",
        })

        // 配列を使いやすいようにmapに変換しておく
        const pointData = this._processArray2Obj(pointJsonData, "id") // ← こっち不要だったかも
        const pictureData = this._processArray2ObjWithArray(pictureJsonData, "id")

        // wordpress csv取り込み用カラム定義をもつobjectの配列に変換する
        const res = Object.keys(pointData)
            .map(key=> 
                this._convOriginalObj2WpObj(pointData[key], pictureData[key])
            )


        // csvデータに変換する
        const output = this._getValuesFromObject(res)

        return output
    }
    
    /**
     * オブジェクトからcsv文字列を生成する
     * @param {Array<Object>} arr 
     */
    _getValuesFromObject(arr){

        // ヘッダのカラム情報を生成
        let res = JsonWpcsvConverter.HEADER_DEF.map(s=> `"${s}"`).join(",") + "\r\n"

        // body部を生成
        res += arr
            .map(v=> {

                const res = JsonWpcsvConverter.HEADER_DEF
                    .map(s=> `"${v[s]}"`)
                    .join(",")

                return res
            })
            .join("\r\n")

        return res

    }

    /**
     * keyPropNameをキーとするobjectに変換
     * @param {Array<Object>} data 
     * @param {string} keyPropName 
     */
    _processArray2Obj(data, keyPropName){

        const res = data
            .reduce((p, c)=> {
                return {
                    ...p,
                    [c[keyPropName]]: c
                }
            }, {})

        return res
    }

    /**
     * keyPropNameをキーとするobjectに変換(1キーあたり複数要素の紐づけ)
     * @param {Array<Object>} data 
     * @param {string} keyPropName 
     */
    _processArray2ObjWithArray(data, keyPropName){
        const res = data
            .reduce((p, c)=> {

                const current = p[c[keyPropName]]
                const newVal = !!current ? [...current, c] : [c]

                return {
                    ...p,
                    [c[keyPropName]]: newVal
                }
            }, {})

        return res
    }

    /**
     * jsonデータをwordpress取り込み向けのobjectに変換
     * @param {Object} pointData 
     * @param {Object} pictureData 
     */
    _convOriginalObj2WpObj(pointData, pictureData){

        const pd = pointData

        return {
            post_id: "",
            post_name: pd.name,
            post_author: "hidetaso",
            post_date: "",
            post_type: "post",
            post_status: "draft",
            post_title: `【${pd.prefecture}の絶景】${pd.name}`,
            post_content: this._getPostContent(pd, pictureData),
            post_category: "",
            post_tags: "",
            custom_field: "",

        }
        /*
        ↓こんな感じのデータが入ってる↓
        accessibility: "7"
        address: "山梨県南都留郡富士河口湖町小立"
        caption: "山梨県の河口湖南岸に位置する公園です。/brみたいなコメントがね！"
        create_datetime: "2017-06-23 00:19:48"
        crowdness_ave: "3"
        favorite: "7"
        gmap_by_latlng: ""
        id: 2017042301
        image_url_top: "http://korega-watashino-img/test.png"
        is_private: ""
        lat: "35.5120911"
        lng: "138.7528314"
        name: "八木崎公園"
        place_type: "P"
        prefecture: "山梨"
        update_datetime: "2017-06-23 00:19:48"
        zip_no: "401-0302"
        */
    }

    /**
     * 位置objectと一致する画像情報をもとに、wordpress記事コンテンツ部を生成する
     * @param {Object} pd 
     * @param {Array<Object>} pict 
     */
    _getPostContent(pd, pict){

        // templeteから簡単な情報を一旦置換
        let postContent = JsonWpcsvConverter.TEMPLATE
            .replace(/__REPLACED__/g, pd.name)
            .replace(/__ADDRESS__/, `<p>住所：${!!pd.zip_no ? "〒" + pd.zip_no : ""} ${pd.address}</p>`)
            .replace(/__MAP_LINK__/, `<a href='https://www.google.com/maps/@${pd.lat},${pd.lng},15z?hl=ja'>GoogleMapで開く</a>`)

        
        // 元captionのデータを加工
        let mainContents = this._getMainComment(pd.caption)

        // 画像情報がある場合、それをwordpress記事コンテンツ部に付加
        if(!!pict){

            const mainContentsStr = pict.map((v, i)=> {

                const imgUrl = (v.image_url || "").replace("http://tasokori.net/webapps/zekkei-map/pictures/", "https://love-moto-touring.info/webapps/zekkei-map/pictures/")

                return `
[caption id="" align="alignnone" width="1024"]
<a href="${imgUrl}">
    <img class="size-large" src="${imgUrl}" alt="${pd.name}-${i}" width="1024" height="768" />
</a>
${v.comment} ${pd.name}-${v.visit_date}撮影
[/caption]
    `
            })

            mainContents += mainContentsStr.join("<br />")
        }

        // メイン部のコンテンツの書き換え、エスケープすべき文字を無効化
        return postContent.replace(/__MAIN_CONTENTS__/g, mainContents).replace(/"/g, "'").replace(/\n/g, " ")

        /*
        author: "hide"
        comment: ""
        create_datetime: "2017-06-23 00:20:10"
        id: 2013061601
        image_url: "http://watashino-img/mytest-main.png"
        image_url_thumb: "http://watashino-img/mytest-main.png"
        is_private: "0"
        month: ""
        recomend: ""
        seq: 1
        timing_of_month: ""
        update_datetime: "2017-06-23 00:20:10"
        visit_date: "2015-05-31"
        */
    }

    /**
     * 元データcaption部をいい感じに整形
     * @param {string} comment 
     */
    _getMainComment(comment){

        if(!comment) return ""

        const res = comment
            .split("/br")
            .map(s=> {
                return "<p>" + s + "</p>"
            })
            .join("")

        return res

    }

    /**
     * wordpress取り込み用カラム定義
     */
    static HEADER_DEF = ["post_id", "post_name", "post_author", "post_date", "post_type", "post_status", "post_title", "post_content", "post_category", "post_tags", "custom_field"]

    /**
     * 本文テンプレート
     */
    static TEMPLATE = `
    <h2>__REPLACED__とは</h2>
    &nbsp;
    <h2>__REPLACED__の基本情報</h2>
    &nbsp;
    <h3>__REPLACED__の場所(地図)</h3>
    <p>__REPLACED__の場所は下記の通りです。</p>
    &nbsp;
    __ADDRESS__
    &nbsp;
    __MAP_LINK__
    &nbsp;
    <h2>__REPLACED__はこんな場所です</h2>
    &nbsp;
    __MAIN_CONTENTS__
    &nbsp;
    <h2>おわりに</h2>
    &nbsp;
    `

}

```

特段解説は要らないと思いますが、基本的にはTEMPLATEという実際の投稿記事のテンプレート文字列を用意しておいて、それのバインド文字列部分(__HOGEHOGE__)をreplaceで置換していくという感じです。

また、1. でDBのテーブルからjsonデータを抽出してきていますが、それも一番最初に

```javascript
$.ajax({
  url: "data/MHM_M_POINT_DATA.json",
  dataType: "json",
})

```
で取得してきてます。

また、途中もいい感じで絶景の位置に関する情報と、画像情報を紐づけて、Wordpressの投稿データを構築しています。

### 変換ツールを実行してcsvを取得する

上記変換ツールを実際に実行して、wordpress取り込み用のcsvを取得します。

![tool-import-data.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145371/b7723ccb-6601-632f-6091-c408501173de.png)

結果のcsvはこちら。

![tool-import-data2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145371/0a754607-b386-34ad-aa41-21b3d36a2549.png)

ようやくここまで来ました！

## 3. Wordpressに一括記事登録(5分)

あとは簡単です。wordpressの「Really Simple CSV Importer」を使用して、wordpressに一括登録します。

1. プラグインで「Really Simple CSV Importer」を検索してインストール & 有効化
2. wordpressメニューのツール⇒インポート
3. ファイルを選択で、先ほど作成したcsvを選択
4. 「ファイルをアップロードしてインポート」で取り込み実行

です。

取り込みは今回200件以上おこないましたが、割と一瞬で終わりました。

結果がこちら。

![wp-import2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145371/b6f9c04c-cf10-831a-9440-4fc223182227.png)

下書き208とありますが、これが全て今回一括記事登録したデータです。

実際に、1つ1つの記事を見ていくと、ちゃんと記事が作成されていました！感動！！

![wp-import3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145371/23fa4d6e-0c48-63bf-d513-2ba87971dcea.png)

今回は全て下書き(draft)にしています。

というのも、あくまでブログ記事を書くための土台にしようという案ですので。このドラフトをベースに、さらに必要情報を追記していって、ユーザに魅力的な情報を提供していこうと思います！

# おわりに

今回は、Webアプリのデータを溜めていたDBのテーブル情報を引っこ抜いて、変換ツールを噛まして変換、Wordpressに一括記事登録したよ！という内容でした。

何か真新しいことがあるわけではないので恐縮ですが...

まあしかし、こんな感じで「とあるデータ⇒いい感じに加工⇒別システムに投入」とかっていうことはまあよくあると思うので、何かの参考になれば幸いです。