Day18 財務分析

先人の方達が書いてくれたものを自分なりにまとめてみた。先に言っておきます。ありがとうございます。
以下は書かれているものを写経したものです。私が実践して得たものではなく、記事を読んだものをただただまとめただけです。
私はこれをもとに実践するつもりです。それではいきまそいきまそ。

EDINET APIを使用する財務分析は、単純な機能のプログラムの集まり。


証券コードリスト取得 → ②XBRL取得 → ③XBRLの読み込み → ④データ抽出(分量が多い) → ⑤グラフ化 → ⑥分析
XBRLの取得、XBRLの読み込み、勘定科目の抽出、グラフ化、時系列分析。


これらの機能を一つずつ小さく小さく作成していく。
ただXBRLを取得して、実際に銘柄分析に役立てるまでたどり着くのは時間がかかる。


XBRLの取得方法
XBRLとは財務情報や投資情報などの事業報告用の情報作成・流通・利用できるように標準化されたxmlベースの言語。

EDINETコードリスト(CSV形式)を使用してXBRLを取得する。
EDINETコードリストはZIP形式で圧縮されている。解凍すると、中身はCSV形式。文字のエンコーディングはcp932。文字化けする時はcp932を指定する。


① まずこのEDINETコードリスト(CSV)をpandasライブラリで読みこむ。

EDINETコードリストをpandasで読み込む方法
pandas.read_csv()で読み込む。
データは文字列で(str)として読み込むのが良い。
最初はいらない行なので1行分だけスキップ。
スキップしてから最初の行(0行目)がラベル行。

import pandas as pd
def get_edinet_codes_df(file):
    """EDINETコードリストのデータフレーム取得"""
    # データフレーム生成
    df = pd.read_csv(
        file,
        encoding='cp932',
        dtype='str',
        skiprows=1, # 最初の行をスキップ
        header=0,
        )
    return df


ここから上場企業の情報だけを抽出したりする。
以下抽出例。データフレームはリストに変換することもできる。

 # 上場企業だけを抽出
    df = df[df['上場区分']=='上場']

    # 必要な列だけ抽出
    df = df.loc[:, [
        'EDINETコード', '上場区分',
        '提出者名', '提出者名(ヨミ)', '提出者名(英字)',
        '提出者業種', '証券コード', '提出者法人番号',
        ]]

    # データフレームをリストに変換
    datas = df.values.tolist()


作成したデータフレームの保存と読み込みをする。データベース保存にも使える。
インデックス列も保存したいときは「index=None」を削除する。
CSV保存:pandas.DataFrame.to_csv()
CSV読み込み:pandas.read_csv()

    # データフレームをCSVで保存
    df.to_csv(symbols_csv, encoding='utf-8', index=None)

    # CSVから読み込み
    df = pd.read_csv(symbols_csv, encoding='utf-8')

②そしてこのコードリストを使用してXBRLを取得する。

サンプルコード:
SUMMARY_TYPE = 2
def download_all_documents(date, save_path, 
                           doc_type_codes=['120', '130', '140', '150', '160', '170']):
    params = {'date': date, 'type': SUMMARY_TYPE}
    doc_summary = get_submitted_summary(params)
    df_doc_summary = pd.DataFrame(doc_summary['results'])
    df_meta = pd.DataFrame(doc_summary['metadata'])
     
    # 対象とする報告書のみ抽出
    if len(df_doc_summary) >= 1:
        df_doc_summary = df_doc_summary.loc[df_doc_summary['docTypeCode'].isin(doc_type_codes)]
 
        # 一覧を保存
        if not os.path.exists(save_path + date):
            os.makedirs(save_path + date)
        df_doc_summary.to_csv(save_path + date + '/doc_summary.csv')
 
        # 書類をを保存
        for _, doc in df_doc_summary.iterrows():
            download_document(doc['docID'], save_path + date + '/')
            open_zip_file(doc['docID'], save_path + date + '/')
        return df_doc_summary
     
    return df_doc_summary 


XBRLを読み込み方。zipファイルをそのまま読み込む方法、beautiful soupで読み込む方法、lxmlで読み込む方法の三つぐらいあるらしい。

今回はzipファイルをそのまま読み込む方法をとる。
XBRLのファイル群はzipでひとつにまとめて公開されている。
これは解凍せずに読み込むことができる。
Python のzipモジュールにXBRLのzipファイルパスを渡すと読み込む事ができる。
そうするとバイナリデータが返ってくるので、それをそのままBeautifulSoup()やlxmlに渡せば解析してくれる。

zipを展開せずに直接読み込む方法[python, XBRL]
つまり、Python で zipを解凍せずに直接読み込む方法

zipファイルの中身を読み込む時は、いったんHDD か SSD に解凍(展開)するのが一般的であるが、zipから直接読み込むこともできる。

やり方は、「標準のzipfileモジュール」を使用する。
zipの中身を解凍しなくて良いため、大量のXBRLを読み込む時に、容量を省く事ができる。

zipfileモジュールでzipファイルを開いたら、
中身のファイルリストを取得するメソッド「.infolist()」と
データを読み込むメソッド「.read()」を使用する。

使い方:

zipファイルを開く:class zipfile.ZipFile(file, mode='r', compression=ZIP_STORED, allowZip64=True)

中身のファイルリストを取得:ZipFile.infolist()

名前を指定して読み込む:ZipFile.read(name, pwd=None)


zipを読み込む
zipを解凍せずに中身を見るには、zipfileモジュールのZipFile()を使用する

読み込み手順
拡張子「.xbrl」のファイルだけを読み込んで辞書に入れる例。
zipfile.ZipFile()でzipファイルを開いてから、zip_data.infolist()でファイルリストを取得する。

ファイルリストの要素にはinfo.filenameという属性があるので、これをzip_data.read()に渡すと、データが読み込める。

info.filenameは、実際には以下のような「ファイル構造を含んだファイルパス」

'S100AM8K/XBRL/PublicDoc/jpcrp030000-asr-001_E00518-000_2017-03-31_01_2017-06-26.xbrl'

そのため、ファイル名を使用して読み込む・読み込まないを判定する時は、このファイルパスを意識して
判定部分を作ることになる。

コード例
まずzipfileモジュールを使用する部分

import zipfile
from collections import OrderedDict
import traceback

def read_file_from_zip(zip_file, re_match):
    """zipからファイル名を指定して読み込む"""
    file_datas = OrderedDict()

    try:
        with zipfile.ZipFile(zip_file, 'r') as zip_data:
            # ファイルリスト取得
            infos = zip_data.infolist()

            for info in infos:
                # ファイルパスでスキップ判定
                if re_match(info.filename) is None:
                    continue

                # zipからファイルデータを読み込む
                file_data = zip_data.read(info.filename)

                # ファイルパスをキーにして辞書に入れる
                file_datas[info.filename] = file_data

    except zipfile.BadZipFile:
        print(traceback.format_exc())

    return file_datas

次に、上記の関数を使用する部分。
メイン関数。
zipから読み込んだデータは、そのままlxml.etree.fromstring()に渡す事ができる。
データの中身はバイナリデータ。

import re
from lxml.etree import fromstring as etree_fromstring

def main():
    """メイン関数"""
    # zipファイルの場所
    zip_file = r'*****\S100AM8K.zip'

    # 拡張子でファイルを選択してみる
    re_match = re.compile(r'^.*?\.xbrl$').match

    # zipからファイルデータを読み込む
    files = read_file_from_zip(zip_file, re_match)

    # 読み込んだXBRLファイルを1つずつ解析
    for (file, data) in files.items():
        # ファイル名
        print('file: %s' % file)

        # XML解析 lxml.etree
        root = etree_fromstring(data)
        print('root.tag: %s' % root.tag)

        print('')
    return

そしてメイン関数を呼び出す。
if __name__=='__main__':
    main()

zipから読み込んだXMLデータをBeautifulSoupで読み込む場合。
バイナリデータをそのままわたす。単に文字列に変換したい場合は、適当なエンコーディングを指定して『.decode()』のメソッドを呼び出す。
バイトオーダーマーク (BOM) 付きの UTF-8 をデコードするときは、'utf-8-sig' を指定する。
'utf-8' でデコードしたら、先頭にパイトオーダーマークの文字列が残ってしまう。

from bs4 import BeautifulSoup

# XML解析 BeautifulSoup(HTMLパーサー)
soup = BeautifulSoup(data, features='lxml')

# XML解析 BeautifulSoup(XMLパーサー)
soup = BeautifulSoup(data, features='xml')

# バイナリをデコードして文字列に変換
text = data.decode('utf-8-sig')

参考:https://srbrnote.work/archives/1297





その勘定科目の抽出には2種類ある。

XBRLを一気に読み込んでから抽出するアプローチ。(今回はこっち)
XBRLを解析しながら狙った科目だけ抽出するアプローチ

XBRLを一気に読み込んでCSVに保存しておくと、内容を一望でき、タグを探すのが楽になる。
CSVは企業ごとに全ての決算を連結していったものを作成し、
これを一つ開くだけでその企業の何年分というタグを一覧する事ができる。

そして、「タグの読み込み」と「勘定科目の抽出」を分けたら、プログラムの分割がうまくいく。

PythonXBRLの読み込みに専念して、時系列分析のための表作成はpandasデータフレームの機能に任せる。
できる人であれば、専用のデータベースに取り込んで、時系列をリクエストするのが良い。


参考:https://srbrnote.work/archives/1100
参考:https://srbrnote.work/archives/1136
参考:https://srbrnote.work/archives/1123


あとは抽出してグラフ化するだけ。

Day17

前回の続き。

def download_document(doc_id, save_path):
    params = {'type': 1}
    doc = get_document(doc_id, params)
    if doc.status_code == 200:
        with open(save_path + doc_id + '.zip', 'wb') as f:
            for chunk in doc.iter_content(chunk_size=1024):
                f.write(chunk)


・文書をダウンロードする関数。paramsでtype:1を指定して、この上に書かれていたget_document関数にdoc_idとparamsを渡して、返ってきたstatus_codeが200つまりOKだったら、save_path+doc_id+.zipで構成されているファイルを開いて、doc.iter_content()でバイナリデータを1024バイトずつ数回に分けてファイルに書き込んでいる。chunk とは一塊の意味で、このwith からの文は何かにおいての定番の書き方らしい。

今日は以上。備忘録。

Day17

PythonをAtomエディタのatom-runnerで実行する | RemoNote
ImportError: no module named requestsでハマった時は上記を見る。

MacのAtomでPython3.x.xを適用する方法 - Qiita
importError が出るのはAtom Runnerが原因ではないかと思い、これも参考にした。でも違ったが、前進。

結局PATHが原因でImportErrorが出ていた。

import sys
print(sys.path)
['', '--------------', '-----------------']

sys.path.append('requestsやモジュールが置いてあるところのPATH')

import requests

これでできる。

以上。ここや、以前に記したものはすべて備忘録。以上。

Day16 わからないが頑張るで!

今日からはデータ分析の基本コードを書いていく。

import pandas as pd 
data = [
    [60,90,89],
    [89,79,89],
    [99,88,77],
]
df = pd.DataFrame(data)
df

pandasのデータフレームにデータを入れると表が出力される

df.columns = ["english", "math", "art"]
df.index = ["a-ta","b-ko","c-suke"]

とすると縦の列にeng math art が出力される
横の行にはa-ta、b-ko、c-sukeが表示される


次にデータ量が多くなると大変なのでそういう時はCSVファイルを読み込むようにする。以下はUTF-8形式。

import pandas as pd 
df = pd.read_csv("test.csv")
df 


ファイル形式がShift-JIS形式もあるのでUTF-8形式かどちらか確認したら、以下のように使用する。

import pandas as pd 
df = pd.read_csv("test.csv", encording="Shift_JIS")
df
df.columns
df.index

で列の項目と、行の項目を取ってこれる。

列名をリストに変換することもできる。

list1 = [i for i in df.columns]
print(list1)

出力結果:["www", "eee", "rrrrrrr", "tttt"]

list2 = [i for in df.index]
print(list2)

出力結果:["赤か", "ええええ", "かかか", "djdjdjd"]

データの数を取ることもできる.

len(df)
出力結果:6


欠損値の処理もできる。以下など。

# 欠損値を前の値で埋める
df = df.fillna(method="ffill")

# 欠損値を平均値で埋める
df = df.fillna(df.mean())

以上!今日は以上!

edinet xbrl python 写経

# データをまとめて取得する関数
SUMMARY_TYPE = 2
def download_all_documents(date, save_path, doc_type_codes=['120', '130', '140', '150','160','170']): 
    params = {'date':date, 'type':SUMMARY_TYPE}
    doc_summary = get_submitted_summary(params)
    df_doc_summary = pd.DataFrame(doc_summary['results'])
    df_meta = pd.DataFrame(doc_summary['metadata'])

    # 対象となる報告書のみ抽出
    if len(df_doc_summary) >= 1:
        df_doc_summary = df_doc_summary.loc[df_doc_summary['docTypeCode'].isin(doc_type_codes)]

        # 一覧を保存
        if not os.path.exists(save_path + date):
            os.makedirs(save_path + date)
        df_doc_summary.to_csv(save_path + date + '/doc_summary.csv')

        # 書類を保存
        for _, doc in df_doc_summary.iterrows():
            download_document(doc['docID'], save_path + date + '/')
            open_zip_file(doc['docID'], save_path + date + '/')
        return df_doc_summary

    return df_doc_summary


・SUMMARY_TYPEを2に設定しておく。
・download_all_documents関数には引数をdate, save_path, doc_type_codeを指定する
・get_submitted_summaryにparamsを渡すとjsonが取得できるため、paramsを用意する
・doc_summaryに取ってきたjsonを入れる。
・df_doc_summaryに取ってきたjsonからresultsデータを取り出して代入
・df_metaにも同様に取ってきたjsonからmetaデータを取り出して代入

~~~~if文~~~~~~~
・len(df_doc_summary).......df_doc_summaryの長さが1以上だったら
 ・df_doc_summaryに、locで位置を指定して、ここではdf_doc_summary['docTypeCode'] で列を指定、.isinでdoc_type_codesが含まれているものを返す。それをdf_doc_summaryに代入
  
  locの記述について。ありがとうございます。
  Pandasで要素を抽出する方法(loc,iloc,iat,atの使い方) - DeepAge

   #一覧を保存
  ・os.path(ファイルやディレクトリが指定したパスに存在するかを確認したり、パスからファイル名や拡張子を取得するなどを行う際に利用される)にsave_pathとdateが存在していなかったら、
   ・os.makedirsでディレクトリを作成
  ・df_doc_dummaryを、save_pathとdateとdoc_summary.csvを渡してcsvファイルに書き出す。

  #書類を保存
  ・繰り返し処理を用いてdf_doc_summaryのデータフレームに入っているデータをiterrows()メソッドを用いて、1行ずつ、インデックス名(行名)とその行のデータ(pandas.Series型)のタプル(index, Series)を取得する。
    ・前回作成したdownload_document関数にdoc['docID']とパス名を渡す
    ・open_zip_fileで解凍。


# zipファイルを解凍する関数
def open_zip_file(doc_id, save_path):
    if not os.path.exists(save_path):
        os.makedirs(save_path + doc_id)

    with zipfile.ZipFile(save_path + doc_id + '.zip') as zip_f:
        zip_f.extractall(save_path + doc_id)

・この関数はdoc_idとsave_pathを渡して使用する関数
・もしos.pathにsave_pathが存在していなかったら
 ・save_pathにdoc_idを付け加えたディレクトリを作成

 ・with zipfile.ZipFile........ZipFileオブジェクトはclose()で閉じる必要があるが、with文を使うと中身が終わったときに自動で閉じてくれる。 .extractall().........ZipFileオブジェクトのextractall()メソッドで、ZIPファイルの中身がすべて解凍(展開)される。第一引数pathに展開先のディレクトリのパスを指定する。省略するとカレントディレクトリに解凍される。アリ10。
PythonでZIPファイルを圧縮・解凍するzipfile | note.nkmk.me
つまりzip_f.extractall()で中身を解凍して解凍先に展開。


# 日付でループするので、そのための設定をし、とりあえず2020年3月1日から2020年6月30日までに提出された報告書を取得する。
from datetime import datetime, timedelta

def date_range(start_date: datetime, end_date: datetime):
    diff = (end_date - start_date).days + 1
    return (start_date + timedelta(i) for i in range(diff))

start_date = datetime(2020, 3, 1)
end_date = datetime(2020, 6, 30)
# 取得する。今回は有価証券報告書のみに限定する。指定したフォルダに4か月間に提出された有価証券報告書が保存される。
doc_type_code = ['120']
save_path = 'original_data/'

for i, date in enumerate(date_range(start_date, end_date)):
    date_str = str(date)[:10]
    df_doc_summary = download_all_documents(date_str, save_path)
    if i == 0:
        df_doc_summary_all = df_doc_summary.copy()
    else:
        df_doc_summary_all = pd.concat([df_doc_summary_all, df_doc_summary])
# 上記で取得したデータを使用して、取得したいデータを抽出する方法
from edinet_xbrl.edinet_xbrl_parser import EdinetXbrlParser

# 以下の関数でkeyとcontext_refを指定し、pathで指定したファイルからデータを取得する。
def get_one_xbrl_data(path, key, context_ref):
    parser = EdinetXbrlParser()

    # 指定したxbrlファイルをパースする
    xbrl_file_path = path
    edinet_xbrl_object = parser.parse_file(xbrl_file_path)

    # データの取得
    data = edinet_xbrl_object.get_data_by_context_ref(key, context_ref)
    if data is not None:
        return data.get_value()
# 例えば今期の売上を取りたい場合は、
# keyがjpcrp_cor:NetSalesSummaryOfBusinessResultsで、contextRefがCurrentYearDurationなので、以下のように指定する。
import glob

seccode = 7816
# 文書IDと提出日を取得
doc_id = df_doc_summary_all.query(f'secCode=="{seccode}0"').docID.values[0]
date = df_doc_summary_all.query(f'secCode=="{seccode}0"').submitDateTime.values[0]

# XBRLを取得
file_path = save_path + date[:10] + '/' + doc_id + '/XBRL/PublicDoc/*.xbrl'
file_path = glob.glob(file_path)[0]

# データを取得
get_one_xbrl_data(file_path, key='jpcrp_cor:NetSalesSummaryOfBusinessResults', context_ref='CurrentYearDuration')

# テキスト情報も取得可能
get_one_xbrl_data(file_path, key='jpcrp_cor:BusinessRisksTextBlock', context_ref='FilingDateInstant')


# 欲しいデータをまとめて取得
account_df = pd.read_csv('account_list.csv').fillna('')

def get_xbrl_data(path, account_df):
    parser = EdinetXbrlParser()

    # parse xbrl file and get data container
    xbrl_file_path = path
    edinet_xbrl_object = parser.parse_file(xbrl_file_path)
    ## データの取得
    account_df['value'] = ''
    for idx, account in account_df.iterrows():
        key = account['key']
        context_ref = account['context_ref']
        data = edinet_xbrl_object.get_data_by_context_ref(key, context_ref)

        if data is not None:
            account_df.loc[idx, 'value'] = data.get_value()
            #print(key, context_ref, account['value'])
    return account_df


また新しい発見したら更新する。

ありがとうございます!
https://data-analytics.fun/2020/12/22/ufo-analysis-data-collection/

Day15

import requests
import os
import zipfile

import numpy as np
import pandas as pd
from pandas import DataFrame


import matplotlib.pyplot as plt
import seaborn as sns; sns.set();

・seaborn.....pythonでグラフを作るためのライブラリ
sns.set()でグラフが作れちゃうらしい。

# 一覧の取得関数を作成
EDINET_API_URL = "https://disclosure.edinet-fsa.go.jp/api/v1"
def get_submitted_summary(params):
    url = EDINET_API_URL + '/documents.json'
    response = requests.get(url, params=params)

    assert response.status_code==200

    return response.json()

・edinet_api_urlを作成して、

・そこにparamsを渡すget_submitted_summary関数を作る。
・urlに最初に作成したUR Lにdocuments.jsonを付け加える。
・responseにrequestした情報をurlとparamsを取得して、
・status_codeが200じゃなかったらエラーを表示する。assertは条件文がTrue以外の時に例外を投げる。
・response.json().......jsonを読み取る。

# 作成した関数を呼び出して一覧を取得し、pandasのDataFrameに変換することで利用できる
date = '2020-03-30'
SUMMARY_TYPE = 2
params = {'date':date, 'type':SUMMARY_TYPE}
doc_summary = get_submitted_summary(params)
df_doc_summary = pd.DataFrame(doc_summary['results']) # 1
df_doc_summary


# 書類を取得する関数
def get_document(doc_id, params):
    url = EDINET_API_URL + '/documents/' + doc_id
    response = requests.get(url, params)

    return response

# zip形式でダウンロードする関数
def download_document(doc_id, save_path):
    params = {'type': 1}
    doc = get_document(doc_id, params)
    if doc.status_code == 200:
        with open(save_path + doc_id + '.zip', 'wb') as f:
            for chunk in doc.iter_content(chunk_size=1024):
                f.write(chunk)


df_doc_summary.query('edinetCode=="E31070"')

download_document(doc_id='S100IC1B', save_path='./')

・df_doc_summary.query('edinetCode=="E31070"').........queryメソッドを使用することで、行を選択する事ができる。


次回からはコードを読むことに労力をかけよう。そしてそれがたまったら再度それを書いてみよう。そうしよ。だから次回からは、説明を多くとることにしよ。

ありがとうございます!
https://data-analytics.fun/2020/12/22/ufo-analysis-data-collection/

Day14 Django session json 取り出し

# zip_code
{
    "message": null,
    "results": [
        {
            "address1":"北海道",
            "address2":"札幌市",
            "address3":"札幌",
            "kana1":"ホッカイドウ",
            "kana2":"サッポロシ",
            "kana3":"サッポロ",
            "prefcode":"1",
            "zipcode":"1000001"
        }
    ],
    "status":200
}
{
    "message":null,
    "results":null,
    "status":200
}

def fetch_address(zip_code):
    REQUEST_URL = f'http://zipcloud.ibsnet.co.jp/api/search?zipcode={zip_code}'
    address = ' '
    response = request.get(REQUEST_URL)
    response = json.loads(response.text)
    result, api_status = response['results'], response['status']
    if api_status == 200;
        result = result[0]
        address = result['address1'] + result['address2'] + result['address3']
        return address

~~~~~get_address関数について~~~~~~~
・この関数はzip_codeで入力し、ちゃんと情報が返って来たら、その情報を合わせて返す。
・response = request.get(REQUEST_URL).............responseに、返ってきたURLを取得して、代入
json.loads(response.text) .......... おそらくjson.loads()でresponse.textを読み込んでいる。ただresponse.textがなんでできるのかがわからない。
・result, api_status = response['results'], response['status'] .............. これでresultにresponse['results']を、api_statusにresponse['status']を入れる。
  
 ~~~~if文の中身~~~~~~~~
 api_statusが200だったら、
  resultに最初の辞書を入れる。
  次にaddressに辞書の中身を追加する
  addressを返す。



#Buy Form
classBuyForm(forms.Form):
    zip_code = forms.CharField(
        label = '郵便番号',
        max_length = 7,
        required = False,
        widget=forms.TextInput(attrs={'placeholder': '数字7桁(ハイフンなし)'})
    )

    address = forms.CharField(
        label = '住所',
        max_length = 100,
        required = False
    )

・formで住所検索とアドレスを入力するフォームを作成
・label ......ただのラベルでは.....なかった。これはhtmlで参照する事ができるlabelらしい。例えばhtmlで{{ buy_form.zip_code.label_tag }}のように使う事ができるらしい。ありがとうございます!
Django - フォームの改造 - Qiita

@login_required
def bag(request):
    user = request.user
    bag = request.session.get('bag', {})
    bag_products = dict()
    total_price = 0
    for product_id, num in bag.items():
        product = Product.objects.get(id=product_id)
        bag_products[product] = num
        total_price = product.price * num

    buy_form = BuyForm(request.POST or None)
    if buy_form.is_valid():
        if 'search_address' in request.POST:
            zip_code = request.POST['zip_code']
            address = get_address(zip_code)
            if not address:
                messages.warning(request, "住所を取得できませんでした。")
                return redirect('app:bag')
            buy_form = BuyForm(initial={'zip_code':zip_code, 'address':address})

        if 'buy_product' in request.POST:
            if not buy_form.cleaned_data['address']:
                messages.warning(request, "住所の入力は必須だ")
                return redirect('app:bag')

            if not bool(bag):
                messages.warning(request, "バッグは空やで")
                return redirect('app:bag')

            if total_price > user.point:
                messages.warning(request, "ポイントがねえぜ")
                return redirect('app:bag')

            for product_id, num in bag.items():
                if not Product.objects.filter(pk=product_id).exists():
                    del bag[product_id]
                product = Product.objects.get(pk=product_id)
                sale = Sale(product=product, user=request.user, amount=num, price=product.price)
                sale.save()

            user.point -= total_price
            user.save()
            del request.session['bag']
            messages.success(request, "商品の購入が完了や。")
            return redirect('app:bag')

        context = {
            'buy_form': buy_form,
            'bag_products': bag_products,
            'total_price': total_price, 
        }

・buy_form = BuyForm(request.POST or None) ............ buy_formにBuyFormでrequest.POSTを入れる

~~~~~~if文の中身~~~~~~~~~~
・buy_form.is_valid():......buy_formが有効だったら
 ・送られてきた情報が''search_addressだったら
  ・送られてきた情報からzip_codeを取り出す
  ・addressにget_address関数のzip_codeを入れて
  ・もしアドレスがなかったら...
  ・buy_formにBuyFormにinitialで初期値を設定。

 ・送られてきた情報が'buy_product'だったら
  ・もしbuy_formにaddressが入力されていなかったら
  ・bagが空か否か
  ・ポイント不足か否か

   ~~for文の中身~~~
  ・product_idとnumにbag辞書から取り出したキーとバリューを入れて
   ・もしproduct_idが存在していなかったら、
    ・bag辞書のproduct_idを削除
   ・productにProductテーブルからproduct_idをとってきて代入
   ・saleにSaleテーブルに入れる値を渡して代入
   ・保存

  ・total_priceからuserpointを減らす
  ・userを保存。
  ・終わったsessionを削除
  ・bag関数に返す。

 ・contextにbuy_formとbag_productsとtotal_priceをセット。