Concrete CMS エクスプレスデータベースに一括CSV入力

2025/04/26
Concrete CMSのエクスプレスデータベースには標準でCSV入力が無いのですね。そこでJOBに登録できる簡単なものを作ってみました。

PIXTAで販売中のイラストが1万点近くに達しました。さらなる販売促進のため、独自のイラスト紹介ページを作成することにしました。
公開方法を検討する中で、親しみのあるConcrete CMSを思い出しました。これまで使用したことはありませんが、簡易データベース「エクスプレスデータベース」という機能があることに気付きました。
エンティティと属性の作成を終え、CSVの一括登録を試みましたが、そのような項目は見当たりません。一括出力の機能はあるものの、登録の方法が存在しないようです。Googleで調べても、多くの人が同じ悩みを抱えているようで、明確な解決策は見つかりませんでした。
PHPのバージョンの関係で、Concrete CMSのバージョン8であることが影響しているのかもしれませんが、仕方がありません。ここは自分で作るしかないでしょう。

実際に作ってみる

Job化

  • 頻繁にデータが追加されるため、毎回コマンドラインから命令を実行するのは避けたい。
  • また、操作を簡単にするため、Jobとして登録することにする。
  • Jobを登録するために、controller.php と 実行プログラム csv_import_entries.php を作成する。

データ構造

  • 汎用性を高めるため、データ構造をPHPコード内に固定するのではなく、CSVデータの1行目に各カラム(属性)のハンドル名を記載する仕様にする。

各PHPコードについて

以下にコードを示します。
csv_import_entries.php 内の "/application/files/csv/input.csv"データ収納用CSVファイル)と 'illust'エンティティ)は、使用する環境に合わせて適宜変更してください。

改善 csv_import_illust_final.php コード

<?php namespace Concrete\Package\CsvImportEntries\Job; use Concrete\Core\Job\Job; use Concrete\Core\Support\Facade\Express; class CsvImportEntries extends Job { public function getJobName() { return t("CSVデータインポート"); } public function getJobDescription() { return t("CSVファイル(ヘッダー有り)からExpressエンティティにデータを登録します。"); } public function run() { $csvFile = DIR_BASE . "/application/files/csv/input.csv"; $entityHandle = 'illust'; $entity = Express::getObjectByHandle($entityHandle); if (($handle = fopen($csvFile, "r")) !== FALSE) { $count = 0; $headers = fgetcsv($handle, 10000, ","); while (($data = fgetcsv($handle, 10000, ",")) !== FALSE) { $entry = Express::buildEntry($entity); foreach ($headers as $index => $attributeHandle) { if (isset($data[$index])) { $entry->setAttribute($attributeHandle, $data[$index]); } } $entry->save(); $count++; } fclose($handle); } return t("CSV import completed. Total entries processed: $count"); } }
Job登録用 controller.php  コード

<?php
namespace Concrete\Package\CsvImportEntries;

use Concrete\Core\Package\Package;
use Concrete\Core\Job\Job;

class Controller extends Package
{
    protected $pkgHandle = 'csv_import_entries';
    protected $appVersionRequired = '8.5.0';
    protected $pkgVersion = '1.0.0';

    public function getPackageName()
    {
        return t('Csv Import Entries');
    }

    public function getPackageDescription()
    {
        return t('CSVデータインポートジョブを追加するパッケージ');
    }

    public function install()
    {
        $pkg = parent::install();
        Job::installByPackage('csv_import_entries', $pkg);
    }
}

構造

Job に登録するための controller.php と実行プログラム csv_import_entries.php を、上図のようにpackage ディレクトリ下に配置してください。
そうすることで、システムの設定 → Concrete の拡張 → Csv Import Entries のインストール の手順を実行すると、Job に登録され、いつでも使用できるようになります。

登録データの作成(開発譚)

CSVファイル /application/files/csv/illustdata.csv の内容について

コーディングの問題よりも、登録データの不備が開発の遅延要因となりました。PHPを実行するとエラーが発生し、コードを確認しても原因が分からないことが度々ありました。結果として、データの問題が原因であるケースが何度も続きました。以下の点に注意することで、問題の発生を防ぐことができます。

1行目の属性ハンドル

  • これは、エクスプレスデータベースに使用のハンドルと全く同じでなければなりません。大文字/小文字の違いも許されません。エクスプレス属性情報からコピペするのが正解です。

文字コードの設定

  • データ処理にはUTF-8を使用します。Excelの標準設定ではShift-JISになっている場合があるため、注意が必要です。
  • CSVファイルの処理においては、シェルで登録Job URLを呼び出す前にnkfコマンドを適用し、適切な文字コードに変換することで対応しました。

UTF-8のBOMについて

  • UTF-8のBOM(Byte Order Mark)は付けません。
    BOMは見た目では分かりづらく、プログラムの動作に影響を与える厄介な存在です。実際、このBOMが原因でプログラムのエラーが続き、問題を特定するまでに開発工程の約3分の2の時間を費やしてしまいました。
  • 特に、このJobを実行した際に「attribute_keyが適切でない」というエラーが発生した場合、最初に疑うべきポイントがBOMの影響です。この問題も、nkfコマンドを活用することで回避できました。

データのフォーマット

  • 各データは "(ダブルクォーテーション) で囲むことを推奨します。省略することもできますが、文章内に ,(カンマ) があると、データの区切りと誤認され、エラーの原因になります。

  • 日付・時間データは YYYY-MM-DD HH:MM:SS の形式に統一します。

  • データの左右に空白があると、予期せぬエラーを招く可能性があります。データ作成の折はTrim処理を推奨します。

データの大きさ

  • データ形式に散々悩まされながら、ようやく1万件のデータを取り込むことができました。しかし、約1割のレコードが2つに分割されているものが見つかりました。

    またデータに原因があるのだろうと、ダウンロードしたデータ内にASCIIコード 0-31の制御コードが含まれている可能性など、さまざまなデータクリーニングを試したり、半角を全角に変換してから読み込ませたりと、試行錯誤を繰り返しましたが、一向に改善しません。

    そこで分割されているデータを詳しく分析したところ、キーワードの多いイラストに関連するデータで頻繁に発生していることに気付きました。結局のところ、データサイズを過小評価していたことが原因でした。

    CSVの読み込み時、$data = fgetcsv($handle, 1000, ",") という設定でバイト数を1000に制限していましたが、これでは十分ではありませんでした。そこで、制限を10000バイトに変更したところ、問題なく取り込めるようになりました。

    たったこれだけのことに丸一日かかってしまいました。

1万件のデータを登録しました

更新データをホストと同期するフォルダーに入れるだけで、自動的にアップロードされます。また、Job形式を採用することでCronの設定が可能となり、新規登録作業の自動化を実現できるため、非常に便利です。

今回のデータインポートは約1万件と規模が大きく、負荷によるタイムアウトのリスクが懸念されました。
実際に全データを処理すると(時間計測のためCron処理を使用)、ちょうど5分後にタイムアウトし、無限ループに陥りました。

このため、実用面を考慮し、数百件ずつのバッチ処理を導入する仕様が必要だと判断しました。

バッチ処理では、メモリの解放やスリープを挟む設計を試みました。しかし、データ登録には時間がかかるため、作業が正常に進行しているか、あるいはエラーで停止しているかを判別できる仕組みが必要です。そのため、シェルスクリプトの利用が現実的だと考えました。さらに、シェル利用によりCSVデータのUTF-8化とBOMの削除にはnkfコマンドを活用し、作業の省力化も図りました。

今回、私が1万件のデータ(実験データ:12項目、1万件、7M容量)を導入した方法は、データを100レコードずつに分割し、Linuxシェルスクリプトを使用して逐次実行する手法でした。

具体的には、以下のスクリプトを使用しました。1ファイル100件のデータ処理を完了後、10秒待機して次のデータ処理を開始する設計です。

100レコード×100ファイルの1万件を処理するのに要した時間は1時間22分でした。決して高速とは言えませんが、実用レベルといえるでしょう。

100レコードずつに分割して処理するスクリプト

#!/bin/sh
for i in $(seq 1 100); do
    echo $(date '+%H:%M:%S') "${i}_of_all.csv Start"
    nkf -wLu --overwrite "${i}_of_all.csv"
    mv "${i}_of_all.csv" input.csv
    wget -q -O temp "Job URL"
    echo $(date '+%H:%M:%S') "${i}_of_all.csv Finished waiting 10sec..."
    sleep 10
done

ダウンロードとインストール手順

以下のリンクからファイルをダウンロードし、解凍後に packages ディレクトリ内へ配置してください。

その後、システム設定 → Concreteの拡張 → Csv Import Entriesのインストール を実行すると、Jobに登録され、いつでも使用可能 になります。

なお、動作確認はConcrete CMS 8.5.19のみで行っており、他のバージョンでの動作は未確認です。

ダウンロードzip


本稿の題材のイラスト紹介ページは、以下のリンクからご覧いただけます。エクスプレスデータベースに登録したPIXTAの画像URLを使用し、画像を表示できるようにしました。

イラストページを見る


Job URLの取得方法

知人に本稿を紹介したところ「Job URLの取得方法がわからない」とのクレームをもらいました。

以下に紹介します。

システムと設定の自動実行ジョブに行き、導入した「CSVデータインポート」の右の方の時計マークをクリックします。

出てきた画面の「Cronを使用」をオンにして、URLをコピーしておき、保存を押します(※重要です)。「Cronを使用」をオンにしておかないとShell からのアクセスが受け付けられません。


コメント欄を読み込み中