Python + Zeep で SOAP API クライアントを作ってみたため、その時のメモを残します。
目次
環境
なお、WSDLがあるSOAP APIを使うことを想定しています。
WSDLのバージョンなどはWikipediaが参考になりました。
Web Services Description Language - Wikipedia
Zeepとは
公式ドキュメントによると
A fast and modern Python SOAP client Highlights:
- Compatible with Python 2.7, 3.3, 3.4, 3.5, 3.6 and PyPy
- Build on top of lxml and requests
- Support for Soap 1.1, Soap 1.2 and HTTP bindings
- Support for WS-Addressing headers
- Support for WSSE (UserNameToken / x.509 signing)
- Support for tornado async transport via gen.coroutine (Python 2.7+)
- Support for asyncio via aiohttp (Python 3.5+)
- Experimental support for XOP messages
とのことで、いい感じのSOAP API クライアントが作れそうでした。
使えそうな SOAP APIを探す
SOAP API サーバを自作することも考えたのですが、ゼロから作るには先が長くなりそうでした。
そのため、使えそうな SOAP Web API を探してみました。
有名なところではFlickrのAPIがありました。
https://www.flickr.com/services/api/
ただ、WSDLがなかったため、今回は見送りました。
他のAPIを探したところ、以下がありました。
今回はWSDLを使ってリクエスト/レスポンスができればよいため、デ辞蔵Webサービス - SOAP版API を使います。
mzeepオプションにて、WSDLを解析する
WSDLはXMLで書かれていますが、開発するために読み解くのは手間がかかります。
Zeepでは、 python -mzeep <WSDLのパス>
で、WSDLファイルを解析できます。
今回のWSDLファイルは上記サイトに記載されていたため、ダウンロードして実行します。
$ python -mzeep SoapServiceV11.xml > wsdl.txt
wsdl.txtを開くと、
Prefixes: xsd: http://www.w3.org/2001/XMLSchema ... Global elements: ns0:ArrayOfDicInfo(ns0:ArrayOfDicInfo) ... Global types: xsd:anyType ns0:ArrayOfDicInfo(DicInfo: ns0:DicInfo[]) ... Bindings: Soap11Binding: {http://MyDictionary.jp/SOAPServiceV11}SOAPServiceV11Soap ... Service: SOAPServiceV11 Port: SOAPServiceV11Soap (Soap11Binding: {http://MyDictionary.jp/SOAPServiceV11}SOAPServiceV11Soap) Operations: GetDicItem(AuthTicket: xsd:string, DicID: ns1:guid, ItemID: xsd:string, LocID: xsd:string, ContentProfile: ns0:ContentProfile, QueryListForHighLight: ns0:ArrayOfQuery) -> GetDicItemResult: ns0:DicItem ...
と、各種情報が分かりやすくなりました。
型情報を見やすくする
mzeepオプションでだいぶ分かりやすくなりました。
ただ、型情報が1行で表示されるため、複数の項目を持つ型の場合、見づらいことに気づきました。
そのため、
import pathlib read_file = pathlib.Path('./wsdl.txt') with read_file.open(mode='r') as r: f = r.read() formatted = f.split(',') write_file = pathlib.Path('./formatted.txt') with write_file.open(mode='w') as w: for f in formatted: w.write(f'{f.strip()}\n')
のようなスクリプトを作成し、フォーマットしてみました。
実行前
ns0:DicInfo(DicID: ns1:guid, FullName: xsd:string, ShortName: xsd:string, Publisher: xsd:string, Abbrev: xsd:string, StartItemID: xsd:string, ScopeList: ns0:ArrayOfScope, SearchOptionList: ns0:ArrayOfSearchOption, DefSearchOptionIndex: xsd:int, ItemMapList: ns0:ArrayOfString)
実行後
ns0:DicInfo(DicID: ns1:guid FullName: xsd:string ShortName: xsd:string Publisher: xsd:string Abbrev: xsd:string StartItemID: xsd:string ScopeList: ns0:ArrayOfScope SearchOptionList: ns0:ArrayOfSearchOption DefSearchOptionIndex: xsd:int ItemMapList: ns0:ArrayOfString)
いろいろと手を加えたいところですが、今はこのくらいで十分なので、良しとします。
ZeepでAPIクライアントを作ってみる
辞書検索サービスの使い方にあるように、まずは
GetDicListで呼び出し可能な辞書の一覧を取得
をZeepを使って作ってみます。
clientの生成
公式ドキュメントの「A simple use-case」に従い、今回のSOAPクライアントを生成してみます。
https://python-zeep.readthedocs.io/en/master/#a-simple-use-case
import pathlib from zeep import Client WSDL = pathlib.Path.cwd().joinpath('SoapServiceV11.xml') client = Client(str(WSDL))
SOAP APIのメソッドを呼び出す (client.get_type()を使う)
クライアントができたため、次は SOAP API のメソッドを呼び出してみます。
今回使う GetDicList
メソッドでは、引数 AuthTicket
を持つことが分かっています。
そのため、引数を渡すような実装方法を見たところ、公式ドキュメントの「Creating objects - Datastructures」に記載がありました。
https://python-zeep.readthedocs.io/en/master/datastructures.html#creating-objects
引数 AuthTicket
の型は xsd:string
ですので、 client.get_type('xsd:string')
で型用のオブジェクトを生成しておきます。
xsd_string = client.get_type('xsd:string')
あとは、Zeepが client.service
にSOAP APIのメソッドをいい感じに生成・実行してくれます。
response = client.service.GetDicList(AuthTicket=xsd_string('')) print(response)
ここまでのコード全体は以下です。
import pathlib from zeep import Client WSDL = pathlib.Path.cwd().joinpath('SoapServiceV11.xml') client = Client(str(WSDL)) xsd_string = client.get_type('xsd:string') response = client.service.GetDicList(AuthTicket=xsd_string('')) print(response)
実行結果です。いい感じの結果が返ってきました。
$ python run_GetDicList.py [{ 'DicID': 'xxxx', 'FullName': 'Edict和英辞典', 'ShortName': 'Edict和英辞典', ...
SOAP APIのメソッドを呼び出す (client.get_type()を使わない)
上記で実装ができていましたが、型オブジェクトを生成するために
xsd_string = client.get_type('xsd:string')
としているのが手間でした。
よく見ると、公式ドキュメントの「Creating objects - Datastructures」には続きがあり、
However instead of creating an object from a type defined in the XSD you can also pass in a dictionary. Zeep will automatically convert this dict to the required object (and nested child objects) during the call.
とのことでした。
そのため、先ほどのコードは
client = Client(str(WSDL)) response = client.service.GetDicList(AuthTicket='') print(response)
でもOKです。
実行結果も同じでした。
$ python run_GetDicList.py [{ 'DicID': 'xxxx', 'FullName': 'Edict和英辞典', 'ShortName': 'Edict和英辞典', ...
引数の型が複雑な SOAP API を呼び出す (get_type()を使う)
先ほどの GetDicList
APIは、型が xsd:string
と単純なものでした。
引数の型が複雑なAPIを探したところ、 SearchDicItem
がありました。
そこで、まずは get_type()
を使う書き方で実装してみます。
import pathlib from zeep import Client WSDL = pathlib.Path.cwd().joinpath('SoapServiceV11.xml') def get_guid_list_from_api(): client = Client(str(WSDL)) response = client.service.GetDicList(AuthTicket='') return [response[0]['DicID'], response[1]['DicID']] def call_api_with_get_type(): def create_query(word): ns0_merge_option = client.get_type('ns0:MergeOption') ns0_match_option = client.get_type('ns0:MatchOption') query = client.get_type('ns0:Query')( Words=xsd_string(word), ScopeID=xsd_string('HEADWORD'), MatchOption=ns0_match_option('EXACT'), MergeOption=ns0_merge_option('OR') ) return query client = Client(str(WSDL)) xsd_string = client.get_type('xsd:string') xsd_unsigned_int = client.get_type('xsd:unsignedInt') ns1_guid = client.get_type('ns1:guid') guid_list = get_guid_list_from_api() guids = client.get_type('ns0:ArrayOfGuid')([ ns1_guid(guid_list[0]), ns1_guid(guid_list[1]), ]) queries = client.get_type('ns0:ArrayOfQuery')([ create_query('apple'), create_query('america'), ]) response = client.service.SearchDicItem( AuthTicket=xsd_string(''), DicIDList=guids, QueryList=queries, SortOrderID=xsd_string(''), ItemStartIndex=xsd_unsigned_int('0'), ItemCount=xsd_unsigned_int('2'), CompleteItemCount=xsd_unsigned_int('2'), ) for r in response['ItemList']['DicItem']: print(r['Title']['_value_1'].text) print(dir(r['Title']['_value_1'])) print('=' * 5)
ここで、ネスト & 配列を持つ引数 QueryList
について取り上げてみます。
まず、配列の要素である ns0:Query
型のオブジェクトを作ります。
def create_query(word): ns0_merge_option = client.get_type('ns0:MergeOption') ns0_match_option = client.get_type('ns0:MatchOption') query = client.get_type('ns0:Query')( Words=xsd_string(word), ScopeID=xsd_string('HEADWORD'), MatchOption=ns0_match_option('EXACT'), MergeOption=ns0_merge_option('OR') ) return query
次に、配列となる ns0:ArrayOfQuery
を生成します。
queries = client.get_type('ns0:ArrayOfQuery')([ create_query('apple'), create_query('america'), ])
response = client.service.SearchDicItem( QueryList=queries, ... )
実行結果は以下の通りです。
レスポンスが返ってきているので、WSDLでのやり取りは成功しているんだろうな程度にしておき、深入りはしないようにします。
$ python run_SearchDicItem.py apple ['__bool__', '__class__', '__contains__', ...] ===== America ...
引数の型が複雑な SOAP API を呼び出す (get_type()を使わない)
上記の通り、 client.get_type()
を使って実現できましたが、それぞれの型を get_type()
で生成しておかなければならないため、いろいろと手間に感じました。
そこで、先ほどと同じように client.get_type()
を使わないやり方で実装してみます。
# importや get_guid_list_from_api() は省略 def call_api_without_get_type(): def create_query(word): return { 'Words': word, 'ScopeID': 'HEADWORD', 'MatchOption': 'EXACT', 'MergeOption': 'OR', } client = Client(str(WSDL)) guids = {'guid': get_guid_list_from_api()} queries = { 'Query': [ create_query('apple'), create_query('america'), ] } response = client.service.SearchDicItem( AuthTicket='', DicIDList=guids, QueryList=queries, SortOrderID='', ItemStartIndex=0, ItemCount=2, CompleteItemCount=2, ) for r in response['ItemList']['DicItem']: print(r['Title']['_value_1'].text) print(dir(r['Title']['_value_1'])) print('=' * 5)
では、複雑な型を持つ引数 QueryList
について、 get_type()
を使う場合との差分を見ていきます。
def create_query(word): ns0_merge_option = client.get_type('ns0:MergeOption') ns0_match_option = client.get_type('ns0:MatchOption') query = client.get_type('ns0:Query')( Words=xsd_string(word), ScopeID=xsd_string('HEADWORD'), MatchOption=ns0_match_option('EXACT'), MergeOption=ns0_merge_option('OR') ) return query queries = client.get_type('ns0:ArrayOfQuery')([ create_query('apple'), create_query('america'), ])
が
def create_query(word): return { 'Words': word, 'ScopeID': 'HEADWORD', 'MatchOption': 'EXACT', 'MergeOption': 'OR', } queries = { 'Query': [ create_query('apple'), create_query('america'), ] }
となりました。
ポイントは
create_query()
のようなdict
を用意 (これがArrayOfQueryの要素)- keyに
Query
を、valueに 1. で作成した要素を持つdictを用意し、それを配列の要素とすることで、ArrayOfQuery
になる
です。
get_type()
を使うよりも、かなり簡潔になり、見やすくなりました。
以上より、Zeepによる SOAP APIのリクエスト/レスポンスができました。
ソースコード
GitHubに上げました。 dejizo_client
ディレクトリ以下が今回のファイルです。
https://github.com/thinkAmi-sandbox/python_zeep-sample