見出し画像

【仮想通貨】【PHP】【ツール】coinmarketcapから情報取得プログラム【スクレイピング】

今回は各コインやトークンの時価総額を見ることができる

coinmarketcapのサイトを解析してコインの時価総額ランキングリストをCSVに出力するプログラムを作っていきます。

まず今回のサイト解析(スクレイピング)ではphpQueryというライブラリを使用するので

以下の公式サイトよりphpQuery-onefile.phpをダウンロードしておいてください。

https://code.google.com/archive/p/phpquery/

プログラムを作成する前にcoinmarketcapについての説明と今回の記事を書いた意図について少し書きたいと思います。

1 . coinmarketcapとは

coinmarketcapは現存する様々な仮想通貨の情報を閲覧できるサイトとなっています。

具体的には各コインのチャートや、扱っている取引所及びペアの閲覧、取引ボリュームの履歴から、コインのホームページ、

コイン全体の時価総額ランキング等詳細な情報から全体の情報まで閲覧可能となっています。

また同様のサイトとしてcoingecko等がありますが、私見ですがcoinmarketcapは新しいコインの情報が載ってくる速度が

早く、サイトの応答時間も他のサイトより早い気がしました。

今回はこちらの情報から時価総額ランキングを取得します。

2 . なぜサイトで閲覧できる情報をプログラム化するのか

coinmarketcapや他のサイトでも同様ですが、1ページあたりに表示できるコインの情報はおおよそ100位です。

私はよくbinanceに上場されたばかりのコインや、coinexchangeに上場されている草コインをよく購入しますが、

そういったコインが今

・どこのランクにいるか

・どれくらい価格が上昇、下落しているか

・発行枚数はどれくらいか

を全体を俯瞰してみたいと思ってこのプログラムを作成しました。

また、実際作ってみると他にも様々な事を知る事ができました。

今回の記事で作成したプログラムを整形したり、加工したりする事で新たな事に気がつけると思います。

3 . プログラムの全体像

今回のプログラムの主な流れとしては

①coinmarketcapの時価総額ランキング(100コイン分のデータ)からHTMLを取得

②取得したHTMLを解析して必要な情報のみを抜き出す

③上記①〜②を15回繰り返す(1500コイン分のデータ)

④データをCSV化してファイルに出力

といった流れになります。

4 . 実装(htmlの取得処理)

それでは実装していきましょう。

今回のプログラムのヘッダーになる部分を作成していきます。

<?php
require_once dirname(__FILE__).'/phpQuery-onefile.php';
const MARKET_URL = "https://coinmarketcap.com/";
const CSV_FILE   = "coinmarketcap.csv";

まず、先ほどダウンロードしたphpQuery-onefile.phpを参照します。

上のプログラムではプログラムのファイルと同じディレクトリにphpQuery-onefile.phpがある想定でパスを設定しています。

MARKET_URLはcoinmarketcapのTOPのURLを設定しています。特に変更は必要ありません。

CSV_FILEは最後にダウンロードするCSVファイルの名称を設定しています。

次にcoinmarketcapからhtmlを取得する処理を記載します。

先ほどのプログラムの下に追加してください。

$url  =  MARKET_URL;
$html = createGetCurl($url);
function createGetCurl($url){
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => "GET",
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_URL            => $url,
        CURLOPT_FRESH_CONNECT  => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_COOKIEJAR      => 'cookie',
        CURLOPT_COOKIEFILE     => 'tmp',
        CURLOPT_FOLLOWLOCATION => true,
    ]);
    $ret = curl_exec($ch);
    curl_close($ch);
    return $ret;
}

$url変数にMARKET_URLを入れた後にcreateGetCurl関数でhtmlを取得する処理をします。

createGetCurl関数は過去の記事でも作成しているので詳細な説明はしませんが、この関数を使用する事によって、パラメータに渡されたurlにhttpリクエストを送り結果(html)を受け取る事ができます。

MARKET_URL定数をわざわざ$urlに格納し直している理由は後で違うページの情報も取得するので、後ほどこの処理を修正していきます。

5 . 実装(HTMLの解析)

次に取得したhtmlを解析して必要な情報を取り出す処理を記載していきます。

createGetCurl以外の行を以下の通りに書き換えてください。

$ret = array();
$url  =  MARKET_URL;
$html = createGetCurl($url);
$tmp  = getElement($html, count($ret));
$ret = array_merge($ret, $tmp);

getElementはこれから作成するサイト解析関数となります。

それ以外に追加した$retや$tmpは解析した結果を入れる変数として用意しています。

今は1ページ分のhtml取得及び解析処理としていますが、後で15ページ分の情報を取得するための準備をしている状態です。

$tmpには1ページ分の解析結果が入り、

$ret = array_merge($ret, $tmp);

で$retに各ページの解析結果($tmp)をマージ(結合)しています。

では実際に解析の関数を作成します。createGetCurlの関数の下に以下の関数を追加してください。

function getElement($html, $rank=0) {
    $doc  = phpQuery::newDocument($html);
    $jpy_per = parseQuery($doc, "#currency-exchange-rates", "data-jpy");
    $jpy_per = $jpy_per[0];
    $ret = array();
    $name_elms   = parseQuery($doc, ".currency-name-container", "text");
    $symbol_alms = parseQuery($doc, ".currency-symbol a", "text");
    $mcap_elms   = parseQuery($doc, ".market-cap", "data-usd");
    $price_elms  = parseQuery($doc, ".price", "data-usd");
    $per_elms    = parseQuery($doc, ".percent-change", "text");
    foreach ($name_elms as $k => $v) {
        $rank++;
        $tmp = array();
        $tmp["rank"]  = $rank;
        $tmp["name"]  = $v;
        $tmp["symbl"] = $symbol_alms[$k];
        $tmp["mcap"]  = $mcap_elms[$k]/$jpy_per;
        $tmp["price"] = $price_elms[$k]/$jpy_per;
        $tmp["per"]   = $per_elms[$k];
        $ret[] = $tmp;
    }
    // $ret = $jpy_per;
    return $ret;
}
function parseQuery($doc, $id, $attr){
    $elements = $doc->find($id);
    $ret = array();
    foreach ($elements as $key => $value) {
        $tmp_array = array();
        if($attr == "text") {
            $tmp_array = htmlspecialchars(pq($value)->text());
        }else{
            $tmp_array = htmlspecialchars(pq($value)->attr($attr));
        }
        $ret[] = $tmp_array;
    }
    return $ret;
}

先ほど記載したgetElementとgetElement関数の中で使用するparseQuery関数を追加しました。

まずgetElementでは

$doc = phpQuery::newDocument($html);

で$htmlをphpQueryオブジェクトに変換しています。(phpQueryで扱えるようにしている)

次に円の情報を取得しています。

$jpy_per = parseQuery($doc, "#currency-exchange-rates", "data-jpy");
$jpy_per = $jpy_per[0];

coinmarketcapでは米ドル表記がサイトの基準となっているため、今回のプログラムでは日本円に表記を変換する処理も入れています。

coinmarketcapでは日本円レートの情報もサイト内に持っているため、それを取得しています。

また、作成したparseQuery関数を呼び出していますが、この関数に必要パラメータとしては

・phpQueryオブジェクト

・セレクタ(htmlタグの位置を表す情報)

・情報を取得したいタグ内の属性

としています。

上の例だとcoinmarketcapのhtmlで日本円のレートが格納されているセレクタ(タグ位置)は「#currency-exchange-rates」であり、

そのタグの属性「data-jpy」に日本円のレートが設定されていることとなります。

$name_elms   = parseQuery($doc, ".currency-name-container");
$symbol_alms = parseQuery($doc, ".currency-symbol a");
$mcap_elms   = parseQuery($doc, ".market-cap", "data-usd");
$price_elms  = parseQuery($doc, ".price", "data-usd");
$per_elms    = parseQuery($doc, ".percent-change");

はそれぞれコイン名称、コインシンボル、時価総額、価格、変動率を取得しています。

実際のサイトとセレクタを照らし合わせてみると以下のとおりとなります。

シンボルはサイト上には表示されていませんが、ブラウザ上では見えないシンボルに関するタグがhtml内に埋め込まれているため、

そこから取得しています。ブラウザ上では見えない情報を取得したい場合は各ブラウザに付随している開発者ツールを使用して探すことができます。

また、今回取得していない総発行枚数は「時価総額/価格」で計算できるので取得していません。

    foreach ($name_elms as $k => $v) {
        $rank++;
        $tmp = array();
        $tmp["rank"]  = $rank;
        $tmp["name"]  = $v;
        $tmp["symbl"] = $symbol_alms[$k];
        $tmp["mcap"]  = $mcap_elms[$k]/$jpy_per;
        $tmp["price"] = $price_elms[$k]/$jpy_per;
        $tmp["per"]   = $per_elms[$k];
        $ret[] = $tmp;
    }

こちらはサイト(html)内に100件分のコイン情報が格納されているため、ループして各コインの情報を取得しています。

parseQuery関数はセレクタに該当するデータを複数件取得してくるため、

先ほどの$jpy_per = $jpy_per[0];は0番(1番目)を取得してくるようにしていましたが、

コイン情報は100件あるため、$kは0~99の添字でそれぞれの情報を引っ張ってくることができます。

ではparseQuery関数について説明します。

$elements = $doc->find($id);

こちらは$doc(phpQuery)オブジェクトから指定のセレクタ($id)を検索(find)してくる処理になります。

変数 $elementには検索結果が格納されますが、前述の通り該当するタグを複数件取得します。

    $ret = array();
    foreach ($elements as $key => $value) {
        $tmp_array = array();
        if($attr == "text") {
            $tmp_array = htmlspecialchars(pq($value)->text());
        }else{
            $tmp_array = htmlspecialchars(pq($value)->attr($attr));
        }
        $ret[] = $tmp_array;
    }

こちらは複数件取れてきたタグ毎にループしています。

各タグにはそれぞれ複数の属性が付与できますが、今回はそのうちの取得したい情報が格納されている

属性($attr)のみを抽出してくるようにしています。

例えばcoinmarketcapの時価総額のタグを見てみると以下のようになっています。

<td class="no-wrap market-cap text-right" data-usd="1.43127364844e+11" data-btc="16934362.0" data-sort="1.43127364844e+11">
$143,127,364,844
</td>

セレクタに「.market-cap」を指定することで上のタグがサイトに表示されているコイン分取得できますが、時価総額は後で日本円に変換したいので、

数値の部分が欲しいということになります。

その場合、取得した内の「1.43127364844e+11」の部分が欲しいので

属性にdata-usdを指定しています。

また、余談ですが、textは属性ではなく、タグのouterTextとなるのでparseQuery関数での取得方法が他の属性と異なるため、

if文で条件分岐させ、textの場合のみ

$tmp_array = htmlspecialchars(pq($value)->text());

といった取得の仕方をしています。

6 . 実装(繰り返し処理)

ここまでで1ページ(100コイン)分の情報が取得できたので、繰り返し処理を行い、15ページ(1500コイン)分の情報が取得できるように修正していきます。

$ret = array();
$url  =  MARKET_URL;
$html = createGetCurl($url);
$tmp  = getElement($html, count($ret));
$ret = array_merge($ret, $tmp);

を以下の通り変更してください。

$ret = array();
for ($i=0; $i < 15; $i++) { 
    $tmp  = array();
    $url  = ($i > 0) ? MARKET_URL.strval($i+1) : MARKET_URL;
    $html = createGetCurl($url);
    $tmp  = getElement($html, count($ret));
    $ret = array_merge($ret, $tmp);
}

まず15回ページ分の情報が欲しいので15回ループするfor文を作成し、

先ほどのhtml取得処理(createGetCurl)と解析処理(getElement)をループの中に入れます。

同じurlを設定して15回ループをしても同じ情報が15回取得されるだけなので、ループ毎にurlが変更されるように

$url = ($i > 0) ? MARKET_URL.strval($i+1) : MARKET_URL;

としています。

coinmarketcapはurlのドメインの後に数値を与えると時価総額ランキングページの次ページという風になっているので、ループのカウンタ($i)として使用している値をurlの後ろに付与していけば良いです。

また、1ページ目はドメインのみで接続となっているため、ループの1周目はカウンタをurlに付与しないようにしています。

各ループの解析結果は $tmpに格納し、ループの最後に$retに結合していくことで、毎ループの解析結果が$retに溜まるようにしています。

7 .実装(CSV出力)

最後に集めた解析結果をCSVに変換してファイルとしてダウンロードできるようにします。

6で作成したループ文とcreateGetCurl関数の間に以下のコードを追加してください。

$csv = replaceCSV($ret);
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename='.CSV_FILE ."; filename*=UTF-8''".urlencode(CSV_FILE));
echo $csv;

また、ファイルの一番最後にreplaceCSVを追加します。

function replaceCSV($array){
    $head  = array();
    $lines = array();
    foreach ($array as $key => $value) {
        $line = array();
        foreach ($value as $k => $v) {
            if($key == count($array) - 1) $head[] = $k;
            $line[] = $v;
        }
        $lines[] = implode(',', $line);
    }
    return implode(',', $head)."\n".implode("\n", $lines);
}

CSVに変換する関数は前回の記事のおまけとして記載したものなので、説明はしません。

headerは今回作成したプログラムをブラウザで実行した際にどういったサイトなのかを表す

サイトの属性を設定しています。

こちらも詳細には解説しませんが、HTMLに準拠した決まり事なので簡単に調べることができると思います。

これでプログラムは完成です。

ブラウザで実行すると以下のようなCSVで時価総額の一覧ファイルが取得できるはずです。

rank,name,symbl,mcap,price,per
1,Bitcoin,BTC,14964041931345,883639.14763953,-6.82%
2,Ethereum,ETH,5328466043795.9,54173.505629648,-9.40%
3,Ripple,XRP,2606613721183.6,66.675156443182,-8.28%
4,Bitcoin Cash,BCH,1732762282851.8,101731.1362647,-7.54%
...
1498,Pirate Blocks,SKULL,0,0.2906534989147,-10.26%
1499,BatCoin,BAT,0,0.0088076719353622,
1500,Dashs,DASHS,0,4.4126489557199,

8 .おまけ

ここまでで作成したCSVファイルですが、何も加工せずにEXCELで開くと

こんな感じで桁あふれが発生すると思います。

時価総額やコインの価格については通貨表示に切り替えることで桁あふれせずに表示できます。

最後までお読みいただきありがとうございました。

他の記事も是非読んでいただけたら幸いです。

■【BTC/JPY-FX】【PHP】bitFlyerAPIとinvestingのテクニカルデータを使った自動売買システムの構築

■【自動売買】【PHP】テクニカル分析を実装する方法【MACD編】

■【自動売買】【PHP】テクニカル分析 を簡単に実装する方法 【ボリンジャーバンド】

■【BTC/JPY-FX】【PHP】bitFlyer自動売買システムをカスタマイズするためのtips

■【自動売買】【PHP】テクニカル分析 を独自実装してみる【スパンモデル】

では、いつもの通りプログラム全体を記載して終わります。

<?php
require_once dirname(__FILE__).'/phpQuery-onefile.php';
const MARKET_URL = "https://coinmarketcap.com/";
const CSV_FILE   = "coinmarketcap.csv";
$ret = array();
for ($i=0; $i < 15; $i++) { 
    $tmp  = array();
    $url  = ($i > 0) ? MARKET_URL.strval($i+1) : MARKET_URL;
    $html = createGetCurl($url);
    $tmp  = getElement($html, count($ret));
    $ret = array_merge($ret, $tmp);
}
$csv = replaceCSV($ret);
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename='.CSV_FILE ."; filename*=UTF-8''".urlencode(CSV_FILE));
echo $csv;
function createGetCurl($url, $query=null){
    $ch = curl_init();
    if($query != null) {
        $url = $url.'?' . http_build_query($query);
    }
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => "GET",
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_URL            => $url,
        CURLOPT_FRESH_CONNECT  => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_COOKIEJAR      => 'cookie',
        CURLOPT_COOKIEFILE     => 'tmp',
        CURLOPT_FOLLOWLOCATION => true,
    ]);
    $ret = curl_exec($ch);
    curl_close($ch);
    return $ret;
}
function getElement($html, $rank=0) {
    $doc  = phpQuery::newDocument($html);
    $jpy_per = parseQuery($doc, "#currency-exchange-rates", "data-jpy");
    $jpy_per = $jpy_per[0];
    $ret = array();
    $name_elms   = parseQuery($doc, ".currency-name-container", "text");
    $symbol_alms = parseQuery($doc, ".currency-symbol a", "text");
    $mcap_elms   = parseQuery($doc, ".market-cap", "data-usd");
    $price_elms  = parseQuery($doc, ".price", "data-usd");
    $per_elms    = parseQuery($doc, ".percent-change", "text");
    foreach ($name_elms as $k => $v) {
        $rank++;
        $tmp = array();
        $tmp["rank"]  = $rank;
        $tmp["name"]  = $v;
        $tmp["symbl"] = $symbol_alms[$k];
        $tmp["mcap"]  = $mcap_elms[$k]/$jpy_per;
        $tmp["price"] = $price_elms[$k]/$jpy_per;
        $tmp["per"]   = $per_elms[$k];
        $ret[] = $tmp;
    }
    // $ret = $jpy_per;
    return $ret;
}
function parseQuery($doc, $id, $attr){
    $elements = $doc->find($id);
    $ret = array();
    foreach ($elements as $key => $value) {
        $tmp_array = array();
        if($attr == "text") {
            $tmp_array = htmlspecialchars(pq($value)->text());
        }else{
            $tmp_array = htmlspecialchars(pq($value)->attr($attr));
        }
        $ret[] = $tmp_array;
    }
    return $ret;
}
function replaceCSV($array){
    $head  = array();
    $lines = array();
    foreach ($array as $key => $value) {
        $line = array();
        foreach ($value as $k => $v) {
            if($key == count($array) - 1) $head[] = $k;
            $line[] = $v;
        }
        $lines[] = implode(',', $line);
    }
    return implode(',', $head)."\n".implode("\n", $lines);
}

プログラムのことに関しては言語問わず対応できます。主にこれからプログラムを覚えていきたい+仮想通貨で自動売買をしたい人向けに記事を書いていきます。