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

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のみで行っており、他のバージョンでの動作は未確認です。
本稿の題材のイラスト紹介ページは、以下のリンクからご覧いただけます。エクスプレスデータベースに登録したPIXTAの画像URLを使用し、画像を表示できるようにしました。
Job URLの取得方法
知人に本稿を紹介したところ「Job URLの取得方法がわからない」とのクレームをもらいました。
以下に紹介します。
システムと設定の自動実行ジョブに行き、導入した「CSVデータインポート」の右の方の時計マークをクリックします。
出てきた画面の「Cronを使用」をオンにして、URLをコピーしておき、保存を押します(※重要です)。「Cronを使用」をオンにしておかないとShell からのアクセスが受け付けられません。