bookmeterのAPIをつくってみた。 #1 (データの取得)

#はじめに

 昔は図書館の本すら、自由に読めなかったらしいが、今は自由に読めるし、ブックオフもあるし、更に bookmter もある。恵まれた時代である。僕も bookmeter を愛用させていただいてる。美しいデザイン、知的なユーザー、痒いところに手の届く機能。人類の幸福に多大な貢献をしている bookmeter だが、だからこそ、なぜこれが出来ないのかと思うことがある。例えば、「読みたい本」というブックマークのような機能があって、素晴らしいのだが、**検索ができない**のである。VIM はヘルプすら検索できるのに。同様に「読んだ本」や「積読」も検索できない。また、**タグがない**ので、バラバラに本がある印象で、どうにもまとまらない。しかし不満ばかり言っても仕方ない。使う側が工夫すれば良いのである。モンスターカスタマーでは、素晴らしい運営者も疲れて辞めてしまうかもしれない。

#したいこと
- 「読みたい本」などの検索
- 本のタグ付け
- ...

まずは、上の2つをやりたい。他にも色々できたら。

#本のデータの取得

まずは本のデータを取る。本のデータの一覧は同じ形式なので同じように取れる。

```python:bookmeter.py
import requests
from bs4 import BeautifulSoup as bs
import time
import urllib.parse as up
import json

HEADERS={
        "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36"
}

class Selector:
        BOOK="li.group__book"
        THUMB=".book__thumbnail"
        TITLE=".detail__title"
        DATE=".detail__date"
        AUTHOR=".detail__authors"
        PAGE=".detail__page"

def toData(book):
        thumb=book.select_one(Selector.THUMB)
        if thumb:
                img=thumb.find("img")
                imgUrl=img.get("src") if img else ""
        else:
                imgUrl=""
        title_e=book.select_one(Selector.TITLE)
        title=title_e.text if title_e else ""
        href=title_e.find("a").get("href") if title_e else ""
        date=book.select_one(Selector.DATE)
        date=date.text if date else ""
        author=book.select_one(Selector.AUTHOR)
        author=author.text if author else ""
        page=book.select_one(Selector.PAGE)
        page=page.text if page else ""
        return {
                "imgUrl":imgUrl,
                "title":title,
                "href":href,
                "date":date,
                "author":author,
                "page":page
        }


def __getBooks(soup):
        return map(toData,soup.select(Selector.BOOK))

def _getBooks(url,page=1,session=requests):
        params={
                "page":page
        }
        res=session.get(url,headers=HEADERS,params=params)
        soup=bs(res.text,"html.parser")
        return __getBooks(soup)
def getBooks(url,start=1,n=5):
        session=requests.Session()
        for page in range(start,start+n):
                data=list(_getBooks(url,page,session))
                if not data:
                        return
                for dat in data:
                        dat["href"]=up.urljoin(url,dat["href"])
                        yield dat
                time.sleep(1)

```


同じような関数ばかり並んでるのはご愛敬。肝心なのは getBooks だけだが、一応説明。


###toData

引数 |意味
--- |---
book |本のデータのスープ

###toDataの返り値

キー |値
--- |---
imgUrl |画像のURL
title |タイトル
href |本のURL
date |日付(追加日)
author  |著者名
page |本のページ数


###__getBooks

引数 |意味
--- |---
soup |スープ

返り値は、データのリスト。

###_getBooks

引数 |意味
--- |---
url |URL
page |サイトのページ数
session |セッション

返り値は、データのリスト。sessionは内部のもの。

###getBooks

引数 |意味
--- |---
url |URL
start |最初のページ
n |データを取るページの数

返り値は、データのリスト。


要するに、toData はスープをデータに変換する、__getBooksはスープから本のデータを返す、_getBooks は __getBooks のラッパーのようなもので、getBooks は _getBooks のラッパーのようなもの。

pageというのはページ数で、例えば、[ホーム](https://bookmeter.com/home)の検索窓で、「数学」と入れて調べると、
[https://bookmeter.com/search?keyword=%E6%95%B0%E5%AD%A6](https://bookmeter.com/search?keyword=%E6%95%B0%E5%AD%A6)
に飛ぶ。これに `page=3` を足して
[https://bookmeter.com/search?keyword=%E6%95%B0%E5%AD%A6&page=3](https://bookmeter.com/search?keyword=%E6%95%B0%E5%AD%A6&page=3)
とすると3ページ目に行く。

##試す

```python:main.py
import bookmeter
import time
import json

url="https://bookmeter.com/users/1035737/books/wish"
for data in bookmeter.getBooks(url,start=1,n=10**32):
        print(json.dumps(data,ensure_ascii=False,indent=4))
```

```shell:実行結果
$ python main.py

{
    "imgUrl": "https://m.media-amazon.com/images/I/41OCTQEQrbL._SL500_.jpg",
    "title": "安部公房とわたし",
    "href": "https://bookmeter.com/books/7020962",
    "date": "",
    "author": "山口 果林",
    "page": ""
}
{
    "imgUrl": "https://m.media-amazon.com/images/I/510TTCKFQNL._SL500_.jpg",
    "title": "イスラーム文化−その根柢にあるもの (岩波文…",
    "href": "https://bookmeter.com/books/579483",
    "date": "",
    "author": "井筒 俊彦",
    "page": ""
}
{
    "imgUrl": "https://m.media-amazon.com/images/I/51vdg2TkNfL._SL500_.jpg",
    "title": "日本史有名人の身体測定",
    "href": "https://bookmeter.com/books/10276713",
    "date": "",
    "author": "篠田 達明",
    "page": ""
}


~~ 以下略 ~~
```


#簡単な検索

データを取れたので、簡単な検索をしてみる。

```python:main.py
import time
import json
import docopt
import re
import docopt

__doc__="""
Usage:
        search 
 <url>
"""

args=docopt.docopt(__doc__)

s=args["
"]
url=args["<url>"]
for data in bookmeter.getBooks(url,start=1,n=10**32):
        if re.search(s,data["title"]):
                print(json.dumps(data,ensure_ascii=False,indent=4))
```


```shell:実行結果
$ python main.py  数学 https://bookmeter.com/users/1035737/books/wish

{
    "imgUrl": "https://m.media-amazon.com/images/I/51xDl2z9K+L._SL500_.jpg",
    "title": "高校数学でわかる流体力学 (ブルーバックス)",
    "href": "https://bookmeter.com/books/8095831",
    "date": "",
    "author": "竹内 淳",
    "page": ""
}
{
    "imgUrl": "https://m.media-amazon.com/images/I/51G4jU5hGhL._SL500_.jpg",
    "title": "高校数学でわかるフーリエ変換―フーリエ級数か…",
    "href": "https://bookmeter.com/books/424303",
    "date": "",
    "author": "竹内 淳",
    "page": ""
}
{
    "imgUrl": "https://m.media-amazon.com/images/I/416Yus64pNL._SL500_.jpg",
    "title": "物語 数学の歴史―正しさへの挑戦 (中公新書)",
    "href": "https://bookmeter.com/books/491408",
    "date": "",
    "author": "加藤 文元",
    "page": ""
}

～～　以下略　～～           
```
                 

#終わりに

 次回はデータベースを作って、もう少し本格的にできるようにしたい。

##[コードはこちら](https://github.com/tetsuya111/util/blob/main/bookmeter.py)