この記事は、OIT Advent Calendar 2018の17日目の記事です.
##こんにちは
こっそり歩くです.まいかた出て奈良なんとかで院生してます.
twitter
##やったこと
読書メーターをスクレイピングして協調フィルタリングを使った推薦モデルを作った.
##モチベーション
恒川光太郎の作品が好きだけど,そろそろ全作読み終わるのでいい感じの本が探したい
→Amazonの推薦が微妙だったので,とりあえず自分で作ってみようと思った
##スクレイピング
協調フィルタリングをするのにデータが欲しい
→読書メーターからユーザごとの読んだ本を持ってくれば協調フィルタリングいけるか?
→スクレイピングしよう
(→なんかスクレイピングってハッカーぽくてカッコイイ🤓(やったことないが))
###使ったもの
####beatifulsoup4
有名どころらしい.
HTMLをパースしてくれたり,欲しいタグのみ取り出したりしてくれる.
####urllib
有名らしい.
URLからHTMLファイルを持ってくる?やつらしい
###やった
Azunyan1111さんの記事を参考に組んだ.
Pythonからの実行だとサーバー側からストップを食らうので,redandwhiteさんの記事を参考にユーザエージェントを書き換えた.
###スクレイピングのコード
Python全然書かないので試行錯誤となったので,冗長な処理があるかもしれない(Python教えて)
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)
ざっくりまとめるとユーザごとの読んだ本のページに20冊ずつ読んだ本のタイトルや作者等が表示されるので,読んだ冊数/20回,ページを取得しタイトルと作者名をまとめてCSV形式で保存という処理を3000人分行なった.
##協調フィルタリング
協調フィルタリング自体についてはこのあたりを参照すればいいらしい
hik0107さんの記事を参考に協調フィルタリング的な機能を実装した.
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個出力
類似度?は本当に簡単な式にした.
類似度=\frac{僕の読んだ本\cap対象の読んだ本}{全ての僕の読んだ本}
[('氷菓 (角川文庫)', 12),
('春期限定いちごタルト事件 (創元推理文庫)', 11),
('オーデュボンの祈り (新潮文庫)', 11),
('重力ピエロ (新潮文庫)', 11),
('四畳半神話大系 (角川文庫)', 11),
('火車 (新潮文庫)', 11),
('わたしを離さないで (ハヤカワepi文庫)', 11),
('星を継ぐもの (創元SF文庫)', 10),
('死神の精度 (文春文庫)', 10),
('その女アレックス (文春文庫)', 10)]
できた?
##反省と今後
- 取得先のタイトル表記がブレるので,もっと掘ってISBNコード等を使った方がいいかもしれない(四畳半神話体系は読んだことある)
- 読んだ xor 読んでないでデータを取っているので,レビューでできるようにしたい
- ユーザベースの手法よりアイテムベースの方が向いてるかもしれない
- Webアプリとかにしたら楽しそう
##感想
読む本考えるのが割とめんどくさかったので,割と使えそう
もっと改良はしたいが