書籍のタイトルが降ってくるウェブサイト「BookRain」をPythonでつくりました

# タイトルの通りですが
BookRainというウェブサイトを作成しました。
書籍のタイトルが雨のように降ってくるだけです。
たき火感覚で楽しんでいただけたら嬉しいです。

※2022/08/30　モバイル対応ができていません。申し訳ございません。。。
~~https://bookrain.link/~~

※2024/07/25　AWSからGitHub Pagesへ移行しました。
https://kemelman.github.io/bookrain/

ソースコードはこちらです。
https://github.com/kemelman/bookrain

GitHub Pagesでのサイト作成方法
https://docs.github.com/ja/pages/getting-started-with-github-pages/creating-a-github-pages-site

# 作ろうと思ったきっかけ
BookRainは[ゆうもやさん](https://mononichi.com/blog/)のWordCascadeのオマージュになります。

https://river.tango-gacha.com/

このウェブサイトをはじめて目にしたのは約２年前ほどです。
フロントエンドってこんなにも叙情的なのかと感動したのを覚えています。

私も直感的にだれでも楽しめるものを作ってみたい！　と燃え上がりましたが、人並みに書けるのはPythonくらい。
以来、諦めていましたが、最近になってbrythonなるものを知りました。

https://brython.info/


brythonを知ったのはなんとYouTubeです。

https://www.youtube.com/watch?v=CVWYS_9ZtfM

[やねうら王さん](https://yaneuraou.yaneu.com/)がpythonでブラウザゲームを作る動画です。

yanesdk-for-brythonというツールを作成されていらっしゃいました。
ありがたいことにgithubでサンプルのゲームも一緒に公開されていらっしゃいます。

https://github.com/yaneurao/yanesdk-for-brython

日本語でコメントが入っているなら、Pythonで書かれているのであれば、サンプルのソースコードもあるならば、時間をかけてＳＤＫを理解すれば私にもフロントエンドを動かす楽しさが味わえるかもしれない！
早速ダウンロードしてコードを読み解きはじめました。

# yanesdk-for-brythonの勉強
ブラウザゲーム作成用のＳＤＫに触れるのは初めてのことです。
最初はまったくわかりませんでしたが、クラス間の関係がわかってくると、なんとか動かせるようになってきました。
（私が解釈した限りの全貌は、シーンクラスの中にゲームオブジェクトがあり、そのゲームオブジェクトに付与するプロパティのようなクラスを設定していく、というイメージです。）


所感としてはインスタンスって本当に実体として扱えるのだなと、なにを今さらと咎められてしまうような素朴な学びがありました。

ＳＤＫを使っていると、処理そのものをハードコーディングしているというよりは、本当に生物を扱っているような感覚でした。
転じて、今までフレームワークを関数型のコードとしてしか認識していなかったのだなと反面教師的な自省がありました。（関数型が悪い、ということではないです。）

また、サンプルのソースコードにコメントがたくさん書いてあるので、とても助かりました。
これが巨人の肩の上に立つということか。。。


# openBDって？
WordCascadeライクに、文字が降ってくるウェブサイトを作るのは決めていました。
とはいえ、なにを降らせればいいのかわかりません。
なにを降らせるかを考えるのは神ぐらいで、なにが降ってくるのかを考えるのは気象庁ぐらいですしね。

WordCascadeはランダムな単語が降ってくるので、本のタイトルが降ってきたらちょうどいい差別化になるのでは、と調べたところ、openBDというＡＰＩを発見しました。

https://openbd.jp/

>私たちopenBDプロジェクト（カーリル・版元ドットコム）は書誌情報・書影を、だれでも自由に使える、高速なAPIで提供します。
・個人が、SNSやブログで本を紹介するとき
・書店が、仕入れや、販売のために本を紹介するとき
・図書館が、選書し、利用者に本を紹介するとき
・メディアが、本を紹介し評するとき
・企業が、書誌情報・書影を利用したあらたなサービスを開発するとき
こうしたときに、自由に使える書誌情報・書影を、高速なAPIで提供するopenBDの提供を開始します。
オープンな本のデータが、本の世界をますます豊かにすると考えるからです。
openBDは、カーリルがAPIシステムを開発します。
カーリルは、図書館蔵書・貸出情報を横断的に高速で検索するサイトを提供しています。
ここで培ったノウハウを活用します。
openBDに掲載する書誌情報・書影は版元ドットコムが収集します。
版元ドットコムは、会員出版社229社のつくった本の書誌情報・書影をネット上で公開し、書店・取次などに送り届けるシステムをつくる出版社団体です。
さらに会員出版社だけでなく、大手から中小出版社まで、24,747社の約76万タイトルの本の書誌情報、書影、ためし読み、書評掲載情報などをあつめてサイトやAPIで提供しています。
提供のために、出版社・出版業界・国立国会図書館などからの収集の蓄積を活用して取組みます。

上記は公式トップページからの引用です。
pythonでの使い方はこちらを参考にさせて頂いて勉強しました。

https://github.com/ryuuji/openbd-downloader/blob/master/download.py

# どうやって公開しようか
データベースを作成して書籍のタイトルを記録して、FlaskとEC2でウェブアプリケーションとして作ろうと考えていましたが、ＡＰＩが使えるとなると話が変わってきます。
BookRainの提供にはユーザーからの入力をほぼ受け付ける必要がありません。
画面をクリックすると白黒反転させるくらいです。（ここも本家オマージュです）
ならば静的サイトとして作ったほうが考えることが減るはずです。

そこでＳ３を使ってbrythonを試してみたところ、動いてくれました。
ウェブサイトの公開方法は以下の記事を参考にさせて頂きました。
王道？のS3+CloudFront+Route53+ACMです。

https://zenn.dev/wakkunn/articles/66a6e8372611dc

# 開発環境・使った拡張機能
vscodeで開発を行いました。
デバッグはこちらの拡張機能を使いました。

https://webdesign-trends.net/entry/14461

すぐに結果が確認できるのでとても便利でした。
エディターで簡易サーバーを立てられるって改めて考えるとすごい技術だな。。。

以下に躓いてしまったところを挙げます。


## 画面いっぱいに表示したいけど、文字がぼやける
サンプルゲームのindex.html内のcanvas要素は固定長となっています。
画面いっぱいに表現したいので、canvasをユーザーの画面の大きさに合わせる必要があります。
これくらいは疎い私でも知っています。cssでwidthとheightを100%に指定すればいいのです。
脂下げながらcssを作成してリロードしてみたところ、文字がぼやけてしまいました。

調べてみたところ、canvas要素を使う際に初心者がよくやってしまう失敗のようでした。
canvas内の比率とCSSでの指定した比率が異なってしまうのが原因です。

https://qiita.com/ShinyaOkazawa/items/9e662bf2121548f79d5f

原因はわかりましたが、当たり前ですが対処方法はjavascriptを使ったものばかり。
諦めてjavascriptを一部だけ使ってしまうかとよぎりましたが、仕事ならともかく個人開発でそれは味気ない。
２週間ほどコードをいじっていましたが、結局のところ、私に必要だったのはちゃんと原理を理解しようとする態度でした。

https://brython.info/static_doc/en/html.html

難しく考えていたのですが、なんのことはない。pythonでjavascriptと同じコードを再現すればいいだけでした。
yanesdk-for-pythonのCanvasクラスの大きさをwrapper要素に合わせるようにコードを変更しました。
まず、canvasをwrapperを作成してその中に入れます。

```html:index.html
<body onload="brython()">
  <div id="wrapper">
    <canvas id="canvas"></canvas>
  </div>
  <script type="text/python" src="main.py"></script>
</body>
```

```css:style.css
body{
	margin:0;
	padding:0;
	height:100vh;
}

canvas{
	display: block;
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	margin: auto;
}

#wrapper{
	width: 100%;
	height: 100%;
  }

/* スクロールバーを非表示 */
body::-webkit-scrollbar { 
	display: none;
  }
```


そして、SDKのコードを変更、以下のようにしました。

### 元のSDKのコード
```python:yanesdk.py
# 描画用canvas
class Canvas:
    # canvas_id_name : HTML5のcanvasにつけたid名。defaultでは"canvas"
    def __init__(self, canvas_id_name:str = "canvas"):
        # 描画するcanvasのcontextの取得。
        self.canvas = document[canvas_id_name]
        self.ctx = self.canvas.getContext("2d")

        # canvasの幅と高さ
        self.width :int = self.canvas.width
        self.height:int = self.canvas.height

        # canvasのRect
        self.rect = Rect(
            Vector2D(0,0) ,
            Vector2D(self.width, self.height)
        )

```

### 変更後のSDKのコード
```python:yanesdk.py
# 描画用canvas
class Canvas:
    # canvas_id_name : HTML5のcanvasにつけたid名。defaultでは"canvas"
    def __init__(self, canvas_id_name:str = "canvas"):
        # 描画するcanvasのcontextの取得。
        # wrapper要素の縦横を取得して、self.canvasもそれに合致させることで文字のぼやけを防ぐ
        self.canvas = document[canvas_id_name]
        self.wrapper = document['wrapper']
        self.ctx = self.canvas.getContext("2d")

        # debug
        # print('キャンバスの要素')
        # for key, value in self.canvas.__dict__.items():
        #     print(key, ':', value)
        # print('wrapperの要素')
        # for key, value in self.wrapper.__dict__.items():
        #     print(key, ':', value)

        # 画面いっぱいにcanvasを合わせる
        # スマホ、PCどちらでも同じレイアウト
        self.canvas.attrs['width'] = self.wrapper.clientWidth
        self.canvas.attrs['height'] = self.wrapper.clientHeight
        self.width:int = self.canvas.width
        self.height:int = self.canvas.height
        
        # canvasのRect
        self.rect = Rect(
            Vector2D(0,0) ,
            Vector2D(self.width, self.height)
        )

```


やはり小手先で解決しようとするよりも、しっかりとインプットをした方が建設的ですね。
ですが、後述しますが完璧な対応にはなっていません。
とりあえずは完成を目指して、自分が満足できるレベルで改善ができました。



# requestsがつかえない
表示についてはいい感じになってきました。文字のサイズや落ちるスピードなどもランダムでとる範囲を指定して雨らしくなってきました。
しかしここで、重大な問題に気付きました。requests（HTTP通信のためのpythonのライブラリ）がbrythonでは使えないのです。urllibでなんとか代用ができないかと考えましたが、杞憂でした。

https://qiita.com/yutaYasunaga/items/0b3b7de52348bba0ebb7

世の中の広さに頭が下がるばかりです。おかげですんなりとＡＰＩを導入できました。
この記事が同じく、だれかの役に立つことを願います。

# 高速化
さて、これで目的のものができましたが、完成は完全とは異なります。
とにかく遅いのです。最初のタイトルが降ってくるまでに１５秒もかかります。これではいけません。
そこで高速化を図りました。

### まさか競技プログラミングが活きるとは

ところで、私は趣味で競技プログラミングをしており、AtCoderというサービスを利用させて頂いています。

https://atcoder.jp/

これがとても面白くて、毎週末コンテストに参加して結果に一喜一憂しています。
たしかにコーディングが速くなったり、読解能力が上がった気がしたりなど、業務にプラスになったりもしたのですが、たとえば動的計画法を使いこなすのが現在の業務にクリティカルに必要な能力かと言われればそうではないです。（そもそも楽しいだけで充分なんですけどね）

さて、マクラはおしまいで、BookRainの仕様について少し解説します。
まず、ISBNコードと呼ばれる書籍を管理する１３桁の番号を取得して、ISBNの先頭４文字が９７８４である（日本で発刊された本である）本だけを抽出したあと、さらにランダムに複数選んで、書籍のタイトルを再度openBDに問い合わせます。

このISBNをフィルタする処理に時間がかかっていたのが大きなネックでした。
そのフィルタ処理は以下の通りです。
```python
# seqはISBNコードが入ったリスト
seq = [s for s in seq if s[:4] == '9784']
```
愚直に９７８４であるかを判定したわけです。
ここをなんとか高速化する必要がありました。
色々と考えたのですが、ある日、取得するISBNのリストが最初からソートされていることに気づきました。

```python
import bisect

# isbnが9784からはじまるものだけにする
seq = seq[bisect.bisect_left(seq,'9784000000000'):bisect.bisect_right(seq,'9784999999999')]
```

二分探索です。最適なのかはわかりませんが、これで線形で探索する必要がなくなりました。
（計測してみたところ、１５秒ほどかかっていたのが６秒ほどに短縮されました。ＡＣ！）

https://www.howisit.jp/2017/03/19/linear-and-binary-search/

https://qiita.com/T_Wakasugi/items/c979e977f56531942de4

AtCoderといういわば箱庭の中だけで使っていた知識が実践で活きるというのは、卑近な体験ですが数学やアルゴリズムに支えられて色々なウェブサービスが提供されているのだと肌で感じる出来事でした。
そして、競技プログラミングをしていなければ、恐らくソートされているという特徴を見過ごしていたと思います。知識はスタティックなものではない、ということを改めて感じました。

### ウェルカムメッセージをつくろう
６秒までもってこれたなら大丈夫です。言い方は悪いですが、尺をなんとかつなげばいい。
読書に関する名言が最初にウェルカムメッセージとして出れば導入に最高だと思い、実装しました。

https://shuppankagaku.com/knowledge/proverb/

こちらの名言をpythonのソースコードに辞書として書き込み、ランダムにひとつ、選ぶようにしました。
先哲の言葉です。６秒どころかたっぷり一生を費やせるほどの深みがあります。
とても雰囲気が締まり、ウェブサイトに没入させるのにこの６秒をうまく使えたのではないかなと感じています。


# 個人サービスで設定するべきこと
なんとか満足いくものが作れましたが、個人で開発したものを公開するのははじめてのことです。
ogpというメタタグを書いたらリンクに画像が出る、みたいに定石として設定するべきことがあるとは知っていたのですが、具体的にどうすればいいのかわかりません。
が、qiitaで流れてきた記事をブックマークしておいたのが功を奏しました。

https://qiita.com/ikuosaito1989/items/61b285ce200257047cef

この記事に書いてあることを設定しました。本当にありがたい。。。

一点だけ躓いたのは、twitterのカードで画像が出なかったことです。
こちらも既知の失敗でした。

https://digitalidentity.co.jp/blog/seo/ogp-share-setting.html

こちらの記事にある通り、絶対パスで指定する必要がありました。
絵は友人がわざわざ紙に手書きで仕立ててくれました。
手書きらしいざらついた雰囲気が出ていて、余白を残したセンスに脱帽です。

# 改善点
なんとか公開までたどり着けましたが、積み残しはたくさんです。

### モバイルだとすごく遅いし、レイアウトもおかしい
レイアウトが縦長のスマートフォンだと崩れてしまいます。文字もぼやけてしまったままです。
そしてとにかく遅いんです。（そもそもＰＣ版で充分に高速かと考えるとそうでもないし）スマホのＣＰＵやメモリに対してデータが大きすぎるのかな、とさる方から助言を頂きましたので、使うタイトルの数をモバイル版とＰＣ版とで分けるのを試してみようと考えています。やり方は今から調べますが。。。

yanesdk-for-brythonは画像や音声出力だけでなく、タッチイベントまで網羅しています。モバイルでやってみたいアイデアが浮かんだらぜひ挑戦してみたいですし、改善できたらパソコンを持っていない友人にも見てほしいなと思っています。

### 成人指定
私は本に貴賤はないと考えています。作家さんや編集さん、出版社さんが一冊の本を上梓するのに並々ならぬ時間と熱意を注いでいらっしゃいます。ですが、不特定多数に公開しているものとして一定の公共性は必要です。

成人指定が入っているものだけでもフィルタして外そうとしましたが、openBDで取得できるonix（書誌情報の交換のために設計された書誌データ規格）の情報やタイトルなどの額面的な情報だけではうまくできませんでした。
普通のミステリ小説と成人向けであろう小説を区分するはずの項目がまったく同じだったりするんですよね。。。
（以下のデータ仕様のDescriptiveDetailのAudienceのところです）

https://openbd.jp/spec/

### リロードするとウェルカムメッセージが出ないときがある
brythonの仕様なのか、はたまたクッキーが影響しているのかはわかりません。
きっと私の実装に原因はあるのでしょう。

けれど、あくまで個人開発です。わからないことを楽しみたいと思います。

>「僕もわからない」犀川は微笑んだ。
「でも、これが、あらゆる感情の中で、最も知的で、最も人間的なものだよ」
「え？　何がですか？]
「わからない、という感情」


https://www.amazon.co.jp/exec/obidos/ASIN/B009GAIS86/soh335-22/

# まとめ
以上となります。ちょくちょくと触っていって、改善できたらなと考えています。
もし、原因などわかるかたがいらっしゃいましたら、そっとコメントで教えていただけたら幸いです。
そして、BookRainで思いもしなかった本との出会いがあれば、作者冥利に尽きます。