flask-RESTfulとSeleniumで、スクレイピング・サイトを作る

## 作ったもの

###**（ 入力 ）**

1. 入力場所：Webブラウザ
2. 入力内容：URLの__リクエストパラメータ "word"__に、__記事の検索単語__を渡したlocal hostのURL（port: 5000）：__str__

```url:url
 http://127.0.0.1:5000/scraping?word=Qiita記事検索文字列
```

####__例 ：検索文字列として、"haskell"を入力__

```url:url
 http://127.0.0.1:5000/scraping?word=haskell
```

- - -

###**（ 出力 ）**

####**出力１：ブラウザへの出力**

・__検索単語でヒットした、Qiita記事一覧画面（7ページ分）__からスクレイピングした、次の２つの項目

1. __記事投稿者__を格納したList型オブジェクト：__List[str]__
2. __記事タイトル__を格納したList型オブジェクト：__List[str]__

####**出力２：カレント・ディレクトリへの出力**

注：「カレント・ディレクトリ」とは、Pythonスクリプトファイルが格納されているディレクトリです

3. __画像ファイル （拡張子: .png）__： Qiitaの記事事検索結果画面（7ページ分）の各画面のキャプチャ画像 （ファイル数：7ファイル）
4. __Excelファイル__：記事の投稿者名、記事タイトル、記事URLなどが書き込まれたもの（ファイル数：1ファイル）


###**操作手順**

・Seleniumのwebdriver(chromedriver)が置かれたディレクトに移動し、__Terminalで以下を実行__する

```bash:Terminal
$ python3 selenium_flask_qiita_multi_pages_get_data_while.py
```

・__Webブラウザ__を立ち上げて、__以下のURLにアクセス__する

```url:url
 http://127.0.0.1:5000/scraping?word=検索文字列
```

###__（実行例）__

**ブラウザ画面に以下を入力**

```url:url
http://(127.0.0.1:5000/scraping?word=haskell
```

####**出力１：Webブラウザ**

![スクリーンショット 2020-11-11 1.28.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/c778c0fb-1ba6-5b06-898d-c05c64c4e7ab.png)


また、裏側でSeleniumがアクセスした、Qiitaの記事検索結果画面の画面キャプチャ画像と、スクレイピング取得した情報が記録されたExcelファイルが出力される。

####**出力２：Excelファイル（1件）**と**画像ファイル（7件）**

![スクリーンショット 2020-11-11 1.39.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/aae04307-8346-6a32-0291-95841195323e.png)

#### __( Excelファイル ）__

![スクリーンショット 2020-11-11 1.41.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/a1b48831-d4b5-941d-6f83-8d619abf1e8e.png)

#### __( 画像ファイル ）__

・__（画面キャプチャ画像）1ページ目の記事一覧ページ__
![スクリーンショット 2020-11-11 1.42.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/71ef8f2f-fc94-b16b-8bd7-e277634ad50c.png)

・__（画面キャプチャ画像）1〜6ページ目の記事一覧ページ__
（ 掲載省略 ）

・__（画面キャプチャ画像）7ページ目の記事一覧ページ__
![スクリーンショット 2020-11-11 1.42.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/45c53608-c096-186b-a10a-1d0ffdd3ca69.png)

---

###**（ 別解 ） 操作手順**

・Seleniumのwebdriver(chromedriver)が置かれたディレクトに移動し、__Terminalで以下を実行__する

```bash:Terminal
$ python3 selenium_flask_qiita_multi_pages_get_data_while.py
```

・ __*pip install httpie*__ を実行した環境で、__もう１つ、別のTerminalを立ち上げる__
・立ち上げた新しいTerminalから、__httpコマンド__を用いてlocalhost (port 5000）にアクセスする


```bash:Terminal
$ http 'http://127.0.0.1:5000/scraping/?word=検索文字列'
```

####__（ 別解の実行例 ）__

```bash:Terminal
$ http 'http://127.0.0.1:5000/scraping/?word=haskell'
```

####**出力１：上記を実行したTerminal**

```bash:Terminal
$ http 'http://127.0.0.1:5000/scraping?word=haskell'
HTTP/1.0 200 OK
Content-Length: 4091
Content-Type: text/html; charset=utf-8
Date: Wed, 11 Nov 2020 15:18:57 GMT
Server: Werkzeug/1.0.1 Python/3.9.0

{
    "取得した記事の投稿者": [
        "Tatsuki-I",
        "mod_poppo",
        "hiratara",
        "Afo_guard_enthusiast",
        "sgmryk",
        "sparklingbaby",
        "oskats1987",
        "atsuyoshi-muta",
        "dd0125",
        "Izawa_",
        "Izawa_",
        "Tatsuki-I",
        "takenobu-hs",
        "satosystems",
        "F_cy",
        "sparklingbaby",
        "a163236",
        "pumbaacave",
        "gogotanaka",
        "nka0i",
        "gogotanaka",
        "ryo0ka",
        "tgck",
        "sahara",
        "legokichi",
        "arowM",
        "tnoda_",
        "yutasth",
        "t-mochizuki",
        "CPyRbJvCHlCs",
        "TTsurutani",
        "kanimum",
        "Lugendre",
        "eielh",
        "reotasosan",
        "busyoumono99",
        "inatatsu_csg",
        "hiroyuki_hon",
        "Cj-bc",
        "nakamurau1@github",
        "gangun",
        "makoraru",
        "eielh",
        "sk427",
        "reotasosan",
        "kitsukitsuki",
        "ttatsf",
        "airtoxin",
        "hiruberuto",
        "KenjiYamauchi",
        "reotasosan",
        "Tatsuki-I",
        "reotasosan",
        "ogata-k",
        "takenobu-hs",
        "junjihashimoto@github",
        "hyone",
        "lotz",
        "nwtgck",
        "gangun",
        "reotasosan",
        "sp4ghet",
        "makoraru",
        "yurucamp",
        "kencoba",
        "eielh",
        "sk427",
        "ttatsf",
        "airtoxin",
        "KenjiYamauchi"
    ],
    "取得した記事タイトル": [
        "HaskellでMD5 #Haskell",
        "stylish-haskellをHexFloatLiteralsやNumericUnderscoresに対応させる",
        "Haskell チュートリアル (Haskell Day 2016)",
        "【Selenium】「次ページ」への繰返し移動は、while文を使う",
        "VSCodeのHaskell拡張を使って最速開発環境構築",
        "stylish-haskellをBlockArgumentsに対応させる",
        "Haskell で Functor を使う",
        "Windows10 HomeでDocker + VSCodeによるhaskellの環境構築",
        "Docker + Haskell の Hello Worldビルド",
        "HaskellのEitherについて",
        "HaskellのEitherについて",
        "Haskellの関数の型とかカリー化とか #Haskell",
        "Haskell/GHC 記号の意味を検索するためのリファレンス集",
        "Haskell と SQLite",
        "Haskell入門記事備忘録",
        "Haskell入門 - Stackのインストールと設定",
        "Haskell 入門",
        "Approaching Haskell",
        "Ruby内にHaskellのコード埋め込むHaskellというGem作ったヨ！",
        "Typing Haskell in Haskellを読んでみる",
        "Ruby内にHaskellのコード埋め込むHaskellというGem作ったヨ！",
        "SublimeREPLにstackのサポートを追加する",
        "EmacsでHaskellプログラミングするための環境構築手順(haskell-mode.elの導入)",
        "Haskellメモ",
        "VisualStudioCodeでHaskell開発環境を整える",
        "なぜHaskellを学ぶと良いか",
        "Haskell インストールメモ",
        "VSCodeでCouldn't start client Haskell IDEが出る（Windows 10）",
        "Stackをhaskell-modeで使ってみよう",
        "Haskellのお勉強その1-Haskell環境構築",
        "Haskellの($)と(.)の違い",
        "HaskellerがRustに入門してみた",
        "Haskellの入門から中級者になるまでの指針",
        "Atom EditorでHaskell",
        "Haskellについてのアンケート",
        "Atomエディタでide-haskellを使うまでの手順",
        "Haskellで逆ポーランド記法の計算機を作る",
        "MacでHaskellを触ってみるメモ 0.1",
        "Haskellを始めてみた",
        "ATOMのide-haskell導入手順（MacOS X）",
        "HaskellでFizzBuzz",
        "Haskellと層",
        "IntelliJでHaskell",
        "haskellでBayes",
        "Haskellで食べていけるよ！!",
        "Windows10でHaskell開発環境構築",
        "Haskellで素数",
        "Haskell入門",
        "【翻訳】PureScriptとHaskellの違い　＋α",
        "UbuntuでHaskell",
        "Haskellについてのアンケート",
        "Haskellerの1週間Rust入門チャレンジ Day 1 #Rust",
        "４名のHaskeller先生方からの応援メッセージ",
        "Haskellや周辺ツールについてのリンク集",
        "Haskell GHC開発に関する情報源いろいろ",
        "Inline-c：C++のネームスペースやテンプレートの対応について",
        "hsenv を使って ghc のサンドボックス環境を作る",
        "HerokuにHaskellのアプリを公開する",
        "IntelliJでHaskellを実行する（haskell stackプロジェクト）",
        "HaskellでFizzBuzz",
        "Samuel Gélineau からの返信その1",
        "Windows上のAtomで Haskell開発環境を整える。",
        "Haskellと層",
        "Haskellをすこしだけ紹介（フィボナッチ数列を書いてみる）",
        "RustとHaskellのちょっとした違い",
        "IntelliJでHaskell",
        "haskellでBayes",
        "Haskellで素数",
        "Haskell入門",
        "UbuntuでHaskell"
    ]
}


$
```

####**出力２：Excelファイル（1件）**と**画像ファイル（7件）**

#####__（ 掲載省略 ）__

---

##__実装コード__

```python:selenium_flask_qiita_multi_pages_get_data_while.py
# coding: utf-8
import time, json, argparse, datetime
from flask import Flask, abort, make_response
from flask_restful import Api, Resource, reqparse
from selenium import webdriver
from bs4 import BeautifulSoup
from pprint import pprint
import pandas as pd
import numpy as np
import time

app = Flask(__name__)
api = Api(app)

### 定数 ###
url = "http://qiita.com"

### 変数の初期化宣言 ###
pagenum = 0
num_of_search_pages = 7

class GetData(Resource):
        def __init__(self, *args, **kwargs):
            self.parser = reqparse.RequestParser()

            self.parser.add_argument('word', type=str, default='Python')
            super().__init__(*args, **kwargs)
        
        def proceed_each_page(self, page_num, this_page_html, search_word, driver):
            from bs4 import BeautifulSoup
            this_soup = BeautifulSoup(this_page_html, 'lxml')
            print("""
                ================================
                {}ページ目を処理中...
                ===============================
                """.format(page_num))
            results = this_soup.find_all("h1", class_="searchResult_itemTitle")
            # 結果を記事タイトルリストに格納
            this_page_title_list = []
            for result in results:
                title_texts = result.findAll("a")
                title_texts = str(title_texts[0]).replace("<em>", "").replace("</em>", "").split(">")[1:]
                title_texts = title_texts[0]
                pos = title_texts.find('</a')
                title_text = title_texts[:pos]
                this_page_title_list.append(title_text)

            console_message = "検索結果画面の{}ページ目の記事件数:  ".format(pagenum) + str(len(this_page_title_list)) + "\n\n"
            pprint(this_page_title_list)
            
            # 結果をURLリストに格納
            this_page_url_list = []
            for result in results:
                href = result.findAll("a")[0].get("href")
                this_page_url_list.append(str(url + href))

            # 投稿者を投稿者リストに格納
            # <div class="searchResult_header"><a href="/niiku-y">niiku-y</a>が2019/08/07に投稿</div>
            this_page_author_list = []

            results = this_soup.findAll(class_="searchResult_header")
            for result in results:
                author = result.findAll("a")[0].get("href")
                author = author.replace("/", "")
                this_page_author_list.append(author)

            # nページ目から取得した記事であることをデータ保存
            this_page_num_list = [page_num]*len(this_page_title_list)

            ## 検索結果画面のnページ目の画面スクリーンキャプチャを取得
            # 画面の縦横サイズのデータを取得
            w = driver.execute_script("return document.body.scrollWidth;")
            h = driver.execute_script("return document.body.scrollHeight;")
            driver.set_window_size(w,h)
    
            # 画面スクリーンキャプチャファイル（画像ファイル）の保存場所とファイル名を指定
            FILENAME = "./{word}_page{number}_screen_{datetime}.png".format(word=search_word, number=page_num, datetime=str(datetime.datetime.now()))
            # 画像を保存
            driver.save_screenshot(FILENAME)

            #処理したWebページの情報を格納した各listを返す
            return [this_page_num_list, this_page_author_list, this_page_title_list, this_page_url_list, driver]

        @api.representation('application/json') #追加
        def get(self):
            args = self.parser.parse_args()
            search_word = args['word']
            print("\n\n入力された検索文字列：      ", search_word, "\n")
            print("記事一覧ページが複数ページに及ぶ場合、{}ページまでで処理を打ち切ります。".format(num_of_search_pages))
            driver = webdriver.Chrome(executable_path='./chromedriver')
            # Qiitaのトップページにアクセス
            driver.get(url)
            # 記事の検索ボックス欄に、キーワードを入力
            search = driver.find_element_by_class_name("st-Header_searchInput")
            search.send_keys(search_word)
            search.submit()
            time.sleep(5)
            # 検索結果の記事一覧ページの1ページ目のHTMLを取得
            first_page_html = driver.page_source.encode('utf-8')
            # 1ページ目を処理
            page_num = 1
            all_page_num_list = []
            all_page_author_list = []
            all_page_title_list = []
            all_page_url_list = []
            this_page_num_list, this_page_author_list, this_page_title_list, this_page_url_list, driver = self.proceed_each_page(page_num, first_page_html, search_word, driver)
            all_page_num_list = all_page_num_list + this_page_num_list
            all_page_author_list = all_page_author_list + this_page_author_list
            all_page_title_list = all_page_title_list + this_page_title_list
            all_page_url_list = all_page_url_list + this_page_url_list
            # 受け取ったdriverの指示対象のWebページに、次のページがある場合は、次のページに移動する
            # next_page_urlの返り値はlist型。次のページが記載された上記のタグが存在しない場合は、空のlistが返る
            next_page_url = driver.find_element_by_class_name("js-next-page-link").get_attribute("href")
            # 2ページ目から(最終ページ目 もしくは、{num_of_search_pages}ページ目までのいずれか小さいページ番号目)までをループ処理する
            # (num_of_search_pages)ページ目を最終ページにするためには、{num_of_search_pages -1)回、次のページをめくる
            while len(next_page_url) > 0 and page_num <= (num_of_search_pages - 1):
                driver.get(next_page_url)
                #要素がロードされるまでの待ち時間を10秒に設定
                driver.implicitly_wait(10)
                #time.sleep(5)
                next_page_html = driver.page_source.encode('utf-8')
                page_num += 1
                this_page_num_list, this_page_author_list, this_page_title_list, this_page_url_list, driver = self.proceed_each_page(page_num, next_page_html, search_word, driver)
                all_page_num_list = all_page_num_list + this_page_num_list
                all_page_author_list = all_page_author_list + this_page_author_list
                all_page_title_list = all_page_title_list + this_page_title_list
                all_page_url_list = all_page_url_list + this_page_url_list
                next_page_url = driver.find_element_by_class_name("js-next-page-link").get_attribute("href")
                print("=======")
                print("次のページのURL : " + str(next_page_url))
            else:
                print("\n\n記事検索結果の最後のページの処理が終わりました。\n\n")

            # Excelファイル出力
            message = "取得した検索結果のデータをExcelファイルに出力します"
            print("\n\n" + message + "\n\n")
            data = np.array([all_page_num_list, all_page_author_list, all_page_title_list, all_page_url_list]).T
            index_list = list(range(len(all_page_num_list)))
            column_list = ['検索結果画面内の掲載ページ番号', '投稿者', '記事のタイトル', '記事のURL']
            output_df = pd.DataFrame(data, columns=column_list, index=index_list)
            print("\n\n取得したデータ\n")
            pprint(output_df)
            # outputファイル名
            output_file_name = str(datetime.datetime.now()) + "_Search: " + search_word
            # 結果をExcelファイルに出力
            output_df.to_excel('./'+output_file_name + '.xlsx', sheet_name='Qiita_Articles_list')
            #　Webページに自動アクセスするために生成したdriverインスタンスを閉じて消去（メモリ開放）する
            time.sleep(5)
            driver.close()
            driver.quit()
            #return output_df
            api_response = {"取得した記事タイトル" : all_page_title_list,
                "取得した記事の投稿者" : all_page_author_list}

            response = make_response(json.dumps(api_response, ensure_ascii=False))
            return response

### メソッド定義おわり

api.add_resource(GetData, '/scraping')

if __name__ == "__main__":
    app.run(debug=False)
```

##__（ 参考にしたwebサイト ）__

・[Flask-RESTfulにおける文字化け対策](http://terabytesmemo.hatenablog.jp/entry/2016/09/07/151717)