前回の記事では、Pythonを使ったスクレイピング(Scraping)の方法について、価格COMのスペック情報を抜き出すというテーマで解説しました。
今回は、前回のソースコードの中から汎用的に使える部分をクラス化したので紹介したいと思います。
ついでに、前回のサンプルコードを今回のクラスで置き換えてスッキリさせましたので、併せてご紹介したいと思います。
クラスはコピペで使えますので、是非ご活用下さい。
スクレイピング便利クラスの概要
今回はScrape というクラス名にしました。
クラスの構成は次の様になっています。

下記は指定したURLのページを読み込み、title タグの text を読み出すサンプルになります。
1 2 3 |
scr = Scrape(2,5) soup = scr.request('https://kakaku.com/camera/digital-camera/itemlist.aspx') title = soup.find('title').text |
コンストラクタで2と5を指定していますので、URLを1ページ読み込むごとに最低2秒、最大5秒の間でランダムなウェイトが発生します。
リファレンス
詳細はソースコードのコメントに記載していますので、ここでは各メソッドについて簡単に解説しておきます。
内容 | メソッド名と引数 | 補足 |
---|---|---|
コンストラクタ | Scrape(wait=1,max=None) | リクエストごとに、wait~max の間で ランダムな待ち時間が入る。 |
サイトにリクエストを発行し、取得したHTMLを保持したBeautifulSoupのインスタンスを返す | request(url, wait=None, max=None, console=True) | wait,maxが指定されると、コンストラクタよりこちらの値が優先される。 console=Trueの場合、URLと処理時間 がコンソールに出力される。 戻り値として、BeautifulSoupのインスタンスが返される。 |
アンカータグからURLを取得し リストで返す 。 | get_href(soup,contains = None) | contains を指定すると、その文字列が含まれるURLだけが返される。 |
Imgタグから画像のURLを取得し リストで返す 。 | get_src(soup,contains = None) | contains を指定すると、その文字列が含まれるURLだけが返される。 |
文字列にキーワードが 含まれているかチェックする 。 | contains(line,kwd) | line もしくは kwd が None 又は ” の場合は Trueを返す。 |
指定した文字を削除する 。 | omit_char(values,omits) | omits はリスト形式で指定する。 例:[’\u3000’,’\n’] |
DataFrameに1行追加する 。 | add_df(values,columns, omits = None) | クラス内部に保持しているDataFrameに1行追加する。 |
DataFrameの内容を CSVとしてファイルに書き込む 。 | to_csv(filename,dropcolumns=None) | クラス内部に保持しているDataFramをCSV出力する。 |
テキストプロパティの値を取得する。 | get_text(soup) | findの結果が bs4.element.Tag で返されるので、これを引数として渡す。そfindの結果がNoneの場合は半角スペース(’ ‘)を、Noneでなければ text の値を返す。 |
重複する列名の末尾に連番を 付けてユニークにする 。 | rename_column(columns) | リストで指定したカラム名において、重複しているカラム名には末尾に連番を付けて返す。 |
指定したファイルに任意の文字 列を追記する 。 | write_log(filename,message) | スクレイプした結果、エラーになったURLなどをファイルに記録しておくためのもの。 |
指定したファイルを読み込んで リストで返す 。 | read_log(filename) | エラーになったURLリストを読み込んで、再読み込み等何らかの処理を行いたい場合に使う。 |
Scrapeクラスのソースコード
以下はScrapeクラスのソースコードになります。
そのままコピペしてお使いいただけますが、必要に応じて適宜修正してお使い下さい。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
import requests from bs4 import BeautifulSoup import time import random import pandas as pd import time import datetime class Scrape(): def __init__(self,wait=1,max=None): self.response = None self.df = pd.DataFrame() self.wait = wait self.max = max self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36"} self.timeout = 5 def request(self,url,wait=None,max=None,console=True): ''' 指定したURLからページを取得する。 取得後にwaitで指定された秒数だけ待機する。 max が指定された場合、waitが最小値、maxが最大値の間でランダムに待機する。 Params --------------------- url:str URL wait:int ウェイト秒 max:int ウェイト秒の最大値 console:bool 状況をコンソール出力するか Returns --------------------- soup:BeautifulSoupの戻り値 ''' self.wait = self.wait if wait is None else wait self.max = self.max if max is None else max start = time.time() response = requests.get(url,headers=self.headers,timeout = self.timeout) time.sleep(random.randint(self.wait,self.wait if self.max is None else self.max)) if console: tm = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S') lap = time.time() - start print(f'{tm} : {url} 経過時間 : {lap:.3f} 秒') return BeautifulSoup(response.content, "html.parser") def get_href(self,soup,contains = None): ''' soupの中からアンカータグを検索し、空でないurlをリストで返す containsが指定された場合、更にその文字列が含まれるurlだけを返す Params --------------------- soup:str BeautifulSoupの戻り値 contains:str 抽出条件となる文字列 Returns --------------------- return :[str] 条件を満たすurlのリスト ''' urls = list(set([url.get('href') for url in soup.find_all('a')])) if contains is not None: return [url for url in urls if self.contains(url,contains)] return [url for url in urls if urls is not None or urls.strip() != ''] def get_src(self,soup,contains = None): ''' soupの中からimgタグを検索し、空でないsrcをリストで返す containsが指定された場合、更にその文字列が含まれるurlだけを返す Params --------------------- soup:str BeautifulSoupの戻り値 contains:str 抽出条件となる文字列 Returns --------------------- return :[str] 条件を満たすurlのリスト ''' urls = list(set([url.get('src') for url in soup.find_all('img')])) if contains is not None: return [url for url in urls if contains(url,self.contains)] return [url for url in urls if urls is not None or urls.strip() != ''] def contains(self,line,kwd): ''' line に kwd が含まれているかチェックする。 line が None か '' の場合、或いは kwd が None 又は '' の場合は Trueを返す。 Params --------------------- line:str HTMLの文字列 contains:str 抽出条件となる文字列 Returns --------------------- return :[str] 条件を満たすurlのリスト ''' if line is None or line.strip() == '': return False if kwd is None or kwd == '': return True return kwd in line def omit_char(self,values,omits): ''' リストで指定した文字、又は文字列を削除する Params --------------------- values:str 対象文字列 omits:str 削除したい文字、又は文字列 Returns --------------------- return :str 不要な文字を削除した文字列 ''' for n in range(len(values)): for omit in omits: values[n] = values[n].replace(omit,'') return values def add_df(self,values,columns,omits = None): ''' 指定した値を DataFrame に行として追加する omits に削除したい文字列をリストで指定可能 Params --------------------- values:[str] 列名 omits:[str] 削除したい文字、又は文字列 ''' if omits is not None: values = self.omit_char(values,omits) columns = self.omit_char(columns,omits) df = pd.DataFrame(values,index=self.rename_column(columns)) self.df = pd.concat([self.df,df.T]) def to_csv(self,filename,dropcolumns=None): ''' DataFrame をCSVとして出力する dropcolumns に削除したい列をリストで指定可能 Params --------------------- filename:str ファイル名 dropcolumns:[str] 削除したい列名 ''' if dropcolumns is not None: self.df.drop(dropcolumns,axis=1,inplace=True) self.df.to_csv(filename,index=False,encoding="shift-jis",errors="ignore") def get_text(self,soup): ''' 渡された soup が Noneでなければ textプロパティの値を返す Params --------------------- soup: bs4.element.Tag bs4でfindした結果の戻り値 Returns --------------------- return :str textプロパティに格納されている文字列 ''' return ' ' if soup == None else soup.text def rename_column(self,columns): ''' 重複するカラム名の末尾に連番を付与し、ユニークなカラム名にする 例 ['A','B','B',B'] → ['A','B','B_1','B_2'] Params --------------------- columns: [str] カラム名のリスト Returns --------------------- return :str 重複するカラム名の末尾に連番が付与されたリスト ''' lst = list(set(columns)) for column in columns: dupl = columns.count(column) if dupl > 1: cnt = 0 for n in range(0,len(columns)): if columns[n] == column: if cnt > 0: columns[n] = f'{column}_{cnt}' cnt += 1 return columns def write_log(self,filename,message): ''' 指定されたファイル名にmessageを追記する。 Params --------------------- filename: str ファイル名 message: str ファイルに追記する文字列 ''' message += '\n' with open(filename, 'a', encoding='shift-jis') as f: f.write(message) print(message) def read_log(self,filename): ''' 指定されたファイル名を読み込んでリストで返す Params --------------------- filename: str ファイル名 Returns --------------------- return :[str] 読み込んだ結果 ''' with open(filename, 'r', encoding='shift-jis') as f: lines = f.read() return lines |
価格COMのデジカメ情報を取得する
では、次に今回のクラスを使って、前回記事で掲載したプログラムを書き直した結果をご紹介しましょう。
全体の構造
今回は Scrape クラスを使うので、その分ソースコードが簡略化できます。
とは言うものの、詳細ページからスペック情報を取得する部分が少々煩雑なので、これをget_detailという名前で関数化することにしました。

get_detail関数は価格COMの詳細情報からスペック情報を抜き出すための専用関数であるため、Scrape クラスにメソッドは実装せず、今回専用の関数として用意しました。

以下がそのソースコードになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
def get_detail(link): ''' 指定した商品明細のURLに記載されているページからスペック情報を取得する関数 Params --------------------- link:str 価格comの商品詳細ページのURL ''' scr = Scrape() soup = scr.request(f'{link}spec/#tab') table = soup.find('table',class_='tblBorderGray mTop15') if table is None: return None,None #商品ID,商品名,発売日,最安価格,価格帯(下限、上限)を取得 columns = ['商品ID','商品名','発売日','最安価格','価格帯(下限)','価格帯(上限)'] values = [ link[-12:-1], scr.get_text(soup.find('h2',itemprop='name')), scr.get_text(soup.find('span',class_='releaseDate')), scr.get_text(soup.find('span',class_='priceTxt')).replace(',','')[1:], scr.get_text(soup.find('span',itemprop='lowPrice')).replace(',','')[1:], scr.get_text(soup.find('span',itemprop='highPrice')).replace(',','')[1:] ] #tableタグの中身からスペック情報を取り出す trs = table.find_all('tr') for tr in trs: ths = tr.find_all('th') tds = tr.find_all('td') for th,td in zip(ths,tds): columns.append(th.text) values.append('' if td is None else td.text) return columns,values |
メインのスクレイピング処理
メインのスクレイピング処理は、次の様なフローになります。

実際にスクレイピングをして分かったことですが、一覧ページの中には中古デジカメのURLも含まれていました。
これを処理しないようにするため、URLに ‘used’ の文字が含まれていた場合、処理を飛ばすようにしています。
また、何らかの理由でページ読み込みに失敗することがあったので、それが分かるようにログファイルにURLを出力するようにしています。
正しくスペック情報が取得できた場合、ScrapeのインスタンスにあるDataFrameに結果を格納していき、最後にCSV出力するという方法を取っています。
以上のことを念頭に、もう少しフローを細かく記載したのが下記の図です。

そして、最終的なスクレイピングのソースコードは次の様になりました。
デジカメの商品一覧ページは全部で6ページありますので、for ループで1~7でカウントをしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
scr = Scrape() for n in range(1,7): soup = scr.request(f'https://kakaku.com/camera/digital-camera/itemlist.aspx?pdf_pg={n}') links = scr.get_href(soup,'https://kakaku.com/item/') for link in links: if 'used' in link: continue values,columns = get_detail(link) if values is None: #失敗したURLをログに追加 scr.write_log('p:\err_list.txt',link) else: #DataFrameにスペック情報を格納 scr.add_df(columns,values,['\u3000']) scr.to_csv('p:/camera-src.csv') |
出力されたCSVは次の様になりました。

まとめ
今回はPythonを使ったスクレイピングで便利に使えるScrapeクラスを作ってみたので、それについて解説致しました。
また、前回記事で掲載したソースコードをScrapeクラスを使うように書き換えたサンプルも紹介致しました。
クラスにどのような機能を持たせるかについては、個人の考え方がありますので、あくまでも1つの例として参考にして頂ければと思います。
ちなみに、商品一覧のURL部分を書き換えるだけで、価格.COM に掲載されている他の商品にも対応できそうです。
全て試したわけではありませんが、ノートPCとかデジタル一眼は、この方法でうまくいきました。
今回の記事が皆様のお役に立てば幸いです。