PhpSpreadsheetでExcelの日付・日時を読み込む:数値・文字列・空欄の扱い方
作成日:2026.11.06
PhpSpreadsheetでExcelの日付・日時を読み込み、取り込み先の形式へ変換する方法を解説します。セルの値・型・表示形式の違いを整理し、数値と文字列の混在、空欄、不正な日付、時刻部分を扱うPHPコードを紹介。1900・1904日付システムとタイムゾーンの注意点も確認します。
目次
PhpSpreadsheetでExcelの日付を読み込むと、画面では日付に見えるのに、PHPでは数値が返ってくることがあります。同じ列に文字列の日付や空欄が混ざると、何でも日付へ変換するわけにもいきません。
今回は、セルの値と表示形式の関係を確認してから、日付用・日時用の項目へ取り込む例を作ってみます。日付だけ必要なのか、時刻まで残したいのかを先に決めておくのがポイントです。
Excelの日付・日時は、値と表示形式を分けて考える
Excelの日付・日時は、通常、日数を表す数値と表示形式の組み合わせです。整数部分が日付、小数部分が1日に対する時刻の割合になります。例えば0.5は半日なので12時です。Unixタイムスタンプとは別の数値ですね。
| 保存値の例 | セルの型 | 表示形式の例 | 意味 |
|---|---|---|---|
| 46332 | 数値 | yyyy-mm-dd | 1900日付システムで2026-11-06 |
| 46332.5 | 数値 | yyyy-mm-dd hh:mm:ss | 同日の12:00:00 |
| 46332.5 | 数値 | yyyy-mm-dd | 日付表示でも値には時刻部分が残る |
| 2026-11-06 | 文字列 | General | 日付の形をした文字列 |
同じ数値でも、ブックの1900・1904日付システムによって日付が変わります。変換時には読み込んだブックの設定も必要です。仕組みはPhpSpreadsheet公式の日時の説明を参照してください。
値・型・表示形式を確認する基本形
次は、XLSXのA2を確認する例です。PhpSpreadsheetのオートローダーは読み込み済みとします。
$book = (new \PhpOffice\PhpSpreadsheet\Reader\Xlsx())
->load(__DIR__ . '/dates-1900.xlsx');
$cell = $book->getActiveSheet()->getCell('A2');
var_dump($cell->getValue());
var_dump($cell->getDataType());
var_dump($cell->getStyle()->getNumberFormat()->getFormatCode());
var_dump(\PhpOffice\PhpSpreadsheet\Shared\Date::isDateTime($cell));
$book->disconnectWorksheets();
getValue()は保存値、getDataType()はセルの型を取得します。Date::isDateTime()は数値と表示形式から日付・時刻として扱えるかを調べるためのもので、取り込み先の業務ルールまで確認してくれるわけではありません。数式セルでは計算も関わるため、今回の取り込み例では数式を受け付けないことにします。
数値の変換にはDate::excelToDateTimeObject($value, $timezone, $calendar)を使います。第3引数へブックのgetExcelCalendar()を渡せるのは、今回使用する5.10.0で確認した形です。古い版へそのまま転用せず、対象版のAPIを確認してください。
日付と日時の取り込みルールを決める
このサンプルでは、A列を日付、B列を日時として扱います。列の用途が決まっているので、数値セルは表示形式によらずExcelのシリアル値として解釈します。用途の分からない数値を、値の大きさだけで日付と推測する処理ではありません。
| 項目 | 日付用(A列) | 日時用(B列) |
|---|---|---|
| 出力形式 | Y-m-d | Y-m-d H:i:s |
| 文字列の入力形式 | 2026-11-06の形だけ | 2026-11-06 12:00:00の形だけ |
| 数値の時刻部分 | 含まれていたらエラー | 保持し、秒単位へ四捨五入 |
| 空セル・空文字列 | nullとして受け入れる。空白だけの文字列はエラー | |
| 許容範囲 | 2000-01-01から2099-12-31。日時は丸めた結果も範囲内であること | |
| タイムゾーン | Asia/Tokyoとして解釈。UTCへの変換はしない | |
| その他の型 | 数式、真偽値、Excelのエラー値などは拒否 | |
2000~2099年という範囲は説明用の業務ルールです。ExcelやPhpSpreadsheetの対応範囲そのものではありません。1900年付近の互換性上の特殊な扱いや、時刻だけ・経過時間の値も、このサンプルでは対象外にできます。
日時のシリアル値自体にタイムゾーンは付いていません。「日本時間として入力された日時」という前提を取り込み側で決めています。日付だけの値をUTCへ変換すると日付が前日に変わる場合もあるので、日付と日時は使い分けましょう。
比較用のXLSXを作る
以下はPHP 8.3.36・PhpSpreadsheet 5.10.0で、Docker上のLinux(x86_64)から保存・再読み込みを確認したCLI用サンプルです。Excelアプリでの表示・再保存は未確認です。
PhpSpreadsheetはComposerで導入済みとし、PHPファイルをvendorディレクトリと同じ場所へ置きます。導入はComposerでPhpSpreadsheetをインストールする記事、必要なPHP・拡張は5.10.0の動作要件を参照してください。
導入記事の古いplatform.php設定は流用せず、実際に使うPHPのバージョンに合わせてください。
次をcreate.phpとして保存し、サンプル専用の書き込み可能な場所で実行します。dates-1900.xlsxとdates-1904.xlsxが作られ、同名ファイルは上書きされます。
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
foreach ([1900, 1904] as $calendar) {
$book = new Spreadsheet();
$book->setExcelCalendar($calendar);
$sheet = $book->getActiveSheet();
$sheet->setTitle('Dates');
$sheet->fromArray(['日付', '日時'], null, 'A1');
$serial = Date::formattedPHPToExcel(2026, 11, 6, 0, 0, 0, $calendar);
$rows = [
2 => [$serial, $serial + 0.5],
3 => ['2026-11-06', '2026-11-06 12:00:00'],
4 => [null, null],
5 => ['2026-02-30', '2026-02-30 12:00:00'],
6 => [$serial + 0.5, '46332.5'],
7 => [0, 0.5],
];
foreach ($rows as $row => $values) {
foreach (['A', 'B'] as $index => $column) {
$value = $values[$index];
if ($value === null) {
continue;
}
$coordinate = $column . $row;
$sheet->setCellValueExplicit(
$coordinate,
$value,
is_string($value) ? DataType::TYPE_STRING : DataType::TYPE_NUMERIC
);
if (!is_string($value)) {
$sheet->getStyle($coordinate)->getNumberFormat()->setFormatCode(
$column === 'A' ? 'yyyy-mm-dd' : 'yyyy-mm-dd hh:mm:ss'
);
}
}
}
(new Xlsx($book))->save(__DIR__ . '/dates-' . $calendar . '.xlsx');
$book->disconnectWorksheets();
}
php create.php
文字列は型を明示して保存し、作成時の自動変換と読み込み後の変換を混同しないようにしています。値・型・表示形式の違いは、PhpSpreadsheetで先頭ゼロを保持する記事でも比較しています。
日付用・日時用に変換する
次をnormalize.phpとして保存します。変換結果は文字列またはnullとし、不正な入力は例外にします。呼び出し側でセル位置と組み合わせれば、どこを直せばよいか分かります。
<?php
declare(strict_types=1);
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Shared\Date;
function normalizeExcelDate(
Cell $cell,
string $kind,
int $calendar,
DateTimeZone $timezone
): ?string {
if (!in_array($kind, ['date', 'datetime'], true)
|| !in_array($calendar, [1900, 1904], true)) {
throw new InvalidArgumentException('取り込み設定が不正です');
}
$value = $cell->getValue();
$type = $cell->getDataType();
if ($value === null || $value === '') {
return null;
}
$format = $kind === 'date' ? 'Y-m-d' : 'Y-m-d H:i:s';
if ($type === DataType::TYPE_NUMERIC && (is_int($value) || is_float($value))) {
$min = Date::formattedPHPToExcel(2000, 1, 1, 0, 0, 0, $calendar);
$max = Date::formattedPHPToExcel(2100, 1, 1, 0, 0, 0, $calendar);
if (!is_finite((float) $value) || $value < $min || $value >= $max) {
throw new UnexpectedValueException('数値が許容範囲外です');
}
if ($kind === 'date' && $value != floor($value)) {
throw new UnexpectedValueException('日付用の項目に時刻が含まれています');
}
$date = Date::excelToDateTimeObject($value, $timezone, $calendar);
if ($kind === 'datetime') {
Date::roundMicroseconds($date);
}
} elseif (in_array($type, [DataType::TYPE_STRING, DataType::TYPE_INLINE], true)
&& is_string($value)) {
$pattern = $kind === 'date'
? '/\A[0-9]{4}-[0-9]{2}-[0-9]{2}\z/'
: '/\A[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\z/';
if (preg_match($pattern, $value) !== 1) {
throw new UnexpectedValueException('文字列の形式が不正です');
}
$date = DateTimeImmutable::createFromFormat('!' . $format, $value, $timezone);
$errors = DateTimeImmutable::getLastErrors();
if ($date === false
|| ($errors !== false && ($errors['warning_count'] > 0 || $errors['error_count'] > 0))
|| $date->format($format) !== $value) {
throw new UnexpectedValueException('存在しない日付・日時です');
}
} else {
throw new UnexpectedValueException('対象外のセル型です');
}
$year = (int) $date->format('Y');
if ($year < 2000 || $year > 2099) {
throw new UnexpectedValueException('変換後の日付が許容範囲外です');
}
return $date->format($format);
}
文字列は数値っぽくてもシリアル値へ変換しません。例えば文字列の46332.5はエラーです。strtotime()などで広く解釈するより、取り込み仕様に合う形式だけを受け付ける方が結果を確認しやすくなります。
createFromFormat()は存在しない日付を補正する場合があるため、戻り値だけでなく警告・エラーと、同じ形式へ戻した結果も確認します。先頭の!は指定していない時刻などを初期化するためのものです。PHP 8.2以降のgetLastErrors()は問題がなければfalseを返す点にも注意しましょう。
日時の数値では、浮動小数点の誤差を含めてDate::roundMicroseconds()で秒へ丸めます。本来の秒未満も四捨五入する仕様なので、精度が必要な取り込みではこの部分を変えてください。丸めによる翌日への繰り上がりもあり、最後に年の範囲を再確認しています。
セル位置と結果を表示する
次をread.phpとして保存します。ファイルの読み込みエラーと、各セルの入力エラーを分けて扱います。この例は検証用に結果を表示するだけで、DBへの登録は行いません。
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/normalize.php';
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
$book = null;
$failed = false;
try {
$path = $argv[1] ?? __DIR__ . '/dates-1900.xlsx';
$book = (new Xlsx())->load($path);
$calendar = $book->getExcelCalendar();
$timezone = new DateTimeZone('Asia/Tokyo');
$sheet = $book->getActiveSheet();
echo 'calendar=', $calendar, PHP_EOL;
for ($row = 2; $row <= 7; ++$row) {
foreach (['A' => 'date', 'B' => 'datetime'] as $column => $kind) {
$coordinate = $column . $row;
try {
$result = normalizeExcelDate(
$sheet->getCell($coordinate), $kind, $calendar, $timezone
);
echo $coordinate, ': ', $result ?? '(空欄)', PHP_EOL;
} catch (UnexpectedValueException $error) {
echo $coordinate, ': ERROR ', $error->getMessage(), PHP_EOL;
}
}
}
} catch (Throwable $error) {
fwrite(STDERR, '読み込み処理に失敗しました: ' . $error->getMessage() . PHP_EOL);
$failed = true;
} finally {
$book?->disconnectWorksheets();
}
if ($failed) {
exit(1);
}
php read.php dates-1900.xlsx
php read.php dates-1904.xlsx
先頭のcalendar以外は、どちらのファイルも次の結果になりました。
A2: 2026-11-06
B2: 2026-11-06 12:00:00
A3: 2026-11-06
B3: 2026-11-06 12:00:00
A4: (空欄)
B4: (空欄)
A5: ERROR 存在しない日付・日時です
B5: ERROR 存在しない日付・日時です
A6: ERROR 日付用の項目に時刻が含まれています
B6: ERROR 文字列の形式が不正です
A7: ERROR 数値が許容範囲外です
B7: ERROR 数値が許容範囲外です
A6は日付の表示形式ですが、保存値には12時が含まれています。表示だけを見て日付へ切り詰めず、今回はエラーにしました。時刻を捨ててよい業務なら、その仕様を決めてから変更すればOKです。
日付システムと読み込み設定の注意点
今回のサンプルでは、同じ2026-11-06の保存値が1900日付システムで46332、1904日付システムで44870でした。変換関数へ各ブックのgetExcelCalendar()を渡すことで、どちらも同じ日付・日時になりました。
5.10.0にはDateクラス側の日付システム設定もあります。別のブックを読み込む処理などの影響を避けるため、この例では既定値に任せず、変換する値とそのブックの日付システムを組にして渡しています。
また、setReadDataOnly(true)で書式を読み込まない場合、今回のA2は数値のままで、表示形式がGeneralとなり、Date::isDateTime()はfalseになりました。それでも、この変換関数では「A列は日付」という列定義があるため変換できます。用途不明の列を表示形式から判定したい場合には、その情報を捨てないようにしましょう。読み込み設定の比較はExcel読み込みのメモリ使用量を減らす記事も参考にしてください。
数式セルは、数式そのもの・保存済みの計算結果・PHPで再計算した結果を分けて考える必要があります。今回は拒否する実装です。CSVやXLS、時刻だけ・経過時間、夏時間のある地域、Excelアプリ上の表示は検証範囲に含めていません。
日付・日時の取り込みでは、見た目をそろえる前に、受け入れる値を決めることが大切です。セルの保存値、文字列の形式、時刻の有無を分けて確認すると、不正な入力を別の日付へ変えてしまう事故も防ぎやすくなるかと思います。
参考資料
奈良市を拠点に、27年以上の経験を持つフリーランスWebエンジニア、阿部辰也です。
これまで、ECサイトのバックエンド開発や業務効率化システム、公共施設の予約システムなど、多彩なプロジェクトを手がけ、企業様や制作会社様のパートナーとして信頼を築いてまいりました。
【制作会社・企業様向けサポート】
Webシステムの開発やサイト改善でお困りの際は、どうぞお気軽にご相談ください。小さな疑問から大規模プロジェクトまで、最適なご提案を心を込めてさせていただきます。
ぜひ、プロフィールやWeb制作会社様向け業務案内、一般企業様向け業務案内もご覧くださいね。
PhpSpreadsheetでExcel読み込みのメモリ使用量を減らす:シート・列・行を絞って比較
2026.10.31
PhpSpreadsheetで必要なシート・列・行だけを読み込む方法を解説します。約18万セルのXLSXで処理時間とピークメモリを比較し、119.5MiBから16.0MiBへ削減した結果を紹介。書式を省く場合の日付判定や、読み込み範囲を絞る場合の数式への影響も確認します。
PhpSpreadsheetで先頭ゼロを保持する方法:文字列型とゼロ埋め表示の違い
2026.10.26
PhpSpreadsheetで商品コードなどの先頭ゼロを保持する方法を解説します。文字列型での保存と、数値の表示形式によるゼロ埋めを比較し、XLSXの再読み込みで値・型・表示の違いを確認します。長い番号や「=」で始まる文字列を出力する際の注意点も紹介します。
PhpSpreadsheetでExcelシートのタイトルを設定する方法
2023.05.22
PhpSpreadsheetを使ったExcel操作で、シートのタイトルを変更する方法をご紹介します。
PhpSpreadsheetでセルに自動改行を設定する方法
2023.05.21
PhpSpreadsheetを使用して、セル内のテキストを自動で改行させる設定方法を解説します。