Project Fx 2.0

怪文書と備忘録を書きます

読書メーターとGitHub Actionsで、大学図書館の本を『積読』しよう

この記事は、「LipersInSlums Advent Calendar 2023」第14日目の記事です。

昨日の記事は、ターニャ・デグレチャフさんの「Monadic Parserの俺的理解」でした。

adventar.org

知らない人のために触れておくと、LipersInSlums とは謎の Discord サーバです。 旧 Twitter ヘビーユーザーの計算機オタクがなぜかいっぱい集まっているという特徴があります。住民からは「スラム」と通称されています。

具体的にどんなところがスラム街っぽいのかについては、melovilijuさんの5日目の記事を読めば何となくわかると思います。

lipersinslums.github.io

はじめに

突然ですが、皆さんは積読してますか?私はしてます。

受験生の頃に神保町の古本市でストレスを紛らわすことを覚えて以降、ろくに読んでない本がまだ大量にあるのになぜか色々な本屋を巡っては、インターンの給与の大半を紙の山に変換するという日々を送るようになりました。

本を渉猟している間の脳内は「面白そうな本がこんなにたくさん並んでるぞ!ここに住みてえ!」という多幸感で満ち溢れているのですが、いざレジに並んでバーコードリーダーの電子音が耳に入ってくると、快楽に代わって絶起時にも似た謎の虚無感が襲いかかってきます。

有料の紙袋を手に退店し、下りエスカレータの手すりに体重を預けているうちに頭をもたげるのは「また買っちゃったな、早く読まなきゃ」という義務感です。 「最近の新刊書は店頭に並んでいるうちに買わなきゃすぐに絶版になるんだからしょうがない!!!」と自己正当化を図りながら自動ドアをくぐり、外の冷たい空気を吸うと、喪失感のいくばくかは充実感へスーッと変換されます。 陰と陽両方の感情を胸に抱きつつ、自宅の床の空きスペースを頭の中で勘定しようとすると、途端に丸善のロゴ入り紙袋が子泣き爺のようにズシリと重量を増し始めるのです。

丸善丸の内本店の店内には、何か人を恍惚とさせるような謎の物質が散布されているのかもしれません。 個人的には大麻よりも先にあちらを法規制すべきだと思います。

なお、新刊書だけでなく古本収集にも手を出している私の本棚は既にキャパオーバーしているため、家族の本棚を間借りしています。 約2年前に行った京都旅行で一番楽しかったレジャーは、日程を丸一日費やしての古本屋巡りでした。 「積読本は謎のエネルギー波を出しており、持ち主が寝ている間に脳を活性化させてくれる」なんて話もありますが、少なくとも財布と精神の健康にはまるでよろしくないと思います。

そんな書痴気味の私にとってなくてはならないのが、蔵書管理ツールです。 読んだ本や積読本が多くなってくると、ダブリ購入防止のため蔵書を良い感じに管理したくなってくるものですよね。 私は4年ほど前から「読書メーター」というWebサービスを使って蔵書・読書状況を管理しています。

bookmeter.com

私が読書管理の中で最も重視している営みは、今ある蔵書の管理というよりも『読みたい本の管理』です。 書店の棚・旧Twitterのタイムライン・面白そうな授業のシラバスなどを見ていると、面白そうな雰囲気の本に思いがけず出会うことがしばしばあります。 そんな時はいわばToDoリストのような感覚で、読書メーターを開いて『読みたい本』に登録しています。 この習慣を4年ほど続けているうちに、いつの間にか『読みたい本』が800冊を数えるようになりました(2023年12月現在)。

ここで話はB1の冬休み(『読みたい本』がまだ300冊くらいだった頃)に遡ります。 その頃の私は、読書メーターだけでは『読みたい本』が身近な図書館に置いてあるかが分からないという深刻な悩みを抱えていました。

当時は何度目かの緊急事態宣言が明けて、キャンパスへの入構規制が少し緩和されていた頃です。 入学以来初めて大学図書館をそこそこ自由に使えるようになったことで、「この本、大学図書館に置いてあったのかよ…Amazonから自腹で買う必要なかったじゃん…」 という悲劇にたびたび見舞われるようになっていました。 まして高額な専門書ともなると、自分に合わない本と知らずに買ってしまった時の損失は本当にバカになりません(金銭的にも精神的にも)。合うかどうかも分からないのに高額な本を買う方がバカだと? うるせぇ〜!!

そもそも、図書館の資料購入費や維持管理費は(少なくともある程度は)学費で賄われているはずです。言い換えれば、図書館の本は実質学生の金で買われたようなもの。 ということは、実質的には私の所有物…すなわち私の積読本と言っても全く過言ではないでしょう。 何万冊もの積読本が所有しているにも拘らず、その内容を自分では全く把握できていない! なんと嘆かわしいことでしょう……。これこそ、技術の力でどうにかすべきです!

ともかく、そうなると当然こういうモチベーションが湧いてきます。

というわけで、そういうツールを自分で作って現在まで運用を続けている話について、B1~B2の頃の自分を振り返る意味も込めて書いてみます。

TL;DR

あらかじめお断りしておくと、技術的に高度なことは一切せず、あるものを素直に組み合わせているだけです。

  • 言語は JavaScript → TypeScript (+ Node.js)
  • いろんな WebAPI を組み合わせ、書誌情報と所蔵の有無を調べている
  • GitHub Actions 上で稼働している
  • データは CSV ファイルとして GitHub で管理

『読みたい本』リストを大学図書館で見ている様子

なお、完成品はこちらです。 github.com

やったこと

技術選定

求めている機能はだいたいこんな感じでした。

  • 読書メーターの『読みたい本』が大学図書館にあるかどうかをリスト化したい
  • 書名だけじゃなく、著者とか出版年とかも見れるようにしたい
  • 図書館の中でもスマホでパッと見れるようにしたい
  • 「大学にある本」「大学にない本」を分けて見れるようにしたい

やるべきことはこんな感じです。

  1. 読書メーターから『読みたい本』の情報をエクスポートする
  2. どこかから書誌情報を取ってくる
  3. どこかから大学図書館の所蔵検索をする
  4. アプリケーションをどこかにデプロイして定時実行する

1. 読書メーターから『読みたい本』の情報をエクスポートする

読書メーターから直接『読みたい本』の情報を出力できれば良いのですが、読書メーターにそうした機能は備わっていません。 したがって、読書メータースクレイピングして『読みたい本』の情報を自力で回収する必要があります。

当時の私は Node.js と puppeteer を使った Web E2E テストを書いて給料を貰っていたので、自然な発想として、ここでも使い慣れた puppeteer を使うことにしました。 なお、作った当時は生の JavaScript で書いていましたが、今年に入ってから TypeScript で全面的に書き直しました。

まず、読書メーターから具体的に何の情報を取ってくるべきかという点が重要です。 DevTools で読書メーターの『読みたい本』一覧ページの中身を確認してみると、次のようになっていました(画像は2023年12月撮影)。

ここから個別の本を識別できそうな要素を選び、なんらかの方法で対応する本の書誌情報を取ってくることにしました。 書名は途中までしか表示されないので、使えそうな情報は Amazon 商品ページへの外部リンク・個別書籍ページへの内部リンクに限られます。

Amazon の商品情報を直接取得できる WebAPI は一応存在するようですが、どうも Amazon と連携して EC サイトやら何やらを作りたい事業主のためのサービスらしく、利用条件が厳しかったため断念しました。

jsstudy.hatenablog.com

hnavi.co.jp

Amazon の商品ページを直接スクレイピングして商品情報を取ってくるという手もありますが、手間が増えるので一旦保留。

しばらく DevTools をグッと睨んでいたところ、Amazon への外部リンク URL に ASIN コードが含まれていることに気づきました。

前述したように、ASINコードとは Amazon における商品の識別コードです。ASINコードの命名規則は公表されていませんが、紙の書籍商品ではISBN-10コードがそのまま ASIN コードとして扱われることが知られています(Kindle 電子書籍には紙書籍版と異なるASINコードが採番されます)。 つまり、紙の書籍であれば、読書メーターの URL 文字列を正規表現に食わせるだけでその本の ISBN をゲットできるということです。 ISBN さえ分かれば、何らかの WebAPI を使って書誌情報もゲットできそうですね。Amazon の商品ページを直接読むよりも明らかに簡単そうです。

万が一、アカウントをBANされたり神奈川県警に逮捕されたりしたら困るので、読書メーター利用規約も確認することにしました。 第9条は「故意に本サービスのサーバ又はネットワークへ著しく負荷をかける行為(14号)」および「当社又は、第三者著作権、商標権などの知的財産権を侵害する行為、又は侵害するおそれのある行為(18号)」を禁じています。 しかし、サービスに負荷をかけない範囲でのスクレイピング行為は禁じていません。また、取得しようとしているのは単なるURL文字列なので、誰かの知的財産権を侵害することもありません。

bookmeter.com

読書メーターでは同じ出版物であっても紙の書籍版と Kindle 版とでページを区別しているため、『読みたい本』に紙書籍版ではなく Kindle 版を誤登録してしまいやすい構造になっています。 このため、ツールを動かし始めた当初は、Kindle 版の ASIN コードのまま書誌情報を検索しようとしてしまい「そんな本はねえよ」とエラーを吐かれる事象が頻発していました。

ただし、ツールを動かし始めてからしばらくして読書メーターの検索機能が改修され、紙書籍版が優先的に上位に来るようになったため、この問題に見舞われることは少なくなりました。

読書メーター公式ブログ — ブラウザ版 書誌検索オプション アップデートのお知らせ

2. 書誌情報を取ってくる

ISBN から書誌情報を取得する WebAPI には、主に以下のものがあります。 どれも一長一短なので、私のコードではこれらを組み合わせて使用しています。

本1冊の書誌情報は下のBook型のオブジェクトとして管理し、APIを叩きながらBookオブジェクトを更新する形になっています。 BookBookList に蓄積されていき、最終的に BookList の中身をCSV形式で出力して処理を終えます。 後述する問題から、BookListオブジェクトの主キーとしては、ISBNではなく読書メーターのURLを使っています。

type Book = {
  bookmeter_url: string;
  isbn_or_asin: ISBN10 | ASIN | null;
  book_title: string;
  author: string;
  publisher: string;
  published_date: string;
  central_opac_link: string;
  mathlib_opac_link: string;
} & {
  [key in ExistIn]: "Yes" | "No";
};
type BookList = Map<string, Book>;
type ExistIn = `exist_in_${CiniiTargetOrgs}`;
type CiniiTargetOrgs = (typeof CINII_TARGET_TAGS)[number];
const CINII_TARGET_TAGS = ["Sophia", "UTokyo"] as const;

ISBNを主キーとして使うことの問題

日本の出版業界におけるISBNの運用状況は、はっきり言って非常にいい加減です。この問題の大変わかりやすい説明として、以下の一連のツイート群を提示しておきます。

上のツイートでも触れられていますが、2016年には岩波書店が同じ本の旧訳・新訳でISBNを使い回していることが判明し、Twitterの読書趣味界隈(+プログラマ界隈の一部)で問題になりました。

togetter.com

stonebeach-dakar.hatenablog.com

こうした問題のため、本当はISBNを書誌情報の主キーとして実用することはできません

しかしそれにもかかわらず、書誌情報を WebAPI として取ってくる際は ISBN を実質的な主キーとして用いざるを得ません。 そのため、本ツールでは前回の出力結果を活用できず、「実行する度に毎回全ての書誌情報を新しく取ってくる」という効率の悪い実装を強いられる状況になっています(同じ API に同じ ISBN をリクエストしても、返ってくる書誌情報がいつも同じとは限らず、しかも変化が起こるタイミングはこちらから観測できないため)。

OpenBD

みんな大好き株式会社カーリルと、出版業界団体の「一般社団法人 版元ドットコム」が共同で提供している書誌情報APIです。 非常に多様な書誌情報を提供しているのが魅力的です。

openbd.jp

最近の本の情報は8割方ここで取れますが、古い本・一部の出版社の本は網羅されていない場合があります。 また、今年になって書誌情報の有力な提供元(別の出版業界団体)と深刻な仲違いをしたらしく、書籍の網羅率や取得可能な書誌情報の量が以前よりも減っています。

このAPIの最大の特徴は、「全件取得やデータ同期を推奨するAPI設計(大量アクセス対応)」を謳っていることです。 運営元のお言葉に甘えて、1回のリクエストで『読みたい本』全ての書誌情報を一度に検索する贅沢な使い方をしています。

const bulkFetchOpenBD = async (bookList: BookList): Promise<BiblioInfoStatus[]> => {
  const bulkTargetIsbns = [...bookList.values()].map((bookmeter) => bookmeter["isbn_or_asin"]).toString();
  const bookmeterKeys = Array.from(bookList.keys());

  const response: AxiosResponse<OpenBdResponse> = await axios({
    method: "get",
    url: `https://api.openbd.jp/v1/get?isbn=${bulkTargetIsbns}`,
    responseType: "json"
  });
  const results = [];

  for (const [bookmeterURL, bookResp] of zip(bookmeterKeys, response.data)) {
    if (bookResp === null) {
      //本の情報がなかった
      const statusText: BiblioinfoErrorStatus = "Not_found_in_OpenBD";
      const part = {
        book_title: statusText,
        author: statusText,
        publisher: statusText,
        published_date: statusText
      };
      results.push({
        book: { ...bookList.get(bookmeterURL)!, ...part },
        isFound: false
      });
    } else {
      //本の情報があった
      const bookinfo = bookResp.summary;
      const part = {
        book_title: bookinfo.title ?? "",
        author: bookinfo.author ?? "",
        publisher: bookinfo.publisher ?? "",
        published_date: bookinfo.pubdate ?? ""
      };
      results.push({
        book: { ...bookList.get(bookmeterURL)!, ...part },
        isFound: true
      });
    }
  }
  return results;
};

上述の通り、OpenBDだけでは『読みたい本』全ての書誌情報を得ることができないため、後述する他サービスからの情報で不足分を補完しています。

国立国会図書館サーチ

みんな大好き国立国会図書館の資料検索機能のAPIです。

iss.ndl.go.jp

さすがに国会図書館だけあって、ISBN付きの和書なら十中八九ヒットします。 が、かつてのWeb2.0時代の遺産というべきか、レスポンスがXML形式です。私はfast-xml-parserでパースしています。

github.com

const fetchNDL: FetchBiblioInfo = async (book: Book): Promise<BiblioInfoStatus> => {
  const isbn = book["isbn_or_asin"]; //ISBNデータを取得

  if (isbn === null || isbn === undefined) {
    //有効なISBNではない
    const statusText: BiblioinfoErrorStatus = "INVALID_ISBN";
    const part = {
      book_title: statusText,
      author: statusText,
      publisher: statusText,
      published_date: statusText
    };
    return {
      book: { ...book, ...part },
      isFound: false
    };
  }

  // xml形式でレスポンスが返ってくる
  const response: AxiosResponse<string> = await axios({
    url: `https://iss.ndl.go.jp/api/opensearch?isbn=${isbn}`,
    responseType: "text"
  });
  const json_resp = fxp.parse(response.data) as NdlResponseJson; //xmlをjsonに変換
  const ndlResp = json_resp.rss.channel;

  //本の情報があった
  if ("item" in ndlResp) {
    // 該当結果が単数か複数かによって、返却される値がObjectなのかArray<Object>なのか変わる。
    // fast-xml-parserの設定をいじれば多分もっとスマートにできると思うが、とりあえず目的を達成するにはこれだけ判定すれば十分。
    // 面倒なので、該当件数に関わらず配列の先頭だけをチェックしておく
    const bookinfo = Array.isArray(ndlResp.item) ? ndlResp.item[0] : ndlResp.item;
    const part = {
      book_title: bookinfo["title"] ?? "",
      author: bookinfo["author"] ?? "",
      publisher: bookinfo["dc:publisher"] ?? "",
      published_date: bookinfo["pubDate"] ?? ""
    };
    return {
      book: { ...book, ...part },
      isFound: true
    };

    //本の情報がなかった
  } else {
    const statusText: BiblioinfoErrorStatus = "Not_found_in_NDL";
    const part = {
      book_title: statusText,
      author: statusText,
      publisher: statusText,
      published_date: statusText
    };
    return {
      book: { ...book, ...part },
      isFound: false
    };
  }
};

Google Books

みんな大好きGoogleが提供しているGoogle BooksというサービスのAPIです。 developers.google.com

1日に1000回までという利用上限があるため、使いどころをかなり考えて叩く必要があります。 今のところ、OpenBDや国会図書館サーチでは手薄になっている洋書の情報を補う目的で採用しています。

const fetchGoogleBooks: FetchBiblioInfo = async (book: Book): Promise<BiblioInfoStatus> => {
  const isbn = book["isbn_or_asin"];

  if (isbn === null || isbn === undefined) {
    //有効なISBNではない
    const statusText: BiblioinfoErrorStatus = "INVALID_ISBN";
    const part = {
      book_title: statusText,
      author: statusText,
      publisher: statusText,
      published_date: statusText
    };
    return {
      book: { ...book, ...part },
      isFound: false
    };
  }

  //有効なISBNがある
  const response: AxiosResponse<GoogleBookApiResponse> = await axios({
    method: "get",
    url: `https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}&key=${google_books_api_key}`,
    responseType: "json"
  });
  const json = response.data;

  if (json.totalItems !== 0 && json.items !== undefined) {
    //本の情報があった
    const bookinfo = json.items[0].volumeInfo;
    const part = {
      book_title: `${bookinfo.title}${bookinfo.subtitle === undefined ? "" : " " + bookinfo.subtitle}`,
      author: bookinfo.authors?.toString() ?? "",
      publisher: bookinfo.publisher ?? "",
      published_date: bookinfo.publishedDate ?? ""
    };
    return {
      book: { ...book, ...part },
      isFound: true
    };
  } else {
    //本の情報がなかった
    const statusText: BiblioinfoErrorStatus = "Not_found_in_GoogleBooks";
    const part = {
      book_title: statusText,
      author: statusText,
      publisher: statusText,
      published_date: statusText
    };
    return {
      book: { ...book, ...part },
      isFound: false
    };
  }
};

楽天ブックスAPI

みんな大好き楽天が提供している楽天ブックスというサービスのAPIです。 ECサイトの商品検索APIなので、和書だけでなく雑誌やCDや洋書にも対応しているのが強みです。

webservice.rakuten.co.jp

現在のところは上述の3つで十分な情報を取れているので採用していませんが、そのうち雑誌検索用に使おうかと考えています。

高速化のために

ここまで述べてきた通り、このツールは何種類もの WebAPI を合計で数百回ほど叩くことによって実現されています。

しかし、何も考えず直列的に API を叩くと、読書メーターの走査開始からCSVファイルの出力までに30分は掛かってしまいます(リクエスト1回につき1~2秒のインターバルを置くと、さらに時間がかかります)。 そのため、こちらの記事を参考に複数のPromiseを同時並行で処理しています。

zenn.dev

これが功を奏し、APIリクエストのインターバル込みでも12~13分で処理を終わらせられるようになりました。

const fetchBiblioInfo = async (booklist: BookList): Promise<BookList> => {
  const mathLibIsbnList = await configMathlibBookList("ja");
  const updatedBookList = await bulkFetchOpenBD(booklist);

  const fetchOthers = async (bookInfo: BiblioInfoStatus) => {
    let updatedBook = { ...bookInfo };

    // NDL検索
    if (!updatedBook.isFound) {
      updatedBook = await fetchNDL(updatedBook.book);
    }

    await sleep(randomWait(1500, 0.8, 1.2));

    // GoogleBooks検索
    if (!updatedBook.isFound) {
      updatedBook = await fetchGoogleBooks(updatedBook.book);
    }

    await sleep(randomWait(1500, 0.8, 1.2));

    // CiNii所蔵検索
    for (const library of CINII_TARGETS) {
      const ciniiStatus = await searchCiNii({ book: updatedBook.book, options: { libraryInfo: library } });
      if (ciniiStatus.isOwning) {
        updatedBook.book = ciniiStatus.book;
      }
    }

    // 数学図書館所蔵検索
    const smlStatus = searchSophiaMathLib({
      book: updatedBook.book,
      options: { resources: mathLibIsbnList }
    });
    if (smlStatus.isOwning) {
      updatedBook.book = smlStatus.book;
    }

    booklist.set(updatedBook.book.bookmeter_url, updatedBook.book);
  };

  const ps = PromiseQueue();
  for (const book of updatedBookList) {
    ps.add(fetchOthers(book));
    await ps.wait(6); // 引数の指定量だけ並列実行
  }
  await ps.all(); // 端数分の処理の待ち合わせ

  console.log(`${JOB_NAME}: Searching Completed`);
  return new Map(booklist);
};

3. 大学図書館の所蔵情報を取ってくる

日本語の学術論文をネットで検索したことがある人なら、国立情報学研究所の運営する「CiNii」というデータベースを一度は使ったことがあるでしょう。 その一部である「CiNii Books」の WebAPI を使うと、大学図書館の蔵書を好きなクライアントから検索することができます。

support.nii.ac.jp

レスポンスがちゃんとJSONで返ってくるのは嬉しいところですが、重要な個人情報(本名・所属元)を開示しないと利用申請できないのは少しマイナスポイントかもしれません。

support.nii.ac.jp

CiNii では図書館を区別するための情報として、大学単位で割り振られる「機関ID」と、大学内にある個別の図書館施設ごとに割り振られる「図書館ID」というコードを持っています。 APIを叩く際、これらの少なくとも一方を含めてリクエストしてあげると、指定した本が狙った図書館に所蔵されているかどうかを調べることができます。 コードの一覧は、CiNii の基盤となっている「目録所在情報サービス」というシステムの公式サイトから確認できます。

contents.nii.ac.jp

contents.nii.ac.jp

なお、弊学には CiNii に情報を公開していない「数学図書室」という施設があるのですが、こちらの所蔵検索はあまり良い感じで実現できていません。 今のところ、司書さんにお願いして蔵書一覧のPDFファイルを公式サイトに掲示してもらい(サイト容量の問題でPDFじゃないとアップロードできなかったらしい)、それをpdfdataextractというnpmパッケージで無理やりパースする形を取っています。元ファイルの問題で誤認識が多いのが目下の課題です。

github.com

ともあれ、上のような手段で大学に所蔵されている本だと分かったら、大学図書館OPACリンクをCSVファイルに出力します。

2024/01/26追記:「CiNiiには登録されていないが、大学図書館OPAC上では登録されている本」が、少なからず存在することに気づきました。一例を挙げておきます。

bookmeter.com

ci.nii.ac.jp

www.lib.sophia.ac.jp

この問題のため、CiNiiだけでは正確な所蔵情報を取ってくることができません。対策として、CiNiiの所蔵検索で見つからなかった場合は、大学図書館OPACに直接アクセスしてページの実在をチェックするようにしました。

const searchCiNii: IsOwnBook<null> = async (config: IsOwnBookConfig<null>): Promise<BookOwningStatus> => {
  const isbn = config.book["isbn_or_asin"]; //ISBNデータを取得
  const library = config.options?.libraryInfo;

  if (library === undefined) {
    throw new Error("The library info is undefined");
  }

  if (isbn === null || isbn === undefined) {
    //異常系(与えるべきISBN自体がない)
    const statusText: BiblioinfoErrorStatus = "INVALID_ISBN";
    const part = {
      book_title: statusText,
      author: statusText,
      publisher: statusText,
      published_date: statusText
    };
    return {
      book: { ...config.book, ...part, [`exist_in_${library.tag}`]: "No" },
      isOwning: false
    };
  }

  // const title = encodeURIComponent(config.book["book_title"]);
  const url = `https://ci.nii.ac.jp/books/opensearch/search?isbn=${isbn}&kid=${library?.cinii_kid}&format=json&appid=${cinii_appid}`;
  const response: AxiosResponse<CiniiResponse> = await axios({
    method: "get",
    responseType: "json",
    url
  });
  const graph = response.data["@graph"][0];

  if ("items" in graph) {
    //検索結果が1件以上

    const ncidUrl = graph.items[0]["@id"];
    const ncid = ncidUrl.match(REGEX.ncid_in_cinii_url)?.[0]; //ciniiのURLからncidだけを抽出

    return {
      book: {
        ...config.book,
        [`exist_in_${library.tag}`]: "Yes",
        central_opac_link: `${library.opac}/opac/opac_openurl?ncid=${ncid}` //opacのリンク
      },
      isOwning: true
    };

  } else {
    //検索結果が0件

    // CiNiiに未登録なだけで、OPACには所蔵されている場合
    // 所蔵されているなら「"bibid"」がurlに含まれる
    const opacUrl = `${library.opac}/opac/opac_openurl?isbn=${isbn}`;
    const redirectedOpacUrl = await getRedirectedUrl(opacUrl);

    await sleep(randomWait(1000, 0.8, 1.2));

    if (redirectedOpacUrl !== undefined && redirectedOpacUrl.includes("bibid")) {
      return {
        book: {
          ...config.book,
          [`exist_in_${library.tag}`]: "Yes",
          central_opac_link: opacUrl
        },
        isOwning: true
      };
    }

    return {
      book: { ...config.book, [`exist_in_${library.tag}`]: "No" },
      isOwning: false
    };
  }
};

4. デプロイして定時実行する

以下の理由から、バックエンド・フロントエンドの両方を GitHub に頼ることにしました。

  • 人類の根源的かつ本能的な欲求として、高頻度で更新されるテキストデータの差分は Git で管理したい。
  • GitHub には様々な条件をきっかけに処理を自動で実行してくれる GitHub Actions という機能がある。
  • GitHub には CSV ファイルを良い感じの表形式にレンダリングしてくれる機能がある。検索機能までついている。

docs.github.com

また、単なる『読みたい本の一覧』1つだけでは情報が多すぎて小回りが効きません。 そのため「q」というCSV操作ツールで場合分けし、大学にある本・ない本ごとに別々のCSVファイルを追加で作成しています。

harelba.github.io

#!/bin/bash

source_from="FROM ./bookmeter_wish_books.csv"
base_columns="isbn_or_asin, book_title, author, publisher, published_date"
filter_book_title_error="(book_title NOT LIKE 'Not_found_in%' AND book_title NOT LIKE '%INVALID_ISBN%')"

q -d, -O -H "SELECT $base_columns \
$source_from \
WHERE exist_in_Sophia='No' AND $filter_book_title_error"\
> not_in_Sophia.csv

q -d, -O -H "SELECT $base_columns, central_opac_link, mathlib_opac_link \
$source_from \
WHERE exist_in_Sophia='Yes'"\
> in_Sophia.csv

毎週日曜日の午前0時・水曜日の午後0時になると GitHub Actionsの cron 機能が発火し、これらの一連の処理を実行します。 更新されたCSVファイルはそのまま GitHub リポジトリにコミットされます。

出力した『読みたい本』リストを眺めてみる

現在在籍している大学と今度進学する大学院とで、どれだけ図書館の収蔵内容が違うのか見比べてみました。

まず、大学院にあって学部にない本がこちら。

github.com

ジャンルを問わず、全体的に大学院の方が蔵書がキメ細やかであることがわかります。

一方で「学部にあって大学院にない本」がこちら。

github.com

「大学の規模から言っても今の大学にしかない本なんてほとんどないだろう」と思っていましたが、意外に多くて驚きでした。全体的に、思想史(の中でもニッチな西洋神秘主義関連の分野)・文化史・ルポルタージュ系の本が目立ちますが、数学書や新刊技術書でも意外と漏れがあるようです。 春休みは図書館に籠もって、このリストにある本を中心に積読を消化していきたいと思います。

さて、現在の大学にも進学先の大学院にもない本もかなりありました。というかこれが一番多いんじゃないか?

github.com

当たり前と言えば当たり前ですが、サブカル系の本はどちらの図書館にも所蔵されていないことが分かります。

ただ『行動経済学の逆襲』がないのはおかしくない? ノーベル賞受賞者の本やぞ。ISBNの問題かしら…。 あと『楽器の物理学』が今の大学にないのもおかしいって。こないだ中央図書館の9Fで見たぞ。やっぱりISBNの問題かなあ…。 ISBNの問題でした(丸善出版の旧版とシュプリンガー・ジャパンの新版でISBNが異なっていたせいだった)。

今回の記事には間に合いませんでしたが、クラスタリングとかで読みたい本の傾向を分析してみるのも面白いかもしれません。

今後の課題

ISBNのない出版物はどうする?

以下のような本にはISBNがないため、現行のシステムでは書誌検索の対象にできません。

  • 雑誌
  • 80年代以前の古い本(ISBNのなかった時代の本)
  • Kindle

ASINコードとかJANコードとかの情報を使って、どうにかうまいこと書誌検索できたらいいなあと思っていますが、そもそもどこから情報を取ってこれるのか全く分かっていません。 良い感じの方法をご存じの方がいらしたら教えてください。

Amazonほしい物リストと連携させたい

欲しい物が多いからには、やっぱり他人に買ってもらいたいですよね。 何とは言いませんが、ちなみに私は先月末に誕生日を迎えました。

最後に

みんなも積読、しよう!!!

明日はstepney141さんの「このJavaScriptライブラリがキモい(褒め言葉)・2023」です。どんな記事なのか楽しみですね。