Python + Zeep で SOAP API クライアントを作ってみた

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

https://python-zeep.readthedocs.io/en/master/

とのことで、いい感じのSOAP API クライアントが作れそうでした。

 

使えそうな SOAP APIを探す

SOAP API サーバを自作することも考えたのですが、ゼロから作るには先が長くなりそうでした。

 
そのため、使えそうな SOAP Web API を探してみました。

有名なところではFlickrAPIがありました。
https://www.flickr.com/services/api/

ただ、WSDLがなかったため、今回は見送りました。

 
他のAPIを探したところ、以下がありました。

 
今回はWSDLを使ってリクエスト/レスポンスができればよいため、デ辞蔵Webサービス - SOAPAPI を使います。

 

mzeepオプションにて、WSDLを解析する

WSDLXMLで書かれていますが、開発するために読み解くのは手間がかかります。

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.serviceSOAP 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'),
])

 
最後に、SOAP APIの引数として渡します。

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'),
    ]
}

となりました。

ポイントは

  1. create_query() のような dict を用意 (これがArrayOfQueryの要素)
  2. keyに Query を、valueに 1. で作成した要素を持つdictを用意し、それを配列の要素とすることで、ArrayOfQuery になる

です。

get_type() を使うよりも、かなり簡潔になり、見やすくなりました。

 
以上より、Zeepによる SOAP APIのリクエスト/レスポンスができました。

 

ソースコード

GitHubに上げました。 dejizo_client ディレクトリ以下が今回のファイルです。
https://github.com/thinkAmi-sandbox/python_zeep-sample

なお、WSDLファイルをリポジトリに含めていません。含めてよいか分からなかったためです。