見出し画像

【高速化】Pythonで風俗嬢のデータベース作成

前回の記事では、Pythonでデリヘルタウンのスプレイピングを行い、デリヘル嬢のデータベースを作成するプログラムをご紹介しました。これで約35万人のデリヘル嬢のデータが手に入り、このプログラムを応用していくことで風俗に関する分析がさらに進んでいけるのですが、ここで一つ大きな問題に直面しました。

それは処理速度です。
実施してみるとわかりますが、風俗嬢のデータベース作成までに多くの時間がかかります。以下の①~④の実施工程がありますが、すべて実施すると半日以上かかります。。

①地域別のお店一覧ページのURLを取得
②各お店の女の子一覧ページのURLを取得
③各お店の女の子一覧ページのHTMLを取得
④HTMLから女の子情報を抽出

ここで①~③はデリヘルタウンにアクセスするため、サーバー負荷を軽減させるために1回のアクセス毎に1~2秒sleepさせています。これは、スプレイピングを行う上での鉄則ですので、ここの部分の高速化は不可能です。
そのため、本記事では④の高速化を図ります。

動作環境

今回、速度計測を実施するPC環境は以下のとおりです。
Windows10 Pro 64bit
CPU:Intel core-i7-7700 3.50GHz (4コア)
Memory:16GB

④の処理時間

まずは、既存の処理時間を計測してみます。計測するコードは以下のとおりで、以前の記事で紹介したコードと同じです。
計測結果は1357.37秒で約23分かかりました。

# 1行分のdataframeに変換する関数
def to_gal_df(gal_infos, prefecture, shop_name, shop_sub_info):
  import pandas as pd
  import re

  # 1店舗分の女の子のdataframe
  gal_df_1shop = pd.DataFrame(
      index=[],
      columns=['名前', '年齢', '身長',
               'バスト', 'カップ', 'ウエスト', 'ヒップ',
               '一言説明', '都道府県', '店舗名', '店舗種類']
  )

  for gal_info in gal_infos:

      # 1行分のdataframeを作る
      df = pd.DataFrame(
          index=[0],
          columns=['名前', '年齢', '身長',
                   'バスト', 'カップ', 'ウエスト', 'ヒップ',
                   '一言説明', '都道府県', '店舗名', '店舗種類']
      )

      try:
          # split結果の例: ['', '咲歩(さほ)', '34歳/168cm/84(C)-58-87', '【敏感過ぎる身体】', ' 1', '']
          gal_info_split = gal_info.text.split('\n')

          # 名前の格納
          df['名前'][0] = gal_info_split[1]

          # 歳、身長、スリーサイズの格納
          # split後の例:['34歳', '168cm', '84', 'C', '', '58', '87']
          gal_spec = re.split('[/()-]', gal_info_split[2])
          df['年齢'][0] = int(re.sub("\\D", "", gal_spec[0]))
          df['身長'][0] = int(re.sub("\\D", "", gal_spec[1]))
          df['バスト'][0] = int(gal_spec[2])
          # カップは書いている女の子と書いていない女の子がいるので処理分岐
          if (gal_spec[4] == ''):
              df['カップ'][0] = gal_spec[3]
              df['ウエスト'][0] = int(gal_spec[5])
              df['ヒップ'][0] = int(gal_spec[6])
          else:
              df['カップ'][0] = '-'

              df['ウエスト'][0] = int(gal_spec[3])
              df['ヒップ'][0] = int(gal_spec[4])

          # 一言説明
          df['一言説明'][0] = gal_info_split[3]

          ### prefecture_infoの処理
          df['都道府県'][0] = prefecture.text.strip()

          ### shop_name_infoの処理
          df['店舗名'][0] = shop_name.text.strip()

          ### shop_sub_infoの処理
          shop_sub_info_split = re.split('[|【]', shop_sub_info.text)
          shop_kind = shop_sub_info_split[1].split(' ')
          df['店舗種類'][0] = shop_kind[1]

          # 女の子のデータと判断したものはdataframeに追加
          if (is_valid_df(df)):
              gal_df_1shop = gal_df_1shop.append(df)

      # 配列に合わないデータになる場合は例外
      except:
          continue

  return gal_df_1shop

# 有効な情報かどうかを判定
def is_valid_df(df):
  import logging as lg

  removenames = ['割', 'キャンペーン', 'ツイッター', 'LINE',
                 '日記', '店長', '会員', 'メルマガ', 'WEB', '番長',
                 'デリバリー', '募集', 'プラン', 'おしらせ', 'お知らせ',
                 '交通費', '拡大', 'パック', '急募', '拡張']

  for removename in removenames:
      if removename in df['名前'][0]:
          lg.info('名前:%s in %s',str(removename), str(df['名前'][0]))
          return False
  if (df['年齢'][0] < 15 or 100 < df['年齢'][0]):
      lg.info('年齢:%s < 15 or 100 < %s', str(df['年齢'][0]), str(df['年齢'][0]))
      return False
  if (df['バスト'][0] < 40 or 300 < df['バスト'][0]):
      lg.info('バスト:%s < 40 or 300 < %s', str(df['バスト'][0]), str(df['バスト'][0]))
      return False
  if (df['ウエスト'][0] < 40 or 300 < df['ウエスト'][0]):
      lg.info('ウエスト:%s < 40 or 300 < %s', str(df['ウエスト'][0]), str(df['ウエスト'][0]))
      return False
  if (df['ヒップ'][0] < 40 or 300 < df['ヒップ'][0]):
      lg.info('ヒップ:%s < 40 or 300 < %s', str(df['ヒップ'][0]), str(df['ヒップ'][0]))
      return False
  if(df['バスト'][0] == df['ウエスト'][0] and df['ウエスト'][0] == df['ヒップ'][0]):
      lg.info('スリーサイズ:%s == %s == %s', str(df['バスト'][0]), str(df['ウエスト'][0]), str(df['ヒップ'][0]))
      return False
  if(df['身長'][0] == df['バスト'][0] or df['身長'][0] == df['ウエスト'][0] or df['身長'][0] == df['ヒップ'][0]):
      lg.info('身長とスリーサイズ:%s : %s : %s : %s', str(df['身長'][0]), str(df['バスト'][0]), str(df['ウエスト'][0]), str(df['ヒップ'][0]))
      return False

  # すべてのチェックをパスしたらTrueを返す
  return True


def gen_database():
  # 本プログラムの目的:
  # 「女の子一覧」ページのソースコードをもとに、
  # 全デリヘル嬢のデータベースを作る
  import bs4
  import pandas as pd
  import glob
  import os

  # ファイルリストを作る
  file_paths = glob.glob('./htmls/*.html')

  # 出力先のディレクトリ確認。なければ作成。
  if not os.path.exists('database'):
      os.mkdir('database')

  # 進捗確認用の変数
  count_shops = 0
  num_shops = len(file_paths)

  # 空のdataframeを作る
  global gal_df
  gal_df = pd.DataFrame(
      index=[],
      columns=['名前', '年齢', '身長',
               'バスト', 'カップ', 'ウエスト', 'ヒップ',
               '一言説明', '都道府県', '店舗名', '店舗種類']
  )

  for file_path in file_paths:
      # 対象のhtmlファイルからsoupを作成
      html = bs4.BeautifulSoup(open(file_path, encoding='utf-8'), 'html.parser')

      gal_infos = html.find_all(class_="text")  # 女の子のスペックのみを取り出してリストにする
      prefecture_info = html.find(class_="logo")  # このタグの中に都道府県が書いてある
      shop_name_info = html.find(class_="header_shop_title")  # このタグの中に店舗名が書いてある
      shop_sub_info = html.find("title")  # このタグの中に店舗種類が書いてある(人妻デリヘルとか)

      # gal_infosから、1店舗分のdataframeを生成
      df = to_gal_df(gal_infos, prefecture_info, shop_name_info, shop_sub_info)

      # 1店舗分のdfをgal_dfのケツに追加。最終的にはgal_dfをファイルに書き出す
      gal_df = gal_df.append(df)

      count_shops += 1
      print('\r' + '現在 %04d / %04d 店舗目を処理中' % (count_shops, num_shops), end='')

      # 100店舗ごとにgal_dfをファイル書き出し(途中でプログラムをストップしても中間結果が残るように)
      if (count_shops % 100 == 0):
          gal_df.to_csv("database/gals_database.csv", encoding='utf_8_sig', index=None)

  # 最終結果をファイル書き出し
  gal_df.to_csv("database/gals_database.csv", encoding='utf_8_sig', index=None)
  

並列処理の実施

Pythonは普通にプログラムを実行しても1つのコアしか使用されず、CPUが100%使用されることはありません。私のPCの場合、コアが4つありますので25%までしか使われません。
このコアを複数使用して処理を高速化することを並列処理といいます。Pythonには並列処理を行うライブラリがいくつか用意されていますが、今回は最も手軽に実施できる「joblib」というものを使用しました。
これにより、処理時間は463.74秒と約8分で完了しました。実施前に比べて3倍以上高速化できています。
 ※詳しい並列処理の実施方法は別ブログをご確認ください。

参考サイト①:https://qiita.com/simonritchie/items/1ce3914eb5444d2157ac
参考サイト②:https://qiita.com/Yuhsak/items/1e8533343cf5458e2e08


def gen_gals_database(file_path):
   import bs4

   # 対象のhtmlファイルからsoupを作成
   html = bs4.BeautifulSoup(open(file_path, encoding='utf-8'), 'html.parser')

   gal_infos = html.find_all(class_="text")  # 女の子のスペックのみを取り出してリストにする
   prefecture_info = html.find(class_="logo")  # このタグの中に都道府県が書いてある
   shop_name_info = html.find(class_="header_shop_title")  # このタグの中に店舗名が書いてある
   shop_sub_info = html.find("title")  # このタグの中に店舗種類が書いてある(人妻デリヘルとか)

   # gal_infosから、1店舗分のdataframeを生成
   return to_gal_df(gal_infos, prefecture_info, shop_name_info, shop_sub_info)

def gen_database():
   # 本プログラムの目的:
   # 「女の子一覧」ページのソースコードをもとに、
   # 全デリヘル嬢のデータベースを作る
   import pandas as pd
   import glob
   import os
   from joblib import Parallel, delayed

   # ファイルリストを作る
   file_paths = glob.glob('./htmls/*.html')
   file_nums = len(file_paths)

   # 出力先のディレクトリ確認。なければ作成。
   if not os.path.exists('database'):
       os.mkdir('database')

   df = Parallel(n_jobs=-1, verbose=3)([delayed(gen_gals_database)(file_path) for file_path in file_paths])

   # 空のdataframeを作る
   gal_df = pd.DataFrame(
       index=[],
       columns=['名前', '年齢', '身長',
                'バスト', 'カップ', 'ウエスト', 'ヒップ',
                '一言説明', '都道府県', '店舗名', '店舗種類']
   )

   num_current = 0

   for dff in df:
       gal_df = gal_df.append(dff)
       num_current += 1
       print('\r' + '現在 %04d / %04d 店舗目を結合中' % (num_current, len(df)), end='')

   # 最終結果をファイル書き出し
   gal_df.to_csv("database/gals_database.csv", encoding='utf_8_sig', index=None)
```

Appendの廃止

 今回のプログラムではこれでも十分なのですが、今後取得するデータが増えた場合に備えて、コード内のデータフレームの追加で使用しているAppendを使用しない方向に修正しておきます。
 こちらのコードでの処理時間は295.51秒で約5分とさらに時短になりました。今回append関数を使用する回数は約6000回でしたのでこの時間となりましたが、数万回以上の処理が必要になった時効果を発揮します。

```python
def gen_gals_database(file_path):
   import bs4

   # 対象のhtmlファイルからsoupを作成
   html = bs4.BeautifulSoup(open(file_path, encoding='utf-8'), 'html.parser')

   gal_infos = html.find_all(class_="text")  # 女の子のスペックのみを取り出してリストにする
   prefecture_info = html.find(class_="logo")  # このタグの中に都道府県が書いてある
   shop_name_info = html.find(class_="header_shop_title")  # このタグの中に店舗名が書いてある
   shop_sub_info = html.find("title")  # このタグの中に店舗種類が書いてある(人妻デリヘルとか)

   # gal_infosから、1店舗分のdataframeを生成
   return to_gal_df(gal_infos, prefecture_info, shop_name_info, shop_sub_info)

def gen_database():
   # 本プログラムの目的:
   # 「女の子一覧」ページのソースコードをもとに、
   # 全デリヘル嬢のデータベースを作る
   import pandas as pd
   import glob
   import os
   from joblib import Parallel, delayed

   # ファイルリストを作る
   file_paths = glob.glob('./htmls/*.html')
   file_nums = len(file_paths)

   # 出力先のディレクトリ確認。なければ作成。
   if not os.path.exists('database'):
       os.mkdir('database')

   df = Parallel(n_jobs=-1, verbose=3)([delayed(gen_gals_database)(file_path) for file_path in file_paths])

   all_lens = 0
   for file_num in range(file_nums):
       all_lens += len(df[file_num])

   print(all_lens)

   # 空のdataframeを作る
   gal_df = pd.DataFrame(
       index=range(all_lens),
       columns=['名前', '年齢', '身長',
                'バスト', 'カップ', 'ウエスト', 'ヒップ',
                '一言説明', '都道府県', '店舗名', '店舗種類']
   )

   num_current = 0

   for file_num in range(file_nums):
       df_lens = len(df[file_num])
       dff = df[file_num]

       for df_len in range(df_lens):
           gal_df.iloc[num_current, :] = dff.iloc[df_len, :]
           num_current += 1

       print('\r' + '現在 %04d / %04d 目を取得  ' % (num_current, all_lens), end='')

   # 最終結果をファイル書き出し
   gal_df.to_csv("database/gals_database.csv", encoding='utf_8_sig', index=None)
```

まとめ

いかがでしたでしょうか。
今回の高速化により、22分かかっていた処理を5分で完了させることができました。約4倍の速度UPです。
とても不親切な説明でとても分かりにくかったかと思いますが、伝えたかったことは、

Pythonで処理速度を上げたい場合は「並列処理とAppendの廃止」を心がけてください

ということです。まずはこれだけで大幅に改善されます。

この記事が気に入ったらサポートをしてみませんか?