madokaのブログ

勉強したことのoutput先として使ってます。内容はpythonがらみが多いかもです。

デデンネの総合順位をスクレイピングして求めた話

2月の初旬に好きなポケモンgoogleで投票できましたが、皆さんはデデンネに投票してくださいましたでしょうか?

さて、2020/02/27はポケモンの日。前述した投票の結果も、この日の23時に公開されました。公開された情報は、総合TOP30および各地方のTOP30のみです。残念ながらデデンネは総合順位30位内には入ることができず、総合順位はわかりませんでした。しかし、カロス地方にて7位に輝き、その投票数は21,691でした。他の地方のTOP30の子たちの投票数を見るに、デデンネと同等数の投票数を獲得しているポケモンたちはどうやら15位以内に収まっているようでした。デデンネの総合順位が出せる...!ということで出してみました。

f:id:xmadoka:20200307022822p:plain
デデンネ 2020年ポケモン投票結果

ついでに学べたこともあったので、合わせてこの記事に載せようと思います。

BeautifulSoupでは取得できない?

requests + BeautifulSoupを使ってスクレイピングをした経験があるので、安易に今回もこちらを使ってスクレイピングをしようとしました。

from urllib.request import urlopen
from bs4 import BeautifulSoup

soup=BeautifulSoup(urlopen(url))

上記のコードで拾ってこれで図鑑番号、ポケモン名と投票数が入っているものが取れると思ったのですが、実際に図鑑番号、ポケモン名と投票数が入るはずのところには違うものが入っていました。

<div class="ranking__text">
 <p class="ranking__number">No.<span>{{data.no}}</span></p>
 <h3 class="ranking__name">{{data.jp}}</h3>
 <p class="ranking__vote"><span>{{data.vote}}</span></p>
</div>

すごい何処かで見たことあるやつだと思いました。vueと同じようなやつだなと思いました。(jsのテンプレートエンジンですね。)

jsのテンプレートエンジンを使っている画面は取得できない...?

ひとまず確認のため、最近触っているvueでサンプルページを作ってBeautifulSoupに読ませてみました。

f:id:xmadoka:20200307003719p:plain
vue-cliを使ってできたページ

読み込んでみると以下が帰ってきました。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <meta content="IE=edge" http-equiv="X-UA-Compatible"/>
    <meta content="width=device-width,initial-scale=1.0" name="viewport"/>
    <link href="/favicon.ico" rel="icon"/>
    <title>sample</title>
    <link as="script" href="/js/app.js" rel="preload"/>
    <link as="script" href="/js/chunk-vendors.js" rel="preload"/>
</head>
<body>
<noscript>
    <strong>We're sorry but sample doesn't work properly without JavaScript enabled. Please enable it to
        continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<script src="/js/chunk-vendors.js" type="text/javascript"></script>
<script src="/js/app.js" type="text/javascript"></script>
</body>
</html>

<div id="app"></div>がそのままなところから、確かに図と同じものは読み込めてなさそうなことがわかりました。ソースコード見るに、index.htmlを読んだだけのように見えます。

We're sorry but sample doesn't work properly without JavaScript enabled. Please enable it to continue. ここも重要ですね、表示されてはいないものの、JavaScriptがないと十分に動かないとの注意書きがされています。(index.htmlにある文章なので、スクレイピングしたから出たわけではないです。)

requests_html

先ほどの調査で、requests + BeautifulSoupだけではindex.htmlなるjsの実行前のものが取得されてしまい、jsで書き換えているようなページはきちんと取得できないということがわかりました。見てるページをきちんと取得するためには、index.htmlだけでなくjsも読み込み反映させたものを取得する必要があります。

pythonにはこのようなことをしてくれるライブラリがいくつかあり、Seleniumが特に有名ですが、今回色々用意しなくても楽に実装できそうなrequests_htmlを使うことにしました。

from requests_html import HTMLSession

session = HTMLSession()
r = session.get(url)

# ブラウザエンジンでHTMLを生成させる
r.html.render()

ちょっとBeautifulSoupと比べると手間ですが、これでjs反映後のhtmlが取得できるはずです。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="/favicon.ico">
    <title>sample</title>
    <link href="/js/app.js" rel="preload" as="script">
    <link href="/js/chunk-vendors.js" rel="preload" as="script">
    <style type="text/css">
        h3[data-v-469af010] {
            margin: 40px 0 0;
        }

        ul[data-v-469af010] {
            list-style-type: none;
            padding: 0;
        }

        li[data-v-469af010] {
            display: inline-block;
            margin: 0 10px;
        }

        a[data-v-469af010] {
            color: #42b983;
        }
    </style>
    <style type="text/css">
        #app {
            font-family: Avenir, Helvetica, Arial, sans-serif;
            -webkit-font-smoothing: antialiased;
            -moz-osx-font-smoothing: grayscale;
            text-align: center;
            color: #2c3e50;
            margin-top: 60px;
        }
    </style>
</head>
<body>
<noscript>
    <strong>We're sorry but sample doesn't work properly without JavaScript enabled. Please enable it to
        continue.</strong>
</noscript>
<div id="app"><img alt="Vue logo" src="/img/logo.82b9c7a5.png">
    <div data-v-469af010="" class="hello"><h1 data-v-469af010="">Welcome to Your Vue.js App</h1>
        <p data-v-469af010=""> For a guide and recipes on how to configure / customize this project,<br
                data-v-469af010=""> check out the <a data-v-469af010="" href="https://cli.vuejs.org" target="_blank"
                                                     rel="noopener">vue-cli documentation</a>. </p>
        <h3 data-v-469af010="">Installed CLI Plugins</h3>
        <ul data-v-469af010="">
            <li data-v-469af010=""><a data-v-469af010=""
                                      href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
                                      target="_blank" rel="noopener">babel</a></li>
            <li data-v-469af010=""><a data-v-469af010=""
                                      href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
                                      target="_blank" rel="noopener">eslint</a></li>
        </ul>
        <h3 data-v-469af010="">Essential Links</h3>
        <ul data-v-469af010="">
            <li data-v-469af010=""><a data-v-469af010="" href="https://vuejs.org" target="_blank" rel="noopener">Core
                Docs</a></li>
            <li data-v-469af010=""><a data-v-469af010="" href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a>
            </li>
            <li data-v-469af010=""><a data-v-469af010="" href="https://chat.vuejs.org" target="_blank" rel="noopener">Community
                Chat</a></li>
            <li data-v-469af010=""><a data-v-469af010="" href="https://twitter.com/vuejs" target="_blank"
                                      rel="noopener">Twitter</a></li>
            <li data-v-469af010=""><a data-v-469af010="" href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
            </li>
        </ul>
        <h3 data-v-469af010="">Ecosystem</h3>
        <ul data-v-469af010="">
            <li data-v-469af010=""><a data-v-469af010="" href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a>
            </li>
            <li data-v-469af010=""><a data-v-469af010="" href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
            </li>
            <li data-v-469af010=""><a data-v-469af010="" href="https://github.com/vuejs/vue-devtools#vue-devtools"
                                      target="_blank" rel="noopener">vue-devtools</a></li>
            <li data-v-469af010=""><a data-v-469af010="" href="https://vue-loader.vuejs.org" target="_blank"
                                      rel="noopener">vue-loader</a></li>
            <li data-v-469af010=""><a data-v-469af010="" href="https://github.com/vuejs/awesome-vue" target="_blank"
                                      rel="noopener">awesome-vue</a></li>
        </ul>
    </div>
</div>
<!-- built files will be auto injected -->
<script type="text/javascript" src="/js/chunk-vendors.js"></script>
<script type="text/javascript" src="/js/app.js"></script>

<div id="app"></div>の中身が埋まったことがわかります。

準備が整ったのでデデンネの総合順位を取得する

今回、順位を出すのにデータ取り扱いに強いpandasを使って計算を行いました。

import pandas as pd

from requests_html import HTMLSession

def get_region_rank_data(region):
    ls = list()
    url = 'https://pokemonday.pokemon.co.jp/jp/result/' + region + '/'

    # セッション開始
    session = HTMLSession()
    r = session.get(url)

    # ブラウザエンジンでHTMLを生成させる
    r.html.render()
    headers = r.html.find('.ranking__header')
    for header in headers:
        name = header.find('.ranking__name')[0].text
        vote = int(header.find('.ranking__vote > span')[0].text.replace(",", ""))
        number = int(header.find('.ranking__number > span')[0].text)
        ls.append([number, name, vote])
    return ls


if __name__ == '__main__':
    regions = [
        'kanto',
        'johto',
        'hoenn',
        'sinnoh',
        'unova',
        'kalos',
        'alola',
        'galar',
    ]
    data_list = list()
    for region in regions:
        data_list.extend(get_region_rank_data(region))

 # 集計
    array = pd.np.array(data_list)
    df = pd.DataFrame({
        "number": array[:, 0],
        "name": array[:, 1],
        "vote": array[:, 2]
    })
    df["vote"] = df["vote"].astype(int)
    df = df.sort_values("vote", ascending=False)
    df["rank"] = df["vote"].rank(0, ascending=False)
    df = df[["rank", "name", "number", "vote"]]
    df.to_csv('pokemon_vote_ranking.csv', header=True, index=False)

これによってpokemon_vote_ranking.csvに吐き出されたデータは以下のようになりました。

rank,name,number,vote
1.0,ゲッコウガ,658,140559
2.0,ルカリオ,448,102259
3.0,ミミッキュ,778,99077
4.0,リザードン,6,93968
5.0,ブラッキー,197,67062
6.0,ニンフィア,700,66029
7.0,ガブリアス,445,61877
8.0,レックウザ,384,60939
9.0,サーナイト,282,60596
10.0,ゲンガー,94,60214
11.0,ドラパルト,887,57973
12.0,バンギラス,248,56834
13.0,フシギダネ,1,56015
14.0,ストリンダー,849,55032
15.0,ルギア,249,53268
16.0,モクロー,722,52367
17.0,ギルガルド,681,51517
18.0,シャンデラ,609,50943
19.0,ピカチュウ,25,48060
20.0,イーブイ,133,47762
21.0,レントラー,405,46032
22.0,ジュナイパー,724,44011
23.0,ゾロアーク,571,43782
24.0,ルガルガン,745,42792
25.0,アーマーガア,823,41711
26.0,フライゴン,330,41420
27.0,サザンドラ,635,40054
28.0,ジュカイン,254,38724
29.0,バシャーモ,257,38307
30.0,ユキハミ,872,38034
31.0,ミズゴロウ,258,36920
32.0,カイリュー,149,36873
33.0,ミュウ,151,36266
34.0,メタグロス,376,35631
35.0,バクフーン,157,35184
36.0,オンバーン,715,34795
37.0,ハッサム,212,34691
38.0,ポッチャマ,393,34680
39.0,ミュウツー,150,34585
40.0,ゴウカザル,392,33267
41.0,デンリュウ,181,32009
42.0,ゼラオラ,807,31691
43.0,マホイップ,869,30612
44.0,ダークライ,491,30544
45.0,ヌメルゴン,706,30209
46.0,エーフィ,196,30052
47.0,ガオガエン,727,29925
48.0,ウインディ,59,29795
49.0,ジラーチ,385,29611
50.0,ヒノアラシ,155,28332
51.0,ミロカロス,350,28295
52.0,アブソル,359,27781
53.0,グソクムシャ,768,26975
54.0,エースバーン,815,26892
55.0,ラグラージ,260,26540
56.0,スイクン,245,26277
57.0,グレイシア,471,26161
58.0,ザシアン,888,26158
59.0,アシレーヌ,730,25953
60.0,ワンパチ,835,25695
61.0,ボーマンダ,373,24920
62.0,アルセウス,493,24502
63.5,ウルガモス,637,24389
63.5,ココドラ,304,24389
65.0,ラプラス,131,23411
66.0,オノノクス,612,22937
67.0,ワニノコ,158,22526
68.0,ファイアロー,663,22328
69.0,ジャローダ,497,22269
70.0,ミジュマル,501,21990
71.0,エンペルト,395,21773
72.0,デデンネ,702,21691
73.0,クロバット,169,21548
74.0,ゼクロム,644,21477
75.0,オオタチ,162,21447
76.0,ギラティナ,487,21366
77.0,ウールー,831,21266
78.0,ビクティニ,494,20957
79.0,リーフィア,470,20859
80.0,イベルタル,717,20852
81.0,インテレオン,818,20697
82.0,ドダイトス,389,20632
83.0,セレビィ,251,20492
84.0,ヌメラ,704,20299
85.0,ネギガナイト,865,20217
86.0,レシラム,643,20123
87.0,ヒバニー,813,20058
88.0,カビゴン,143,19768
89.0,ワルビアル,553,19628
90.0,キュウコン,38,19044
91.0,ガチゴラス,697,18778
92.0,ブリムオン,858,18581
93.0,チコリータ,152,18521
94.0,ゼニガメ,7,18476
95.0,ホウオウ,250,18278
96.0,オーダイル,160,18245
97.0,ボスゴドラ,306,18120
98.0,エルフーン,547,17855
99.0,ラティアス,380,17478
100.0,シェイミ,492,17465
101.0,ゼルネアス,716,17415
102.0,キテルグマ,760,17181
103.0,メッソン,816,17155
104.0,ツタージャ,495,17020
105.0,チルタリス,334,16814
106.0,カメックス,9,16795
107.0,ヘラクロス,214,16577
108.0,ソルガレオ,791,16274
109.0,メルタン,808,16077
110.0,タイレーツ,870,16009
111.0,タルップル,842,15989
112.5,ムゲンダイナ,890,15699
112.5,ネイティ,177,15699
114.0,ピチュー,172,15695
115.0,カイオーガ,382,15585
116.0,クチート,303,15523
117.0,ゾロア,570,14910
118.0,シャワーズ,134,14887
119.0,マッシブーン,794,14747
120.0,ヘルガー,229,14742
121.0,ルチャブル,701,14607
122.0,ペンドラー,545,14536
123.5,ナマコブシ,771,14358
123.5,ベトベトン,89,14358
125.0,ディアルガ,483,14292
126.0,トゲピー,175,14288
127.0,エルレイド,475,14144
128.0,ランクルス,579,14129
129.0,ムクホーク,398,14054
130.0,ヒトカゲ,4,14049
131.0,ニャビー,725,14005
132.0,モルペコ,877,13945
133.0,メロエッタ,648,13915
134.0,シルヴァディ,773,13897
135.0,メタモン,132,13843
136.0,ニャオニクス,678,13661
137.0,フォッコ,653,13508
138.0,サルノリ,810,13478
139.0,トゲキッス,468,13426
140.0,クワガノン,738,13375
141.0,ヌオー,195,13308
142.0,オーロンゲ,861,12923
143.0,エンニュート,758,12863
144.0,キノガッサ,286,12801
145.0,ジャラランガ,784,12790
146.0,グライオン,472,12676
147.0,ザマゼンタ,889,12641
148.0,アチャモ,255,12568
149.0,ラティオス,381,12487
150.0,ヤドン,79,12369
151.0,メルメタル,809,12356
152.0,マーシャドー,802,12107
153.0,グラードン,383,11982
154.0,ジガルデ,718,11943
155.0,マルヤクデ,851,11619
156.0,ニドキング,34,11586
157.0,ダイケンキ,503,11444
158.0,ウオノラゴン,882,11436
159.0,ミミロップ,428,11411
160.0,ドリュウズ,530,11376
161.0,ポリゴン,137,11311
162.0,ドレディア,549,11292
163.0,ロコン,37,11224
164.0,コダック,54,11212
165.0,ライチュウ,26,11196
166.0,カイロス,127,11162
167.0,ヒトモシ,607,11140
168.0,サンダース,135,11064
169.0,ハクリュー,148,11051
170.0,モスノウ,873,10988
171.0,イワンコ,744,10986
172.0,キリキザン,625,10975
173.0,ビッパ,399,10924
174.0,ディアンシー,719,10918
175.0,アマージョ,763,10900
176.0,マフォクシー,655,10889
177.0,ナエトル,387,10865
178.0,マニューラ,461,10854
179.0,デオキシス,386,10842
180.0,テールナー,654,10807
181.0,ジュプトル,253,10746
182.0,ジュペッタ,354,10579
183.0,フシギバナ,3,10454
184.0,フリーザー,144,10450
185.0,ユキメノコ,478,10408
186.0,エンテイ,244,10404
187.0,ニャスパー,677,10402
188.0,コオリッポ,875,10392
189.0,キュレム,646,10338
190.0,フーパ,720,10327
191.0,アマルルガ,699,10321
192.0,ヤンチャム,674,10187
193.0,マグマラシ,156,10032
194.0,コイル,81,9955
195.0,ポリゴン2,233,9902
196.0,チラチーノ,573,9892
197.0,ルナアーラ,792,9887
198.0,ドラミドロ,691,9882
199.0,エモンガ,587,9798
200.0,パチリス,417,9694
201.0,ロトム,479,9417
202.0,キモリ,252,9339
203.0,ウパー,194,9323
204.0,バケッチャ,710,9162
205.0,ロズレイド,407,9092
206.0,オーロット,709,9019
207.0,ヤミラミ,302,9012
208.0,ケルディオ,647,8915
209.0,ラランテス,754,8889
210.0,ゴロンダ,675,8859
211.0,ムウマージ,429,8833
212.0,バチュル,595,8796
213.0,チラーミィ,572,8739
214.0,ルンパッパ,272,8708
215.0,タチフサグマ,862,8705
216.0,ゴリランダー,812,8625
217.0,トゲデマル,777,8531
218.0,ウッウ,845,8436
219.0,コリンク,403,8373
220.0,ボクレー,708,8231
221.0,ドヒドイデ,748,8221
222.0,ムウマ,200,8156
223.0,ツボツボ,213,8129
224.0,カプ・コケコ,785,8090
225.0,ゴルーグ,623,8035
226.0,ゲノセクト,649,7905
227.0,バンバドロ,750,7787
228.0,ソーナンス,202,7745
229.0,ネクロズマ,800,7739
230.0,アシマリ,728,7542
231.0,コスモッグ,789,7515
232.0,キングドラ,230,7418
233.0,ゴンベ,446,7289
234.0,アブリボン,743,7126
235.0,ポリゴンZ,474,7040
236.0,ヌケニン,292,6703
237.0,カラマネロ,687,6503
238.0,グラエナ,262,6459
239.0,エレザード,695,6386
240.0,ケロマツ,656,6293

デデンネがいた!!!

72.0,デデンネ,702,21691

デデンネの図鑑番号702から0を抜いた順位になっていました。

まとめ

デデンネは総合順位72位!!!