【WordPress財務データアプリ】DLページ作成

WordPress

前回は、アプリの詳細ページを作成しました。

今回は、個別有価証券報告書のダウンロードページを作成します。

概要

今回も固定ページテンプレートを使います。
URLにdoc_idを含めることで、個別の有価証券報告書を判断します。

基本的な機能としては、

  1. EDINETから対象docIDの有価証券報告書をダウンロード(XBRL,CSV)
  2. XBRLから主要なテキスト項目を取り出して表示
  3. CSVファイルをDataTableで表示、CSV出力

になります。


ポイントとしては、
ダウンロード処理には、時間がかかるので、Ajaxを使って、読み込み画面が表示されるようにしている部分と、
大きな処理はfunctions.phpにまとめていることになります。

全体のコード

今回のコードは、新規固定ページ(financial-data-dl.php)と、functions.phpへの追記になります。

financial-data-dl.php

<?php
/**
 * Template Name: Financial data DL
 */
function get_doc_id_from_url() {
    if (isset($_GET['doc_id'])) {
        return sanitize_text_field($_GET['doc_id']);
    }
    return null;
}

get_header(); 
?>
<link rel="stylesheet" href="https://cdn.datatables.net/1.11.5/css/jquery.dataTables.min.css">

<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

<!-- DataTables JS -->
<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>

<!-- Font Awesome の CDN リンク -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">



<link rel="stylesheet" href="https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.1/css/buttons.dataTables.min.css">

<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.1/js/dataTables.buttons.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.html5.min.js"></script>


<?php
$doc_id = get_doc_id_from_url();
// PDFリンクと別ページリンク
echo '<p><a href="https://disclosure2dl.edinet-fsa.go.jp/searchdocument/pdf/' . esc_attr($doc_id) . '.pdf" target="_blank" class="btn btn-cyan">PDFを新しいページで開く</a></p><br>';
echo '<p><a href="https://disclosure2.edinet-fsa.go.jp/WZEK0040.aspx?' . esc_attr($doc_id) . '" target="_blank" class="btn btn-cyan">EDINETのビューワーで開く</a></p>';



?>
<div id="xbrl-data-container"></div>

<?php
get_footer();
?>


<script>

jQuery(document).ready(function($) {
    var docId = '<?php echo $doc_id; ?>';  // PHP変数をJavaScriptに渡す

    if (docId) {
        // ローディング画面を表示
        $('body').append('<div id="loading-screen" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); color: #fff; display: flex; justify-content: center; align-items: center; z-index: 1000;">Loading...</div>');

        // 非同期でデータを取得
        $.ajax({
            url: '<?php echo esc_js(admin_url('admin-ajax.php')); ?>',
            type: 'POST',
            dataType: 'json',
            data: {
                action: 'get_xbrl_data',
                doc_id: docId
            },
            success: function(response) {
                if (response.success) {
                    // 取得したHTMLを挿入
                    $('#xbrl-data-container').html(response.data.html);

                    // 取得したJavaScriptを実行
                    eval(response.data.script);
                } else {
                    console.error(response.data);
                }
            },
            error: function(xhr, status, error) {
                console.error(error);
            },
            complete: function() {
                // ローディング画面を非表示
                $('#loading-screen').remove();
            }
        });
    } else {
        console.log('doc_idが指定されていません。');
    }

    // イベント委譲を使用して、#csv-exportボタンがクリックされたときに処理を実行
    // CSV出力のためのJavaScript
    $(document).on('click', '#csv-export', function(e) {
        e.preventDefault();

        let table = $('#data-table').DataTable();
        let csv = [];

        // テーブルヘッダーの取得
        let headers = table.columns().header().toArray().map(header => '"' + $(header).text().replace(/"/g, '""') + '"');
        csv.push(headers.join(','));

        // 全データの取得
        let data = table.data().toArray();
        data.forEach(row => {
            let rowData = row.map(cell => '"' + cell.replace(/"/g, '""') + '"');
            csv.push(rowData.join(','));
        });

        // Blobオブジェクトの作成とダウンロード
        let csvContent = csv.join('\n');
        let blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
        let link = document.createElement('a');
        if (link.download !== undefined) { // feature detection
            let url = URL.createObjectURL(blob);
            link.setAttribute('href', url);
            link.setAttribute('download', 'data.csv');
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            URL.revokeObjectURL(url);
        }
    });




    // アコーディオンの初期設定
    // ドキュメント全体に対してクリックイベントを委譲
    $(document).on('click', '.accordion-header', function() {
        var $header = $(this);
        var $content = $header.next('.accordion-content');

        $header.toggleClass('active'); // activeクラスをトグル
        $content.toggleClass('show');  // showクラスをトグル

        // 展開された要素以外を閉じる処理
        $('.accordion-header').not($header).removeClass('active');
        $('.accordion-content').not($content).removeClass('show');
    });



});


</script>
<style>
    #xbrl-data-container {
        padding: 0 10px; /* モバイル向けのデフォルトのパディング */
    }

    @media (min-width: 768px) {
        #xbrl-data-container {
            padding: 0 100px; /* PC向けのパディング */
        }
    }

    /* アコーディオンのスタイル */
    .accordion {
        margin-bottom: 1em;
    }


    .accordion-header {
        cursor: pointer;
        background-color: #f1f1f1;
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
        position: relative; /* アイコンの位置を調整するため */
        padding-right: 40px; /* アイコンのスペースを確保 */
    }

    .accordion-header::before {
        content: "\f078"; /* Font Awesomeの矢印アイコン(展開) */
        font-family: 'Font Awesome 5 Free';
        font-weight: 900;
        margin-right: 10px;
        transition: transform 0.3s ease;
        position: absolute;
        right: 10px; /* 右端に配置 */
        top: 50%;
        transform: translateY(-50%);
    }

    .accordion-header.collapsed::before {
        content: "\f077"; /* Font Awesomeの矢印アイコン(閉じる) */
        transform: rotate(-180deg); /* 矢印を180度回転 */
    }

    .accordion-content {
        display: none; /* 初期状態では非表示 */
        padding: 10px;
        border: 1px solid #ddd;
        border-top: none;
        border-radius: 0 0 5px 5px;
        max-height: 500px; /* 最大高さを設定 */
        overflow-y: auto; /* コンテンツがはみ出す場合にスクロールバーを表示 */
        background-color: #fff; /* 背景色を設定 */
    }

    .accordion-content.show {
        display: block; /* 展開時に表示 */
    }

    .accordion-header.active::before {
        transform: rotate(180deg); /* 展開時に矢印を回転 */
    }
    .table-container {
        width: 100%;
        margin-bottom: 20px;
        overflow-x: auto;  /* 横スクロールを有効にする */
    }

    .dataTables_wrapper {
        width: 100%;
        overflow-x: auto;  /* DataTablesのラッパーで横スクロールを有効にする */
    }

    table.dataTable {
        width: 100%;
        margin: 0 auto; /* テーブルの中央配置 */
    }

    th, td {
        white-space: nowrap; /* テキストの折り返しを防ぐ */
    }


</style>

functions.php


// XBRLファイルの読み込みが完了した後に、フォルダ内のファイルを削除
function delete_directory($dir) {
    if (!file_exists($dir)) {
        return;
    }
    $files = glob($dir . '/*'); // ディレクトリ内のすべてのファイルを取得
    foreach ($files as $file) {
        if (is_dir($file)) {
            delete_directory($file); // サブディレクトリの場合は再帰的に削除
        } else {
            unlink($file); // ファイルを削除
        }
    }
    rmdir($dir); // ディレクトリ自体を削除
}
function display_xbrl_element($xml) {
    // ネームスペースの取得
    $namespaces = $xml->getDocNamespaces();
    $jpcrp_namespace = $namespaces['jpcrp_cor']; // jpcrp_corのネームスペースを取得

    // ラベルとコンセプトIDの組み合わせを指定
    $concepts = [
        '事業の内容' => 'DescriptionOfBusinessTextBlock',
        '沿革' => 'CompanyHistoryTextBlock',
        '経営方針、経営環境及び対処すべき課題等' => 'BusinessPolicyBusinessEnvironmentIssuesToAddressEtcTextBlock',
        '事業等のリスク' => 'BusinessRisksTextBlock',
        '重要事象等の内容、分析及び対応策、事業等のリスク' => 'MaterialMattersRelatingToGoingConcernEtcBusinessRisksTextBlock',
        '経営者による財政状態、経営成績及びキャッシュ・フローの状況の分析' => 'ManagementAnalysisOfFinancialPositionOperatingResultsAndCashFlowsTextBlock',
        '経営上の重要な契約等' => 'CriticalContractsForOperationTextBlock',
        '研究開発活動' => 'ResearchAndDevelopmentActivitiesTextBlock',
        '設備投資等の概要' => 'OverviewOfCapitalExpendituresEtcTextBlock',
        '主要な設備の状況' => 'MajorFacilitiesTextBlock',
        '設備の新設、除却等の計画' => 'PlannedAdditionsRetirementsEtcOfFacilitiesTextBlock',
        '自己株式の取得等の状況' => 'AcquisitionsEtcOfTreasurySharesTextBlock',
        '配当政策' => 'DividendPolicyTextBlock',
        '連結貸借対照表' => 'ConsolidatedBalanceSheetTextBlock',
        '連結財政状態計算書(JMIS)' => 'ConsolidatedStatementOfFinancialPositionJMISTextBlock',
        '連結貸借対照表 (US GAAP)' => 'ConsolidatedBalanceSheetUSGAAPTextBlock',
        '連結損益計算書' => 'ConsolidatedStatementOfIncomeTextBlock',
        '連結損益計算書(JMIS)' => 'ConsolidatedStatementOfIncomeJMISTextBlock',
        '連結損益計算書(US GAAP)' => 'ConsolidatedStatementOfIncomeUSGAAPTextBlock',
        '連結包括利益計算書' => 'ConsolidatedStatementOfComprehensiveIncomeTextBlock',
        '連結包括利益計算書(2計算書)(JMIS)' => 'ConsolidatedStatementOfComprehensiveIncomeJMISTextBlock',
        '連結包括利益計算書(US GAAP)' => 'ConsolidatedStatementOfComprehensiveIncomeUSGAAPTextBlock',
        '連結損益及び包括利益計算書' => 'ConsolidatedStatementOfComprehensiveIncomeSingleStatementTextBlock',
        '連結包括利益計算書(1計算書)(JMIS)' => 'ConsolidatedStatementOfComprehensiveIncomeSingleStatementJMISTextBlock',
        '連結損益及び包括利益計算書(US GAAP)' => 'ConsolidatedStatementOfComprehensiveIncomeSingleStatementUSGAAPTextBlock',
        '連結株主資本等変動計算書' => 'ConsolidatedStatementOfChangesInEquityTextBlock',
        '連結持分変動計算書(JMIS)' => 'ConsolidatedStatementOfChangesInEquityJMISTextBlock',
        '連結資本勘定計算書(US GAAP)' => 'ConsolidatedStatementOfEquityUSGAAPTextBlock',
        '連結キャッシュ・フロー計算書' => 'ConsolidatedStatementOfCashFlowsTextBlock',
        '連結キャッシュ・フロー計算書(JMIS)' => 'ConsolidatedStatementOfCashFlowsJMISTextBlock',
        '連結キャッシュ・フロー計算書(US GAAP)' => 'ConsolidatedStatementOfCashFlowsUSGAAPTextBlock',
        '継続企業の前提に関する事項、連結財務諸表' => 'NotesUncertaintiesOfEntitysAbilityToContinueAsGoingConcernConsolidatedFinancialStatementsTextBlock',
        '連結財務諸表作成のための基本となる重要な事項' => 'NotesSignificantAccountingPoliciesForPreparationOfConsolidatedFinancialStatementsTextBlock',
        '株主資本等変動計算書' => 'StatementOfChangesInEquityTextBlock',
        'キャッシュ・フロー計算書' => 'StatementOfCashFlowsTextBlock',
        '継続企業の前提に関する事項、財務諸表' => 'NotesUncertaintiesOfEntitysAbilityToContinueAsGoingConcernFinancialStatementsTextBlock',
        'サステナビリティに関する考え方及び取組' => 'DisclosureOfSustainabilityRelatedFinancialInformationTextBlock',
        'ガバナンス' => 'GovernanceTextBlock',
        '人材の育成及び社内環境整備に関する方針、戦略' => 'PolicyOnDevelopmentOfHumanResourcesAndInternalEnvironmentStrategyTextBlock',
        'リスク管理' => 'RiskManagementTextBlock',
    ];



    // 出力用の変数
    $output = '';

    // 各ラベルとコンセプトIDについてループ処理
    foreach ($concepts as $label => $concept_id) {
        $found = false; // 要素が見つかったかどうかのフラグ

        // 指定されたネームスペースの子要素を検索
        foreach ($xml->children($jpcrp_namespace) as $element) {
            // 要素の名前が指定されたconcept_idと一致するかをチェック
            if ($element->getName() == $concept_id) {
                $output .= '<div class="accordion">';
                $output .= '<h3 class="accordion-header">' . htmlspecialchars($label) . '</h3>';
                $output .= '<div class="accordion-content">' . preg_replace('/(<br\s*\/?>\s*){2,}/i', '<br>', preg_replace('/(&nbsp;)+/', ' ', (string)$element)) . '</div>';
                $output .= '</div>';
                $found = true;
                break; // 該当の要素が見つかったらループを抜ける
            }
        }

        // 要素が見つからなかった場合の処理
        // if (!$found) {
        //     $output .= '<div class="accordion">';
        //     $output .= '<h3 class="accordion-header">' . htmlspecialchars($label) . '</h3>';
        //     $output .= '<div class="accordion-content">情報が見つかりませんでした。</div>';
        //     $output .= '</div>';
        // }
    }

    return $output;
}
function show_xbrl_data($doc_id){
    // データのダウンロード (XBRL)
    $download_result = download_and_extract_zip($doc_id, '1'); // 例: type = 1
    echo '<p>' . esc_html($download_result) . '</p>';

    // データの読み込み (XBRL)
    $upload_dir = wp_upload_dir(); // WordPressのアップロードディレクトリを取得
    $xbrl_file_pattern =  sys_get_temp_dir() . '/' . $doc_id . '/XBRL/PublicDoc/*.xbrl';
    $xbrl_files = glob($xbrl_file_pattern);

    if (empty($xbrl_files)) {
        echo '<p>XBRL ファイルが見つかりませんでした。</p>';
    } else {
        $file_path = $xbrl_files[0];
        $xml = simplexml_load_file($file_path);
        if ($xml === false) {
            echo '<p>XBRLファイルを読み込めませんでした。</p>';
        } else {
            // 必要なデータの表示 (XBRL)
            echo display_xbrl_element($xml);

        }
    }

}
function show_csv_data($doc_id){
    // CSVのダウンロード
    $csv_download_result = download_and_extract_zip($doc_id, '5'); // CSVファイルのダウンロード (type = 5)
    echo '<p>' . esc_html($csv_download_result) . '</p>';

    // CSVファイルの読み込み
    $csv_file_pattern =  sys_get_temp_dir()  . '/' . $doc_id . '/XBRL_TO_CSV/jpcrp*.csv'; // ダウンロードしたCSVファイルのパスパターン
   // CSVファイルの表示部分
    $csv_files = glob($csv_file_pattern);


    if (empty($csv_files)) {
        echo '<p>CSV ファイルが見つかりませんでした。</p>';
    } else {
        $csv_file_path = $csv_files[0];

        // ファイル全体を読み込んでUTF-16からUTF-8に変換
        $file_contents = file_get_contents($csv_file_path);
        $utf8_contents = mb_convert_encoding($file_contents, 'UTF-8', 'UTF-16');

        // 変換後の内容を行ごとに分割
        $rows = explode(PHP_EOL, $utf8_contents);

        // テーブルの開始タグ
        echo '<table id="data-table" class="display" style="width:100%">';
        echo '<thead><tr>';

        // ヘッダー行を作成
        $header = explode("\t", array_shift($rows)); // 最初の行をヘッダーと仮定
        foreach ($header as $head) {
            echo '<th>' . htmlspecialchars($head) . '</th>';
        }
        echo '</tr></thead><tbody>';


        $num_columns = count($header);
        foreach ($rows as $row) {
            $cells = explode("\t", $row);

            // 空の行やヘッダーと列数が一致しない行を無視
            if (empty(trim($row)) || count($cells) != $num_columns) {
                continue;
            }



            echo '<tr>';
            foreach ($cells as $cell) {
                echo '<td>' . htmlspecialchars($cell) . '</td>';
            }
            echo '</tr>';
        }


        echo '</tbody></table>';
    }

    echo '<p><a href="#" id="csv-export" class="btn btn-cyan">CSVを出力する</a></p>';

}
function download_and_extract_zip($doc_id, $type) {
    $api_key = defined('EDINET_API') ? EDINET_API : '';
    $url = "https://api.edinet-fsa.go.jp/api/v2/documents/$doc_id?type=$type&Subscription-Key=$api_key"; // URLにAPIキーをクエリパラメータとして追加

    // 一時ファイルを作成
    $temp_zip_path = wp_tempnam($doc_id . '.zip'); // 一時ファイルパスを取得

    // 一時ディレクトリへの解凍先ディレクトリを作成
    $temp_dir = sys_get_temp_dir() . '/' . $doc_id;
    if (!file_exists($temp_dir)) {
        mkdir($temp_dir, 0755, true);
    }

    // ダウンロード
    $response = wp_remote_get($url, array(
        'timeout' => 30, // タイムアウトを30秒に設定
        'stream' => true,
        'filename' => $temp_zip_path,
    ));

    if (is_wp_error($response)) {
        $error_message = $response->get_error_message();
        return "ZIP ファイルのダウンロードに失敗しました: $error_message";
    }

    // HTTP ステータスコードの確認
    $status_code = wp_remote_retrieve_response_code($response);
    if ($status_code != 200) {
        // エラーメッセージの取得
        $body = wp_remote_retrieve_body($response);
        return "ZIP ファイルのダウンロードに失敗しました。HTTP ステータスコード: $status_code。レスポンス: $body";
    }

    // 解凍
    $zip = new ZipArchive();
    $res = $zip->open($temp_zip_path);

    if ($res === TRUE) {
        // 全てのファイルを解凍
        $zip->extractTo($temp_dir);
        $zip->close();

        // ZIPファイルを削除(オプション)
        unlink($temp_zip_path);

        return ""; // 成功時には空文字を返す
    } else {
        // エラーコードを取得
        switch ($res) {
            case ZipArchive::ER_NOZIP:
                $error = 'ZIP ファイルではありません';
                break;
            case ZipArchive::ER_INCONS:
                $error = 'ZIP ファイルが壊れています';
                break;
            case ZipArchive::ER_CRC:
                $error = 'CRC エラー';
                break;
            default:
                $error = '不明なエラー';
                break;
        }
        return "ZIP ファイルの解凍に失敗しました: $error";
    }
}

function download_and_extract_zip_old($doc_id, $type) {
    $api_key = defined('EDINET_API') ? EDINET_API : '';
    $url = "https://api.edinet-fsa.go.jp/api/v2/documents/$doc_id?type=$type&Subscription-Key=$api_key"; // URLにAPIキーをクエリパラメータとして追加

    $filename = $doc_id . '.zip';
    $upload_dir = wp_upload_dir(); // WordPressのアップロードディレクトリを取得
    $zip_path = $upload_dir['path'] . '/' . $filename; // ダウンロード先のパス
    $extract_dir = $upload_dir['path'] . '/' . $doc_id; // 解凍先のディレクトリ

    // ダウンロード
    $response = wp_remote_get($url, array(
        'timeout' => 30, // タイムアウトを30秒に設定
        'stream' => true,
        'filename' => $zip_path,
    ));

    if (is_wp_error($response)) {
        $error_message = $response->get_error_message();
        return "ZIP ファイルのダウンロードに失敗しました: $error_message";
    }

    // HTTP ステータスコードの確認
    $status_code = wp_remote_retrieve_response_code($response);
    if ($status_code != 200) {
        // エラーメッセージの取得
        $body = wp_remote_retrieve_body($response);
        return "ZIP ファイルのダウンロードに失敗しました。HTTP ステータスコード: $status_code。レスポンス: $body";
    }

    // 解凍
    $zip = new ZipArchive();
    $res = $zip->open($zip_path);

    if ($res === TRUE) {
        // 解凍先ディレクトリを作成(存在しない場合)
        if (!file_exists($extract_dir)) {
            mkdir($extract_dir, 0755, true);
        }

        // 全てのファイルを解凍
        $zip->extractTo($extract_dir);
        $zip->close();

        // ZIPファイルを削除(オプション)
        unlink($zip_path);

        return "";
    } else {
        // エラーコードを取得
        switch ($res) {
            case ZipArchive::ER_NOZIP:
                $error = 'ZIP ファイルではありません';
                break;
            case ZipArchive::ER_INCONS:
                $error = 'ZIP ファイルが壊れています';
                break;
            case ZipArchive::ER_CRC:
                $error = 'CRC エラー';
                break;
            default:
                $error = '不明なエラー';
                break;
        }
        return "ZIP ファイルの解凍に失敗しました: $error";
    }
}

// XBRLデータの取得処理
function get_xbrl_data() {
    if (isset($_POST['doc_id'])) {
        $doc_id = sanitize_text_field($_POST['doc_id']);

        // XBRLデータを表示
        ob_start();
        show_xbrl_data($doc_id);
        show_csv_data($doc_id);
        $output = ob_get_clean();

        // フォルダ全体を削除
        $upload_dir = wp_upload_dir();
        $doc_dir = sys_get_temp_dir() . '/' . $doc_id . '/';
        delete_directory($doc_dir);

        // 結果とJavaScriptコードをJSONで返す
        wp_send_json_success([
            'html' => $output,
            'script' => "
                $(document).ready(function() {
                    $('#data-table').DataTable({
                        'scrollY': '500px',
                        'scrollX': true,
                        'scrollCollapse': true,
                        'paging': true,
                        'searching': true,
                        'ordering': true,
                        'info': true
                    });
                });
            "
        ]);
    } else {
        wp_send_json_error('doc_idが指定されていません。');
    }
}
add_action('wp_ajax_get_xbrl_data', 'get_xbrl_data');
add_action('wp_ajax_nopriv_get_xbrl_data', 'get_xbrl_data');

EDINET APIの設定

このページではEDINET APIを使用します。
APIの情報はwp-config.phpに保存します。以下の行を追加します。

define('EDINET_API', "APIキー");

APIキーの取得については、以下の記事を参考にしてください。

financial-data-dl.phpの詳細

このコードは、WordPressのカスタムテンプレートとして作成されたもので、特定のドキュメントID (doc_id) を使って財務データを表示し、ダウンロード機能などを提供するものです。

テンプレート名の設定と関数定義

<?php
/**
 * Template Name: Financial data DL
 */
function get_doc_id_from_url() {
    if (isset($_GET['doc_id'])) {
        return sanitize_text_field($_GET['doc_id']);
    }
    return null;
}
?>
  • Template Name: Financial data DL: このコメントは、WordPressがこのファイルを「Financial data DL」という名前のテンプレートとして認識するためのものです。
  • get_doc_id_from_url 関数: この関数は、URLのクエリパラメータ doc_id を取得し、その値を安全に取り扱うために sanitize_text_field 関数でサニタイズします。もし doc_id がURLに存在しない場合は null を返します。

ヘッダーとスタイル、スクリプトの読み込み

<?php get_header(); ?>
<link rel="stylesheet" href="https://cdn.datatables.net/1.11.5/css/jquery.dataTables.min.css">

<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

<!-- DataTables JS -->
<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>

<!-- Font Awesome の CDN リンク -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
  • get_header(): WordPressのヘッダー部分をテンプレートに追加します。
  • CSSとJSの読み込み:
  • DataTables は、データテーブルを簡単に管理・表示するためのライブラリです。
  • jQuery は、JavaScriptライブラリで、DOM操作やイベント処理を簡単にするためのものです。
  • Font Awesome は、アイコンフォントライブラリで、アイコンを簡単に表示できます。

テンプレートの内容(ドキュメントIDのリンク表示)

<?php
$doc_id = get_doc_id_from_url();
// PDFリンクと別ページリンク
echo '<p><a href="https://disclosure2dl.edinet-fsa.go.jp/searchdocument/pdf/' . esc_attr($doc_id) . '.pdf" target="_blank" class="btn btn-cyan">PDFを新しいページで開く</a></p><br>';
echo '<p><a href="https://disclosure2.edinet-fsa.go.jp/WZEK0040.aspx?' . esc_attr($doc_id) . '" target="_blank" class="btn btn-cyan">EDINETのビューワーで開く</a></p>';
?>
  • $doc_id: 上で定義した get_doc_id_from_url 関数を使って取得した doc_id を格納します。
  • PDFリンクと別ページリンク: doc_id を用いて、PDFファイルへのリンクとEDINETビューワーへのリンクを生成し、それを表示します。

データ表示エリアとフッター

<div id="xbrl-data-container"></div>

<?php get_footer(); ?>
  • <div id="xbrl-data-container"></div>: この <div> は、後でJavaScriptによって財務データが表示される場所です。
  • get_footer(): WordPressのフッター部分をテンプレートに追加します。

JavaScriptの処理(データ取得、CSVエクスポート、アコーディオン)

<script>

jQuery(document).ready(function($) {
    var docId = '<?php echo $doc_id; ?>';  // PHP変数をJavaScriptに渡す

    if (docId) {
        // ローディング画面を表示
        $('body').append('<div id="loading-screen" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); color: #fff; display: flex; justify-content: center; align-items: center; z-index: 1000;">Loading...</div>');

        // 非同期でデータを取得
        $.ajax({
            url: '<?php echo esc_js(admin_url('admin-ajax.php')); ?>',
            type: 'POST',
            dataType: 'json',
            data: {
                action: 'get_xbrl_data',
                doc_id: docId
            },
            success: function(response) {
                if (response.success) {
                    // 取得したHTMLを挿入
                    $('#xbrl-data-container').html(response.data.html);

                    // 取得したJavaScriptを実行
                    eval(response.data.script);
                } else {
                    console.error(response.data);
                }
            },
            error: function(xhr, status, error) {
                console.error(error);
            },
            complete: function() {
                // ローディング画面を非表示
                $('#loading-screen').remove();
            }
        });
    } else {
        console.log('doc_idが指定されていません。');
    }

    // CSV出力のためのJavaScript
    $(document).on('click', '#csv-export', function(e) {
        e.preventDefault();

        let table = $('#data-table').DataTable();
        let csv = [];

        // テーブルヘッダーの取得
        let headers = table.columns().header().toArray().map(header => '"' + $(header).text().replace(/"/g, '""') + '"');
        csv.push(headers.join(','));

        // 全データの取得
        let data = table.data().toArray();
        data.forEach(row => {
            let rowData = row.map(cell => '"' + cell.replace(/"/g, '""') + '"');
            csv.push(rowData.join(','));
        });

        // Blobオブジェクトの作成とダウンロード
        let csvContent = csv.join('\n');
        let blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
        let link = document.createElement('a');
        if (link.download !== undefined) { // feature detection
            let url = URL.createObjectURL(blob);
            link.setAttribute('href', url);
            link.setAttribute('download', 'data.csv');
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            URL.revokeObjectURL(url);
        }
    });

    // アコーディオンの初期設定
    $(document).on('click', '.accordion-header', function() {
        var $header = $(this);
        var $content = $header.next('.accordion-content');

        $header.toggleClass('active'); // activeクラスをトグル
        $content.toggleClass('show');  // showクラスをトグル

        // 展開された要素以外を閉じる処理
        $('.accordion-header').not($header).removeClass('active');
        $('.accordion-content').not($content).removeClass('show');
    });

});
</script>
  • データ取得:
  • docId を使用して、admin-ajax.php に対して POST リクエストを送り、サーバーから財務データを取得します。
  • 成功時には、取得したデータを指定された <div> に挿入し、必要なJavaScriptを実行します。

  • CSVエクスポート:

  • #csv-export ボタンがクリックされると、DataTables のデータをCSVフォーマットに変換し、ダウンロードします。

  • アコーディオン:

  • アコーディオンの動作を設定し、クリックで展開・折りたたみができるようにします。

スタイル設定

<style>
    #xbrl-data-container {
        padding: 0 10px; /* モバイル向けのデフォルトのパディング */
    }

    @media (min-width: 768px) {
        #xbrl-data-container {
            padding: 0 100px; /* PC向けのパディング */
        }
    }

    /* アコーディオンのスタイル */
    .accordion {
        margin-bottom: 1em;
    }
    .accordion-header {
        cursor: pointer;
        background-color: #f1f1f1;
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
        position: relative; /* アイコンの位置を調整するため */
        padding-right: 40px; /* アイコンのスペースを確保 */
    }
    .accordion-header::before

 {
        content: '\f068'; /* Font Awesome の折りたたみアイコン */
        font-family: FontAwesome;
        position: absolute;
        right: 10px;
        top: 50%;
        transform: translateY(-50%);
        font-size: 1.2em;
    }
    .accordion-header.active::before {
        content: '\f067'; /* Font Awesome の展開アイコン */
    }
    .accordion-content {
        display: none;
        padding: 10px;
        border: 1px solid #ddd;
        border-top: none;
        border-radius: 0 0 5px 5px;
    }
    .accordion-content.show {
        display: block;
    }
</style>
  • #xbrl-data-container: モバイルとPCで異なるパディングを設定しています。
  • アコーディオン:
  • .accordion-header: アコーディオンのヘッダー部分で、クリックできるスタイルに設定されています。
  • .accordion-content: アコーディオンの内容部分で、デフォルトでは非表示になっています。show クラスが付くと表示されます。
  • Font Awesome アイコン: 折りたたみと展開のアイコンが使用されています。

functions.phpの詳細

このコードは、XBRL(eXtensible Business Reporting Language)ファイルとCSVファイルを処理し、WordPressサイトで表示するためのものです。

ディレクトリの削除

function delete_directory($dir) {
    if (!file_exists($dir)) {
        return;
    }
    $files = glob($dir . '/*'); // ディレクトリ内のすべてのファイルを取得
    foreach ($files as $file) {
        if (is_dir($file)) {
            delete_directory($file); // サブディレクトリの場合は再帰的に削除
        } else {
            unlink($file); // ファイルを削除
        }
    }
    rmdir($dir); // ディレクトリ自体を削除
}
  • delete_directory 関数は指定されたディレクトリを削除します。
  • まず、指定されたディレクトリが存在するか確認します。存在しない場合は何もしません。
  • 次に、そのディレクトリ内のすべてのファイルとサブディレクトリを取得します。
  • 各ファイルやサブディレクトリに対して、サブディレクトリがあれば再帰的に削除し、ファイルならば削除します。
  • 最後に、ディレクトリ自体を削除します。

XBRL要素の表示

function display_xbrl_element($xml) {
    $namespaces = $xml->getDocNamespaces();
    $jpcrp_namespace = $namespaces['jpcrp_cor'];

    $concepts = [
        '事業の内容' => 'DescriptionOfBusinessTextBlock',
        // ...他のラベルとコンセプトID
    ];

    $output = '';

    foreach ($concepts as $label => $concept_id) {
        $found = false;

        foreach ($xml->children($jpcrp_namespace) as $element) {
            if ($element->getName() == $concept_id) {
                $output .= '<div class="accordion">';
                $output .= '<h3 class="accordion-header">' . htmlspecialchars($label) . '</h3>';
                $output .= '<div class="accordion-content">' . preg_replace('/(<br\s*\/?>\s*){2,}/i', '<br>', preg_replace('/(&nbsp;)+/', ' ', (string)$element)) . '</div>';
                $output .= '</div>';
                $found = true;
                break;
            }
        }
    }

    return $output;
}
  • display_xbrl_element 関数は、XBRL XMLデータから特定の要素を抽出し、HTMLを生成します。
  • まず、XMLのネームスペースを取得し、jpcrp_cor というネームスペースを選択します。
  • 次に、ラベルとコンセプトIDのペアを定義します。
  • 各ラベルについて、指定されたネームスペース内の要素を検索し、コンセプトIDと一致する要素を見つけた場合、その内容をHTMLのアコーディオン形式で表示します。
  • 結果は $output に格納され、最終的に返されます。

XBRLデータの表示

function show_xbrl_data($doc_id){
    $download_result = download_and_extract_zip($doc_id, '1');
    echo '<p>' . esc_html($download_result) . '</p>';

    $upload_dir = wp_upload_dir();
    $xbrl_file_pattern =  sys_get_temp_dir() . '/' . $doc_id . '/XBRL/PublicDoc/*.xbrl';
    $xbrl_files = glob($xbrl_file_pattern);

    if (empty($xbrl_files)) {
        echo '<p>XBRL ファイルが見つかりませんでした。</p>';
    } else {
        $file_path = $xbrl_files[0];
        $xml = simplexml_load_file($file_path);
        if ($xml === false) {
            echo '<p>XBRLファイルを読み込めませんでした。</p>';
        } else {
            echo display_xbrl_element($xml);
        }
    }
}
  • show_xbrl_data 関数は、指定されたドキュメントIDに基づいてXBRLデータを表示します。
  • まず、download_and_extract_zip 関数を使ってXBRLファイルをダウンロードし、解凍します。
  • 次に、解凍されたXBRLファイルを検索し、見つからない場合はメッセージを表示します。
  • ファイルが見つかった場合、そのファイルを読み込んで、display_xbrl_element 関数を使って内容を表示します。

CSVデータの表示

function show_csv_data($doc_id){
    $csv_download_result = download_and_extract_zip($doc_id, '5');
    echo '<p>' . esc_html($csv_download_result) . '</p>';

    $csv_file_pattern =  sys_get_temp_dir()  . '/' . $doc_id . '/XBRL_TO_CSV/jpcrp*.csv';
    $csv_files = glob($csv_file_pattern);

    if (empty($csv_files)) {
        echo '<p>CSV ファイルが見つかりませんでした。</p>';
    } else {
        $csv_file_path = $csv_files[0];
        $file_contents = file_get_contents($csv_file_path);
        $utf8_contents = mb_convert_encoding($file_contents, 'UTF-8', 'UTF-16');

        $rows = explode(PHP_EOL, $utf8_contents);

        echo '<table id="data-table" class="display" style="width:100%">';
        echo '<thead><tr>';

        $header = explode("\t", array_shift($rows));
        foreach ($header as $head) {
            echo '<th>' . htmlspecialchars($head) . '</th>';
        }
        echo '</tr></thead><tbody>';

        $num_columns = count($header);
        foreach ($rows as $row) {
            $cells = explode("\t", $row);

            if (empty(trim($row)) || count($cells) != $num_columns) {
                continue;
            }

            echo '<tr>';
            foreach ($cells as $cell) {
                echo '<td>' . htmlspecialchars($cell) . '</td>';
            }
            echo '</tr>';
        }

        echo '</tbody></table>';
    }

    echo '<p><a href="#" id="csv-export" class="btn btn-cyan">CSVを出力する</a></p>';
}
  • show_csv_data 関数は、CSVデータを表示します。
  • XBRLファイルと同様に、CSVファイルをダウンロードし解凍します。
  • CSVファイルの内容をUTF-16からUTF-8に変換し、行ごとに分割します。
  • テーブルのヘッダーを作成し、CSVの内容をテーブルとして表示します。
  • テーブルの下にCSVを出力するためのボタンを追加します。

ZIPファイルのダウンロードと解凍

function download_and_extract_zip($doc_id, $type) {
    $api_key = defined('EDINET_API') ? EDINET_API : '';
    $url = "https://api.edinet-fsa.go.jp/api/v2/documents/$doc_id?type=$type&Subscription-Key=$api_key";

    $temp_zip_path = wp_tempnam($doc_id . '.zip');

    $temp_dir = sys_get_temp_dir() . '/' . $doc_id;
    if (!file_exists($temp_dir)) {
        mkdir($temp_dir, 0755, true);
    }

    $response = wp_remote_get($url, array(
        'timeout' => 30,
        'stream' => true,
        'filename' => $temp_zip_path,
    ));

    if (is_wp_error($response)) {
        $error_message = $response->get_error_message();
        return "ZIP ファイルのダウンロードに失敗しました: $error_message";
    }

    $status_code = wp_remote_retrieve_response_code($response);
    if ($status_code != 200) {
        $body = wp_remote_retrieve_body($response);
        return "ZIP ファイルのダウンロードに失敗しました。HTTP ステータスコード: $status_code。レスポンス: $body";
    }

    $zip = new ZipArchive();
    $res = $zip->open($temp_zip_path);

    if ($res === TRUE) {
        $zip->extractTo($temp_dir);
        $zip->close();
        unlink($temp_zip_path);
        return "";
    } else {
        switch ($res) {
            case ZipArchive::ER_NOZIP:
                $error = 'ZIP ファイルではありません';
                break;
            case ZipArchive::ER_INCONS:
                $error = 'ZIP ファイルが壊れています';
                break;
            case ZipArchive::ER_CRC:
                $error = 'CRC エ

ラー';
                break;
            default:
                $error = 'ZIP ファイルの解凍中にエラーが発生しました';
                break;
        }
        return "ZIP ファイルの解凍に失敗しました: $error";
    }
}
  • download_and_extract_zip 関数は、ZIPファイルをダウンロードして解凍します。
  • APIキーとドキュメントIDを使ってZIPファイルのURLを構築し、WordPressの wp_remote_get 関数でダウンロードします。
  • ダウンロードが成功した場合、ZIPファイルを指定されたディレクトリに解凍し、ZIPファイルを削除します。
  • エラーメッセージがあればそれを返します。

Ajaxを使った表示処理

// XBRLデータの取得処理
function get_xbrl_data() {
    // POSTリクエストからdoc_idを取得し、サニタイズする
    if (isset($_POST['doc_id'])) {
        $doc_id = sanitize_text_field($_POST['doc_id']);

        // XBRLデータとCSVデータを表示する
        ob_start();
        show_xbrl_data($doc_id); // XBRLデータの表示
        show_csv_data($doc_id);  // CSVデータの表示
        $output = ob_get_clean(); // 出力を取得し、バッファをクリア

        // フォルダ全体を削除
        $upload_dir = wp_upload_dir();
        $doc_dir = sys_get_temp_dir() . '/' . $doc_id . '/';
        delete_directory($doc_dir); // 一時ファイルやディレクトリを削除

        // 結果とJavaScriptコードをJSONで返す
        wp_send_json_success([
            'html' => $output, // HTML出力
            'script' => "
                $(document).ready(function() {
                    $('#data-table').DataTable({
                        'scrollY': '500px',
                        'scrollX': true,
                        'scrollCollapse': true,
                        'paging': true,
                        'searching': true,
                        'ordering': true,
                        'info': true
                    });
                });
            "
        ]);
    } else {
        wp_send_json_error('doc_idが指定されていません。'); // doc_idが指定されていない場合のエラー
    }
}

// Ajaxリクエストを処理するアクションフック
add_action('wp_ajax_get_xbrl_data', 'get_xbrl_data');
add_action('wp_ajax_nopriv_get_xbrl_data', 'get_xbrl_data');
  1. get_xbrl_data 関数:
    $_POST['doc_id'] がセットされているかを確認し、セットされていればその値を取得してサニタイズします。
    show_xbrl_data($doc_id)show_csv_data($doc_id) 関数を呼び出し、XBRLデータとCSVデータを表示します。
    ob_start()ob_get_clean() を使って、表示されたデータをバッファから取得します。
    – 一時ファイルを削除するために delete_directory($doc_dir) を呼び出します。
    wp_send_json_success() を使って、取得したHTMLとJavaScriptコードをJSON形式で返します。このJSONは、Ajaxリクエストの成功応答としてクライアントに送信されます。

  2. add_action:
    wp_ajax_get_xbrl_data は、ログインユーザーがAjaxリクエストを送信したときに get_xbrl_data 関数を呼び出すためのフックです。
    wp_ajax_nopriv_get_xbrl_data は、未ログインのユーザーからのリクエストにも対応するためのフックです。

次回

長くなりましたが、これでダウンロードページも完成です。
次回はおまけとして、財務データを検索、ダウンロードできるページを作りたいと思います。

外部からPythonで今回作成したDBにデータを登録する方法についてはこちら。

コメント

タイトルとURLをコピーしました