おいちゃんと呼ばれています

ウェブ技術や日々考えたことなどを綴っていきます

Jakarta Commons HttpClient による SBI 証券での自動売買(6)- 会社四季報の企業概要をデータベースへ格納編

例によって、以前書いた件のつづきです。

データベースの操作のためのクラスを作っておく意義:

  1. SQLを実行する度に同じコードを書かなくて済む
  2. まとめて行う処理があるとき、データベースへの接続回数を減らせば処理が速くなる

データベースへの接続って結構時間かかるみたいです。ただ、このあたりは実際に呼び出しているところも含めて見ないとなかなかイメージが湧かないかもしれませんので、そこは次回以降に書きたいと思います。

これまでに SBI 証券から会社四季報のデータを取得すべくコードを書いてきましたが、やはり 100% 完璧に取得できるわけではないようです。企業コード 1001 から 9999 まで実行してみると、次のような問題がありました。

  • 【URL】の項目がない企業がある(例: 7538 大水)
  • 【連結事業】または【単独事業】の項目がない企業がある(例: 8303 新生銀行
  • 「対象銘柄はありません」のエラーが続くと、対象銘柄がある企業コードについてもデータを取得できなくなる(-> ログインし直せば取得できるようになる)

これらを踏まえて、今日のコードのポイントは次の点になるかと思います。

  • データをうまく取得できなかったときにどうするか?
  • データベースへの接続回数をできるだけ少なくするにはどうするか?

サンプルコード

import java.io.*;
import java.util.regex.*;
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.*;

public class ImportCampanySample {

    /**
     * ログイン時のユーザID
     */
    private final String USER_ID = "inouetakuya";

    /**
     * ログイン時のパスワード
     */
    private final String PASSWORD = "hogehoge";

    /**
     * SBI証券サイトの文字コード
     */
    private final String SBI_CHARACTER_CD = "SJIS";

    /**
     * HttpClient
     * このクラス内で使いまわすことで、
     * ログインによって取得したクッキーを維持する
     */
    private HttpClient client;

    /**
     * ログイン
     *
     * @throws IOException
     * @throws HttpException
     */
    public void login() throws HttpException, IOException {
        // 以前書いたので、省略
    }

    /**
     * 会社四季報の「企業概要」を imp_campany に取り込む
     *
     * @param minCd 取り込む企業コードの最小値
     * @param maxCd 取り込む企業コードの最大値
     * @return imp_campany に取り込んだ企業数.例外発生時には、0
     */
    public int importCampany(String minCd, String maxCd) {
        int minNum = Integer.parseInt(minCd);
        int maxNum = Integer.parseInt(maxCd);
        int campanyCnt = 0;
        boolean retrying = false;   // 意図したデータが取得できずに、
                                    // リトライ中のとき true

        try {
            DB db = new DB();   // これは自作クラスです
                                // 詳細は以前のエントリーを参考にしてください

            db.getConnection();
            db.excuteUpdate("TRUNCATE TABLE imp_campany");

            for (int i = minNum; i <= maxNum; ) {
                String cd = Integer.toString(i);
                CampanyContents contents = new CampanyContents(cd);

                try {
                    this.setCampanyContents(contents);
                    this.perseCampanyContents(contents);


                    // 意図したデータが取得できなかったらときは、
                    // ログインし直してもう一度トライ
                    if (this.varidateCampanyContents(contents) == false) {

                        if (retrying == false) {
                            this.login();
                            retrying = true;
                            continue;

                         // リトライしてもダメだったときは諦めて処理を中断する
                        } else {
                            System.out.println("企業コード: " + i + " について企業概要データを取得できませんでした。");
                            return 0;
                        }
                    }

                    String sql = this.importCampanySQL(contents);
                    db.excuteUpdate(sql);

                    campanyCnt++;

                    System.out.println("---次の企業内容を取込みました。---");
                    this.showField(contents);

                    i++;
                    retrying = false;

                // NoCampanyException のときのみ、Forループを止めない
                } catch (NoCampanyException e) {
                    e.printStackTrace();
                    i++;
                    retrying = false;

                // リトライするときは i をインクリメントしないので、
                // finally を使用しない
                }
            }

            db.close();
            return campanyCnt;

        //NoCampanyException 以外の例外は、Forループを止める
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 会社四季報の「企業概要」ページの内容をセットする
     *
     * @throws IOException
     * @throws HttpException
     */
    private void setCampanyContents(CampanyContents contents) throws HttpException, IOException {
        // 以前書いたので、省略
    }

    /**
     * 会社四季報の企業概要ページの内容から企業データを取得する
     *
     * @throws SBI_Exception
     * @throws IOException
     * @throws NumberFormatException
     */
    private void perseCampanyContents(CampanyContents contents)
            throws SBI_Exception, NoCampanyException, NumberFormatException, IOException {
        // 以前書いたので、省略
        // ただし、今回から、取得した値を別クラスのフィールドにセットする用にしている(後述)
    }

    // (以下、後述)

参考書籍

手を抜いているつもりは全然ないのですが、おそらく、もっともっとうまい書き方ができると思います。

特に例外処理について、改善の余地があります。今回はサンプルなので、あまりこだわりすぎたコードは逆に理解が難しくなるのかなぁと思って適当なところで妥協しましたが、もっと本格的に取り組みたい方は、次の書籍がおススメです。もちろん例外処理についても詳細な解説がなされています。

Java魂―プログラミングを極める匠の技

Java魂―プログラミングを極める匠の技

Java の基礎ができている人が対象ですが、何度読み返してもシビれる本です。オライリーの書籍は最初は敷居が高いのですが、読み返す度に新しい発見があります。

サンプルコード(つづき)

以下、つづきです。

    /**
     * 取得した企業内容を確認する
     *
     * @return 企業内容のうち、必須項目にひとつでも null があれば false
     */
    private boolean varidateCampanyContents(CampanyContents contents) {

        // 必須項目
        if(contents.getCampanyCd()  == null) return false;
        if(contents.getCampany()    == null) return false;
        if(contents.getMemoDay()    == null) return false;

        String errorMessage = null;

        // 以下、任意項目
        if(contents.getMarket3() == null) {
            errorMessage += "市場コードを取得できませんでした。";
        }

        if(contents.getMarket() == null) {
            errorMessage += "市場を取得できませんでした。";
        }

        if(contents.getCampanyUrl() == null) {
            errorMessage += "URLを取得できませんでした。";
        }

        if(contents.getGyosyu() == null) {
            errorMessage += "業種を取得できませんでした。";
        }

        if(contents.getKessanMonth() == 0) {
            errorMessage += "決算月を取得できませんでした。";
        }

        if(contents.getTokusyoku() == null) {
            errorMessage += "特色を取得できませんでした。";
        }

        if(contents.getJigyo() == null) {
            errorMessage += "事業を取得できませんでした。";
        }

        if(contents.getMemo() == null) {
            errorMessage += "メモを取得できませんでした。";
        }

        if(contents.getMemo2() == null) {
            errorMessage += "メモ2を取得できませんでした。";
        }

        if(contents.getSource() == null) {
            errorMessage += "情報提供元を取得できませんでした。";
        }

        // エラーメッセージを備考に書き込む
        if(errorMessage != null) {
            contents.setRemark(errorMessage);
        }

        return true;
    }

    /**
     * 取得した企業内容を確認する
     */
    private void showField(CampanyContents contents) {
        System.out.println("campanyCd:   " + contents.getCampanyCd());
        System.out.println("market3:     " + contents.getMarket3());
        System.out.println("campany:     " + contents.getCampany());
        System.out.println("market:      " + contents.getMarket());
        System.out.println("memoDay:     " + contents.getMemoDay());
        System.out.println("campanyUrl:  " + contents.getCampanyUrl());
        System.out.println("gyosyu:      " + contents.getGyosyu());
        System.out.println("kessanMonth: " + contents.getKessanMonth());
        System.out.println("tokusyoku:   " + contents.getTokusyoku());
        System.out.println("jigyo:       " + contents.getJigyo());
        System.out.println("memo:        " + contents.getMemo());
        System.out.println("memo2:       " + contents.getMemo2());
        System.out.println("source:      " + contents.getSource());
        System.out.println("remark:      " + contents.getRemark());
    }

    /**
     * 会社四季報の「企業概要」を imp_campany に取り込むSQLを返す
     */
    private String importCampanySQL(CampanyContents contents) {
        String sql = "INSERT INTO imp_campany("
            + "campany_cd,"
            + "campany,"
            + "market,"
            + "gyosyu,"
            + "campany_url,"
            + "kessan_month,"
            + "tokusyoku,"
            + "jigyo,"
            + "memo_day,"
            + "memo,"
            + "memo2,"
            + "source,"
            + "remark"
        + ")"
        + " VALUES("
            + "'" + contents.getCampanyCd()     + "',"
            + "'" + contents.getCampany()       + "',"
            + "'" + contents.getMarket()        + "',"
            + "'" + contents.getGyosyu()        + "',"
            + "'" + contents.getCampanyUrl()    + "',"
            + contents.getKessanMonth()         + ","
            + "'" + contents.getTokusyoku()     + "',"
            + "'" + contents.getJigyo()         + "',"
            + "'" + contents.getMemoDay()       + "',"
            + "'" + contents.getMemo()          + "',"
            + "'" + contents.getMemo2()         + "',"
            + "'" + contents.getSource()        + "',"
            + "'" + contents.getRemark()        + "'"
        + ")";

        return sql;
    }
}

おまけ

これまではあまり複雑にしたくなかったのでしませんでしたが、取得した企業概要データは別クラスのインスタンスとして扱ったほうがスッキリするので独立させています。

import java.io.BufferedReader;

public class CampanyContents {

    /**
     * 企業コード
     */
    private String campanyCd;

    /**
     * 市場コード JPN,TKY,OSK...
     */
    private String market3 = "JPN";

    /**
     * 企業概要ページの内容
     */
    private BufferedReader campanyContents;

    /**
     * 企業名
     */
    private String campany;

    /**
     * 優先市場
     */
    private String market;

    /**
     * 会社四季報の記事作成日
     */
    private String memoDay;

    /**
     * 企業サイトのURL
     */
    private String campanyUrl;

    /**
     * 会社四季報に掲載されている業種
     */
    private String gyosyu;

    /**
     * 決算月 数字のみ
     */
    private int kessanMonth;

    /**
     * 特色 【特色】は除く
     */
    private String tokusyoku;

    /**
     * 連結事業または単独事業の売上構成
     * 【連結事業】または【単独事業】は除く
     */
    private String jigyo;

    /**
     * 会社四季報のコメント
     */
    private String memo;

    /**
     * 会社四季報のコメント2
     */
    private String memo2;

    /**
     * 情報提供元
     */
    private String source = "会社四季報";

    /**
     * 備考
     */
    private String remark = "特になし";

    /**
     * コンストラクタ
     */
    public CampanyContents(String cd) {
        this.campanyCd = cd;
    }

    /**
     * 市場コードをセットする JPN,TKY,OSK...
     */
    public void setMarket3(String str) {
        this.market3 = str;
    }

    /**
     * 企業概要ページの内容をセットする
     */
    public void setCampanyContents(BufferedReader reader) {
        this.campanyContents = reader;
    }

    /**
     * 企業名をセットする
     */
    public void setCampany(String str) {
        this.campany = str;
    }

    /**
     * 優先市場をセットする
     */
    public void setMarket(String str) {
        this.market = str;
    }

    /**
     * 会社四季報の記事作成日をセットする
     */
    public void setMemoDay(String str) {
        this.memoDay = str;
    }

    /**
     * 企業サイトのURLをセットする
     */
    public void setCampanyUrl(String str) {
        this.campanyUrl = str;
    }

    /**
     * 会社四季報に掲載されている業種をセットする
     */
    public void setGyosyu(String str) {
        this.gyosyu = str;
    }

    /**
     * 決算月をセットする 数字のみ
     */
    public void setKessanMonth(int i) {
        this.kessanMonth = i;
    }

    /**
     * 特色をセットする 【特色】は除く
     */
    public void setTokusyoku(String str) {
        this.tokusyoku = str;
    }

    /**
     * 連結事業または単独事業の売上構成をセットする
     * 【連結事業】または【単独事業】は除く
     */
    public void setJigyo(String str) {
        this.jigyo = str;
    }

    /**
     * 会社四季報のコメントをセットする
     */
    public void setMemo(String str) {
        this.memo = str;
    }

    /**
     * 会社四季報のコメント2をセットする
     */
    public void setMemo2(String str) {
        this.memo2 = str;
    }

    /**
     * 情報提供元をセットする
     */
    public void setSource(String str) {
        this.source = str;
    }

    /**
     * 備考をセットする
     */
    public void setRemark(String str) {
        this.remark = str;
    }

    /**
     * 企業コードを返す
     */
    public String getCampanyCd() {
        return this.campanyCd;
    }

    /**
     * 市場コードを返す JPN,TKY,OSK...
     */
    public String getMarket3() {
        return this.market3;
    }

    /**
     * 企業概要ページの内容を返す
     */
    public BufferedReader getCampanyContents() {
        return this.campanyContents;
    }

    /**
     * 企業名を返す
     */
    public String getCampany() {
        return this.campany;
    }

    /**
     * 優先市場を返す
     */
    public String getMarket() {
        return this.market;
    }

    /**
     * 会社四季報の記事作成日を返す
     */
    public String getMemoDay() {
        return this.memoDay;
    }

    /**
     * 企業サイトのURLを返す
     */
    public String getCampanyUrl() {
        return this.campanyUrl;
    }

    /**
     * 会社四季報に掲載されている業種を返す
     */
    public String getGyosyu() {
        return this.gyosyu;
    }

    /**
     * 決算月を返す 数字のみ
     */
    public int getKessanMonth() {
        return this.kessanMonth;
    }

    /**
     * 特色を返す 【特色】は除く
     */
    public String getTokusyoku() {
        return this.tokusyoku;
    }

    /**
     * 連結事業または単独事業の売上構成を返す
     * 【連結事業】または【単独事業】は除く
     */
    public String getJigyo() {
        return this.jigyo;
    }

    /**
     * 会社四季報のコメントを返す
     */
    public String getMemo() {
        return this.memo;
    }

    /**
     * 会社四季報のコメント2を返す
     */
    public String getMemo2() {
        return this.memo2;
    }

    /**
     * 情報提供元を返す
     */
    public String getSource() {
        return this.source;
    }

    /**
     * 備考を返す
     */
    public String getRemark() {
        return this.remark;
    }
}

このエントリーのつづき

関連エントリー