Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
31
Help us understand the problem. What is going on with this article?
@ymgn_ll

Re:ゼロからFlaskで始めるHeroku生活 〜Selenium & PhantomJS & Beautifulsoup〜

More than 3 years have passed since last update.

はじめに

初めてPythonのFlaskとHerokuを使って、スクレイピングした情報をjsonで返すAPIを作ったので、その際におこなった方法をまとめたいと思います。

herokuでHelloWorldまでに使用するものやPythonの環境構築などは前編にあたる
Re:ゼロからFlaskで始めるHeroku生活 〜環境構築とこんにちは世界〜
にて、
今回作るプログラムをHerokuにデプロイするまでは後編にあたる
Re:ゼロからFlaskで始めるHeroku生活 〜PhantomJSをHerokuへ〜
にて書いているので合わせてご覧ください

今回やること

勉強になれば車輪の再発明でもいいじゃない

今回はSlideShareを題材として、SeleniumをとPhantomJSを使ったスクレイピングのやり方を書きます。

1つの記事にまとめた際長くなってしまった為、Herokuにデプロイする流れは後編に分けています。

具体的な動き

[HerokuURL]/api/[検索ワード]/[ページ数]
例 : ~herokuapp.com/api/python/2
とアクセスすると、

  1. Heroku で PhantomJS が動き Slideshareの検索ページを開く
  2. URLの[検索ワード]を検索ページの検索欄に入力し検索
  3. 検索結果の言語設定を日本語に変更
  4. スクレイピングでwebページ内のスライドの情報を抽出
    スクリーンショット 2016-10-17 22.10.28.png

  5. URLの[ページ数]の数だけ下のページャーのNextをクリックしスクレイピングを繰り返す。
    スクリーンショット 2016-10-17 22.22.28.png

  6. スクレイピングし終わったらjson形式にして投げる!

という事をするAPIを作りたいと思います。

今回使用したもの

  • Mac OSX El Capitan 10.11.6
  • Python3.5.1
  • Flask
  • selenium
    • 検索欄に入力したり次へボタンをクリックできる
  • beautifulsoup4
    • Webデータを抽出したりする
  • lxml
    • beautifulsoupとセットで使う
  • CORS
    • クロスドメイン制約回避
  • Heroku
    • Herokuにアクセスして、スクレイピングしたデータをJson形式にして返すようにする
  • PhantomJS
    • Herokuでseleniumを使うために使う
    • いわゆるGUIがない裏で動くブラウザ

準備

今回する前にやっておく環境構築

Re:ゼロからFlaskで始めるHeroku生活 〜環境構築とこんにちは世界〜
でHelloWorldをするまでにインストールしている
FlaskGunicornは入れておいてください
環境などはpyenv-virtualenvなどのお好きなものを用意してください。

今回するために追加でする環境構築

PhantomJS

Herokuで動かす前にローカルで動作確認をするためにローカルにもPhantomJSを入れます。
コードから操作できるGUIがないブラウザという認識でいいと思います。
参考 :PhantomJSを使って色々試してみる

$  brew install phantomjs

Selenium

クロスブラウザ、クロスプラットフォームのUIテストツール、らしいです。
普通のスクレイピングだと指定したURLで表示されている分しかできませんが、seleniumを使うことで次のページに行くボタンを押させたり、文字を入力して検索ボタンを押したりすることができます。すごい

Rubyですがどういうものか参考になる記事 :
WebのUIテスト自動化 - Seleniumを使ってみる

$ pip install selenium

beautifulsoup

取得したWebページのデータを加工する時に使います。
参考 : PythonとBeautiful Soupでスクレイピング

$ pip install beautifulsoup4

lxml

beautifulsoupと合わせて使います。

$ pip install lxml

corsでクロスドメイン制約をよけるやつ

普通にJsonを返すAPIを作ってもクロスドメイン制約云々で、対策を行っていないAPIをChromeで使う際に使おうとすると面倒なのでどうせなので対策をしておきます。
コード説明にてリンク置いておきます。

$ pip install -U flask-cors

できたもの

api.py
# -*- coding: utf-8 -*-

import json
# ここからスクレイピング必要分
from bs4 import BeautifulSoup
# ここからseleniumでブラウザ操作必要分
from selenium import webdriver 
from selenium.webdriver.common.keys import Keys # 文字を入力する時に使う

#ここからflaskの必要分
import os
from flask import Flask

#ここからflaskでcorsの設定 ajaxを使う時のクロスドメイン制約用
from flask_cors import CORS, cross_origin
app = Flask(__name__)
CORS(app)

@app.route('/')
def index():
    return "使い方 : /api/検索する単語/取得ページ数"

@app.route('/api/<string:word>/<int:page>') # 検索ワード/ページ数をパスから変数に受け取る
def slide(word,page):

    driver = webdriver.PhantomJS() # PhantomJSを使う 
    driver.set_window_size(1124, 850) # PhantomJSのサイズを指定する
    driver.implicitly_wait(20) # 指定した要素などがなかった場合出てくるまでdriverが最大20秒まで自動待機してくれる

    URL = "http://www.slideshare.net/search/"
    driver.get(URL) # slideshareのURLにアクセスする
    data_list = [] # 全ページのデータを集める配列

    search = driver.find_element_by_id("nav-search-query") # 検索欄要素を取得
    search.send_keys(word) # 検索ワードを入力
    search.submit() # 検索をsubmitする

    lang = driver.find_element_by_xpath("//select[@id='slideshows_lang']/option[@value='ja']") # 言語選択リストの日本語の部分を抽出
    lang.click() # 言語選択の日本語を選択

    for i in range(0,page): 
        print(str(i+1) + u"ページ目")
        data = driver.page_source.encode('utf-8') # ページ内の情報をutf-8で用意する
        soup = BeautifulSoup(data,"lxml") # 加工しやすいようにlxml形式にする
        slide_list = soup.find_all("div",class_="thumbnail-content") # スライド単位で抽出
        for slide in slide_list:
            slide_in = {} # スライドの情報を辞書形式でまとめる

            # スライドの投稿者の名前を入手
            name = slide.find("div",class_="author").text
            slide_in["name"] = name.strip() # strip()は両端の空白と改行をなくしてくれる

            # スライドのタイトルを入手
            title = slide.find("a",class_="title title-link antialiased j-slideshow-title").get("title") # 指定したタグ&クラス内のtitleを出す
            slide_in["title"] = title

            # スライドのリンクを入手
            link = slide.find("a",class_="title title-link antialiased j-slideshow-title").get("href") # 指定したタグ&クラス内のhrefを出す
            slide_in["link"] = "http://www.slideshare.net" + link

            # スライドのサムネのリンクを入手
            imagetag = slide.find("a",class_="link-bg-img").get("style") # 指定したタグ&クラス内のstyleを出す
            image = imagetag[imagetag.find("url(")+4:imagetag.find(");")] # いらない部分を取り除く
            slide_in["image"] = image

            # スライドのページ数であるslidesとlikesを入手
            info = slide.find("div",class_="small-info").string # slidesとlikesの文字列を入手
            slides = info[7:info.find("slides")] # slides部分を抽出
            slide_in["slides"] = slides.strip() # strip()は両端の空白と改行をなくしてくれる
            if "likes" in info:
                likes = info[info.find(", ")+2:info.find("likes")] # likes部分を抽出
            else:
                likes = "0"
            slide_in["likes"] = likes.strip() # strip()は両端の空白と改行をなくしてくれる

            data_list.append(slide_in) # data_listに1ページ分の内容をまとめる

        driver.execute_script('window.scrollTo(0, 3000)') # ページャーのある下に移動
        next = driver.find_element_by_xpath("//li[@class='arrow']/a[@rel='next']") # ページャーのNEXT要素を抽出
        next.click() # Nextボタンをクリック

    driver.close() # ブラウザ操作を終わらせる
    jsonstring = json.dumps(data_list,ensure_ascii=False,indent=2) # 作った配列をjson形式にして出力する
    return jsonstring

# bashで叩いたかimportで入れたかを判定する
if __name__ == '__main__':
    app.run()

コード解説

コード内にもコメントを書いていますが、上から大事そうな箇所を説明していきたいと思います。インポートは書いているそのままなので省略

CORS

ChromeなどでAPIを使用したプログラムが動かない!という経験はあると思います。
せっかくAPIを作成するのですから、対策をしておいてあげましょう。

from flask_cors import CORS, cross_origin

app = Flask(__name__)
CORS(app)

と書くとなんやかんやで対策してくれるそうです。べんり
参考 : https://flask-cors.readthedocs.io/en/latest/

Flaskのパスから引数を取る

Flaskのrouteのところに<型名:変数名>と書いて、defの()内にその変数名を書くとパスの中身を引数として受け取ることができます。

@app.route('/api/<string:word>/<int:page>') # 検索ワード/ページ数をパスから変数に受け取る  
def slide(word,page):

参考 : Flask を使いこなそう

PhantomJSのブラウザサイズを決める

driver.set_window_size(1124, 850)

ブラウザサイズを決めなかった場合、うまく要素をとったり、スクロールすることができません。
サイズの数値の値は調べた時に書いてあったままなので理由は不明。

要素の読み込み待ち処理

driver.implicitly_wait(20)

こう書くことで、以降のdriver.find~~のIDやclassを指定して要素を取得&操作する際に最大10秒間待ってくれ、読み込みが完了してくれたら即実行してくれる便利な状態になってくれます。
いちいち明示的にtime.sleep(3)などと予想待機時間をかかなくて良いのでseleniumで操作する際すごく便利です。
参考 : このサイトのImplicit Waitsの箇所に書かれています

テキストをフォームのボックスに入力しsubmit

search = driver.find_element_by_id("nav-search-query") # 検索欄要素を取得
search.send_keys(word) # 検索ワードを入力
search.submit() # 検索をsubmitする

ブラウザからidでinput要素などを取得した後は、send_keys("hoge")などで値を入れることができます。
その要素がformの中であれば、.submit()をつけることで送信することができます。

ドロップダウンを選択する

lang = driver.find_element_by_xpath("//select[@id='slideshows_lang']/option[@value='ja']") # 言語選択リストの日本語の部分を抽出
lang.click() # 言語選択の日本語を選択

今回は要素の指定方法をidやclassではなくXPATHで指定しています。
理由としては、idが複数、または親にしかidがついていない子要素を選択する時にidやclass以外の部分で指定する必要があるからです。

ちなみに、このように日本語に切り替えなければローカルでは日本語のものをとってきてくれていても、Heroku側の方では全言語のスライドをとってきてしまいます。

idが複数あった場合

lang = driver.find_elements_by_id("slideshows_lang")
lang[1].find_elements_by_tag_name("option")
# 複数抽出する場合はelementからelementsになります

親にしかidがない場合

lang = driver.find_element_by_id("slideshows_lang")
lang.find_element_by_tag_name("option")

XPATHの書き方や他の抽出方法は参考をみてください。
参考 : Locating Elements

スクレイピングできる状態にする

data = driver.page_source.encode('utf-8') # ページ内の情報をutf-8で用意する
soup = BeautifulSoup(data,"lxml") # 加工しやすいようにlxml形式にする

webdriverで入手したWebサイトのページデータをutf-8でエンコードした後、BeautifulSoupと相性のいいlxmlを使ってスクレイピングをし易い状態にします。
forの中に入れているのはページが変わった時は毎回読み込む必要があるためです。

スクロール

driver.execute_script('window.scrollTo(0, 3000)') # ページャーのある下に移動

これでPhantomJSをJavaScriptで下に3000pixelスクロールさせることができます。
PhantomJSってGUIないならスクロール意味なくね?と思うかもしれませんが、スクロールさせなかった場合はエラーが出ます。
3000にしている理由は一番下にいってほしかったのでとりあえず3000にしてみました。

複数存在するクラス名の下にあるidもclassもないaタグ対策

next = driver.find_element_by_xpath("//li[@class='arrow']/a[@rel='next']") # ページャーのNEXT要素を抽出
next.click() # Nextボタンをクリック

Slideshareのページャー部分のNextを押そうとした時、 PreviousとNextどちらにもclass="arrow"がついていて、その中のaタグにはidもclassもついていませんでした。子要素のaタグにはrel="next"と書いていたので、その部分を親とrelまで含めて指定できるXPATHにしています。

作った配列をjson形式にする

jsonstring = json.dumps(data_list,ensure_ascii=False,indent=2) # 作った辞書をjson形式にして出力する
return jsonstring
json.dumps(配列,辞書データ,日本語が含まれる場合False,インデントで整理)

配列、または辞書を渡すとjson形式にしてくれる。indentは省略可能で indent=2とすると半角スペース2文字でインデントし整理して見やすいようにしてくれます。
参考 : [Python] JSONを扱う

動きを確認してからHerokuにデプロイするまでの流れ

ローカルでFlaskを立ち上げ確認する

必要なものが全てインストールされているとします。

フォルダとFlaskの準備

$ mkdir slide
$ cd slide

$ touch api.py Procfile
# flaskのファイルと設定を書くファイルを作る

Flaskを立ち上げる時に必要なファイル

Procfile

web: gunicorn hello:app --log-file=-
api.py
上記を参照

まずはFlaskで動きを確認してみる

$ python api.py
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

http://127.0.0.1:5000/api/python/2
このパスでアクセスして、slideshareでpythonで検索したページを2枚分とってきてもらいましょう。
結果
スクリーンショット 2016-10-16 17.52.34.png
Safariではjsonは羅列して見えていますが、ChromeでJSONViewなどを入れて見るときれいに見れます。

参考

Pythonでクローリング・スクレイピングに使えるライブラリいろいろ

Herokuにデプロイする

Herokuにデプロイする流れは、後編である
Re:ゼロからFlaskで始めるHeroku生活 〜PhantomJSをHerokuへ〜
にて書いているのでよろしくおねがいします。

あとがき

とりあえず基本的なSeleniumでのブラウザの操作(文字入力、submit、ドロップダウンリストの選択、要素のクリック、XPATH指定)とスクレイピング(テキスト、画像、URL、文字列の加工とjson化)
のやり方を書くことができたと思うので、誰かの参考になれば嬉しく思います。

改善点でしたり、間違い等ございましたらコメント欄などでご指摘していただけたら幸いです。
Twitter:@ymgn_ll

31
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ymgn_ll
2018年の春から新社会人エンジニアはじめました。 興味のある関西の勉強会などに顔をだすことがあります。 仕事ではひよこPHPerとしてずっとピヨピヨしています。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
31
Help us understand the problem. What is going on with this article?