Webページの最新更新日時の情報を取得する

Webページ(Webリソース)の最新更新日時を取得する方法について説明しています。curlでWebページのHTTPヘッダーの情報を確認しながら、Webページが更新されたか?未更新か?の判断方法などについて説明しています。

目的(このページで書いてある事)と環境

最終更新日:2022/6/15

ネット上にあるWebページ(HTMLページ・画像ファイル・Webプログラミング言語を使った動的ページなどのWebリソース)の最新更新日時の情報を知りたくて、調べたり試した事を書いています。

その際、curlコマンドやChromeブラウザのデベロッパーツール(Developer Tools)などを使用して色々と試したのですが、このあたりの知識は時間を置いたら忘れそうで、忘れるたびに調べる事になりそうなので、今回調べたり試した事をまとめて書いています。

■curlを実行した環境です。
CentOS Linux 7.9
macOS Big Sur(バージョン11.6.6)
AWS ec2のAmazon Linux release 2 (Karoo)
(*crulのバージョンはすべてcurl 7.61.1)

個人的によく使うcurlコマンドについては別ページで説明していますので、よろしければそちらも参考にしてみてください。
curlで個人的によく使うコマンド(オプション)のまとめ【HTTPリクエスト】

curlを使って、HTTPヘッダーのLast-Modified(Webリソースの最終更新日時)を取得する

ウェブページや画像などのWebリソースにHTTPアクセスすると、Webリソース本体だけでなく、HTTPヘッダーの情報もレスポンスとして返してくれます。

HTTPヘッダーには様々な情報がありますが、その中の1つにLast-Modifiedがあり、Last-ModifiedはWebリソースの最終更新日時の情報を教えてくれます。

実際に、curlコマンドを実行してLast-Modifiedを確認してみます。
curlは-IオプションでHTTPヘッダーの情報を確認できます。また、-iオプションでHTTPヘッダーとWebリソース本体の両方の情報を取得する事ができます。

$ curl -I http://192.168.44.10/test/hello.html
HTTP/1.1 200 OK
Date: Sun, 05 Jun 2022 15:48:59 GMT
Server: Apache/2.4.6 (CentOS) PHP/7.4.16
Last-Modified: Sun, 05 Jun 2022 15:48:44 GMT
ETag: "85-5e0b54a4fcf00"
Accept-Ranges: bytes
Content-Length: 133
Content-Type: text/html; charset=UTF-8

curlの実行結果を見ると、HTTPのステータスコード200なので、リクエスト結果はOKです。
そして、"Last-Modified: Sun, 05 Jun 2022 15:48:44 GMT"の1行があります。
この1行から、HTTPリクエストして取得したHTMLページ(hello.html)の最新更新日時は、"Sun, 05 Jun 2022 15:48:44 GMT"だという事がわかります。

GMTはグリニッジ標準時刻の事で、日本標準時刻とは9時間の時差があります。ですので、GMTの時刻にプラス9時間すれば日本時間になります。

■ curlで「https://〇〇〇〇.com」アクセス時のSSLエラーについて
最近のWebサイトはほとんどがhttps(ssl)ですが、curlでhttpsのサイトにアクセスする場合、macOSのcurlでは問題なくできますが、CentOS7のcurlではSSL証明書エラーが出ます。

$ curl https://〇〇〇〇.com/
curl: (60) SSL certificate problem: certificate has expired
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed …
〜
このエラーが出た時は-kオプションを付ければ、SSLエラーを無視してcurlコマンドを続行できるようになります。
$ curl -k https://〇〇〇〇.com/
〜

Last-Modifiedやこの後に話すETagは、PHPなどの動的Webページでは返したり返さなかったりします。これについては後の方で説明しています。

HTTPリクエストヘッダー"If-Modified-Since"とLast-Modifiedの情報で、Webページが更新されていれば取得する

次に、HTTPリクエスト時にHTTPヘッダーのLast-Modified(最終更新日時)の情報を見て、Webリソースが更新されていればWebリソースを取得して、未更新であれば取得しないという事ができるので、その方法について説明します。

HTTPリクエストヘッダーの"If-Modified-Since"に日時を指定すれば、条件付きのHTTPリクエストになり、If-Modified-Sinceで指定した日時より後にWebリソースが更新されていれば通常通りにWebリソースを取得し、未更新であればサーバがステータスコード"304 Not Modified"を返してきてWebリソース本体は返しません。

実際にcurlで試してみます。curlは-HオプションでHTTPリクエストヘッダーを指定できます。
まずは、-iオプションだけでcurlを実行してみます。

$ curl -i http://192.168.44.10/test/hello.html
HTTP/1.1 200 OK
Date: Sun, 05 Jun 2022 15:49:41 GMT
Server: Apache/2.4.6 (CentOS) PHP/7.4.16
Last-Modified: Sun, 05 Jun 2022 15:48:44 GMT
ETag: "85-5e0b54a4fcf00"
Accept-Ranges: bytes
Content-Length: 133
Content-Type: text/html; charset=UTF-8

<!DOCTYPE html>
<html lang="ja">
〜
Last-Modifiedの値が"Sun, 05 Jun 2022 15:48:44 GMT"なので、この値をHTTPリクエストヘッダーのIf-Modified-Sinceで指定してcurlを実行します。
$ curl -i http://192.168.44.10/test/hello.html \
>  -H 'If-Modified-Since: Sun, 05 Jun 2022 15:48:44 GMT'
HTTP/1.1 304 Not Modified
Date: Sun, 05 Jun 2022 15:53:31 GMT
Server: Apache/2.4.6 (CentOS) PHP/7.4.16
ETag: "85-5e0b54a4fcf00"
Webリソース(hello.html)は未更新のままなので、サーバはステータスコード"304 Not Modified"を返してきて、Webリソース本体も返してきません。

今度は適当にWebページのhello.htmlを修正してから、再度curlでHTTPリクエストを実行してみます。

$ curl -i http://192.168.44.10/test/hello.html \
> -H 'If-Modified-Since: Sun, 05 Jun 2022 15:48:44 GMT'
HTTP/1.1 200 OK
Date: Sun, 05 Jun 2022 15:57:32 GMT
Server: Apache/2.4.6 (CentOS) PHP/7.4.16
Last-Modified: Sun, 05 Jun 2022 15:57:10 GMT
ETag: "86-5e0b56878c180"
Accept-Ranges: bytes
Content-Length: 134
Content-Type: text/html; charset=UTF-8

<!DOCTYPE html>
<html lang="ja">
〜
hello.htmlは更新されたので、"If-Modified-Since"で指定した日時よりも"Last-Modified"の日時が新しくなり、サーバはステータスコード200と、hello.htmlのHTMLソースをレスポンスしてきました。これは期待通りの結果です!

If-Modified-Sinceの条件付きHTTPリクエスト(このすぐ後に話すIf-None-Matchも)で、PHPなどの動的Webページにcurlでアクセスしたら、レスポンスは毎回ステータスコード200を返してきました。これについては後の方でもう少し詳しく説明しています。

HTTPリクエストヘッダー"If-None-Match"とETagの情報で、Webページが更新されていれば取得する

HTTPリクエストヘッダー"If-Modified-Since"と、サーバが返すHTTPヘッダー"Last-Modified"を使えば、Webリソースが更新された場合のみWebリソースを取得する事ができる事を説明しました。

そして、これと同じような事が、HTTPリクエストヘッダー"If-None-Match"と、HTTPレスポンスヘッダーの"ETag"を使う事でできます。

まずETagについてですが、curlを試した時にサーバはLast-Modifiedだけでなく、ETagという情報も返してきました。
上で試したcurl実行例だと、「ETag: "86-5e0b56878c180"」という1行です。

ETagは、エンティティタグ値と呼ばれる情報で、Webリソースを特定するための固有値(文字列)です。
そして、Etagの値はWebリソースを更新(修正)するたびに更新されます。

次に、If-None-Matchは条件付きHTTPリクエストの1つで、If-None-Matchに指定した値(文字列)とETagの値が一致しなかった場合、サーバ側はHTTPリクエストを受け付けます。つまり、Webページを更新するとETagの値も更新されるので、If-None-Matchに指定した過去のETag値と一致しなくなり、HTTPリクエストを受け付けるようになります。

これも実際にcurlで試してみます。上記でcurlを実行した時のhello.htmlページのEtag値は"86-5e0b56878c180"なので、まずはこの値をIf-None-Matchで指定して実行します。

$ curl -i http://192.168.44.10/test/hello.html -H 'If-None-Match: "86-5e0b56878c180"'
HTTP/1.1 304 Not Modified
Date: Sun, 05 Jun 2022 17:37:29 GMT
Server: Apache/2.4.6 (CentOS) PHP/7.4.16
ETag: "86-5e0b56878c180"
サーバはステータスコード"304 Not Modified"を返し、Webリソース本体は返してきません。
これはhello.htmlページは未更新なので、hello.htmlページのETag値とIf-None-Matchで指定したETag値が一致しているためです。

今度はまた適当にhello.htmlを修正して、再度curlを実行します。

$ curl -i http://192.168.44.10/test/hello.html -H 'If-None-Match: "86-5e0b56878c180"'
HTTP/1.1 200 OK
Date: Sun, 05 Jun 2022 17:43:59 GMT
Server: Apache/2.4.6 (CentOS) PHP/7.4.16
Last-Modified: Sun, 05 Jun 2022 17:43:57 GMT
ETag: "85-5e0b6e65bd140"
Accept-Ranges: bytes
Content-Length: 133
Content-Type: text/html; charset=UTF-8

<!DOCTYPE html>
<html lang="ja">
〜
サーバはステータスコード200と、hello.htmlのHTML本体を返してきました。 これは、hello.htmlを修正した事でETag値も更新され、hello.htmlページのETag値とIf-None-Matchで指定したETag値が一致しなくなったからです。

以上の試した結果から、If-None-MatchとETagを使えば、Webリソースが未更新ならばサーバ側はステータスコード304を返すだけで、Webリソースが新しく更新されればサーバ側はステータスコード200とWebリソース本体を返す事がわかりました。

Chromeデベロッパーツールを使って、HTTPリクエストヘッダーやHTTPレスポンスヘッダーの情報を確認する

これまではcurlコマンドを使ってHTTPヘッダーのLast-Modified・ETagを確認していたけど、Chromeブラウザのデベロッパーツール(Developer Tools)でも、簡単にHTTPリクエストヘッダーやHTTPレスポンスヘッダーの値を確認する事ができます。

ちなみにChromeデベロッパーツールはHTTPヘッダーを見れるだけでなく、HTML・CSSをその場で修正したり、JavaScriptを実行したり、JavaScriptコードをデバッグしたりなど非常に色々な事ができて便利です。

デベロッパーツールの起動は、ブラウザ画面を右クリック->"検証"を選択します(また、macならショートカット「Command+Option+I」、Windwosなら「Ctrl+Shift+J 」または F12)。
Chrome のキーボード ショートカット
Chromeデベロッパーツールの起動

デベロッパーツールの上の方に、"Console"、"Sources"、"Network"などのタブが並んでいるので、Networkタブを選択して再度ページを更新します。

すると、取得したWebリソース一覧が表示されるので、対象リソース(画像例ではhello.html)を選択する事でそのリソースの様々な情報を確認する事ができます。
ChromeデベロッパーツールでHTTPステータスコード、リクエストヘッダー、レスポンスヘッダーを確認

表示したリソース(画像例ではhello.html)の情報の中で"Headers"タブを選択すると、HTTPのステータスコード、リクエストヘッダー、レスポンスヘッダーなどの情報を見る事ができるので、Etag,Last-Modified,If-Modified-Since,If-None-Matchなども確認できます。

ちなみにステータスコード"304 OK"を返してくるのは、HTTPリクエストヘッダーでIf-Modified-SinceやIf-None-Matchなどの条件付きHTTPリクエストをしているからだと思います。ブラウザをスーパーリロード(キャッシュを無視してページ更新)すれば、ステータスコードは"200 OK"を返してくるはずです(スーパーリロードのショートカットは、macは「Command+Shift+r」、Windwosは「Shift + F5」です)。

また、デベロッパーツールでは"Console"タブ上でJavaScriptコードを実行する事ができるのですが、WebリソースがLast-Modifiedの値を返す場合、Consoleタブ上でJavaScriptコード"document.lastModified"を実行すれば、Last-Modifiedの値を表示する事ができます。
document.lastModifiedでHTTPヘッダーのLast-Modifiedを表示する

WebリソースがLast-Modifiedの値を返さない場合に"document.lastModified"を実行すると、現在時刻を表示します。

PHPで、ETag・Last-ModifiedなどのHTTPヘッダーを出力する方法(header関数)

サーバ側のWebリソースがhtmlページや画像ファイルの場合、HTTPヘッダーでETag・Last-Modifiedの情報を返してくれますが(100%かどうかはちょっと解らない…)、PHPなどの動的なWebページの場合、ETag・Last-Modifiedの情報を返してくれるかどうかはWebページによります。というか返してくれない動的Webページが多いです。

PHPでHTTPヘッダーを出力するには、header関数でできます。
ETag・Last-Modifiedを出力する簡単なPHPコードです。

$ cat hellophp.php
<?php

$last_modified = filemtime($_SERVER['SCRIPT_FILENAME']); 
header("Last-Modified: ".gmdate("D, d M Y H:i:s", $last_modified)." GMT");
header("ETag: \"xx-$last_modified\"");

echo "<h1>ハロー、PHP!</h1>";

このPHPページに対して、curlを実行してみます。

$ curl -i http://192.168.44.10/test/hellophp.php
HTTP/1.1 200 OK
Date: Sun, 05 Jun 2022 22:39:11 GMT
Server: Apache/2.4.6 (CentOS) PHP/7.4.16
X-Powered-By: PHP/7.4.16
Last-Modified: Sun, 05 Jun 2022 22:37:26 GMT
ETag: "xx-1654468646"
Content-Length: 27
Content-Type: text/html; charset=UTF-8

<h1>ハロー、PHP!</h1>
Last-Modified、ETagを出力してくれました!

ただ、このPHPページに対して、If-Modified-Since・If-None-Matchなどの条件付きHTTPリクエストをcurlで試してみましたが、どんな条件でもHTTPステータスコードは"200"しか返しませんでした。

$ curl -i http://192.168.44.10/test/hellophp.php -H 'If-Modified-Since: Sun, 05 Jun 2022 22:37:26 GMT'
HTTP/1.1 200 OK
Date: Sun, 05 Jun 2022 22:50:59 GMT
Server: Apache/2.4.6 (CentOS) PHP/7.4.16
X-Powered-By: PHP/7.4.16
Last-Modified: Sun, 05 Jun 2022 22:37:26 GMT
ETag: "xx-1654468646"
Content-Length: 27
Content-Type: text/html; charset=UTF-8

<h1>ハロー、PHP!</h1>

$ curl -i http://192.168.44.10/test/hellophp.php -H 'If-None-Match: "xx-1654468646"'
HTTP/1.1 200 OK
Date: Sun, 05 Jun 2022 22:53:36 GMT
Server: Apache/2.4.6 (CentOS) PHP/7.4.16
X-Powered-By: PHP/7.4.16
Last-Modified: Sun, 05 Jun 2022 22:37:26 GMT
ETag: "xx-1654468646"
Content-Length: 27
Content-Type: text/html; charset=UTF-8

<h1>ハロー、PHP!</h1>
本来なら上のcurl実行例では、HTTPステータスコード"304 Not Modified"が返ってくる事を期待したのですが、"200 OK"が返ってくるという事は、If-Modified-Since・If-None-Matchの条件が全く有効でないという事です。

正直この原因はよく解らなかったのですが(もしかしたらそういう物なのかもしれない)、そもそもPHPのheader関数は適当なLast-Modified、ETagの値を出力して返す事ができるので、Last-Modifiedの日時が本当に正確な最終更新日時なのかは保証されていないと思います。

あと、上のPHPのコード例では、PHPファイルの最新更新日時をLast-Modifiedの値として返すようにしていますが、PHPがDB(データベース)からデータを引っ張ってきて画面表示(HTML出力)するようなスクリプトの場合、Webリソースの最終更新日時とPHPファイルの最新更新日時は一致しないため、やはりWeb系のプログラミングを使った動的ページのLast-Modifiedの日時は、本当に最終更新日時なのかは解らないと思います。

結局完璧にWebページが最新情報かどうか、更新されたかどうかを判別するには?

色々と調べたり試してきましたが、Webリソースの最終更新日時の情報は、HTTPヘッダーのLast-Modifiedで確認できる事は解りましたが、ただWebリソースによってLast-Modifiedを返してくれなかったりするし、Last-Modifiedを見れてもそれが本当に最終更新日時の情報かどうかが保証されていないと思うので、Last-Modifiedの情報ではWebリソースが最新かどうかの万能な判断材料にはならなそうです。

それで結局、完全にWebリソースの最終更新日時はいつか?新しく更新されたかどうか?を判別するには、WebページのHTMLソース全体の文言を見て判断する(前回取得したHTMLソースと比較する)しか方法はないのでは?と個人的には考えています(もしかしたら別のもっと良い方法があるのかもしれないですが…)。

そうなると、限られた数のサイトのWebページを対象に更新されたかどうかを判別するならいいけど、多くのサイトを対象とするにはコストが非常に大きすぎるから、ちょっと現実的じゃ無いよねって考えてしまいます。

ですので現時点の結論として、基本的にLast-Modified、ETagを返してくれるWebページはその情報を信じて使って、あとはRSSなどもあれば使って、そういう情報が一切ないWebページでどうしても更新されたかどうかを知りたい時は、HTMLソース全体を見て判断すればいいかなと個人的には考えています。