自分が読了した本の一覧を載せまくる本棚ページを作りました

以前書いたツイッター・オルタナティヴという記事で少し言及しているのですが、hail2u.netというサイトで著者の方が読んだ本の一覧が表示されており、自分もやろうと思ったのがきっかけです。露骨に真似るのもどうかとは思ったのですが、やってみました。

本棚

載せてる本について


買った本のうち、読み終わったものです。積読になっているものは載せていません。2017年くらいから前のものはマンガが多かったのですが、後述する事情によりあまり載せてないです。また、2013年以前の本は諸事情により大半が実家で保管されているためほとんど記録がないです。

マンガについてですが…。以前、後輩に「YoshinoriNさんマンガ読むんですか???」などという謎の質問を受けた(どういう人間だと思われてるんだ…??)のですが、私は普通にマンガ読みます。なんなら2012年くらいまでは週刊誌を5冊くらい買ってました(家に来た友達に雑誌で壁ができてるといわれました)し、単行本もかなり持ってました。最近はかなり減ってきており、当然雑誌は購入してないですし、単行本も継続して購入しているものは5つ前後です…。この通りマンガは過去多く読んでいたのですが、それらは冊数が多くてAmazon購入履歴からコピペすると腱鞘炎が加速しそうなので途中から載せるのをあきらめました。

載せてるリストは全てAmazonアフィリエイトになっています。私はこのサイトでユーザーフレンドリーな広告の表示の仕方(記事の最下部にしか広告を載せてない)をしている自負があって(本当にユーザーフレンドリーならそもそも広告を載せるべきでもないと思うんですが)大して儲かってもないので、別にここにのせるものくらい全部アフィリエイトになっててもいいだろう。という判断です。そもそも、たぶん誰もページ自体を見に行かないし。後は、画像は全部Amazonから引っ張ってきているんですが、Amazonから引っ張ってきている以上はAmazonのアフィリエイトになっていないとAmazonに申し訳ないような気がしたというのもあります。

まあ、しかし、自分で実装したものを見たらマンガの好みも偉く変わったなぁ。と思いました。以前はこんなに表紙が女の子女の子したやつはなかったと思う。

実装について


Excelに登録したものをJSONにしてそのJSONをHTMLに埋め込んで、Vue.jsで加工して表示しているだけです。

Excel

こんなかんじのExcelを用意しました。ISBN, ASIN, タイトル, 購入日, 読了日を設けています。購入日と読了日は結構適当に入力しています。実際に使っているのは ASIN, タイトル, 読了日のみです。

ASINはAmazonの商品管理番号です。これをもとに画像と商品へのリンクを生成しています。書籍の場合にKindleではASINが存在するのですが、紙の書籍の場合はASINが存在せず、代わりに10桁のISBNがASINに相当するようです。たぶん。

もう少し付け加えると、ISBNには10桁と13桁が存在します。ですのでExcelのISBN欄は正確にはISBN13が正しく、加えて紙の本のASINがISBNの10桁と同じとはいえ、ASIN欄にISBNの10桁を混在させるのは誤りであり、ISBN10の欄も設けておくべきだとは思いますが、やってしまったので後の祭りです。

ひさしぶりにExcelを使ったのですが、なんだかんだ言えExcelは便利だなぁ。などと思いました。

Excel から JSONへ

convert-excel-to-jsonというExcelをJSONにしてくれるいい感じのnpmパッケージがあったのでこれをつかいました。

1
'use strict';
2
3
const fs = require('fs');
4
const excelToJson = require('convert-excel-to-json');
5
6
const fromExcel = excelToJson({
7
  sourceFile: 'books.xlsx',
8
  header:{
9
    rows: 1
10
  },
11
  columnToKey: {
12
    'B': '{{B1}}',
13
    'C': '{{C1}}',
14
    'E': '{{E1}}',
15
  }
16
});
17
18
let result = [];
19
fromExcel['Sheet1'].forEach(x => {
20
  result.push({
21
    'image': `https://images-fe.ssl-images-amazon.com/images/P/${x.asin}.jpg`,
22
    'link': `https://www.amazon.co.jp/exec/obidos/ASIN/${x.asin}/`,
23
    'title': x.title,
24
    'readDate': new Date(x.readAt),
25
  })
26
});
27
28
result.sort((x, y) =>{
29
  return (x.readDate < y.readDate ? 1 : -1);
30
});
31
32
fs.writeFileSync('./dist/books.json', JSON.stringify(result));

これを実行すると./dist/books.jsonに下記のようなJSONが生成されます。

1
[
2
  {
3
    "image":"https://images-fe.ssl-images-amazon.com/images/P/B0191Z00MW.jpg",
4
    "link":"https://www.amazon.co.jp/exec/obidos/ASIN/B0191Z00MW/",
5
    "title":"漫画原作者は一体「何」を書いているのか",
6
    "readDate":"2019-11-09T15:00:00.000Z"
7
  },
8
  {
9
    "image":"https://images-fe.ssl-images-amazon.com/images/P/4314011211.jpg",
10
    "link":"https://www.amazon.co.jp/exec/obidos/ASIN/4314011211/",
11
    "title":"〈わたし〉はどこにあるのか: ガザニガ脳科学講義",
12
    "readDate":"2019-11-08T15:00:00.000Z"
13
  },
14
  {
15
    "image":"https://images-fe.ssl-images-amazon.com/images/P/B078MC35RH.jpg",
16
    "link":"https://www.amazon.co.jp/exec/obidos/ASIN/B078MC35RH/",
17
    ...
18
]

JSONの取得と表示

こっちに書いてるやり方でEJSにJSONを埋め込みました。それをJavaScriptで読み込んで加工してVue.jsを使って表示してます。JSONは別にHTMLに埋め込まなくてもAjaxで取得するでもいいと思います。

JavaScriptは雑に下記のような感じにしました。別途、EJS側でCDNのVue.jsを参照しています。

1
'use strict';
2
3
import "core-js/stable";
4
import "regenerator-runtime/runtime";
5
6
const vueInstance = new Vue({
7
  el: '#archives',
8
  data: {
9
    books: books,
10
    currentSearchTitle: "",
11
    searchTitle: "",
12
    currentSearchYear: "",
13
    searchYear: "",
14
    count: 0,
15
    perPage: 18,
16
    pagination: 1,
17
    displayMore: true,
18
  },
19
  computed: {
20
    filteredBooks: function() {
21
      let resultBooks = this.books;
22
23
      if (this.searchTitle.length != 0) {
24
        resultBooks = resultBooks.filter((books) => books.title.includes(this.searchTitle));
25
      }
26
      if (this.searchYear.length != 0) {
27
        resultBooks = resultBooks.filter((books) => books.readDate.startsWith(this.searchYear));
28
      }
29
30
      this.count = resultBooks.length;
31
32
      if(this.perPage * this.pagination >= this.count) {
33
        this.displayMore = false;
34
      } else {
35
        this.displayMore = true;
36
      }
37
38
      if (this.searchTitle != this.currentSearchTitle || this.searchYear != this.currentSearchYear) {
39
        this.pagination = 1;
40
      }
41
      this.currentSearchTitle = this.searchTitle;
42
      this.currentSearchYear = this.searchYear;
43
44
      return resultBooks.slice(0, (this.perPage * this.pagination));
45
    },
46
  },
47
  methods: {
48
    more: function() {
49
      this.pagination = this.pagination + 1;
50
      if (this.perPage * this.pagination >= this.count) {
51
        this.displayMore = false;
52
      }
53
    }
54
  }
55
});

これに対応するHTML(EJS)は次のとおりです。興味ある人がいるとは思えませんが、スタイルシートは開発者ツールで確認してください。なお、私はHTMLのことをちゃんと勉強したことがないので正しいHTMLなのかどうかは不明です。

1
<div id="archives" class="main-content-wrap">
2
    <div style="text-align: center;">
3
      <sub>ちゃんと最後まで読んだ本の一覧です。読了降順です。昔読んだマンガは途中から載せるのを諦めました。</sub>
4
    </div>
5
    <div style="display:inline-flex">
6
        <form id="filter-form" action="#" style="max-width: 150px;">
7
            <input name="date" type="number" maxlength="4" min="2010" class="form-control input--large" placeholder="YYYY(読了年)" v-model="searchYear">
8
        </form>
9
        <form id="filter-form" action="#" style="margin-left: 10px">
10
            <input name="title" type="text" class="form-control input--large" placeholder="タイトル" v-model="searchTitle">
11
        </form>
12
    </div>
13
    <h5 class="text-color-base text-xlarge" style="margin-left: 10px">合計 {{count}} 冊</h5>
14
    <section class="boxes">
15
        <div class="flex-container">
16
          <div class="books-wrapper" v-for="books in filteredBooks">
17
            <a style="color: #255c92 !important;" class="archive-post-title" v-bind:href="books.link" target="_blank">
18
              <img class="book-image" v-bind:alt="books.title" v-bind:src="books.image">
19
            </a>
20
          </div>
21
        </div>
22
    </section>
23
    <div v-if='displayMore'>
24
      <button class='post-action-btn btn btn--default' v-on:click='more'>もっと表示する</button>
25
    </div>
26
</div>

レイアウトについて

モバイルだと一列3冊、そうでなければ6冊表示するようにしました。デフォルトで18冊表示します。もっと表示するを押すと18冊ずつ読み込みます。

CSSも前述のhail2u.netさんのものを参考にしようかと思ったのですが、私には理解できない実装(皮肉ではなくてホントにCSSわからないので…)だったので全く異なるやり方で強引に突っ込んでます。

Amazonの購入履歴


実のところ、実装よりもExcelにひたすら購入履歴を打ち込むほうが時間がかかった(APIでなんとかできたんじゃないかという気もする)のですが、入力の過程で購入履歴を見返すといろいろと面白かったです。

どうも2016年くらいまでは書店で本を買っていたらしく、あまり履歴らしい履歴がありませんでした。そもそもAmazonでの買い物の頻度が少なかったようです。そしてAmazonで買ったものを見ていくと、実店舗でなかったので仕方なしにAmazonで買うというやり方をしていていたようです。

しかしながら、Amazonでの初めての購入は以外と古くて2006年でした。どうもカプースチンのエチュードと信長の野望のサントラを購入したっぽいです。


カプースチン : 8つの演奏会用エチュード

「信長の野望」究極音盤~烈風伝、将星録/覇王伝、風雲録


カプースチンのエチュードは今でも聴くのですが、カッコよくてこれもいつかやりたいなぁ。なんてことを無謀ながらにもひっそりと思っていたりします。これハイレゾで出し直してくれたらもっかい買うんだけどなぁ…。信長の野望のサントラはたぶん、この時に烈風伝をやっててサントラが欲しくなって買ったんだと思います…たぶん。烈風伝のBGMは(いい意味で)耳に残るBGMで、今でも優れたゲーム音楽の一つだと思ってます。

現在は日用品もとりあえずAmazonで雑に購入するというやり方をしているのですが、こうやって見ると生活スタイルも大きく変わるものだなぁ。と思いました。書店でザッピングしたり、発売日に本屋に行ったのを思い出したりして懐かしくなった反面、そういったことをもうやらないんだろうなぁ。とも思ったりして少し寂しい気持ちにもなりました。