PHPで今日は何の日か調べる

(1/1)
Wikipedia には、指定した月日のできごとが記録されている。
これを PHP でスクレイピングすることで、今日は何の日か表示するプログラムを作成する。
さらに、「PHP で記念日を表示する」で作ったプログラムを取り込み、その日の記念日も表示できるようにする。

(2021 年 1 月 26 日)anniv オプションを追加(記念日を表示)
(2021 年 1 月 16 日)PHP8 対応
(2020 年 5 月 17 日)era オプション使用時のバグ修正, リファラチェック追加

目次

サンプル・プログラムの実行例

PHPで今日は何の日か調べる
次の URL オプションが利用できる(URL にクエリパラメータを渡すことで機能を変更できる)。
month, day = 月,日の指定
(例)DayToday.php?month=8&day=2 (8月2日を指定する)
diff = 今日を基準とした差分日数指定(-100~+100)
(例)DayToday.php?diff=+2 (明後日を指定する)
era = 西暦年の下段に和暦などを表示
(例)DayToday.php?era
lfn = 脚注を残す
(例)DayToday.php?lfn
anniv = 記念日を表示する
(例)DayToday.php?anniv

サンプル・プログラム

解説:今日は何の日を取得

0246: /**
0247:  * Wikipedia URL取得
0248:  * @param int $month, $day 月,日
0249:  * @return string URL
0250: */
0251: function getURL_Wikipedia($month$day) {
0252:     $url = 'http://ja.wikipedia.org/wiki/' . urlencode(sprintf('%d月%d日', $month$day));
0253: 
0254:     return $url;
0255: }

0257: /**
0258:  * 今日は何の日 取得
0259:  * @param int    $month, $day 月,日
0260:  * @param array  $items  情報格納配列
0261:  * @param string $errmsg エラーメッセージ格納用
0262:  * @param bool   $lfn    脚注残す(省略時=FALSE)
0263:  * @return int 情報件数
0264: */
0265: function getDayToday($month$day, &$items, &$errmsg$lfn=FALSE) {
0266:     $pat1 = "/<span[\s\S]+id=\"できごと\">/iu";            //ver.1.42
0267:     $pat2 = "/<span[\s\S]+id=\"誕生日\">/iu";                //ver.1.42
0268:     $pat3 = "/<span[\s\S]+id=\"忌日\">/iu";                //ver.1.42
0269:     $pat5 = "/<li>[^\>]+([0-9]+年.+)<\/li>/iu";         //年 - 内容
0270:     $pat6 = "/([^0-9]*)([0-9]+)年(\s*[\((].[^\))]+[)\)].)?[ \-]+(.+)/iu";         //年 - イベント Ver.1.32
0271:     $pat7 = "/([^0-9]*)([0-9]+)年(\s*[\((].[^\))]+[)\)].)?[ \-]+([^、]+)、?([^(\(]+)*([^0-9]*)([0-9]*)/iu"; //年 - 名前、職業(生没年) Ver.1.43
0272:     $pat8 = "/<span[\s\S]+id=\"フィクションのできごと\">/iu";    //ver.1.5
0273: 
0274:     $url = getURL_Wikipedia($month$day);
0275: 
0276:     $infp = @fopen($url, 'r');
0277:     if ($infp == FALSE) {
0278:         $errmsg = 'Wikipediaにアクセスできません.';
0279:         return FALSE;
0280:     }
0281: 
0282:     $mode = '';
0283:     $cnt = 1;
0284:     while (! feof($infp)) {
0285:         $str = fgets($infp);
0286:         if (preg_match($pat1$str) == 1) {
0287:             $mode = 'event';
0288:         } else if (preg_match($pat8$str) == 1) {        //Ver.1.5
0289:             $mode = 'fiction';                         //Ver.1.5
0290:         } else if (preg_match($pat2$str) == 1) {
0291:             $mode = 'birth';
0292:         } else if (preg_match($pat3$str) == 1) {
0293:             $mode = 'death';
0294:         } else if ($mode != '' && preg_match($pat5$str$arr) > 0) {
0295:             $str = strip_tags(trim($str));
0296:             if ($mode == 'event' && preg_match($pat6$str$arr) == 1) {
0297:                 $items[$mode][$cnt]['year']  = ($arr[1] == '') ? (int)$arr[2] : 0 - $arr[2];
0298:                 $items[$mode][$cnt]['era']   = $arr[3];
0299:                 $items[$mode][$cnt]['event'] = $lfn ? $arr[4] : eliminate_footnote($arr[4]);   //Ver.1.43
0300:                 $cnt++;
0301:             } else if (preg_match($pat7$str$arr) == 1) {
0302:                 $items[$mode][$cnt]['year']  = ($arr[1] == '') ? (int)$arr[2] : 0 - $arr[2];
0303:                 $items[$mode][$cnt]['era']     = $arr[3];
0304:                 $items[$mode][$cnt]['name']    = $arr[4];
0305:                 $items[$mode][$cnt]['profile'] = $lfn ? $arr[5] : eliminate_footnote($arr[5]);   //Ver.1.43
0306:                 if (preg_match('/^\([\*\+]./', $arr[6]) > 0) {  //Ver.1.43
0307:                     $items[$mode][$cnt]['plus']  = $arr[7];
0308:                 } else {
0309:                     $items[$mode][$cnt]['plus']  = '';
0310:                 }
0311:                 $cnt++;
0312:             }
0313:         }
0314:     }
0315:     fclose($infp);
0316: 
0317:     if ($cnt == 0) {
0318:         $errmsg = '記念日が見つかりません.';
0319:     }
0320: 
0321:     return $cnt;
0322: }

Wikipedia には、「できごと」「誕生日」「忌日」に分かれて記録されている。
これらを正規表現 $pat1$pat7 を使ってスクレイピングする関数が getDayToday である。

Wikipedia のイベントや生没記述には揺れが大きく、これに対応するパターン $pat6 および $pat7 は、かなり複雑になっている。スクレイピングに成功すれば、配列要素として次のような値が入る。
要素内容
1先頭文字
2西暦年
3和暦など(西暦年の後に括弧書きがあれば)
4イベント、名前など
5プロファイル(生没の場合のみ)
6生年または没年の前の記号
7生年または没年(生没の場合のみ)

解説:今日は何の日をソート

0404: /**
0405:  * 今日は何の日 年号順にソート
0406:  * @param array $sour1 ソート前配列(Wikipedia)
0407:  * @param array $sour2 ソート前配列(日本記念日協会)
0408:  * @param array $dest  ソード後配列
0409:  * @return int 情報件数
0410: */
0411: function sortDayToday($sour1$sour2, &$dest) {
0412:     $cnt = 0;
0413: 
0414:     //できごと
0415:     foreach ($sour1['event'] as $item) {
0416:         $dest[$cnt]['year']  = $item['year'];
0417:         $dest[$cnt]['event'] = $item['event'];
0418:         $dest[$cnt]['era']   = $item['era'];
0419:         $cnt++;
0420:     }
0421:     //誕生日
0422:     foreach ($sour1['birth'] as $item) {
0423:         $event = "{$item['name']}({$item['profile']})誕生";
0424:         if ($item['plus'] != '')    $event .= "(~{$item['plus']}年)";
0425:         $dest[$cnt]['year']  = $item['year'];
0426:         $dest[$cnt]['event'] = $event;
0427:         $dest[$cnt]['era']   = $item['era'];
0428:         $cnt++;
0429:     }
0430:     //命日
0431:     foreach ($sour1['death'] as $item) {
0432:         $event = "{$item['name']}({$item['profile']})死去";
0433:         if ($item['plus'] != '')    $event .= "({$item['plus']}年~{$item['year']}年)";
0434:         $dest[$cnt]['year']  = $item['year'];
0435:         $dest[$cnt]['event'] = $event;
0436:         $dest[$cnt]['era']   = $item['era'];
0437:         $cnt++;
0438:     }
0439:     //記念日
0440:     foreach ($sour2 as $item) {
0441:         $dest[$cnt]['year']  = KINENBI_YEAR;
0442:         $dest[$cnt]['event'] = $item['title'];
0443:         $dest[$cnt]['era']   = '';
0444:         $dest[$cnt]['url']   = $item['url'];
0445:         $cnt++;
0446:     }
0447: 
0448:     //ソート
0449:     usort($destfunction ($a$b) {
0450:         return (int)($a['year'] - $b['year']);
0451:     });
0452: 
0453:     return $cnt;
0454: }

ユーザー関数 getDayToday および getAnniversary によって取得した「できごと」「誕生日」「忌日」「記念日」を年号順にソートする関数が sortDayToday である。
組み込み関数  usort  を利用し、年号順に配列をソートしている。

解説:指定年月日から何日か離れた年月日

0630: $month = getParam('month', FALSE, 0);
0631: if ($month == 0)    $month = date('n');
0632: $day   = getParam('day', FALSE, 0);
0633: if ($day == 0)  $day = date('j');
0634: $year  = getParam('year', FALSE, 0);
0635: if ($year == 0) $year = date('Y');
0636: $diff  = getInteger('diff', -100, +100, 0);
0637: list($year$montjh$day) = diffymd($year$month$day$diff);
0638: $era  = getParam('era', FALSE, 1);
0639: $era = ($era == 1) ? FALSE : TRUE;
0640: $lfn  = getParam('lfn', FALSE, 1);         //Ver.1.43
0641: $lfn = ($lfn == 1) ? FALSE : TRUE;            //Ver.1.43

0177: /**
0178:  * 閏年かどうか判定する
0179:  * @param int $year 西暦年
0180:  * @return bool TRUE:閏年である/FALSE:平年である
0181: */
0182: function isleap($year) {
0183:     $ret = FALSE;
0184:     if ($year % 4 == 0) $ret = TRUE;
0185:     if ($year % 100 == 0)   $ret = FALSE;
0186:     if ($year % 400 == 0)   $ret = TRUE;
0187:     return $ret;
0188: }

0190: /**
0191:  * 指定した年月の日数を返す
0192:  * @param int $year  年
0193:  * @param int $month 月
0194:  * @return int 日数/FALSE:引数の異常
0195: */
0196: function getDaysInMonth($year$month) {
0197:     static $days = array(0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
0198:     if ($month < 1 || $month > 12)  return FALSE;
0199: 
0200:     $dm = (($month == 2) && isleap($year)) ? 29 : $days[$month];
0201: 
0202:     return $dm;
0203: }

0205: /**
0206:  * 指定年月日から$diff日だけ離れた年月日を取得する
0207:  * @param int $year  西暦年
0208:  * @param int $month 月
0209:  * @param int $day   日
0210:  * @param int $diff  差分日数
0211:  * @return array(年,月,日)
0212: */
0213: function diffymd($year$month$day$diff) {
0214:     $dm = getDaysInMonth($year$month);
0215: 
0216:     $day += $diff;
0217:     //月の繰り下がり
0218:     while ($day < 1) {
0219:         $month--;
0220:         if ($month < 1) $year--;
0221:         $dm = getDaysInMonth($year$month);
0222:         $day += $dm;
0223:     }
0224:     //月の繰り上がり
0225:     while ($day > $dm) {
0226:         $dm = getDaysInMonth($year$month);
0227:         $day -= $dm;
0228:         $month++;
0229:         if ($month > 12)    $year++;
0230:     }
0231: 
0232:     return array($year$month$day);
0233: }

ユーザー関数 diffymd は、$year$month$day日から $diff日だけ離れた年月日を取得する。$diff に正数を指定すると未来の日付に、負数を指定するとかこの日付にさかのぼる。
月の大小が異なることから getDaysInMonth を呼び出し、うるう年の判定は isleap を呼び出すようにしてある。

組み込み関数  strtotime  を使えば簡便に書けるのだが、この関数は 32 ビット環境では UNIX タイムスタンプで 1901 年 2 月 13 日から 2038 年 1 月 19 日までしか計算できないため、あえてユーザー関数を用意した。

なお、コマンドラインから "DayToday.php?diff=+2" のように指定することで $diff を直接指定できる。
また、メイン・プログラムで $diff = +1 のように書き換えれば、「明日は何の日」を求めるプログラムに変身する。

活用例

きょうは何の日?:みんなの知識 ちょっと便利帳」では、このサンプル・プログラムを活用し、情報を見やすくレイアウトしている。ありがとうございます。

参考サイト

(この項おわり)
header