2021年12月10日金曜日

自分のサイトをWebスクレイピング

この記事は、ケーシーエスキャロット Advent Calendar 2021 の10日目の記事になります。

「Webスクレイピングやってみようかな」と思ってから半年以上が経って、すっかり忘れていたところにアドベントカレンダーの案内が。「これは今やらないとタイミングを逃す」ってことで、やってみることにしました。


まずは調査

世の中のWebサイトから必要な情報を抜き出すテクニックが「Webスクレイピング」。Python を使って実現できます。ただ、そんなに自由に他所様のWebサイトから情報を取得して良いのだろうかと心配になります。軽く調べてみたところ、いろいろと意見が分かれているようで、使用目的や収集した情報の利用方法によっては問題になることもあるようです。また、明示的にスクリピングを禁止しているサイトも多々ありますので、法律をかざして反論するよりも、サイトの主張を尊重したいところです。スクレイピングを禁止してWebAPIを公開しているサイトもありますので、そういうサイトに対しては素直に提供されたWebAPIを使うべきだろうと思います。

今回はWebスクレイピングをやってみるのが目的なので、自分の趣味のサイトを自分自身でスクレイピングしてみることにしました。

ゴール設定

【対象サイト】https://holy-garaman.ssl-lolipop.jp/

上記のサイトではマジックに関する色々な情報を整理しています。その中でカードやコインを扱ったテクニックがどの本で解説されているのかについてもまとめています。サイトを立ち上げてから18年くらい経っていますが、この情報の中に一体いくつのテクニックが含まれているのか、自分でも分からなくなっているので、これを機に数えてみることにします。

実際の画面はこんな感じです。


左側のメニューには「Card」と「Coin」の2つのカテゴリーがあって、それぞれに「ア〜オ」から「ワ」まで10個のメニューがあります。カタカナでまとめてあるのはちょっとかっこ悪いのですが、「ライプチッヒ・カラー・チェンジってどの本に載ってたっけな?」と思った時に探し易いのでこうしてあります。アルファベットでまとめた場合は、正しいスペル (Leipzig Color Change) が思い出せないと探せないので。。。

各メニューをクリックするとテクニック名が列挙してあるページに遷移します。つまりカードとコインの技法がそれぞれ10ページに分かれて掲載されていということです。まずは全部で20個の URL を取得してみます。70行未満で書けたので



コード解説


01 #-*- coding:utf-8 -*-
02 
03 import time
04 from selenium import webdriver
05 from selenium.webdriver.chrome.options import Options
06 from selenium.webdriver.common.by import By
07 
08 import chromedriver_binary
09 
10 def main():
11   print("Prepare start")
12   driver = prepareDriver()
13   print("Prepare end")
14  
15   url_list = get_url_list(driver)
16   print(len(url_list))
17   scraping_data(driver, url_list)
18 
19   driver.close()
 :
 :
66 if __name__ == "__main__":
67   main()

まずは main() 関数にやりたいことを書いてしまいます。先ほどのページにアクセスするための WebDriver を準備するために prepareDriver() を呼びます。具体的な処理は後述。続けて get_url_list() で20個の URL を取得することにします。取得した URL リストを渡して scraping_data() を呼ぶと、それぞれの URL に遷移して、テクニックの名前を取得してファイルに残すことにします。ではそれぞれのメソッドを順を追って解説します。まずは prepareDriver()から。


21 def prepareDriver():
22   options = Options()
23   options.add_argument('-headless')
24   driver = webdriver.Chrome(options=options)
25 
26   url = "https://holy-garaman.ssl-lolipop.jp/technique.php"
27 
28   driver.set_page_load_timeout(3)
29   driver.get(url)
30   time.sleep(1)
31
32   return driver
33

今回は selenium を使いました。本来はユーザーが実際にブラウザを操作したかのようにプログラムから操作するための python ライブラリです。テストに使われるのが目的だったようですが、Web スクレイピングにも活用されています。23行目に "-headless" というオプションを付けていますが、これは GUI を持たない状態でブラウザを起動するためのモードを表しています。24行目ではそのオプションを付けた上で Chrome ブラウザを生成しています。29行目で get(url) していますが、これで該当の HTML ページの内容が driver に読み込まれます。

<div id="submenu_wrapper">
  <nav id="submenu">
    <form>
      <label for="material">素材別分類</label>
      <section>
        <label class="icon-card" for="card">
          ::before
          "Card"
        </label>
        <ul>
          <li>
            <a href="/technique/card/a_o.php?sid=card">ア〜オ</a>
          </li>
          <li>
            <a href="/technique/card/ka_ko.php?sid=card">カ〜コ</a>
          </li>
          (省略)
      </section>
    </form>
  </nav>
</div>

上記は driver に読み込まれた HTML のうちメニュー部分の一部を抜粋したところです(自分のサイトだと引用するのも気楽で良いもんです)。このうち必要なのは href=" " の UML 部分だけで、全部で20箇所あります。では driver から URL を取得するメソッドを見てみましょう。

34 def get_url_list(driver):
35   nav = driver.find_element(By.ID, 'submenu')
36   form = nav.find_element(By.TAG_NAME, 'form')
37   section = form.find_element(By.TAG_NAME, 'section')
38   ul_list = section.find_elements(By.TAG_NAME, 'ul')
39 
40   url_list = []
41   for ul in ul_list:
42     for li in ul.find_elements(By.TAG_NAME, 'li'):
43       a_element = li.find_element(By.TAG_NAME, 'a')
44       url = a_element.get_attribute('href')
45       url_list.append(url)
46 
47   return url_list
48

selenium では HTML のタグを順に取得する方法が提供されています。タグの階層構造をそのまま find_element() で追いかけていくだけの記述なので、読めば理解できるレベルだと思います。最後の <a> タグを表す element から get_attribute('href') で該当の URL 部分を抜き出しています。それを url_list に入れて戻り値として返します。

最終的に欲しいデータはこれらの URL の先にありますので、scraping_data() メソッドで順番に開いていきます。

49 def scraping_data(driver, url_list):
50   f = open('technique.txt', 'w')
51   for url in url_list:
52     print(url, flush=True)
53     driver.get(url)
54     time.sleep(1)
55     words = url.split('/')
56     output_data(driver, words[4], f)
57   f.close()
58

取得したテクニック名を全て technique.txt に出力しますので、"w" でオープンしておきます。prepareDriver() で行ったのと同じように、20個の URL を get() して driver に順次読み込みます。読み込むページは次のようなものです。

上記の「ハイドアウト・カウント」や「パケット・フォース」等が、テクニック名です。この部分だけを抽出するためには HTML を解析する必要がありますので、構成を見てみましょう。
<section id="main">
  <article id="withmenu">
    <h1>ハ〜ホ</h1>
    <table>
      <tbody>
        <tr>
          <td class="title">ハイドアウト・カウント</td&td;
          <td>Hide-Out Count</td&td;
        </tr>
        <tr>
          <td colspan="2">カードマジック カウント事典 p.98</td&td;
        </tr>
        <tr>
          <td class="title">パケット・フォース</td&td;
          <td>Packet Force</td&td;
        </tr>
        <tr>
          <td colspan="2">ロベルト・ジョビーのカード・カレッジp.9</td&td;
        </tr>
        (省略)
      </tbody>
    </table>
  </article>
</section>

都合よくテクニック名のところだけ、class="title" がついているので、これを利用します。最後の output_data() メソッドを見れば一目瞭然です。

59 def output_data(driver, type, f):
60   title_list = driver.find_elements(By.CLASS_NAME, 'title')
61 
62   for title in title_list:
63     f.write("[" + type + "] " + title.text + "\n")
64

find_elements() に class 名の 'title" を指定すれば、全てのテクニック名を取得できます。あとは、ファイルに出力するだけです。実際に出力されたファイルを覗くと次のようになっています。

   1 [card] アイ・エー・ゴースト・カウント
   2 [card] アイ・カウント
   3 [card] アウト・ジョグ
   4 [card] アウト・ジョグ・スイッチ
   5 [card] アウト・ジョグ・ハーマン・パス
    :
  (省略)
    :
1003 [coin] ワン・ハンド・サムパーム・ターンノーバー・スイッチ
1004 [coin] ワン・ハンド・スイッチ
1005 [coin] ワン・ハンド・スペクテーター・スリーブ・チェンジ
1006 [coin] ワン・ハンド・ターンノーバー・スイッチ
1007 [coin] ワンハンド・バニッシュ
これで、掲載されているテクニック名は1007個であることが確認できました。

0 件のコメント:

コメントを投稿