7.7 クラウドサービスを利用する

(1/1)
クラウドコンピューティングのイラスト
前回学んだとおり、Python では比較的簡単にHTTP通信ができることから、クラウドサービスの利用も容易に実装できる。そこで今回は、郵便番号から住所を検索する、Wikipediaの見出し語を検索する、入力したテキストを要約する――という3つのクラウドサービスを利用するプログラムを作ってみる。
前回同様、インターネットは信頼境界の外側にあることから、バリデーションなどを行いセキュリティ対策に留意する。クラウドサービスが悪意をもった第三者に乗っ取られていることがあるかもしれないので。

目次

サンプル・プログラム

ZIP SEARCH API SERVICE 「JIS X0401」対応版

ZIP SEARCH API SERVICE 「JIS X0401」対応版」は、入力パラメータ(IN)として GETを、出力結果(OUT)がJSON形式で戻るというクラウドサービスである。
利用にあたっては、このクラウドサービスの 利用規約を遵守すること。
WebAPIのURL
URL
https://api.thni.net/jzip/X0401/JSONP/{1}/{2}.js

入力パラメータ
フィールド名 要否 内  容
1 必須 郵便番号(上3桁)
2 必須 郵便番号(下4桁)
応答データ構造(json) state 都道府県コード stateName 都道府県名 city 市区町村名 street 町域名

郵便番号から住所を検索する

プログラム "zip2address.py" を実行してみてほしい。キーボードから郵便番号を入力すると、前述のクラウドサービス「ZIP SEARCH API SERVICE 「JIS X0401」対応版」を利用し、対応する住所をコンソールに表示する。
# メイン・プログラム =======================================================
# 初期値
elapsedTime = 60		# アクセス可能間隔(秒);相手先サイトに負荷をかけないよう
elapsedFile = "./zip2addresse.txt"		# 経過時刻格納ファイル名

# 郵便番号を入力する
zipCode = input("郵便番号(XXX-XXXX)=")

try:
	# アクセス可能間隔のチェック
	if not pv.isElapsedTime(elapsedTime, elapsedFile):
		pv.dispErrorMessageAndExit(f"前回アクセスから {elapsedTime}秒以上の間隔を空けて実行してください")
メイン・プログラムでは、まず、input関数を使ってキーボードから郵便番号を入力する。
次に、「7.6 HTTP通信」でつくったアクセス可能感覚チェック関数 isElapsedTime を通す。
郵便番号から住所を求めるのは、後述するユーザー関数 zip2address で、この関数はタプル型で、都道府県名、市区町村名、町域の3つ組のデータを返すので、3つの変数に代入し、それをコンソールに表示する。
最後に例外処理を書く。
def zip2address(zipCode):
	"""郵便番号から住所を求める.
	
	Args:
		zipCode(str): 郵便番号 XXX-XXXX
	
	Returns:
		tuple: 都道府県, 市区町村, 町域
	
	Raises:
		エラー表示してプログラム終了
	
	Note:
		「ZIP SEARCH API SERVICE 「JIS X0401」対応版をコールする.
		http://project.iw3.org/zip_search_x0401/
	"""
	# 郵便番号パターン(正規表現)
	RE_ZIP = r"([0-9]{3})\-([0-9]{4})"
	# 応答JSONデータのキー
	KEY_JSON = ("stateName", "city", "street")
	# 住所パターン(正規表現)
	RE_ADDRESS = r"^[\u3005\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\uFF01-\uFF5E0-9a-zA-Z]+$"

	try:
		# 郵便番号のバリデーション
		zipStr = pv.validateString(zipCode, [ RE_ZIP ], True)
		# 郵便番号を分解する
		match = re.match(RE_ZIP, zipStr)
		# リクエストURL
		url = f"https://api.thni.net/jzip/X0401/JSON/{match.group(1)}/{match.group(2)}.js"
		# HTTP GET通信を使って応答JSONデータを取得する
		jsonData = httpGet(url)
		# JSONデータを辞書に代入する
		dic = json.loads(jsonData)

		# JSONデータのバリデーション
		for key in KEY_JSON:
			if not dic[key]:
				raise ValueError(f"応答データに {key} がありません")
			pv.validateString(dic[key], [ RE_ADDRESS ], True)

	# バリデーションエラー
	except ValueError as errmsg:
		raise ValueError(f"郵便番号 {zipCode} が不正です")
	# その他の例外処理
	except Exception as errmsg:
		raise Exception(errmsg)

	# タプルで返す
	return (dic["stateName"], dic["city"], dic["street"])
ユーザー関数 zip2address は、クラウドサービス「ZIP SEARCH API SERVICE 「JIS X0401」対応版」を呼び出す処理を行うのだが、その前に、引数のバリデーションを行う。信頼境界の外にデータを渡すので、こちらから異常なデータを出して相手に迷惑をかけるわけにはいかない。きちんとバリデーションしておこう
ここでは validateString関数 を使って、郵便番号が所定の書式 数字3桁-数字4桁 に従っているかどうかをチェックする。ここで使う正規表現は ([0-9]{3})\-([0-9]{4}) だ。[0-9] は0から9の半角数字にマッチし、続くブレースで囲んだ {3} は [0-9] が3つ続くことを意味する。そして、エスケープシーケンスでハイフン \- を挟んで、[0-9] が4つ続くパターンにマッチさせる。
余談になるが、ブレースを使って [0-9]{3, 5} と書くと、3桁以上4桁以下の半角数字にマッチする。

このバリデーションを通過しても存在しない郵便番号はある。たとえば 100-0099 は存在しない。しかし、存在しない郵便番号をバリデーションする方法はないので、ここでは、数字3桁-数字4桁 という書式を満たせば、クラウドサービスにデータを渡すことにする。
そこで、match を使って、郵便番号をハイフンの前後に分解し、フォーマット文字列を使って呼び出すURLを生成する。

クラウドサービスの呼び出しは、前回作ったGETメソッドでHTTP通信する httpGet関数を使う。クラウドサービスはJSON形式データを戻すので、これを json.load を使って辞書型データとして変数 dic に格納する。
戻り値のJSONキーは、あらかじめ変数 KEY_JSON に代入した3つ組で、これを辞書 dic から順次とりだし、キーがあるかどうかをチェックする。なければ存在しない郵便番号を渡した変換エラーとして、例外を発生させる。
次に、受け取った住所データのバリデーションを行う。ここでは、全角日本語文字及び半角英数字を表す正規表現を代入した変数 RE_ADDRESS にマッチするかどうかでバリデーションを行う。クラウドサービスからデータを受け取るとき、再び信頼境界をまたぐので、念のためバリデーションを行う。
最後に例外処理を書き、例外がなければタプルで3つ組みデータを返す。

クラウドサービスの多くは、
  1. こちらからパラメータを渡す
  2. クラウドサービスの応答を受け取る
という2段階の流れになる。それぞれ信頼境界をまたぐことになるので、最低でも2回のバリデーションを行うことになる

Wikipedia API

Wikipedia API は、入力パラメータ(IN)はGET渡しで、出力結果(OUT)はXMLやJSONで返ってくるという無償のクラウドサービスである。
WebAPIのURL
URL
https://ja.wikipedia.org/w/api.php

入力パラメータ
フィールド名 要否 内  容
format 省略可 xml, json, yaml等
action 必須 操作:ここではquery
prop 省略可 action固有のパラメータ。記事の各構成要素を取得する。ここではextractsを指定し、サマリを抽出する。
explaintext 省略可 出力をHTMLではなくプレーンテキストにする。
redirects 省略可 リダイレクト記事を含める。
titles 必須 見出し検索語。
応答データ(xml) api query pages page extract サマリー

Wikipedia検索

プログラム "wikisearch.py" を実行してみてほしい。キーボードから入力した見出し語をクラウドサービス「Wikipedia API」に渡し、マッチするWikipedia記事があれば、その概要をコンソールに表示する。
# メイン・プログラム =======================================================
# 初期値
elapsedTime = 60		# アクセス可能間隔(秒);相手先サイトに負荷をかけないよう
elapsedFile = "./wikisearch.txt"		# 経過時刻格納ファイル名

# 見出し語を入力する
title = input("検索したい見出し語 = ")

try:
	# アクセス可能間隔のチェック
	if not pv.isElapsedTime(elapsedTime, elapsedFile):
		pv.dispErrorMessageAndExit(f"前回アクセスから {elapsedTime}秒以上の間隔を空けて実行してください")
メイン・プログラムの流れは "zip2address" とほぼ同じだ。クラウドサービス「Wikipedia API」を呼び出すのは、後述するユーザー関数 wikisearch である。
def wikisearch(title, redirects=1):
	"""Wikipedia検索
	
	Args:
		title(str)    : 検索する見出し語
		redirects(int): リダイレクト記事を含めるかどうか
						1:含める(デフォルト),0:含めない
	
	Returns:
		str: ヒットした記事内容
	
	Raises:
		エラー表示してプログラム終了
	
	Note:
		MediaWiki APIをコールする.
		https://ja.wikipedia.org/w/api.php
	"""

	# 印字可能な日本語文字パターン(正規表現)
	RE_PRINTABLE = r"^[\u3005\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\uFF01-\uFF5E\x20-\x7E]+$"

	try:
		# 見出し語のバリデーション
		queryStr = pv.validateString(title, [ RE_PRINTABLE ], True)
		# 引数 redirectsのバリデーション
		redirectsInt = pv.validateNumber(redirects, "int", 0, 1, label="リダイレクト記事を含めるかどうか")
		# リクエストURL
		url = f"https://ja.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro&explaintext&redirects={redirects}&titles=" + urllib.parse.quote(queryStr)

		# HTTP GET通信を使って応答JSONデータを取得する
		jsonData = httpGet(url)
		# JSONデータを辞書に代入する
		dic = json.loads(jsonData)
		# 結果を1つずつ取り出す
		res = ""
		for pageID in dic["query"]["pages"]:
			# HTML特殊文字をデコードする
			titleStr = html.unescape(dic["query"]["pages"][pageID]["title"])
			contents = html.unescape(dic["query"]["pages"][pageID]["extract"])
			# 戻り値に追加する
			res += f"*{title}\n{contents}\n"

		return res

	# 例外処理
	except Exception as errmsg:
		raise Exception(errmsg)
ユーザー関数 wikisearchzip2address に似ているが、見出し語などのパラメータを、GETメソッドに備わっているパラメータ渡し手順で行う。具体的には、URLの後のクエスションマークに続けてパラメータを書く。パラメータの間はアンパサンド & で区切る。たとえば ?format=json&action=query は、formatにjsonを、actionにqueryを代入して渡すことを意味する。

前述の通り、クラウドサービス「Wikipedia API」では様々なパラメータを指定できるが、wikisearch関数では、引数として「見出し語」「リダイレクト記事を含めるかどうか」の2つだけを指定できるようにして、その他のパラメータは固定にした。
この2つの引数に対してバリデーションを行うのは、これまでと同じ。

見出し語はUTF-8文字列を URLエンコードして渡す仕様なので、Pythonの urllib.parse モジュールをimportし、urllib.parse.quote を使ってURLエンコードする。

zip2address関数と同様、クラウドサービスの呼び出しは httpGet関数を使い、応答として受け取るJSON形式データを json.load を使って辞書型データとして変数 dic に格納する。
検索結果として複数ページがヒットする可能性があるので、for文を使って1つの変数 res に格納する。
検索結果には、たとえばアンパサンド &&& のように標記する HTML特殊文字が混じっているかもしれないので、これを通常文字に変換する。Pythonの hmtlを importし、html.unescape を使って変換してやる。

A3RT:Text Summarization API

Text Summarization API - A3RT」は、入力パラメータ(IN)としてデータをPOSTで渡し、出力結果(OUT)が JSONで戻るというクラウドサービスである。
A3RT:Text Summarization API」を利用するには、事前に APIキー を入手する必要がある。API発行から、メールアドレスを登録するだけで APIキー が入手できる。
取得した APIキー は、変数 APIKEY_A3RT に代入しておく。
WebAPIのURL
URL
https://api.a3rt.recruit.co.jp/text_summarization/v1

入力パラメータ
フィールド名 要否 内  容
apikey 必須 APIキー
入手手順は前述の通り。無料。
sentences 必須 要約する文章
UTF-8エンコード
linenumber 任意 抽出文章数
1以上の整数で、入力した文章数より少ない数。
デフォルトは1
separation 任意 文章の切れ目文字
UTF-8エンコード
デフォルトは”。”
※要約できる1文の最大文字数は200文字、かつ最大文章数は10。
応答データ構造(xml) api query pages page extract サマリー

テキスト要約

プログラム "textsummary.py" は、クラウドサービス「A3RT:Text Summarization API」を利用して入力したテキストを要約するプログラムだが、これまでの2つのプログラムと異なり、GUIとして、「1.4 ブラウザを使ったGUI」で紹介した pywebview を利用する。あらかじめ pywebview をインストールしておいてほしい。また、HTML部分を独立したファイル "textsummary.html" にしており、Pythonプログラム "textsummary.py" と同じディレクトリに配置すること。
# メイン・プログラム =======================================================
# 初期値
elapsedTime = 60		# アクセス可能間隔(秒);相手先サイトに負荷をかけないよう
elapsedFile = "./textsummary.txt"	# 経過時刻格納ファイル名
WIDTH  = 620						# webviewウィンドウの幅(ピクセル)
HEIGHT = 750						# webviewウィンドウの高さ(ピクセル)
HTML_FILE = "./textsummary.html"	# HTMLファイル名(webview部分)

# A3RTのAPIKEY;各自が取得したキーを代入すること
APIKEY_A3RT = "********************************"

# pywebviewを使ってウィンドウを表示する.
api = Api()
window = webview.create_window("テキスト要約 by Python", HTML_FILE, js_api=api, width=WIDTH, height=HEIGHT)
webview.start(http_server=False, debug=True)
メイン・プログラムの流れは、「6.3 pywebviewを使ったGUI」で学んだとおりだ。
class Api:
	def textSummary(self, text, sentence=1):
		"""テキストを要約する.
		
		Args:
			text(str)     : 要約したいテキスト
			sentence(int) : 要約センテンス数(句点の数);省略時 1
		
		Returns:
			json: { text: 要約テキスト, message: エラーメッセージ }
		
		"""
		# 印字可能な日本語文字パターン(正規表現)
		RE_PRINTABLE = r"^[\p{Hiragana}\p{Katakana}\p{Han}\p{Latin}\p{Number}\p{Punctuation}\p{Symbol}\p{Space}\u3000-\u303F\uFF00-\uFFEF\u3041-\u309F\u30A1-\u30FF]+$"

		# 要約処理
		try:
			# 入力テキストのバリデーション
			textStr = pv.validateString(text, [ RE_PRINTABLE ], True)
			# アクセス可能間隔のチェック
			if not pv.isElapsedTime(elapsedTime, elapsedFile):
				destStr = ""
				message = f"前回アクセスから {elapsedTime}秒以上の間隔を空けて実行してください"
			else:
				dest = textSummary(textStr, sentence, APIKEY_A3RT)
				# 応答テキストのバリデーション
				destStr = pv.validateString(dest, [ RE_PRINTABLE ], True)
				message = ""
		# 例外処理
		except ValueError as errmsg:
			destStr = ""
			message = errmsg

		response = { "text": destStr, "message": str(message) }
		return json.dumps(response)
クラス Api も、「6.3 pywebviewを使ったGUI」で学んだとおりだ。今回、Python側でGUIの初期化を指示する必要がない("textsummary.html" に記述したJavaScriptだけで完結する)ことから、テキストを要約するメソッド textSummary がメインになる。

入力バリデーションでは、要約テキストが印字可能な日本語文字パターンであるかどうかチェックする。全角・半角、英数字・記号などを全て含むパターンを表現したいので、バリデーション・モジュール "pahooValidate.py" で import する正規表現ライブラリを、標準ライブラリ re から、Unicodeサポートを強化した外部ライブラリ regex に置換した。あらかじめ、pipコマンドを使って regex をインストールしておいてほしい。
pip install regex
変数 RE_PRINTABLE に代入した正規表現の意味は下表の通りだ。
パターン意味
\p{Hiragana}ひらがなにマッチする。
\p{Katakana}カタカナ(全角・半角)にマッチする。
\p{Han}半角文字にマッチする。
\p{Latin}ラテン文字(アルファベットと拡張、アクセント付きなど)にマッチする。
\p{Number}アラビア数字(全角・半角)にマッチする。
\p{Punctuation}句読点文字(全角・半角)にマッチする。
\p{Symbol}\記号(全角・半角)にマッチする。
\p{Space}空白(全角・半角)にマッチする。
\u3000-\u303FCJK記号・符号にマッチする。
\uFF00-\uFFEF半角・全角英数字、記号にマッチする。
\u3041-\u309Fひらがなにマッチする。
\u30A1-\u30FFカタカナにマッチする。
def textSummary(text, sentence, apiKey):
	"""テキスト要約
	
	Args:
		text(str)     : 要約したいテキスト
		sentence(int) : 要約センテンス数(句点の数)
		apiKey(str)   : APIキー
	
	Returns:
		str: 要約テキスト
	
	Raises:
		エラー表示してプログラム終了
	
	Note:
		A3RT:Text Summarization API をコールする.
		https://a3rt.recruit.co.jp/product/TextSummarizationAPI/
	"""

	try:
		# テキストの末尾に句点がなければ追加する
		if not text.endswith("。"):
			text += "。"

		# センテンス数のバリデーション
		n = text.count("。") - 1
		if n <= 1:
			n = 1
		ratioFloat = pv.validateNumber(sentence, "int", 1, n, label="センテンス数")

		data = {
			"apikey"		: apiKey,
			"sentences"		: text,
			"linenumber"	: sentence,
			"separation"	: "。"
		}

		# リクエストURL
		url = "https://api.a3rt.recruit.co.jp/text_summarization/v1"
		response = requests.post(url, data=data)

		if response.status_code != 200:
			raise Exception("クラウドサービスで異常が発生しました")
テキスト要約のクラウドサービス「A3RT:Text Summarization API」を呼び出すのはユーザー関数 textSummary である。引数として、要約したいテキスト、要約するときのセンテンス数(句点の数)、APIキーの3つを渡す。センテンス数が少なければ少ないほど圧縮した要約になるが、あまり少なすぎると、意味がわからなくなるだろう。

念のため、要約したいテキストの末尾に句点がなければ、endwithメソッドを追加しておく。
次に、要約するときのセンテンス数(句点の数)が、要約したいテキストに含まれている句点の下図と同じか、それより多くないかどうかをバリデーションする。

クラウドサービス「A3RT:Text Summarization API」はPOSTプロトコルを使うので、これまでとパラメータの渡し方が少し異なる。
まず、渡すパラメータを辞書型変数 data に格納する。キーは、クラウドサービスの仕様の通りだ。
POSTプロトコルは request.postメソッドを用いる。戻り値は HTTPステータスコードになるので、GETプロトコルの時と同様、200以外はエラーとして例外を発生する。

応答として返ってくるJSON形式データは、要約文がバラバラに入っているので、念のため stripメソッドで先頭・末尾の空白文字を除去してから結合して、句点を付加する。

最後に例外処理を記述する。

参考サイト

練習問題

次回予告

Python は、ファイル入出力やHTTP通信、クラウドサービスの利用だけでなく、PC付属デバイスからの入出力手段も用意されている。そこで次回は、PC内蔵カメラを使って書籍のバーコードを読み取り、Amazon商品サイトのURLを求めるプログラムを作ってみる。

コラム:マッシュアップ

木工工作をする人のイラスト(男性)
インターネット時代になり、クラウドサービスが登場してから、私たちの生活に大きな恩恵をもたらしていると思う。今回つくった個人利用はもちろん、大企業も、自社にインフラを置かずにセキュリティ対策のしがらみをアウトソースする目的でクラウドサービスを積極的に活用している。

さて、プログラミングというものは「無いものは作る」という精神が根底にある。
クラウドサービスも同様で、もし求めるようなサービスが見つからないことがある。
たとえば、出先で電源やWi-Fiのある喫茶店を探したいとする――10年前は、適当なサービスがなかった。そこで、「Googleマップサービス」と「モバイラーズオアシスAPI」という2つのクラウドサービスを組み合わせ、「PHPで電源・WiFi利用可能店舗を検索する」Webアプリを作った。
このように複数のクラウドサービスを組み合わせて目的のアプリを作ることをマッシュアップ(Mashup)と呼ぶ。もともとは音楽制作用語だった。

クラウドサービスの多くは、GETメソッドまたはPOSTメソッドで呼び出し、JSONまたはXML形式の応答データが返ってくるパターンである。本編で見たように、Pythonは比較的簡単に――場合によってはJavaScriptやPHPより短いコード量で――目的のアプリケーションを書くことができる。
クラウドサービスやマッシュアップに興味がある方は、当サイトの「PHPでクラウド連携」をご覧いただきたい。PHPを使って多くのクラウドサービスを利用したサンプル・プログラムを公開している。これらを Python に移植してみて欲しい。
(この項おわり)
header