python: webスクレイピングで検索結果をCSVファイルに出力する

プログラミング
スポンサーリンク




こんにちは、おみです。

前回、検索結果を表示する機能を作成しました。

しかし、検索結果を表示するだけでは何か物足りないので、どうせだったら検索結果をCSVファイルに出力して、見たいときに見れるようにしたいと思います。

また、検索結果を全て別ウィンドウで表示するとごちゃごちゃして邪魔なので、1つのウィンドウにまとめて表示できるようにしたいと思います。

スポンサーリンク

機能の説明

検索欄に知りたいことを入力し、searchボタンを押すと

検索結果のうち上位数件を別ウィンドウで表示します

また、csvファイルにも結果が保存されます。

ソースコード

前回と大きく変わった点は、太字になっています。

ファイル構造

・Main.py

import tkinter as tk

from src.Model import Model
from src.View import View
from src.Controller import Controller


class Application(tk.Frame):
    def __init__(self, root):
        # スーパークラスのコンストラクタを呼び出し
        super().__init__(root)

        # モデルをインスタンス化
        self.model = Model(root)

        # ビューをインスタンス化
        self.view = View(root, self.model)

        # コントローラーをインスタンス化
        self.controller = Controller(root, self.model, self.view)

        # Viewにメソッドをセット
        self.view.btn_search["command"] = lambda: self.controller.call_btn_clicked()

        # 画面の設定
        root.geometry(str(self.model.width) + "x" + str(self.model.height))
        root.title(self.model.title)


def main():
    root = tk.Tk()
    window = Application(root)
    window.mainloop()


if __name__ == "__main__":
    main()

・Model.py

import configparser as c
import tkinter as tk
import os

# ---------------------------------
# スクレイピング用のモジュールをインポート
# ---------------------------------
# ブラウザ表示用
import urllib
import webbrowser
# HTMLダウンロード用
import requests
# HTML解析用
import bs4

# ---------------------------------
# csv操作用のモジュールをインポート
# ---------------------------------
import csv


class Model(object):
    def __init__(self, root):
        # ---------------------------------
        # 初期設定
        # ---------------------------------
        # configファイルを読み込み
        self.config = c.ConfigParser()
        self.config.read("config.ini")

        # 画面タイトルを取得
        self.title = self.config["DISPLAY_INFO"]["title"]

        # 画面サイズを取得
        self.width = self.config["DISPLAY_INFO"]["width"]
        self.height = self.config["DISPLAY_INFO"]["height"]

        # 表示ページ数を取得
        self.pages = self.config["SETTING"]["pages"]

        # 画面パーツに入力した値を格納する変数を宣言
        self.ent_search_text = tk.StringVar()

    # ボタンクリック時起動メソッド
    def btn_clicked(self):
        # 検索結果のタイトルとURLを格納するリストを生成する
        list_title = []
        list_url = []

        # 検索結果一覧のwebページのHTMLを取得する
        search_result = requests.get("https://www.google.com/search?q={}".format((self.ent_search_text.get()))).content

        # 取得したHTMLを使用してBeautifulSoupオブジェクトを生成する
        result_soup = bs4.BeautifulSoup(search_result, "html.parser")

        # 検索結果のtitleを含むdivタグのリストを取得
        title_elems = result_soup.select('div[class="kCrYT"] > a > .BNeawe.vvjwJb.AP7Wnd')

        # 検索結果のurlを含むaタグのリストを取得
        url_elems = result_soup.select('div[class="kCrYT"] > a')

        # 指定された件数分検索結果をリストに格納する
        for idx in range(0, min(int(self.pages), len(url_elems))):
            # divタグからタイトルを取得
            title = title_elems[idx].text

            # aタグからurlを取得(日本語はエンコーディングされているので、デコードする)
            url = urllib.parse.unquote(url_elems[idx].get('href').split('&sa=U&')[0].replace('/url?q=', ''))

            # リストに要素を追加
            list_title.append(title)
            list_url.append(url)

            # ブラウザをインスタンス化
            browser = webbrowser.get()

            # urlを指定してwebページを表示
            browser.open(url)

        # CSVファイルの情報を生成
        # ファイルパス
        result_path = "search_result.csv"
        # ヘッダー
        column_names = ["title", "url"]

        # 検索結果ファイルが存在しない場合、作成する
        if not os.path.exists(result_path):
            with open(result_path, "w") as f:
                # CSV書き込み用オブジェクトを生成
                writer = csv.DictWriter(f, column_names)

                # ヘッダーを書き込み
                writer.writeheader()

        # csvファイルにデータを書き込む
        with open(result_path, "a") as f:
            # CSV書き込み用オブジェクトを生成
            writer = csv.DictWriter(f, column_names)

            # 要素を書き込み
            for i in range(0, len(list_title)):
                writer.writerow({"title": list_title[i], "url": list_url[i]})

・View.py

import tkinter as tk


class View(object):
    def __init__(self, root, model):
        self.root = root
        self.model = model

        # ---------------------------------
        # 画面の部品を生成
        # ---------------------------------
        # 検索欄ラベル
        self.lbl_search = tk.Label(
            root,
            text="word"
        )

        # 検索欄
        self.ent_search = tk.Entry(
            root,
            textvariable=model.ent_search_text,
            width=30
        )

        # 検索ボタン
        self.btn_search = tk.Button(
            root,
            text="search",
            command=lambda: model.btn_clicked()
        )

        # ---------------------------------
        # 画面の部品を配置
        # ---------------------------------
        self.lbl_search.grid(row=0, column=0)
        self.ent_search.grid(row=0, column=1)
        self.btn_search.grid(row=0, column=2)

・Controller.py

class Controller(object):
    def __init__(self, root, model, view):
        self.root = root
        self.model = model
        self.view = view

    # 検索ボタンクリック時起動メソッドを定義
    def call_btn_clicked(self):
        self.model.btn_clicked()

・config.ini

[DISPLAY_INFO]
title = "検索結果取得"
width = 400
height = 30

[SETTING]
pages = 3

作成時に詰まった点

・CSSセレクタでスペース区切りのクラスを取得する処理

Googleの検索結果のHTMLには、class名がスペースで区切られているものが存在します。

(例) <div class="aaa bbb ccc">alpha</div>

これは、”aaa bbb ccc”というクラス名が指定されているのではなく、

“aaa”と”bbb”と”ccc”の3つのclassが指定されていることを示します。

従って、先ほどのエレメントをcssセレクタで

div[class="aaa bbb ccc"]

と指定しても取得することができません

classが3つ繋がっているので、

div[class=".aaa.bbb.ccc"]

のように、3つそれぞれをclassとして扱うと、取得することができます。

 

↓使用例

# 検索結果のtitleを含むdivタグのリストを取得
title_elems = result_soup.select('div[class="kCrYT"] > a > .BNeawe.vvjwJb.AP7Wnd')

・URLに日本語が記載されている場合

URLに日本語が記載されている場合、パーセントエスケープ処理が行われてしまうのでそのまま検索してもWebページが表示されません。

パーセントエスケープを解除するには、デコード処理を行う必要があります。

# aタグからurlを取得(日本語はエンコーディングされているので、デコードする)
url = urllib.parse.unquote(url_elems[idx].get('href').split('&sa=U&')[0].replace('/url?q=', ''))

参考文献

PythonでURLエンコード・デコード(urllib.parse.quote, unquote) | note.nkmk.me
Pythonの標準ライブラリのurllib.parseモジュールを使うと、文字列のURLエンコード(パーセントエンコーディング)、および、そのデコードを行うことができる。日本語を含むURLを処理するのに便利。urllib.parseモジュールをインポートする。標準ライブラリなので追加でインストールする必要はない。url...

コメント

タイトルとURLをコピーしました