BookoffオンラインのAPIをつくってみた。 #2 (ブックマーク、カートに追加 )

#今回の目的
 [前回の記事はこちら](https://qiita.com/tetsuya111/items/7064648ca7d4dc218817)
 前回はデータを取得する関数をつくった。勿論データを取るのも良いが、今回はもう少し自作プログラムっぽいこと(？)をしたい。つまり、ブックマークやカートに本を追加したい... ちょっとややこしそうだが、前回同様、bookoffオンラインのサイトの構造はかなり単純なので、スイスイと進んでいく。

#ログインする
 まずはログインする。しなくてもやり方はあるか？... そんな高度そうなことはどうせわからないから、男は黙って selenium の webriver を ... しかしそれは最終手段と言う感じがして使いたくない。ここは流石にbookoffオンラインなので、もっと原始的にイケそうである。

```python:api.py
HOST="www.bookoffonline.co.jp"
URL="https://"+HOST
SEARCH_URL=URL+"/disp/CSfSearch.jsp"
LOGIN_URL=URL+"/common/CSfLogin.jsp"

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

def isloginurl(text):
        return "MemberForm" in text

def login(session,mail,pw):
        params={
                "name":"MemberForm",
                "ID":mail,
                "PWD":pw
        }
        res=session.get(LOGIN_URL,headers=HEADERS,params=params)
        return not isloginurl(res.text)
```

####loginの引数

引数 | 意味|
--- |--- |
session | セッション
mail |メールアドレス
pw |パスワード


今回の session は重要である。もし session=requests にすると、ログイン情報はメモリの彼方に消えてしまう。
 isloginurl はテキストによってそれが LOGIN_URL のものか調べる。 metaタグに、urlが見つからなかったので、こんな有様に ... (笑)
 要するに、ログインして、ページが変わったら成功、変わらなかったら失敗である。
##試してみる

パスワードは getpass で入力してみる。

```python:main.py
import api
import getpass
import requests

session=requests.Session()
mail="tetsuya1729@icloud.com"
pw=getpass.getpass()
logined=api.login(session,mail,pw)
print(logined)
```

正しいのを入力すれば、

```shell
$ python main.py

Password:
True
```

間違えれば

```shell
$ python main.py

Password:
False
```

よしよし。

#ブックマーク、カートに追加する

ログインできたら、もう7合目である。(早い(笑)) 次は、本をブックマークに追加し、カートにも追加する。

```python:api.py
ADD_BM_URL=URL+"/member/BPmAddBookMark.jsp"
ADD_CART_URL=URL+"/disp/CSfAddSession_001.jsp"

def addCart(session,bookid):
        params={
                "iscd":bookid,
                "st":1
        }
        return session.get(ADD_CART_URL,params=params)

def addBM(session,bookid):
        params={
                "iscd":bookid,
                "st":1
        }
        return session.get(ADD_BM_URL,params=params)
```

引数 | 意味|
--- |--- |
bookid | 本のID
session |セッション

bookid とは本のIDで、url で言えば、

`https://www.bookoffonline.co.jp/old/<bookid>`

のように末尾にあたる。砂の女なら、

[https://www.bookoffonline.co.jp/old/0015580570](https://www.bookoffonline.co.jp/old/0015580570)

bookidを渡すのは実用的に見えないので、後でラップする。

実演は難しいので割愛。

#まとめる

```python:api.py
class Client:
        def __init__(self,mail="",pw=""):
                self.session=requests.Session()
                self.__logined=False
                if mail and pw:
                        self.login(mail,pw)
        def __getattr__(self,attr):
                return getattr(self.session,attr)
        @property
        def logined(self):
                return self.__logined
        def login(self,mail,pw):
                self.__logined=login(self.session,mail,pw)
                return self.logined
        def addCart(self,bookid):
                if not self.logined:
                        return False
                addCart(self.session,bookid)
        def addBM(self,bookid):
                if not self.logined:
                        return False
                addBM(self.session,bookid)
```

#実用的に

 次に、上のコードをもう少し実用的にしたい。
 これは、すこし「原始的」ではない感じなので、別のファイルに書いてみる。

##一致した本を返す
 前回の search 関数は、クエリからデータのリストを返したが、今回はぴったり一致するデータを返す。しかし厳密に完全一致なら、クエリを入力する方が大変なので、そこはうまくやる。

```python:util.py
from . import api
import re

ALL_LEFT_KAKKO='\xef\xbc\x88'   #"(" of all
ALL_SPACE='\xe3\x80\x80'        #" " of all
U_SPACE="\u3000"
NBSP="\xa0"     #&nbsp
GET_TITLE_F_2="{0}"+"[\({0} {1}{2}{3}].*".format(ALL_LEFT_KAKKO,NBSP,ALL_SPACE,U_SPACE)
def get(title,author,type_=api.SearchType.BOOK,n=3):
        title=re.escape(title)
        def matchtitle(data_title):
                if re.fullmatch(title,data_title):
                        return True
                return re.fullmatch(GET_TITLE_F_2.format(title),data_title)
        for data in api.searchN(title,start=1,n=n,type_=type_):
                if matchtitle(data["title"]) and re.fullmatch(author,data["author"]):
                        return data
        return None
```

引数 |意味
--- |---
title |タイトル
author |著者名
type |データの型
n |ページ数

まず引数は、基本的に searchN と同じである。start がないのは、一致するものは、最初の方に来るやろう決め打ちしてるからで、本当はページ数 n も不要な気がするが、一応。しかし偏見か？(笑)
 まずデータを取って、タイトルと著者名が一致するか調べる。 matchtitle と言いながら fullmatch を使ってるが、細かいことは気にしない。
 matchtitle は少し複雑になってしまった。見ての通り fullmatch が2つあって、一つ目は普通の完全一致で、2つ目がやや複雑なのは、以下ののような対応を一致してると見なしたかったせい。

title|data_title
---|---
砂の女|砂の女(新潮文庫)
熱砂の女たち|熱砂の女たち　アラブの心
ダレン・シャン 奇怪なサーカス |ダレン・シャン 奇怪なサーカス(１)


###使ってみる

パッケージとしてつかう。

```python:main.py
import bookoff.util as bfu
import sys

title=sys.argv[1]
author=".+"

data=bfu.get(title,author)
print(data)
```



```shell
$ python main.py 砂の
None

$ python main.py 砂の女
{'title': '砂の女(新潮文庫)', 'url': 'https://www.bookoffonline.co.jp/old/0015580570', 'author': '安部公房', 'price': 200, 'stocked': True}    
```

##実用的にまとめる

先ほど、ブックマークやカートに追加するのを[まとめたもの](#まとめる)を、もう少し実用的にする。

```python:util.py
def getidInUrl(url):
        id_=re.search("/(?P<id>[0-9]+)$",url)
        return id_.group("id") if id_ else None

def getid(title,author=".*"):
        data=get(title,author)
        if not data:
                return None
        return getidInUrl(data["url"])

class Client(api.Client):
        def addCart(self,title,author=".*"):
                bookid=getid(title,author)
                return super().addCart(bookid)
        def addBM(self,title,author=".*"):
                bookid=getid(title,author)
                return super().addBM(bookid)
                                              
```

####getidInUrl

引数 |意味
--- |---
url |url

####getid

引数 |意味
--- |---
title |タイトル
author |著者名


ミソは getid で、本と著者名から、その本の ID を取る。単純だが、なんか気持ち良い。(笑) それを使って、Client をラップする。

###実演？
 実演は難しいのでコードだけ

```text:a.txt
砂の女
ロシア的人間
狩猟と遊牧の世界
伊豆の踊子
坂の上の雲
```



```python:main.py
import bookoff.util as bfu
import sys
import getpass


fname=sys.argv[1]

mail="tetsuya1729@icloud.com"
pw=getpass.getpass()
client=bfu.Client(mail,pw)

with open(fname,"r") as f:
        for title in f:
                title=title.rstrip()
                print(title)
                client.addBM(title)
```


時間はかかるが、一応使える。

#終わりに
 もう API は良いかな。やるなら次はシェルを作りたい。それより bookmeter　と連携させたいか。しかしサイトの構造が単純で驚いた。YoutubeやPornhubもこれくらい単純なら嬉しいが、やはり動画は守られてしまうか。。。 しかし、そういう大手のものは、もっと頭の良い方が解析してソフトを作ってくれるので、ボーっと待っておこう。youtube-dl なんて使われてるが、正規なら動画のダウンロードで月1500円取られるわけやから、グレー寄りのブラックと言う感じがする。 しかしそれでも商品やプレミア配信は流石に取れないか。頑張ったら取れるんかな。。。(笑)
 
 全然関係ない話でした、すみません。^-^

[コードはこちら](https://github.com/tetsuya111/util/tree/main/bookoff)