【顔認識＋機械学習でカワイイは作れる】～某サロン検索サイトをスクレイピング・クローリング～

# きっかけ

 あれはイタリアン・バルに飲みに行ったときのこと。
 私はカウンターの中のスタッフ<font color="Blue">ともみ</font>ちゃん が超美人だと気づいた。

 私：「ねぇ、<font color="Blue">ともみ</font>ちゃん、すごい美人じゃない！？」
 男：「あーよく見ると確かに。でも俺的には<font color="SandyBrown">ゆか</font>ちゃん（別スタッフ）の方がいいな。だって<font color="Blue">ともみ</font>ちゃんの<font color="DeepPink">**髪**</font>短すぎじゃん？少年じゃないんだから…」
 　　　
 確かに、<font color="Blue">ともみ</font>ちゃんは刈り上げベリーショト、一方で他スタッフの<font color="SandyBrown">ゆか</font>ちゃんは、ゆるふわミディアム（顔はどこにでもいそうなフツーの子だが…）。

 ここで私は気づいたのだ。
 男は顔の良し悪しなんてよく見ちゃいない、<font color="DeepPink">髪型</font>から作り出される雰囲気を見てるんじゃないか…？
 つまりだ、
 <font color="DeepPink">ヘアスタイル</font>さえそれっぽい状態を作り出せば、美人に勝てる可能性があるんじゃないか！？
 
 **`カワイイは作れる`**という言葉が脳裏をよぎった私、
 技術を使って**`カワイイを作る`**ことを決意した。`

# できたこと
先に、何が出来るようになったか説明する。

① カワイイを作りたい人の顔写真を入力する（既にカワイイけど）
![加工0.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442237/217c7114-34b7-c04a-7f8f-c268665c2f6c.jpeg)



② 顔検出+機械学習で、その人に最適なヘアスタイルをレコメンドする
![加工1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442237/cb990647-91da-a972-bc3b-ee6157e5ec4b.jpeg)



③ その人+最適なヘアスタイルのイメージ画像を出力する
![加工2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442237/9ba60118-8092-090c-552e-f13d92d311b7.jpeg)
（画像処理してるから分かりにくいかもだけど、レコメンドした画像に元画像の顔部分を重ねてるよ）

⇒簡単に**`カワイイを作る`**ことができる！
（あとは現実に実行するのみ！！）

上記を行うために今回行ったのはstepは以下です。
step1☆ ヘアスタイルの画像を大量収集して、データ蓄積する。（⇒スクレイピング・クローリング）
stap2☆ 顔の特徴量を作成し、モデリングする。（⇒顔認識）
step3☆ 最適な画像を提案する。（⇒機械学習）

# step1☆ 某サロン検索サイトをスクレイピング・クローリング

まず、カワイイを作るためにはカワイイデータを拾ってくる必要がある。
カワイイヘアスタイルが大量に集まる場所はどこか　⇒　美容院
美容院のデータがたくさん集まる場所はどこか　⇒　某サロン検索サイト
ということで、今回はそこから情報を集めることに決定した。

※なお、スクレイピング・クローリングに関しては決まりや規定があるので、しっかり確認しましょう。
　以下、今回参考にさせていただいたサイト・ページです。
[【Pythonクローラー入門】クローリング スクレイピング方法 総まとめ]
(https://www.sejuku.net/blog/69383#Beautiful_Soup)
[PythonでWebスクレイピングする時の知見をまとめておく] 
(https://vaaaaaanquish.hatenablog.com/entry/2017/06/25/202924)
[[Python3]スクレイピングで、弊社の情報を取得してみた]
(https://qiita.com/hidetoshi_n_cograph/items/fca19b490fd0210e3633)

## コード

全部は書ききれないので、一部ご紹介します。
※今回このテーマで進行するにあたり偉大な先輩の力を借りました。以下先輩の書いたコードのご紹介となります。…というか先輩がいなければ、カワイイは作れなかった！ありがとうセンパイ！

**ヘアサロン（＝美容院）一覧を取得**

```
def text_to_soup(html_text):
    try:
        soup = BeautifulSoup(html_text, "lxml")
    except:
        soup = BeautifulSoup(html_text, "html5lib")
  
    return soup

def is_pr_parent(salon):
    """PRサロンが含まれるので、その対応
    """
    return 'class' in salon.parent.attrs and 'isPR' in salon.parent.attrs['class']


def scrape_salons(soup):
    salon_list = {
        'url': [],
        'name': [],
        'id': [],
    }

    salons = soup.select('#mainContents > ul > li.searchListCassette')
    for salon in salons:
        if is_pr_parent(salon):
            continue

        a = salon.find('a')
        salon_url = a.attrs['href']
        salon_name = a.text
        salon_id = salon_url.replace(base_url, '').split('/')[1]

        salon_list['url'].append(salon_url)
        salon_list['name'].append(salon_name)
        salon_list['id'].append(salon_id)
    
    return salon_list
salon_df_list = []

for area_id in yamanote["area_id"]: ##山手線沿いに絞って取得したため
    salon_list_url = f'{kanto_url}/{area_id}/salon'

    res = requests.get(salon_list_url, headers={'user-agent': user_agent})
    soup = text_to_soup(res.text)

    page_regex = re.compile('\d+/(\d+)ページ')
    page_text = soup.select_one('#mainContents > div.mT20.bgWhite > div.preListHead > div > p.pa.bottom0.right0').text
    match = page_regex.search(page_text)
    max_page = int(match.group(1))

    salons = scrape_salons(res.text) ##設定したdefを実行
    salon_df = pd.DataFrame(salons)
    salon_df['area_id'] = area_id

    salon_df_list.append(salon_df)
  
    time.sleep(1)
    for page in range(2, max_page + 1):
        salon_page_url = f'{kanto_url}/{area_id}/salon/PN{page}.html?searchGender=ALL'
        res = requests.get(salon_page_url, headers=ua_headers)
        soup = text_to_soup(res.text)  
        salons = scrape_salons(soup)　##設定したdefを実行
        salon_df = pd.DataFrame(salons)
        salon_df['area_id'] = area_id

        salon_df_list.append(salon_df)
        time.sleep(1)

salon_df_all = pd.concat(salon_df_list, ignore_index=True)

```


**ヘアスタイル一覧を取得**

```
style_df_list = []

params = {
    'typeSearch': 'submit',
    'lengthCds': [f'HL{i:02d}' for i in [1,2,3,4,5,7,8]],
    'condition': '絞り込む',
}

for salon_id in salon_df_all['id']: ##上で取得したヘアサロン一覧
    wemens_style_url = f'{base_url}/{salon_id}/style'
    res = requests.get(wemens_style_url, params=params, headers=ua_headers)  
    soup = text_to_soup(res.text)

    page_regex = re.compile('\d+/(\d+)ページ')
    page_elment = soup.select_one('#mainContents > div.mT20 > div.pH10.mT25.pr > p.pa.bottom0.right0')
    if page_elment is None:
        # メンズのみ、JavaScriptでは取れるがPythonではNoneになる
        # どちらにしろメンズのみ対応は必要だったので、これで対処する
        continue
    page_text = page_elment.text
    match = page_regex.search(page_text)
    max_page = int(match.group(1))
  
    styles = scrape_styles(soup)
    style_df = pd.DataFrame(styles)
    style_df['salon_id'] = salon_id

    style_df_list.append(style_df)

    time.sleep(1)
    for page in range(2, max_page + 1):
        style_page_url = f'{base_url}/{salon_id}/style/PN{page}.html'
        res = requests.get(style_page_url, headers={'user-agent': user_agent})
        soup = text_to_soup(res.text)
        styles = scrape_styles(soup)
        style_df = pd.DataFrame(styles)
        style_df['salon_id'] = salon_id

        style_df_list.append(style_df)
        time.sleep(1)
```

## 学んだこと

- ２つめのコード内コメントアウトにもあるように、今回の主旨にそぐわないメンズのヘアスタイルは、ループの中でcontinueを使用しスキップさせた。目的に対して不要な不具合は、無視するのが得策。
- 適時処理を一時停止（time.sleep()）することで、サーバーへの負荷を配慮。またこの[一時停止の時間]×[取得したいデータ数]を先に計算することで、スクレイピングにかかる処理時間を概算できる（結構時間はかかる）。
- 空のリストを作ってappend()でどんどん追加していくのは、python初級者でも使える技。先輩曰く、「別に難しいことをやろうとしなくていい、自分のできる基礎を組み合わせればそれなりのことはできる」。

# まとめ

**`カワイイは技術で作れる`**時代になったヨ。
<font color="DeepPink">髪型</font>で悩んでる女子たち！今こそ<font color="DarkViolet">Python</font>勉強しようぜ！！