LoginSignup
4
5

More than 3 years have passed since last update.

ツタヤの在庫検索が使いづらいので、PythonでどうにかするGUIアプリ作ってみた

Last updated at Posted at 2019-04-28

はじめに

こんにちは。はなたと申します。元々はてなブログをやっていたのですが、プログラミングに関する記事はqiitaで公開しようということで、書いてみようと思います。

ツタヤの在庫検索システム

突然ですが皆さん、TSUTAYAの在庫検索システムを利用したことがありますか?
https://store-tsutaya.tsite.jp/item.html

これはCDやDVDの在庫を、店舗を指定して調べることができるスグレモノです。
しかし1つだけ欠点があります。。。
このシステム、在庫がある店だけを絞り込むことができません。そのため、在庫がある店を探し出すには、全ての店のページを見る必要があります。
5店舗ぐらい調べてみるものの、どこも取扱していないから「この商品はレンタル終わったんだ…」って諦めることが多々あります。

というわけで今回は、在庫がある店の情報のみを抽出し表示するGUIアプリを作ってみました。pyinstallerでexe化もしました。長い記事になりそうなので、最初に動作の様子を貼っておきます。こんな感じです↓

完成GUIプログラムの実際の動作

gui_do.gif
意外と便利ですよね。どうですか?
プログラムの概要はスクレイピングのコードと一緒に後ほど解説します。
GUI部分は…あまり良く分かっておらず、見よう見まねで書いてみたので触れません。

コードの説明

コードの説明をしていきます。頑張って細かく書くつもりですが、たぶん途中で雑になります。予めご了承ください。
完成したコードはgithubにて公開しております。
https://github.com/hatena-hanata/Search_zaiko_app/tree/develop

ライブラリ

main_app.py
from tkinter import *
from tkinter import messagebox
from tkinter import ttk
from bs4 import BeautifulSoup
import requests
from selenium import webdriver
import time
import threading

GUI部分はtkinterを使用しています。
スクレイピングですが、今回は目的のページがJavaScriptでレンダリングされていたので、seleniumを利用します。ブラウザはPhantomJSを使用しました。
ただし、PhantomJSは更新が終了してしまい、現在使用が推奨されていないので、他のヘッドレスブラウザを使用したほうが良いと思います。
threadingはスクレイピングを並列処理させるためです。

スクレイピングの流れ

このアプリの一番の根幹をなす、スクレイピング部分について記述します。
在庫検索には「商品ID」「販売形式(CD or DVD)」「都道府県ID」が必要になります。都道府県IDについては、ひとまず関東地方の4件だけにしています。

1. PhantomJSを起動して商品名を取得

main_app.py
    def scraping(self, item_id, item_type, prefecture_id):
        # ---ブラウザの起動---
        self.print_msg('ブラウザを起動しています…')
        driver = webdriver.PhantomJS()
        driver.implicitly_wait(10)  # 要素が見つかるまで10秒待機
        self.print_msg('ブラウザを起動しました')

        # ---商品名の取得(商品IDの正誤検知)---
        self.print_msg('商品名を取得しています…')
        item_url = 'https://store-tsutaya.tsite.jp/item/{0}/{1}.html'.format(item_type, item_id)
        driver.get(item_url)
        item_html = driver.page_source
        item_soup = BeautifulSoup(item_html, 'html.parser')
        if item_soup.find('div', id='errorBlock') is not None:
            self.print_msg('商品ID、販売形式をもう一度確認してください')
            driver.quit()  # ブラウザを閉じる
            self.start_btn['state'] = NORMAL
            return

        item_title = item_soup.find('div', class_='header').find('h2').find('span').string
        self.print_msg('タイトル:{} の在庫を検索します'.format(item_title))

入力された商品ID・販売形式から、商品ページのURLを開きます。こんなページです。20190203193558.png
URLのうち、赤い丸で囲まれているところから、商品IDと販売形式が分かります。
そしてこのページから商品名を取得し、表示させます。

ぶっちゃけ商品名取得はそこまで重要ではなく、入力された商品IDのバリデーションチェックを行うのが目的です。もし移動したURL先がエラーページであれば、その旨を表示してスクレイピングを中止します。

2. 店舗一覧ページに移動、掲載ページ数を求める

上の画像の青丸で囲まれているボタンを押して、都道府県を指定したページに移動します。
総店舗数しか分からず、何回ページ送りすれば良いかわからないので、総店舗数から掲載ページ数を求めいています。
ここのコード、ダサいですね。いつか修正します。

main_app.py
    def scraping(self, item_id, item_type, prefecture_id):

        ####

        # 店一覧のページをsoupへ
        self.print_msg('店舗一覧を取得しています…')
        shop_search_url = 'https://store-tsutaya.tsite.jp/tsutaya/articleList?account=tsutaya&' \
                          'accmd=1&arg=https%3A%2F%2Fstore-tsutaya.tsite.jp%2Fitem%2F{0}%2F{1}' \
                          '.html&ftop=1&adr={2}' \
            .format(item_type, item_id, prefecture_id)
        driver.get(shop_search_url)
        html = driver.page_source
        soup = BeautifulSoup(html, 'html.parser')
        self.print_msg('店舗一覧の取得が完了しました')

        # 掲載ページ数を求める
        search_result_num = soup.find(class_='txt_k f_left').string
        total_shop_num = int(search_result_num.split('件')[1].split('全')[-1])
        if total_shop_num % 20 == 0:
            lastpage = total_shop_num / 20
        else:
            lastpage = int(total_shop_num / 20) + 1

3. ページに掲載されている全店舗のURLを取得する

20190203194502.png
このページの「在庫を表示する」ボタンを押すと、その店舗での商品の在庫状況が書かれたページに移動します。というわけで、このボタンのリンクを取得します。

main_app.py
    def scraping(self, item_id, item_type, prefecture_id):

        ####

        # ページごと処理
        for page in range(1, lastpage + 1):
            # stopボタンが押された
            if self.is_active is False:
                self.print_msg('作業を中断しました')
                return

            # 店舗一覧ページに戻る
            driver.get(shop_search_url)

            # ページを更新
            driver.execute_script("ExecuteAjaxRequest('./articleList', 'account=tsutaya&accmd=1&" \
                                  "arg=https%3a%2f%2fstore-tsutaya.tsite.jp%2fitem%2f{0}%2f{1}.html" \
                                  "&searchType=True&adr={2}&pg={3}&pageSize=20&pageLimit=10000&" \
                                  "template=Ctrl%2fDispListArticle_g12', 'DispListArticle'); return false;" \
                                  .format(item_type, item_id, prefecture_id, page))
            # ページのsoupを取得する
            p_html = driver.page_source
            p_soup = BeautifulSoup(p_html, 'html.parser')

            # ページに掲載されている全店舗のURL取得
            table = p_soup.find('div', id='DispListArticle').find('table')
            links = table.find_all('a')

4. 各店舗の在庫情報を取得する

ようやく在庫情報が書かれた、目的のページに到達します。ここで在庫情報を取得します。
商品の取扱がされていない場合、店舗名は表示されません。

main_app.py
    def scraping(self, item_id, item_type, prefecture_id):

        ####

        # ページごと処理
        for page in range(1, lastpage + 1):

            ####

            for link in links:
                # stopボタンが押された
                if self.is_active is False:
                    self.print_msg('作業を中断しました')
                    return
                shop_url = link.get('href')
                cnt += self.get_zaiko_info(driver, shop_url)  # 在庫情報を取得
                self.print_msg('{}店舗中 {}店舗の在庫確認が終わりました'.format(total_shop_num, cnt))


    def get_zaiko_info(self, driver, url):
        # urlを開いてsoupへ
        driver.get(url)
        html = driver.page_source
        soup = BeautifulSoup(html, 'html.parser')

        # 店の名前と在庫情報を取得
        shop_name = soup.find('h3', class_='green clearfix').find('a').string
        zaiko = soup.find('div', class_='state').find('span').string

        # 取扱がない場合1をreturnするだけ、取扱がある場合は在庫情報を表示する
        if '-' in zaiko:
            return 1
        else:
            if '○' in zaiko:
                self.textField.insert('end', '{}では在庫があります'.format(shop_name))
            else:
                self.textField.insert('end', '{}では取扱していますが、現在在庫がありません'.format(shop_name))
            return 1

pyinstallerを利用してexe化してみる

GUI部分は割愛!ひとまず完成したので、pyinstallerを利用してexe化してみます。
予めpipでpyinstallerやプログラムで使用している外部ライブラリをインストールしておく必要があります。今回だとbeautifulsoupやrequestsですね。

コマンドプロンプトにて

pyinstaller main_app.py --onefile --noconsole

を実行するとexeが作成されます。--onefileオプションを付けたことで、exeのみで動作するようになっています。
ただ、今回のプログラムではPhantomJSを利用します。私の環境ではpathを通しているため、PhantomJSの起動は問題ありませんでした。
配布することを考えたら、プログラムでPhantomJSの場所を指定してあげたほうが良さそうです。

おわりに

初めてqiitaの記事を書きましたが、すごい疲れました。人にプログラムの説明をするのってすごい大変です。
質問やこのコードダサくね?などあれば気軽にコメントください。豆腐メンタルなので、誹謗中傷は受け付けません。

4
5
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
4
5