読書メーターをスクレイピングして協調フィルタリングっぽいのを実装した

この記事は、[OIT Advent Calendar 2018](https://adventar.org/calendars/2962
)の17日目の記事です．

##こんにちは
こっそり歩くです．まいかた出て奈良なんとかで院生してます．
[twitter](https://twitter.com/walk_to_work)

##やったこと
読書メーターをスクレイピングして協調フィルタリングを使った推薦モデルを作った．

##モチベーション
恒川光太郎の作品が好きだけど，そろそろ全作読み終わるのでいい感じの本が探したい
→Amazonの推薦が微妙だったので，とりあえず自分で作ってみようと思った

##スクレイピング
協調フィルタリングをするのにデータが欲しい
→読書メーターからユーザごとの読んだ本を持ってくれば協調フィルタリングいけるか？
→スクレイピングしよう
(→なんかスクレイピングってハッカーぽくてカッコイイ🤓（やったことないが）)

###使ったもの
####beatifulsoup4
有名どころらしい．
HTMLをパースしてくれたり，欲しいタグのみ取り出したりしてくれる．
####urllib
有名らしい．
URLからHTMLファイルを持ってくる？やつらしい

###やった
[Azunyan1111さんの記事](https://qiita.com/Azunyan1111/items/9b3d16428d2bcc7c9406)を参考に組んだ．
Pythonからの実行だとサーバー側からストップを食らうので，[redandwhiteさんの記事](http://redandwhite.hatenablog.com/entry/2016/01/16/125122)を参考にユーザエージェントを書き換えた．

###スクレイピングのコード
Python全然書かないので試行錯誤となったので，冗長な処理があるかもしれない（Python教えて）

```takeBooks.py
import urllib.request as urllibReq
import urllib.error as urlibErr
import math
import time
from bs4 import BeautifulSoup
import csv

def getPageNum(id):
    url = "https://bookmeter.com/users/" + str(id) + "/books/read"
    req=requestAsFox(url)
    response=getResponse(req)
    if response == -1:
        return -1
    html=response.read().decode('utf-8')
    soup = BeautifulSoup(html, "html.parser")
    num = int((soup.find(class_="content__count").string))
    page = math.ceil(num/20)
    return page

def requestAsFox(url):
        headers={"User-Agent":"Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 Firefox/38.0"}
        try:
            req = urllibReq.Request(url,None,headers)
            return req 
        except:
            print("Error:requestAsFox")
            return None

def getResponse(req):
    try:
        response=urllibReq.urlopen(req)
        time.sleep(1)
        return response
    except urlibErr.HTTPError as e:
        print(e.code)
        if e.code == int(503):
            time.sleep(5)
            return getResponse(req)
        else:
            return -1

for usr_id in range(3000):
    print("usr_id:" + str(usr_id+1))
    page = getPageNum(usr_id+1)
    print(str(page)+"pages")
    if(page is -1):
        continue
    books = []
    for i in range(page):
        url = "https://bookmeter.com/users/"+str(usr_id+1)+"/books/read"
        url += "?page=" + str(i+1)
        req=requestAsFox(url)
        response = getResponse(req)
        if response == -1:
            continue
        html=response.read().decode('utf-8')
        soup = BeautifulSoup(html, "html.parser")
        title_array = []
        author_array=[]
        for results in soup.find_all(class_="book__detail"):
            for title in results.find_all(class_="detail__title"):
                title_array.append(title.string)
            for author in results.find_all(class_="detail__authors"):
                author_array.append(author.string) 
        for i in range(len(title_array)):
            listData = []
            listData.append(title_array[i])
            listData.append(author_array[i])
            books.append(listData)
    f = open('data/'+str(usr_id+1)+'.csv', 'w')
    csvWriter = csv.writer(f)
    csvWriter.writerows(books)
    f.close()
    
    print("writed")
    time.sleep(1)

```

ざっくりまとめると[ユーザごとの読んだ本のページ](https://bookmeter.com/users/378856/books/read)に20冊ずつ読んだ本のタイトルや作者等が表示されるので，読んだ冊数/20回，ページを取得しタイトルと作者名をまとめてCSV形式で保存という処理を３０００人分行なった．

##協調フィルタリング

協調フィルタリング自体についてはこのあたりを参照すればいいらしい
- [kamishima.net 推薦システムのアルゴリズム](http://www.kamishima.net/archive/recsysdoc.pdf)
- [gihyo.co.jp 情報推薦システムの基本](http://gihyo.jp/dev/serial/01/information-recommendation-system)
- [slide share 協調フィルタリングを利用した推薦システム構築](https://www.slideshare.net/masayuki1986/recommendation-ml)


[hik0107さんの記事](https://qiita.com/hik0107/items/96c483afd6fb2f077985)を参考に協調フィルタリング的な機能を実装した．

```recom.py
import pandas as pd
import math as sort

#シリーズものを1つにまとめる
def deleteDuplication(data):
    slice_num = 5
    data.sort()
    for i in data:
        for j in data:
            if i == j:
                continue;
            else:
                buf1 = i[0:slice_num]
                buf2 = j[0:slice_num]
                if buf1 == buf2:
                    #print(buf1)
                    data.remove(j)

def normalization(data):
    len_old = len(data)
    deleteDuplication(data)
    len_new=len(data)
    while len_old != len_new:
        len_old = len_new
        deleteDuplication(data)
        len_new=len(data)

def getSimilarity( data1 , data2 ):
    data1 = set(data1)
    data2 = set(data2)
    set_both = data1.intersection(data2)
    return len(set_both)/len(data1)

def retUnread(data1 , data2):
    data1 = set(data1)
    data2 = set(data2)
    recom_books = data2
    set_both = data1.intersection(data2)
    for book in set_both:
        recom_books.remove(book) 
    return recom_books

def makeRecomLst(recom):
    lst = []
    for data in recom:
        if data['sim'] >= 0.1:
            lst.append(data)
        else:
            break;
    return lst

#自分のデータを取得
my_data = pd.read_csv("data/my_data.csv" , names=('title' ,'author') , usecols=[0]).to_dict()
my_data = list(set(my_data['title'].values()))
normalization(my_data)

data_sets=[]

for i in range(3000):
    usr_id = i+1
    try:
        data = pd.read_csv("data/"+str(usr_id)+".csv" , names=('title' ,'author') , usecols=[0]).to_dict()
        data = list(set(data['title'].values()))
        #normalization(data)
        data_sets.append(data)
        #print(usr_id)
    except:
        continue

similarity=[]
for data in data_sets:
    similarity.append(getSimilarity(my_data , data))

recom = []
for i in range(len(data_sets)):
    recom.append({'data' : data_sets[i] , 'sim' : similarity[i]})
recom = sorted(recom , key=lambda x: x['sim'] , reverse = True)
recom_lst = makeRecomLst(recom)
recom_books = []
print(len(recom_lst))
for r in recom_lst:
    recom_books.append(retUnread(my_data , r['data']))
res = {};
for data in recom_books:
    for book in data:
        if book in res:
            res[book] += 1
        else:
            res[book] = 1
res = sorted(res.items(), key=lambda x: x[1] , reverse=True)
res[0:10]
```

時間が微妙だったので，簡単な実装している

1.ユーザごとの類似度算出
2.類似度が10%を超えるユーザの集める
3.それらの読んでる本の中で多い順に10個出力

類似度？は本当に簡単な式にした．

```math
類似度=\frac{僕の読んだ本\cap対象の読んだ本}{全ての僕の読んだ本}
```


```出力
[('氷菓 (角川文庫)', 12),
 ('春期限定いちごタルト事件 (創元推理文庫)', 11),
 ('オーデュボンの祈り (新潮文庫)', 11),
 ('重力ピエロ (新潮文庫)', 11),
 ('四畳半神話大系 (角川文庫)', 11),
 ('火車 (新潮文庫)', 11),
 ('わたしを離さないで (ハヤカワepi文庫)', 11),
 ('星を継ぐもの (創元SF文庫)', 10),
 ('死神の精度 (文春文庫)', 10),
 ('その女アレックス (文春文庫)', 10)]
```

できた？

##反省と今後

- 取得先のタイトル表記がブレるので，もっと掘ってISBNコード等を使った方がいいかもしれない(四畳半神話体系は読んだことある)
- 読んだ xor 読んでないでデータを取っているので，レビューでできるようにしたい
- ユーザベースの手法よりアイテムベースの方が向いてるかもしれない
- Webアプリとかにしたら楽しそう

##感想
読む本考えるのが割とめんどくさかったので，割と使えそう
もっと改良はしたいが