DjangoでWebアプリを作ってみる。【vol. 06 EdinetのAPIを叩いて、有価証券報告書から役員情報を抜き出す】

※[こちら](https://qiita.com/TsJazz27Sumin/items/ba0b6fee502792301149)の記事の続きです。
### APIをキックするためのボタンをTemplateに配置する。
さすがに素のHTMLのまま進めるのは、デザイン的に悲しくなるので、
[こちら](https://qiita.com/S2notC2/items/82808558383a773da942)の記事を参考に`bootstrap4`を導入しています。

前回、HelloWorldした`edinet/corporate_officer_list.html`をごそっと書き換えます。
ボタンを押しても何も起きない状態です。

```html

{% load static %}

{% bootstrap_css %}
{% bootstrap_javascript jquery='full' %}

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Call Edinet API</title>
  </head>
  <body class="text-center">
    <div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">

  <main role="main" class="inner cover">
    <h1 class="cover-heading">Call Edinet API.</h1>
    <p class="lead">This is My Django Practice.</p>
    <p class="lead">
      <a href="#" class="btn btn-lg btn-secondary">Click</a>
    </p>
  </main>

  <footer class="mastfoot mt-auto">
    <div class="inner">
      <p>@TsJazz27Sumin 2019.</p>
    </div>
  </footer>
</div>
</body>
</html>
```

### ボタンを押下したらメソッドが呼ばれるようにする。

`edinet/views.py`に新たにメソッドを追加します。
とりあえず同じテンプレートを返すようにしています。

```py
def corporate_officer_list(request):
    return render(request, 'edinet/corporate_officer_list.html', {})
```

`edinet/urls.py`には、対応する形で以下を記載しました。

```py
path('call-edinet-api', views.call_edinet_api, name='call_edinet_api'),
```

そして、先程のTemplateの

```html

<a href="#" class="btn btn-lg btn-secondary">Click</a>
```

を

```html

<a href="{% url 'call_edinet_api' %}" class="btn btn-lg btn-secondary">Click</a>
```

に変えます。

これで新規追加したメソッド内でAPIをコールする準備が整いました。
urls.pyに記載された URLパターンを逆引きする機能は、[こちら](https://code-schools.com/django-url/)の記事を参考にさせていただきました。

### APIをコールする。

 - EdinetのAPIの仕様書等については、[こちら](https://disclosure.edinet-fsa.go.jp/EKW0EZ0015.html)にあります。
 - PythonでのAPIの叩き方は、[こちら](https://qiita.com/shunyooo/items/b408b8d61f9f73b21da7)を参考にさせていただきました。
 - Python Requestsモジュールは、installする必要があります。[こちら](https://qiita.com/hththt/items/14bfc2bf23192b020371)の記事を参考にさせていただきました。

EdinetのAPIは、

 - 書類一覧 API
    - 日付単位の提出書類一覧を取得する。
 - 書類取得 API 
    -  書類管理番号を指定して書類を取得する。

の2本立てになっています。

今回は、特定の有価証券報告書をターゲットにするので、いきなり「書類取得 API」を使いたいところです。ただ、書類管理番号自体が「書類一覧 API」を叩かないと分からないので、順を追って進めていきたいと思います。

#### 書類一覧 API

`edinet/views.py`をこういった形で書き換えます。

```py

from django.shortcuts import render

import requests
import json

api = "https://disclosure.edinet-fsa.go.jp/api/v1/documents.json?date={target}"

def corporate_officer_list(request):
    return render(request, 'edinet/corporate_officer_list.html', {})

def call_edinet_api(request):

    url = api.format(target="2018-06-19")
    r = requests.get(url)
    data = json.loads(r.text)
    print("+ メタデータ=", data["metadata"])

    return render(request, 'edinet/corporate_officer_list.html', {})

```

APIをコールして返ってきたJsonをパースして、コンソールに出力させてみるという形です。
データ構造のトップの部分のみ出力させます。

```
+ メタデータ= {'title': '提出された書類を把握するためのAPI', 'parameter': {'date': '2018-06-19', 'type': '1'}, 'resultset': {'count': 304}, 'processDateTime': '2019-06-24 00:07', 'status': '200', 'message': 'OK'}
```

ステータス200で問題なくレスポンスが来ました。
どうやら`2018-06-19`は、`304`件の書類が存在しているようです。

続いて、エンドポイントを

```py
api = "https://disclosure.edinet-fsa.go.jp/api/v1/documents.json?date={target}&type=2"
```
に変更します。

そして、jsonのパース以降を

```py
    data = json.loads(r.text)
    results = data["results"]

    for result in results:
        print("+ 結果セット=", result["edinetCode"], result["filerName"], result["docDescription"])
```
に変更します。

これで実行すると、こんな感じでズラズラと中身が取り出せます。

```
+ 結果セット= E01777 ソニー株式会社 有価証券報告書－第101期(平成29年4月1日－平成30年3月31日)
+ 結果セット= E01777 ソニー株式会社 内部統制報告書－第101期(平成29年4月1日－平成30年3月31日)
+ 結果セット= E00984 第一三共株式会社 訂正発行登録書
+ 結果セット= E01777 ソニー株式会社 確認書
+ 結果セット= E04867 株式会社ナガワ 有価証券報告書－第54期(平成29年4月1日－平成30年3月31日)
+ 結果セット= E09037 株式会社ウィズ・パートナーズ 変更報告書
...
```

### 書類取得 API

次は、有価証券報告書の取得をしたいと思います。
ここでは、`E01777 ソニー株式会社 有価証券報告書`を対象に進めます。

`edinet/views.py`をこういった形で書き換えます。

```py
from django.shortcuts import render

import requests
import json
import zipfile
import io

list_api = "https://disclosure.edinet-fsa.go.jp/api/v1/documents.json?date={target}&type=2"
file_api = "https://disclosure.edinet-fsa.go.jp/api/v1/documents/{document_id}?type=1"

def corporate_officer_list(request):
    return render(request, 'edinet/corporate_officer_list.html', {})

def call_edinet_api(request):

    for result in get_results("2018-06-19"):
        if result["edinetCode"] == "E01777" and "有価証券" in result["docDescription"]:
            document_id = result["docID"]
            url = file_api.format(document_id=document_id)
            r = requests.get(url)

            zip = zipfile.ZipFile(io.BytesIO(r.content))
            zip.extractall()
            
    return render(request, 'edinet/corporate_officer_list.html', {})

def get_results(target):

    url = list_api.format(target=target)
    r = requests.get(url)
    data = json.loads(r.text)
    results = data["results"]

    return results

```
ファイル一覧の取得は、`get_results`という形でメソッド化しています。
あとは、SONY(=E01777)かつ「有価証券」がファイル名に含まれる場合に、有価証券報告書(=zipファイル)をダウンロードして解凍するようにしました。

zipファイルの解凍は、[こちら](https://stackoverflow.com/questions/9419162/download-returned-zip-file-from-url)を参考にしました。
これで実行すると、有価証券報告書のXBRLファイルを含むフォルダが配置されます。

### XBRLファイルから役員情報部分を抜き出す
解凍したフォルダ内にある`XBRL\PublicDoc\xxx.xbrl`が対象になります。

こちらのxrbl内に`  <jpcrp_cor:InformationAboutOfficersTextBlock contextRef="FilingDateInstant">`のブロックで囲まれたエリアがあり、そこに【役員の状況】が記載されています。

それでは、[EDINET XBRLを読み込む方法【Python】](https://srbrnote.work/archives/1236)を参考にさせていただき、【役員の状況】を取得したいと思います。
まずは、読み込み用のライブラリをインストールします。

```
pip install lxml
```
`setting.py`に記述を追加します。

```py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'lxml.etree', #追加
    'bootstrap4',
    'edinet',
]
```
`edinet/views.py`をこういった形で書き換えます。

```py
from django.shortcuts import render
from lxml import etree

import requests
import json
import zipfile
import io
import os

list_api = "https://disclosure.edinet-fsa.go.jp/api/v1/documents.json?date={target}&type=2"
file_api = "https://disclosure.edinet-fsa.go.jp/api/v1/documents/{document_id}?type=1"

def corporate_officer_list(request):
    return render(request, 'edinet/corporate_officer_list.html', {})

def call_edinet_api(request):

    print(os.getcwd())

    for result in get_results("2018-06-19"):
        if result["edinetCode"] == "E01777" and "有価証券" in result["docDescription"]:
            document_id = result["docID"]
            url = file_api.format(document_id=document_id)
            r = requests.get(url)

            zip = zipfile.ZipFile(io.BytesIO(r.content))
            zip.extractall()

            file = os.getcwd() + "\XBRL\PublicDoc\jpcrp030000-asr-001_E01777-000_2018-03-31_01_2018-06-19.xbrl"
            data = get_information_about_officers_text(file)
            print(data.text)
            
    return render(request, 'edinet/corporate_officer_list.html', {})

def get_information_about_officers_text(file):
    root = read_xml_lxml_etree(file)
    namespace = root.nsmap['jpcrp_cor']
    tag = 'InformationAboutOfficersTextBlock'
    attr = 'contextRef'
    value = 'FilingDateInstant'

    xpath = './/{%s}%s[@%s="%s"]' % (namespace, tag, attr, value)
    data = root.find(xpath)

    return data

def read_xml_lxml_etree(file):
    with open(file, 'rb') as f:
        return etree.fromstring(f.read())

def get_results(target):

    url = list_api.format(target=target)
    r = requests.get(url)
    data = json.loads(r.text)
    results = data["results"]

    return results

```

段々処理が増えてきました。
Zipファイルで解凍する部分までは同じですが、

 - def read_xml_lxml_etree(file):
    - バイナリモードでファイルを読み込むためのメソッドです。
 - def get_information_about_officers_text(file):
    - lxml.etreeを使用して役員情報部分を抜き出すためのメソッドです。
 - os.getcwd()を使って[カレントディレクトリの確認](https://qiita.com/Oz-Spade/items/7fa479fae7a0ea6fcddf#%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%88%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E3%81%AE%E7%A2%BA%E8%AA%8D)を行うために`import os`をしています。

を追加しています。

これを実行すると、こんな感じで役員情報を含んだHTMLがコンソールに表示されます。

```html
...
<td style="border-left: 1px solid #000000; border-top: 1px solid #000000; border-right: 1px solid #000000; border-bottom: 1px solid #000000; vertical-align: middle">
<p style="margin-left: 5px; line-height: 15px; margin-right: 5px; text-align: center">
<span style="font-size: 10px">執行役</span>
</p>
</td>
<td style="border-left: 1px solid #000000; border-top: 1px solid #000000; border-right: 1px solid #000000; border-bottom: 1px solid #000000; vertical-align: middle">
<p style="margin-left: 6px; margin-right: 6px; text-align: left">
<span style="font-size: 10px">常務</span>
</p>
<p style="margin-left: 5px; line-height: 15px; margin-right: 5px; text-align: left">
<span style="font-size: 10px; font-weight: normal">（R&amp;Dプラットフォーム担当、メディカル事業担当）</span>
</p>
</td>
<td style="border-left: 1px solid #000000; border-top: 1px solid #000000; border-right: 1px solid #000000; border-bottom: 1px solid #000000; vertical-align: middle">
<p style="line-height: 15px; text-align: left; text-indent: 15px">
<span style="font-size: 10px">勝本　徹</span>
</p>
</td>
<td style="border-left: 1px solid #000000; border-top: 1px solid #000000; border-right: 1px solid #000000; border-bottom: 1px solid #000000; vertical-align: middle">
<p style="margin-left: 10px; line-height: 15px; text-align: center; text-indent: -10px">
<span style="font-size: 10px">1957年10月14日生</span>
</p>
...

```

### HTMLをパースして役員データを抜き出す。
こちらの記事を参考にさせていただきました。

 - [Beautiful Soup での スクレイピング基礎まとめ [初学者向け]](https://qiita.com/U-MA/items/896c49d46585e32ff7b1)
 - [PythonとBeautiful Soupでスクレイピング](https://qiita.com/itkr/items/513318a9b5b92bd56185)
 - [Beautiful Soup 4.2.0 Doc. 日本語訳](http://kondou.com/BS4/#)

HTMLをパースするために`beautifulsoup4`を使用します。

```
pip install beautifulsoup4
```

インストール後、インポートを`edinet/views.py`に追加しておきます。

```py
from bs4 import BeautifulSoup
```

そして、データ取得以降をこんな感じにします。

```py
            data = get_information_about_officers_text(file)
            soup = BeautifulSoup(data.text)

            for table in soup.find_all("table"):
                for tr in table.find_all_next("tr"):

                    if len(tr.find_all("td")) > 7:
                        for child in tr.children:
                            if child.name == "td":
                                print(child.get_text().replace('\n', '/'))
                                print("-------------------------------------") 
```

実行すると画面に表形式で表示するには、まだ加工が必要ですがとりあえず素材となる役員情報は抜き出せました。

```
...
//執行役//
-------------------------------------
//常務///（R&Dプラットフォーム担当、メディカル事業担当）//
-------------------------------------
//勝本　徹//
-------------------------------------
//1957年10月14日生//
-------------------------------------
/ ///////////////1982年４月/////当社入社///////2012年11月/////当社業務執行役員 SVP///////2013年４月/////ソニー・オリンパスメディカルソリューションズ㈱
代表取締役社長///////2016年１月/////ソニー・オリンパスメディカルソリューションズ㈱ 取締役（現在）///////2017年１月/////当社メディカルビジネスグループ長///////2017年４月/////ソニーイメージングプロダクツ&ソリューションズ 代表取締役副社長（現在）///////2018年４月/////当社執行役 EVP///当社R&Dプラットフォー
ム担当、メディカル事業担当（現在）///////2018年６月/////当社執行役 常務（現在）/////// /
-------------------------------------
//*//
-------------------------------------
//1//
...
```

今度は、加工して画面に表示してみたいと思います。