
Python では、インターネット上のコンテンツをファイル入力と同じくらい簡単に取得することができる。そこで今回は――指定したURLにあるHTMLファイルを取得しコンソールに表示する、指定したURLにあるHTMLファイルからTITLEタグの内容を取り出してコンソールに表示する、RSS情報を表示する――という3つのプログラムをつくり、インターネット・コンテンツを扱うためのHTTP通信の基本について学んでいく。
なお、インターネットは信頼境界の外側にあることから、バリデーションなどを行いセキュリティ対策に留意する。
目次
サンプル・プログラム
httpGet1.py | GETメソッドでコンテンツ取得 |
dispTitle1.py | サイトのTITLEタグを表示する |
dispRSS1.py | RSSの内容を表示する |
pahooValidate.py | バリデーション・モジュール |
pahooCache.py | HTTP通信キャッシュ・システム |
dispTitle2.py | キャッシュ・システムを利用してサイトのTITLEタグを表示する |
HTMLコンテンツを取得する
プログラム "httpGet1.py" を実行してみてほしい。ぱふぅ家のホームページのトップページ https:/www.pahoo.org/index.html にアクセスし、その内容(HTMLファイル)をコンソールに表示する。
httpGet1.py
# メイン・プログラム =======================================================
# 初期値
url = "https://www.pahoo.org/index.html" # 読み込むURL
elapsedTime = 60 # アクセス可能間隔(秒);相手先サイトに負荷をかけないよう
elapsedFile = "./httpGet1e.txt" # 経過時刻格納ファイル名
try:
# URLのバリデーション
urlStr = pv.validateURL(url)
# URLが存在するかどうか
if not isURLexists(url):
pv.dispErrorMessageAndExit(f"{url} は存在しません")
それでは、メイン・プログラムから見ていこう。
ファイル入力と同様、例外処理が発生する可能性を考え、全体を try文で囲んでおく。
今回は、インターネットという信頼境界の外側にあるデータを読み込むことになるので、ファイル入力にも増して、バリデーションチェックに配慮する。
まず、URLに使える文字は RFC2396 という規格によって定められている。指定したURLが、この規格の範囲内にあるあるかどうかをバリデーションする関数 validateURL を用意した。詳しくは後述する。
次に、そのURLが存在するかどうかをチェックする関数 isURLexists を通す。これも後述する。
最後に、アクセス可能間隔をチェックする関数 isElapsedTime を通す。これも後述するが、ここではプログラムを使って相手サイトにアクセスしに行くわけだが、もしプログラムの不具合や、悪意のある第三者に乗っ取られ、頻繁に連続して同じアクセスしてしまうと、相手先サイトからは DDoS攻撃と認識されてしまう。このような齟齬が起きぬよう、ここでは60秒以上の間隔を空けないとアクセスできないようにした。

以上のバリデーションをクリアしたら、ユーザー関数 httpGet を使ってHTMLファイルを読み込み、コンソールに表示する。今回はコンソールに出力するだけなので、スクリプトやSQLが実行されるリスクはないと考え、出力バリデーションは実施しない。
最後に例外処理を記述する。
ファイル入力と同様、例外処理が発生する可能性を考え、全体を try文で囲んでおく。
今回は、インターネットという信頼境界の外側にあるデータを読み込むことになるので、ファイル入力にも増して、バリデーションチェックに配慮する。
まず、URLに使える文字は RFC2396 という規格によって定められている。指定したURLが、この規格の範囲内にあるあるかどうかをバリデーションする関数 validateURL を用意した。詳しくは後述する。
次に、そのURLが存在するかどうかをチェックする関数 isURLexists を通す。これも後述する。
最後に、アクセス可能間隔をチェックする関数 isElapsedTime を通す。これも後述するが、ここではプログラムを使って相手サイトにアクセスしに行くわけだが、もしプログラムの不具合や、悪意のある第三者に乗っ取られ、頻繁に連続して同じアクセスしてしまうと、相手先サイトからは DDoS攻撃と認識されてしまう。このような齟齬が起きぬよう、ここでは60秒以上の間隔を空けないとアクセスできないようにした。

以上のバリデーションをクリアしたら、ユーザー関数 httpGet を使ってHTMLファイルを読み込み、コンソールに表示する。今回はコンソールに出力するだけなので、スクリプトやSQLが実行されるリスクはないと考え、出力バリデーションは実施しない。
最後に例外処理を記述する。
URLのバリデーション
バリデーション・モジュール "pahooValidate.py" に、URLのバリデーションを行うユーザー関数 validateURL を追加した。
pahooValidate.py
def validateURL(url, label=""):
"""URLのバリデーションを行う.
Args:
url(str): URL
label(str, optional): データ名称(省略時は"")
Returns:
str: 成功した場合は引数と同じ文字列
Raises:
ValueError: URLではない
"""
# URLパターン
reStr = r"^https?:\/\/[\-\_\.\!~\*'\(\)a-zA-Z0-9\;\?\@\&\=\+\$\,%#\/]+$"
try:
url2 = validateString(url, [ reStr ], True, None, None, label)
# 失敗したら例外を返す.
except ValueError as e:
print("URLエラー")
raise ValueError(f"{url} は URLではありません")
return url2
前述のように、URLとして使える文字は RFC2396 で規定されている。これを正規表現で表したのが変数 reStr である。
冒頭にあるハット ^ は文字列の冒頭を、末尾にあるドルマーク $ は文字列の末尾を意味する。つまり、このパターンは与えられた文字列の一部ではなく、冒頭から末尾までの全文字列が正規表現とマッチすることを求めている。
クエスチョンマーク ? は、その左側の文字が0または1回現れることにマッチする。ここでは https? とあるので、http または hppts のいずれかにマッチする。SSLサイトなら hppts から始まり、そうでないサイトは http ではじまるわけだが、そのいずれにもマッチするという意味だ。
円マーク \ はエスケープ記号で、その右側の文字そのものを示す。正規表現に使う特殊記号を表したいときに使う。
そのあと――http(s)://のあと――は、 RFC2396 で規定されたURLに使える英数記号を並べる。

これを、「7.3 正規表現」で作ったユーザー関数 validateString に渡してバリデーションを行う。
冒頭にあるハット ^ は文字列の冒頭を、末尾にあるドルマーク $ は文字列の末尾を意味する。つまり、このパターンは与えられた文字列の一部ではなく、冒頭から末尾までの全文字列が正規表現とマッチすることを求めている。
クエスチョンマーク ? は、その左側の文字が0または1回現れることにマッチする。ここでは https? とあるので、http または hppts のいずれかにマッチする。SSLサイトなら hppts から始まり、そうでないサイトは http ではじまるわけだが、そのいずれにもマッチするという意味だ。
円マーク \ はエスケープ記号で、その右側の文字そのものを示す。正規表現に使う特殊記号を表したいときに使う。
そのあと――http(s)://のあと――は、 RFC2396 で規定されたURLに使える英数記号を並べる。

これを、「7.3 正規表現」で作ったユーザー関数 validateString に渡してバリデーションを行う。
URLの存在チェック
URLの存在チェックを行うユーザー関数 isURLexists は、「5.1 組み込み関数とモジュール」で紹介した外部モジュール request を利用することから、メインプログラム・ファイル "httpGet1.py" の中に用意した。あらかじめ外部モジュール request をインストールしておくこと。
httpGet1.py
def isURLexists(url):
"""指定URLが存在するかどうかを判定する.
Args:
url(str): URL
Returns:
bool: True 存在する(HTTP 200以上400未満)/False 存在しない
"""
try:
# HTTPヘッダをリクエスト
res = requests.head(url, allow_redirects=True)
# 応答コードが200以上400未満なら存在すると判定
if res.status_code >= 200 and res.status_code < 400:
return True
else:
return False
except requests.RequestException:
# リクエストの例外が発生した場合は存在しないと判定
return False
URLの存在チェックを行うには、HTTP通信の基本的な仕組みを理解しておく必要がある。
HTTP通信では、クライアント側からHTTPサーバ側(ホームページ、ブログサービス、クラウドサービスなど)へURL要求(リクエスト)を送信する。このリクエストには GETメソッドと POSTメソッドの2つがある。単純にサイトを表示するときには GETメソッドを使う。
リクエストを受け取ったHTTPサーバは、HTTPレスポンスコードと要求されたURLのコンテンツを返信する。もし指定したURLが存在しないと、HTTPレスポンスコートとして404が返る――ブラウザによく表示される 404エラーが、これである。
主な HTTPレスポンスコードを下表に整理する。
HTTP通信では、クライアント側からHTTPサーバ側(ホームページ、ブログサービス、クラウドサービスなど)へURL要求(リクエスト)を送信する。このリクエストには GETメソッドと POSTメソッドの2つがある。単純にサイトを表示するときには GETメソッドを使う。
リクエストを受け取ったHTTPサーバは、HTTPレスポンスコードと要求されたURLのコンテンツを返信する。もし指定したURLが存在しないと、HTTPレスポンスコートとして404が返る――ブラウザによく表示される 404エラーが、これである。
主な HTTPレスポンスコードを下表に整理する。
HTTPレスポンスコード | 意味 |
---|---|
200 | 成功(OK) |
201 | 成功(Created);POSTリクエストに対する応答 |
202 | 処理中 |
301 | URL移転 |
302 | URL一時移転 |
307 | 一時的なリダイレクト |
308 | 永続的なリダイレクト |
400 | 不正なリクエスト |
401 | 認証エラー |
403 | アクセス権エラー |
404 | 存在しない |
408 | タイムアウト |
500 | サーバ・エラー |
503 | サーバ・ダウンなど |
HTTPレスポンスコードが200番台は正常、300番台はURLの移転など。400番台はクライアント側のエラー、500番台はサーバ側のエラーなので、requests.head で HTTPレスポンスコードを取得し、200以上400未満なら指定したURLが存在するとみなして True を返すようにした。
アクセス可能間隔
前述の通り、相手サイトにアクセスしにタイミングについて、もしプログラムの不具合や、悪意のある第三者に乗っ取られ、頻繁に連続して同じアクセスしてしまうと、相手先サイトからは DDoS攻撃と認識されてしまう。このような齟齬が起きぬよう、ユーザー関数 isElapsedTime を使ってアクセス可能間隔かどうかをチェックするようにした。
pahooValidate.py
def isElapsedTime(second=60, filename="./elaspedTime.txt"):
"""前回呼び出しから指定秒数が経過しているかどうか判定する.
Args:
second(float) : 前回からの経過時間(秒)
filename(str) : 経過時間を記録するファイル名
Returns:
bool: True 経過時間を過ぎた/False 過ぎていない
Raises:
そのまま例外を発生する
Note:
連続アクセスにより相手先サイトに負荷をかけないようにするために使う.
"""
# 現在日時
now = time.time()
try:
# ファイルが存在しなければ初回アクセス
if not os.path.exists(filename):
# 現在時刻を保存してTrueを返す
with open(filename, "w") as file:
file.write(str(now))
return True
# ファイルから前回のアクセス時間を読み込む
with open(filename, "r") as file:
ti = file.read()
# 読み込んだデータをバリデーション
last = float(validateNumber(ti, "float"))
# 現在の時刻と前回のアクセス時刻の差を調べる
if now - last >= second:
# 指定秒数以上経過していれば現在時刻を更新
with open(filename, 'w') as file:
file.write(str(now))
return True
# 指定秒数未満の場合
return False
ユーザー関数 isElapsedTime は、前回呼び出された日時を引数 filename で示されたファイルに保管しておき、そこからの経過時間を計算、引数 second 以上ならば True を、そうでなければ False を返すことで、アクセス可能間隔かどうかをチェックする。

プログラムの流れは次のようになる。

プログラムの流れは次のようになる。
- 引数 filename で示されたファイルが存在しなければ、現在日時をファイルに保管して True を返す。
- 引数 filename で示されたファイルから前回アクセス時間を読み込む。
- 現在の時刻と前回のアクセス時刻の差を調べ、引数 second 以上ならば True を返す。
- それ以外は False を返す。
GETメソッドでコンテンツ取得
前述の通り、HTTP通信を使って単にサイトを表示する(=HTMLファイルを取得する)には、GETメソッドを使う。指定URLのコンテンツをHTTP GETで取得するユーザー関数 httpGet を用意した。
httpGet1.py
def httpGet(url):
"""指定URLのコンテンツをHTTP GETで取得する.
Args:
url(str): URL
Returns:
str: 取得したコンテンツ
Raises:
エラー表示してプログラム終了
"""
try:
# HTTP GET通信でコンテンツ取得
res = requests.get(url)
# コンテンツのエンコードを自動判定
res.encoding = res.apparent_encoding
# その他の例外処理
except Exception as errmsg:
pv.dispErrorMessageAndExit(f"エラーが発生しました -- {errmsg}")
# 取得したコンテンツ
return str(res.text)
ユーザー関数 httpGet も外部モジュール request を利用することから、メインプログラム・ファイル "httpGet1.py" の中に用意した。
まず、requests.get関数を使って、指定URLのコンテンツを取得する。
取得したコンテンツのエンコードを自動判定するために、[res.encodingプロパティ]に res.apparent_encodingを代入する。apparent_encodingは、内部で文字列のバイトパターンを解析し、最も適切なエンコーディングを決定する chardet というライブラリを使用してエンコーディングを推測する。
まず、requests.get関数を使って、指定URLのコンテンツを取得する。
取得したコンテンツのエンコードを自動判定するために、[res.encodingプロパティ]に res.apparent_encodingを代入する。apparent_encodingは、内部で文字列のバイトパターンを解析し、最も適切なエンコーディングを決定する chardet というライブラリを使用してエンコーディングを推測する。
サイトのTITLEタグを表示する
プログラム "httpGet1.py" を改良し、サイトのHTMLファイルのTITLEタグの内容を取得・表示するプログラム "dispTitle" をつくってみた。
dispTitle1.py
# メイン・プログラム =======================================================
# 初期値
url = "https://www.pahoo.org/index.html" # 読み込むURL
targetTag = "title" # titleタグ
elapsedTime = 60 # アクセス可能間隔(秒);相手先サイトに負荷をかけないよう
elapsedFile = "./dispTitlee.txt" # 経過時刻格納ファイル名
try:
# URLのバリデーション
urlStr = pv.validateURL(url)
# URLが存在するかどうか
if not isURLexists(url):
pv.dispErrorMessageAndExit(f"{url} は存在しません")
メイン・プログラムは "httpGet1.py" とほぼ同じだが、取得したコンテンツ htmlContents を解析し、TITLEタグの内容を取り出す関数 getTag を追加した。
dispTitle1.py
def getTag(htmlContents, targetTag):
"""HTMLコンテンツから指定タグの内容を返す.
Args:
htmlContents(str): HTMLコンテンツ
targetTag(str): タグ;<>は含めない
Returns:
str: タグの内容
Raises:
エラー表示してプログラム終了
"""
# 印字可能な日本語文字パターン(正規表現)
RE_PRINTABLE = r"^[\u3005\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\uFF01-\uFF5E\x20-\x7E]+$"
try:
# BeautifulSoupを使ってHTMLを解析する
soup = BeautifulSoup(htmlContents, "html.parser")
# タグの内容を取得する
tagStr = soup.find(targetTag)
# タグの内容が見つからなかったら例外を発生
if not tagStr:
raise ValueError(f"{targetTag}タグが見つかりません")
# タグの内容をバリデーション
str = pv.validateString(tagStr.get_text(strip=True), [ RE_PRINTABLE ], True)
# その他の例外処理
except Exception as errmsg:
pv.dispErrorMessageAndExit(f"エラーが発生しました -- {errmsg}")
# 取得したタグの内容
return str
ユーザー関数 getTag は、第1引数にHTML文字列を、第2引数にその中から取得したいタグを渡す。「5.1 組み込み関数とモジュール」で紹介した外部モジュール BeautifulSoup を利用する。あらかじめ外部モジュール BeautifulSoup をインストールしておくこと。

BeautifulSoupモジュールは、第1引数に解析したい文字列(この場合はHTML文字列)を、第2引数にHTMLを解析することを意味する "html.parser" を渡す(後述するが、第2引数を変えることでXMLの解析もできる)。
タグを検索するには、findメソッドを使う。
タグが見つからなければ例外を発生させる。
タグが見つかったら、get_textメソッドを使ってタグの内容を取り出す。このとき、タグの内容に対し、念のため文字列バリデーション関数 validateString を使ってバリデーションを行う。正規表現のパターン RE_PRINTABLE には、日本語を含む印字可能な文字を並べた。「7.3 正規表現」で日本語文字にマッチする正規表現を学んだが、これに加え、半角英数記号に相当する文字コード範囲 \x20-\x7E を加えた。
最後に例外処理を記述する。

BeautifulSoupモジュールは、第1引数に解析したい文字列(この場合はHTML文字列)を、第2引数にHTMLを解析することを意味する "html.parser" を渡す(後述するが、第2引数を変えることでXMLの解析もできる)。
タグを検索するには、findメソッドを使う。
タグが見つからなければ例外を発生させる。
タグが見つかったら、get_textメソッドを使ってタグの内容を取り出す。このとき、タグの内容に対し、念のため文字列バリデーション関数 validateString を使ってバリデーションを行う。正規表現のパターン RE_PRINTABLE には、日本語を含む印字可能な文字を並べた。「7.3 正規表現」で日本語文字にマッチする正規表現を学んだが、これに加え、半角英数記号に相当する文字コード範囲 \x20-\x7E を加えた。
最後に例外処理を記述する。
RSSの内容を表示する
サイトの更新情報を RSS というファイルで提供しているサイトがある。ぱふぅ家のホームページでも、https://www.pahoo.org/rss/pahoo.rdf でホームページ内の記事更新情報を RSS にして配信している。
そこで、プログラム "dispTitle" を改良し、RSSの内容を表示するプログラム "dispRSS" をつくってみた。
そこで、プログラム "dispTitle" を改良し、RSSの内容を表示するプログラム "dispRSS" をつくってみた。
dispRSS1.py
# メイン・プログラム =======================================================
# 初期値
url = "https://www.pahoo.org/rss/pahoo.rdf" # RSSのURL
elapsedTime = 60 # アクセス可能間隔(秒);相手先サイトに負荷をかけないよう
elapsedFile = "./dispRSSe.txt" # 経過時刻格納ファイル名
try:
# URLのバリデーション
urlStr = pv.validateURL(url)
# URLが存在するかどうか
if not isURLexists(url):
pv.dispErrorMessageAndExit(f"{url} は存在しません")
メイン・プログラムの流れは "dispTitle"" とほぼ同じだが、HTMLタグの内容を取得する関数 getTag の代わりに、RSSの内容を取得するユーザー関数 getRSS を用意した。
dispRSS1.py
def getRSS(url):
"""RSSの内容を返す.
Args:
url(str): RSSのURL
Returns:
str: RSSの内容
Raises:
エラー表示してプログラム終了
Note:
RSS 1.0専用
"""
# 印字可能な日本語文字パターン(正規表現)
RE_PRINTABLE = r"^[\u3005\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\uFF01-\uFF5E\x20-\x7E]+$"
try:
# HTTP GET通信で RSS取得
rssContents = httpGet(url)
# RSS1.0を解析
soup = BeautifulSoup(rssContents, "xml")
# itemタグを1つずつ取り出す
res = ""
for item in soup.find_all("item"):
# タイトル、リンク、更新日時を取得する
title = item.find("title").get_text(strip=True)
link = item.find("link").get_text(strip=True)
for dc in item.find_all("dc:date"):
pubDate = dc.get_text(strip=True) if item.find_all("dc:date") else "日付なし"
# HTML特殊文字をデコードする
str1 = html.unescape(f"{title} {link} {pubDate}")
# 印字可能な日本語文字かどうかをバリデーション
# str2 = pv.validateString(str1, [ RE_PRINTABLE ], True)
# 1項目分の情報を追加する
res += str2 + "\n"
# その他の例外処理
except Exception as errmsg:
pv.dispErrorMessageAndExit(f"エラーが発生しました -- {errmsg}")
# 取得したRSSの内容
return res
まず、ユーザー関数 httpGet を使ってRSSファイルを取得する。httpGet はHTMLファイルを取得するために作った関数だが、GETメソッドで受け取れるファイルであれば、HTML以外でも取得することができる。

RSS にはいくつかのバージョンがあるが(詳しくは「PHPで作るRSSビューア」参照)、ここでは、ぱふぅ家のホームページで配信している RSS 1.0 に限定して解釈ができるようにする。
RSS 1.0 のファイル形式はXMLなので、外部モジュール BeautifulSoup を使い、第2引数に "xml" を指定することで、第1引数で渡した文字列をXMLとして解釈する。
RSS による配信情報の本体は、itemタグ(複数)に格納されているので、find_allメソッドを使い、itemタグをリストに格納、for文を使って1つ1つ解釈していく。
ここでは、itemタグの中から、タイトル、リンクURL、更新日時の3つをとりだし、変数 res に格納していく。itemの区切りは改行文字 \n とする。
個々のitemタグに対し findメソッドを使うことで、そこにぶら下がるタグを取り出すことができる。見つかったら、get_textメソッドによって、タグの内容を取り出す。このとき strip=True を指定すると、テキストの冒頭と末尾の空白を取り除くことができるのは、HTMLの時と同じである。
日付は dc:date というコロンをともなったタグになっており、少々特殊である。XMLでは名前空間をともかう表記方法なのだが、BeautifulSoup で解析するには、find_allメソッドを使う必要がある。

最後に例外処理を行い、例外がなければRSS情報を格納した変数 res の内容を返す。

RSS にはいくつかのバージョンがあるが(詳しくは「PHPで作るRSSビューア」参照)、ここでは、ぱふぅ家のホームページで配信している RSS 1.0 に限定して解釈ができるようにする。
RSS 1.0 のファイル形式はXMLなので、外部モジュール BeautifulSoup を使い、第2引数に "xml" を指定することで、第1引数で渡した文字列をXMLとして解釈する。
RSS による配信情報の本体は、itemタグ(複数)に格納されているので、find_allメソッドを使い、itemタグをリストに格納、for文を使って1つ1つ解釈していく。
ここでは、itemタグの中から、タイトル、リンクURL、更新日時の3つをとりだし、変数 res に格納していく。itemの区切りは改行文字 \n とする。
個々のitemタグに対し findメソッドを使うことで、そこにぶら下がるタグを取り出すことができる。見つかったら、get_textメソッドによって、タグの内容を取り出す。このとき strip=True を指定すると、テキストの冒頭と末尾の空白を取り除くことができるのは、HTMLの時と同じである。
日付は dc:date というコロンをともなったタグになっており、少々特殊である。XMLでは名前空間をともかう表記方法なのだが、BeautifulSoup で解析するには、find_allメソッドを使う必要がある。

最後に例外処理を行い、例外がなければRSS情報を格納した変数 res の内容を返す。
認証が必要なWebサイト
Webサイトによっては認証――ID・パスワード、ワンタイムパスワード、電子証明書などによる――が必要なことがある。また、IPアドレスやUSER AGENT、REFERERによるアクセス制限を設けているサイトがある。これらへの対応は初学者にとってハードルが高いことと、とくに制限を設けているサイトはプログラムによる自動アクセスを拒否している場合が多いので、このシリーズでは取り上げない。
requests.head を使ってアクセスしたときに 400番台の HTTPレスポンスコードが返ってきたサイトについては、認証が必要などの理由でアクセスすることができないと諦めてほしい。
requests.head を使ってアクセスしたときに 400番台の HTTPレスポンスコードが返ってきたサイトについては、認証が必要などの理由でアクセスすることができないと諦めてほしい。
練習問題
次回予告
今回学んだとおり、Python では比較的簡単にHTTP通信ができることから、クラウドサービスの利用も容易に実装できる。そこで次回は、郵便番号から住所を検索する、Wikipediaの見出しを検索する、入力したテキストを要約する――という3つのクラウドサービスを利用するプログラムを作ってみる。
今回同様、インターネットは信頼境界の外側にあることから、バリデーションなどを行いセキュリティ対策に留意する。クラウドサービスが悪意をもった第三者に乗っ取られていることがあるかもしれないので。
今回同様、インターネットは信頼境界の外側にあることから、バリデーションなどを行いセキュリティ対策に留意する。クラウドサービスが悪意をもった第三者に乗っ取られていることがあるかもしれないので。
コラム:HTTPサーバの負荷

HTTP通信でインターネット上の HTMLファイルを取得することは、たとえば Windowsファイル共有で共有フォルダにある HTTPファイルを取得するのとは通信手順が異なる。
この通信手順をプロトコルと呼び、その違いはOSやミドルウェアが吸収してくれるので、プログラム側からはそれほど気を使う必要はないのだが、HTTPサーバへの負荷はファイルサーバより高くなることが多いので、その点について補足説明する。
この通信手順をプロトコルと呼び、その違いはOSやミドルウェアが吸収してくれるので、プログラム側からはそれほど気を使う必要はないのだが、HTTPサーバへの負荷はファイルサーバより高くなることが多いので、その点について補足説明する。
Windowsファイル共有やプリンタ共有を行っているのは SMB(Server Message Block)と呼ばれるマイクロソフト社の独自プロトコルだ。これに対して HTTP(Hypertext Transfer Protocol)は WWW(World Wide Web)のために開発されたプロトコルで、RFCによって標準化されている。
両者の決定的な違いは、SMBがステートフルなのに対し、HTTPはステートレスであることだ。

コンピュータ間の通信(プロトコル)はキャッチボールに似ている。
まず、通信相手に接続開始の掛け声をかけ、相手は受信準備が整ったことを返答する。そしてデータやファイルのやり取りをはじめる。このときも、受信側は、ちゃんと受信できていることを逐一応答する。すべての通信が終わったら、通信を切断すると連絡し、相手が切断したことを確認する。この接続開始から切断までの一連の通信をセッションと呼ぶ。
この一連の作業がプロトコルである。ステートフルは、セッション中の通信状態をお互いが保持する。一方のステートレスは、データやファイルのやり取りは1回だけで、その都度セッションが切れる。ステートレスでは、複数のファイルの送受信を行うには接続と切断を繰り返し行う必要がある。

一見するとステートフルの方が優秀なプロトコルに見えるが、通信状態が悪いとセッションの維持が難しくなり、通信状態を保持していることがかえって足枷となってしまう。ステートレスであれば、通信状態が悪いときには再度セッションし直せば(再読み込みすれば)解決する。
インターネット黎明期には、いまほどネットワーク回線が安定していなかったので、HTTPプロトコルはステートレスの方が都合がよかったのである。

ステートレスでは接続と切断を繰り返すので、サーバの負荷が大きい。その後 WWWは進化し、ECサイトのように前ページで入力した商品の内容を記憶したりするために Cookieを使ってセッションを継続しているように見せかけるようになった。このCookieも、毎回送受信されるため、サーバへの負荷となっている。

HTTPサーバ側も負荷分散システムを導入するなどして対策は講じているものの、同じURLや同じサーバに対して連続的にHTTP通信することは、そのHTTPサーバに対して余計な負荷をかけることになりかねない。そこで本編では、連続して同じURLへアクセスしないよう、アクセス可能間隔かどうかをチェックするユーザー関数 isElapsedTime を通してからアクセスするようにした。
次回はクラウドサービスの利用方法を学ぶが、無償クラウドサービスではとくに、サーバに負荷がかかるような呼び出しは、IPアドレスを特定してブロックしてくることが多い。相手側のブラックリストに載ってしまわないよう、常識の範囲内でアクセスするよう心がけてほしい。
両者の決定的な違いは、SMBがステートフルなのに対し、HTTPはステートレスであることだ。

コンピュータ間の通信(プロトコル)はキャッチボールに似ている。
まず、通信相手に接続開始の掛け声をかけ、相手は受信準備が整ったことを返答する。そしてデータやファイルのやり取りをはじめる。このときも、受信側は、ちゃんと受信できていることを逐一応答する。すべての通信が終わったら、通信を切断すると連絡し、相手が切断したことを確認する。この接続開始から切断までの一連の通信をセッションと呼ぶ。
この一連の作業がプロトコルである。ステートフルは、セッション中の通信状態をお互いが保持する。一方のステートレスは、データやファイルのやり取りは1回だけで、その都度セッションが切れる。ステートレスでは、複数のファイルの送受信を行うには接続と切断を繰り返し行う必要がある。

一見するとステートフルの方が優秀なプロトコルに見えるが、通信状態が悪いとセッションの維持が難しくなり、通信状態を保持していることがかえって足枷となってしまう。ステートレスであれば、通信状態が悪いときには再度セッションし直せば(再読み込みすれば)解決する。
インターネット黎明期には、いまほどネットワーク回線が安定していなかったので、HTTPプロトコルはステートレスの方が都合がよかったのである。

ステートレスでは接続と切断を繰り返すので、サーバの負荷が大きい。その後 WWWは進化し、ECサイトのように前ページで入力した商品の内容を記憶したりするために Cookieを使ってセッションを継続しているように見せかけるようになった。このCookieも、毎回送受信されるため、サーバへの負荷となっている。

HTTPサーバ側も負荷分散システムを導入するなどして対策は講じているものの、同じURLや同じサーバに対して連続的にHTTP通信することは、そのHTTPサーバに対して余計な負荷をかけることになりかねない。そこで本編では、連続して同じURLへアクセスしないよう、アクセス可能間隔かどうかをチェックするユーザー関数 isElapsedTime を通してからアクセスするようにした。
次回はクラウドサービスの利用方法を学ぶが、無償クラウドサービスではとくに、サーバに負荷がかかるような呼び出しは、IPアドレスを特定してブロックしてくることが多い。相手側のブラックリストに載ってしまわないよう、常識の範囲内でアクセスするよう心がけてほしい。
コラム:キャッシュ・システム
インターネット上のコンテンツは、それほど頻繁に更新されるものではないことから、相手サーバへの負荷軽減を目的とし、コンテンツをローカルドライブにキャッシングし、ある一定時間の間、そのキャッシュを消さずに保持しておくことが考えられる。そして、同じURLへのリスクエストがあったら、インターネットにアクセスするのではなく、ローカルドライブにあるキャッシュを読み込む。
この機能を実現するためのキャッシ・システムをクラス・ファイル "pahooCache.py" として用意した。このクラス・ファイルと同じディレクトリに、TITLEタグを表示するサンプル・プログラム "getTitle2.py" を配置し、このサンプル・プログラムを実行してみてほしい。
この機能を実現するためのキャッシ・システムをクラス・ファイル "pahooCache.py" として用意した。このクラス・ファイルと同じディレクトリに、TITLEタグを表示するサンプル・プログラム "getTitle2.py" を配置し、このサンプル・プログラムを実行してみてほしい。
dispTitle2.py
# メイン・プログラム =======================================================
# 初期値
url = "https://www.pahoo.org/index.html" # 読み込むURL
targetTag = "title" # titleタグ
# キャッシュ保持時間(分)
lifeCache = 10
# キャッシュ・ディレクトリ
# ソースプログラムのあるディレクトリの下にサブディレクトリpcacheを作成
dirCache = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pcache\\")
# HTTP通信用キャッシュ・システムのインスタンス(10分間保持)
pc = pahooCache(lifeCache, dirCache)
try:
# URLのバリデーション
urlStr = pv.validateURL(url)
# キャッシュシステムを利用し,コンテンツを取得する
data = pc.load(url)
# コンテンツを自動デコード
htmlContents = str(from_bytes(data).best())
# TITLEタグの内容を取得する
tagStr = getTag(htmlContents, targetTag)
# コンソールに表示する
print(f"{tagStr} {urlStr}")
# 失敗したら例外を返す.
except ValueError as e:
pv.dispErrorMessageAndExit(str(e))
ユーザー定義クラス pahooCache は、PHPで作ったキャッシュ・システムを移植したものなので、その仕組みは「解説:キャッシュ・システム」をご覧いただきたい。ここでは、pahooCacheクラス の使い方を説明する。

まず、キャッシュの保持時間(分)を変数 lifeCache に、キャッシュファイルを保存するディレクトリを変数 dirCache に代入する。ここでは、ソース・プログラム "getTitle2.py" のあるディレクトリの下にサブディレクトリ "pcache" を作成し、それをキャッシュ・ディレクトリとする。
次に、pahooCacheクラスのインスタンスを生成し、変数 pc に代入する。インスタンス生成時の引数は、前述の lifeCache と dirCache である。この引数は省略可能で、省略時には "pahooCache.py" にあるデフォルト値を用いる。
1つのインスタンスで複数のURLをキャッシュ化できる。ただし、1つのインスタンスの中では、キャッシュ保持時間とキャッシュディレクトリは共通である。別のキャッシュディレクトリを使いたい場合は、あらたに別のインスタンスを生成してほしい。

メイン・プログラムでは、isURLexists関数は実行せず(HTTPサーバにアクセスしてしまうため)、isElapsedTime関数の代わりに pahooCache.loadメソッドを用いる。
pahooCache.loadメソッドは指定URLのコンテンツを読み込んで、その内容をバイナリ型で返す。初回呼び出し時には GETプロトコルでアクセスし、その内容を dirCache のディレクトリにキャッシュファイルとして保存し、その内容を返す。2回目以降の呼び出しでは、前回呼び出し時から lifeCache分以内なら、キャッシュの内容を読み出して返す。lifeCache分を超えたら、再びGETプロトコルでアクセスし、その内容を dirCache のディレクトリにキャッシュファイルとして保存し、その内容を返す。
pahooCache.loadメソッドは、HTMLファイル以外の画像ファイルなどもキャッシュできるよう、バイナリ型(bytes)でコンテンツを返す。受け取った側は、外部ライブラリ charset_normalizerを使い、HTMLコンテンツを自動デコードして文字列型(str)に変換する。

pahooCacheクラス は "getRSS1.py" にも適用できる。各自で取り組んでほしい。

なお、今回は使用していないが、pahooCache.deleteメソッドを使うと、すべてのキャッシュの内容を削除することができる。読み込み内容がおかしくなったり、アプリの初期化を行うときに利用してほしい。

まず、キャッシュの保持時間(分)を変数 lifeCache に、キャッシュファイルを保存するディレクトリを変数 dirCache に代入する。ここでは、ソース・プログラム "getTitle2.py" のあるディレクトリの下にサブディレクトリ "pcache" を作成し、それをキャッシュ・ディレクトリとする。
次に、pahooCacheクラスのインスタンスを生成し、変数 pc に代入する。インスタンス生成時の引数は、前述の lifeCache と dirCache である。この引数は省略可能で、省略時には "pahooCache.py" にあるデフォルト値を用いる。
1つのインスタンスで複数のURLをキャッシュ化できる。ただし、1つのインスタンスの中では、キャッシュ保持時間とキャッシュディレクトリは共通である。別のキャッシュディレクトリを使いたい場合は、あらたに別のインスタンスを生成してほしい。

メイン・プログラムでは、isURLexists関数は実行せず(HTTPサーバにアクセスしてしまうため)、isElapsedTime関数の代わりに pahooCache.loadメソッドを用いる。
pahooCache.loadメソッドは指定URLのコンテンツを読み込んで、その内容をバイナリ型で返す。初回呼び出し時には GETプロトコルでアクセスし、その内容を dirCache のディレクトリにキャッシュファイルとして保存し、その内容を返す。2回目以降の呼び出しでは、前回呼び出し時から lifeCache分以内なら、キャッシュの内容を読み出して返す。lifeCache分を超えたら、再びGETプロトコルでアクセスし、その内容を dirCache のディレクトリにキャッシュファイルとして保存し、その内容を返す。
pahooCache.loadメソッドは、HTMLファイル以外の画像ファイルなどもキャッシュできるよう、バイナリ型(bytes)でコンテンツを返す。受け取った側は、外部ライブラリ charset_normalizerを使い、HTMLコンテンツを自動デコードして文字列型(str)に変換する。

pahooCacheクラス は "getRSS1.py" にも適用できる。各自で取り組んでほしい。

なお、今回は使用していないが、pahooCache.deleteメソッドを使うと、すべてのキャッシュの内容を削除することができる。読み込み内容がおかしくなったり、アプリの初期化を行うときに利用してほしい。
(この項おわり)