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

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

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

# 今回やること
#### 勉強になれば車輪の再発明でもいいじゃない
今回は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](https://qiita-image-store.s3.amazonaws.com/0/93180/28fe5379-2e5f-3309-ac93-652996e26f71.png "スクリーンショット 2016-10-17 22.10.28.png")


5. URLの[ページ数]の数だけ下のページャーのNextをクリックしスクレイピングを繰り返す。
![スクリーンショット 2016-10-17 22.22.28.png](https://qiita-image-store.s3.amazonaws.com/0/93180/4166c923-3259-ce08-b1fb-44ed0aee079a.png "スクリーンショット 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生活 〜環境構築とこんにちは世界〜](http://qiita.com/ymgn_ll/items/96cac1dcf388bc7a8e4e)
でHelloWorldをするまでにインストールしている
 ***Flask***と ***Gunicorn***は入れておいてください
環境などはpyenv-virtualenvなどのお好きなものを用意してください。

## 今回するために追加でする環境構築
### PhantomJS
Herokuで動かす前にローカルで動作確認をするためにローカルにもPhantomJSを入れます。
コードから操作できるGUIがないブラウザという認識でいいと思います。
参考 :[PhantomJSを使って色々試してみる]( 
http://qiita.com/weedslayer/items/5e55f3e5af1f5cebf41f)

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

Rubyですがどういうものか参考になる記事 : 
 [WebのUIテスト自動化 - Seleniumを使ってみる](http://qiita.com/edo_m18/items/ba7d8a95818e9c0552d9)

```
$ pip install selenium
```
### beautifulsoup
取得したWebページのデータを加工する時に使います。
参考 : [PythonとBeautiful Soupでスクレイピング](http://qiita.com/itkr/items/513318a9b5b92bd56185)

```
$ pip install beautifulsoup4
```
### lxml
beautifulsoupと合わせて使います。

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

```
$ pip install -U flask-cors
```

# できたもの
https://github.com/ymgn/SlideShare-API

```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 を使いこなそう](http://methane.github.io/flask-handson/advanced.html)
### PhantomJSのブラウザサイズを決める
```
driver.set_window_size(1124, 850)
```
ブラウザサイズを決めなかった場合、うまく要素をとったり、スクロールすることができません。
サイズの数値の値は調べた時に書いてあったままなので理由は不明。

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

### テキストをフォームのボックスに入力し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](http://selenium-python.readthedocs.io/locating-elements.html)
### スクレイピングできる状態にする
```
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を扱う](http://www.yoheim.net/blog.php?q=20150901)
# 動きを確認してから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](https://qiita-image-store.s3.amazonaws.com/0/93180/d3d5a974-38ba-d667-0557-0d22a257a73a.png "スクリーンショット 2016-10-16 17.52.34.png")
Safariではjsonは羅列して見えていますが、ChromeでJSONViewなどを入れて見るときれいに見れます。

## 参考
[Pythonでクローリング・スクレイピングに使えるライブラリいろいろ](http://orangain.hatenablog.com/entry/scraping-in-python)

#Herokuにデプロイする
Herokuにデプロイする流れは、後編である
[Re:ゼロからFlaskで始めるHeroku生活 〜PhantomJSをHerokuへ〜](http://qiita.com/ymgn_ll/items/585e94a3d2d23ca1fec3)
にて書いているのでよろしくおねがいします。

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

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