6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-11-10

作ったもの

###( 入力 )

  1. 入力場所:Webブラウザ
  2. 入力内容:URLの__リクエストパラメータ "word"__に、__記事の検索単語__を渡したlocal hostのURL(port: 5000):str
url
 http://127.0.0.1:5000/scraping?word=Qiita記事検索文字列

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

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

###( 出力 )

####出力1:ブラウザへの出力

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

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

####出力2:カレント・ディレクトリへの出力

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

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

###操作手順

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

Terminal
$ python3 selenium_flask_qiita_multi_pages_get_data_while.py

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

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

###(実行例)

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

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

####出力1:Webブラウザ

スクリーンショット 2020-11-11 1.28.55.png

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

####出力2:Excelファイル(1件)と画像ファイル(7件)

スクリーンショット 2020-11-11 1.39.05.png

( Excelファイル )

スクリーンショット 2020-11-11 1.41.03.png

( 画像ファイル )

(画面キャプチャ画像)1ページ目の記事一覧ページ
スクリーンショット 2020-11-11 1.42.03.png

(画面キャプチャ画像)1〜6ページ目の記事一覧ページ
( 掲載省略 )

(画面キャプチャ画像)7ページ目の記事一覧ページ
スクリーンショット 2020-11-11 1.42.11.png


###( 別解 ) 操作手順

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

Terminal
$ python3 selenium_flask_qiita_multi_pages_get_data_while.py

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

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

####( 別解の実行例 )

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

####出力1:上記を実行したTerminal

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",
        "4名の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"
    ]
}


$

####出力2:Excelファイル(1件)と画像ファイル(7件)

#####( 掲載省略 )


##実装コード

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における文字化け対策

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?