ブクログに登録している本のうち、Kindle版を持っている本を一気にKindle版に差し替えた

f:id:munieru_jp:20190929205603p:plain

僕は、蔵書や読んだ本の情報をブクログで管理しています。
そしてこの度、ブクログに登録している本のうち、Kindle版を持っている本を一気にKindle版に差し替えるという作業を行ないました。
今後同じ作業をすることはまずないと思いますが、その手順を記しておきます。

モチベーション

今までは、Kindle版を購入した場合も紙の本として登録していました。
ブクログ上では、たとえ同じ本であっても紙の本とKindle版が別物として扱われており、登録者やレビューが分かれているからです。
ほとんどの場合はそれで問題なかったのですが、一部の本はKindle版しか販売されていないことがあります。
それゆえ、Kindle版を買っている本の中でも、紙の本として登録しているものとKindle版として登録しているものとに分かれているという状況でした。
そのようないびつな状況を解消するために、実際に紙の本を持っているものは紙の本として、Kindle版を持っているものはKinlde版として登録することにしたのです。
すでに登録済みのデータについても差し替える必要がありますが、僕はKindle本を2000冊以上購入しているので、とても手作業では間に合いません。
そこで、僕のエンジニアとしての技術力をもって作業を自動化することで、一気に差し替えることにしました。

基本方針

今回の作業は、次のような手順で行なうことにしました。

  1. ブクログの本棚データをエクスポート
  2. エクスポートしたデータの中身を書き換える
  3. ブクログの本棚データを削除
  4. 書き換えたデータをブクログにインポート

抜かりのない僕は事前にブクログに対して次のような問い合わせをし、エクスポート→削除→インポートという手順を踏めば元の状態が再現されることを確認しました。

登録データのインポート/エクスポート機能を提供していただいているかと思いますが、以下のような操作をした場合、データ(内部的なタイムスタンプなどを除く、ユーザーがサービス上で確認できる「登録日時」などの値)は元の状態と変わらないままでしょうか。
1. 「エクスポート」機能でCSVファイルをエクスポート
2. 「まとめて削除」機能ですべての本を削除
3. 「まとめて登録 (CSV)」機能で(1)でエクスポートしたCSVファイルをインポート

購入済みのKindle本リストを作成

Amazonのコンテンツと管理ページで次のようなスクリプトを用いて、購入済みのKindle本のリストを作成しました。

これを実行すると、次のようなTSV形式の文字列がクリップボードにコピーされます。

ゼロの使い魔 (MF文庫J)   ヤマグチ ノボル  2013年8月14日    B009QUKPIM  4040683021

左から順に、「タイトル」「著者」「購入日」「ASIN」「底本のASIN」です。
底本の情報はページ上には存在しないので、Amazonまたはブクログの商品ページから取得しています。
普通に実行するとhttps://www.amazon.co.jpからhttps://booklog.jp/*に対する通信がクロスオリジン制約に引っかかるので、Allow CORS: Access-Control-Allow-OriginというChrome拡張でクロスオリジン制約を無効にしたうえで実行しました。
1ページあたり200件までしか表示できないので、実行してはテキストエディタにペーストし、次のページを表示するという操作をひたすら繰り返すことで1つのTSVファイルを作成しました。

ブクログの本棚データをエクスポート

ブクログのエクスポートページから、本棚に登録済みのデータをCSVファイルにエクスポートしました。
出力されるCSVファイルは、次のようになっています。

"1","4040683021","9784040683027","","","積読","","","","2015-08-23 00:42:38","","ゼロの使い魔 (MF文庫J)","ヤマグチ ノボル","KADOKAWA/メディアファクトリー","2004","本","263",""

左から順に、「サービスID」「アイテムID」「13桁ISBN」「カテゴリ」「評価」「読書状況」「レビュー」「タグ」「読書メモ」「登録日時」「読了日」「タイトル」「作者名」「出版社名」「発行年」「ジャンル」「ページ数」「価格」です。
この中の「アイテムID」がAmazonにおけるASINに相当し、KindleのTSVファイルに記載されているASINとつながります。*1

Kindle版を持っている本のデータを更新

「Kindle」「ブクログ」という2つのシートを持つGoogleスプレッドシートを作成し、KindleのTSVファイルとブクログのCSVファイルをそれぞれのシートに貼り付けました。

f:id:munieru_jp:20190928235728p:plain

f:id:munieru_jp:20190928235730p:plain

さて、GoogleスプレッドシートはGoogle Apps ScriptというJavaScript風のスクリプト言語で操作することができます。
次のようなスクリプトを用いて、Kindle版を持っている本のアイテムIDをKindle版のASINに変更しました。

var SHEET_NAME_KINDLE = 'Kindle'
var SHEET_NAME_BOOKLOG = 'ブクログ'
var INDEX_KINDLE_ASIN = 3
var INDEX_KINDLE_ORIGINAL_ASIN = 4
var INDEX_BOOKLOG_ITEM_ID = 1

/**
* Kindle版を持っている本のアイテムIDをKindle版のASINに変更する。
*/
function updateItemIdToAsinIfHaveKindle () {
  var ss = SpreadsheetApp.getActive()
  var kindleSheet = ss.getSheetByName(SHEET_NAME_KINDLE)
  var kindleRange = kindleSheet.getDataRange()
  var kindleRows = kindleRange.getValues()
  var findKindleRowByOriginalAsin = function (originalAsin) {
    var matchedRows = kindleRows.filter(function (row) {
      return row[INDEX_KINDLE_ORIGINAL_ASIN] === originalAsin
    })
    if (matchedRows.length) {
      return matchedRows[0]
    }
  }
  var booklogSheet = ss.getSheetByName(SHEET_NAME_BOOKLOG)
  var booklogRange = booklogSheet.getDataRange()
  var booklogRows = booklogRange.getValues()

  var newBooklogRows = booklogRows.map(function (booklogRow, i) {
    // Skip header
    if (i === 0) {
      return booklogRow
    }
    var itemId = booklogRow[INDEX_BOOKLOG_ITEM_ID]
    var kindleRow = findKindleRowByOriginalAsin(itemId)
    if (kindleRow) {
      var asin = kindleRow[INDEX_KINDLE_ASIN]
      booklogRow[INDEX_BOOKLOG_ITEM_ID] = asin
    }
    return booklogRow
  })
   booklogRange.setValues(newBooklogRows)
}

ブクログの本棚データを削除

ブクログのまとめて削除ページから、本棚に登録済みのデータをすべて削除しました。
なぜいったん削除するのかというと、同じ本の紙版とKindle版が重複して登録されないようにするためです。

ブクログに本棚データをインポート

ブクログにデータをインポートするためには、次の仕様を満たすCSVファイルが必要になります。*2

  • 文字コードはShift-JIS
  • 各項目をダブルクォーテーション(")で囲む
  • 各項目をカンマ(,)で区切る

Googleスプレッドシートではこの仕様を満たすようなCSVファイルを出力できなかったので、いったんTSVファイルとして出力し、ローカル上で次のようなスクリプトを用いてCSVファイルに変換しました。

const fs = require('fs')
const iconv = require('iconv-lite')

const LINE_SEPARATOR = '\n'
const CSV_DELIMITER = ','
const TSV_DELIMITER = '\t'

const inputPath = process.argv[2]
const outputPath = process.argv[3]

const tsv = read(inputPath)
const tsvLines = tsv.split(LINE_SEPARATOR).filter(line => line !== '')  // Skip empty lines
console.log(`read ${tsvLines.length} lines from ${inputPath}`)

const csvLines = tsvLines.map(line => {
  const tsvCells = line.split(TSV_DELIMITER)
  const csvCells = tsvCells.map(cell => `"${cell}"`)
  return csvCells.join(CSV_DELIMITER)
})
const csv = csvLines.join(LINE_SEPARATOR)
write(outputPath, csv)
console.log(`write ${csvLines.length} lines to ${outputPath}`)

function read (path) {
  return fs.readFileSync(path, 'utf-8')
}

function write (path, data) {
  fs.writeFileSync(path, iconv.encode(data, 'shift_jis'))
}

こちらのスクリプトは、GitHubでも公開しています。

ブクログのまとめて登録ページから、作成したCSVファイルをインポートしました。

漏れているデータを手動でインポート

CSVファイルをインポートすることにより概ね自動で差し替えられましたが、Kindle版を持っているのに差し替わっていない作品がいくつかありました。
それは、Kindle版に対して底本が複数(単行本と文庫など)存在し、かつブクログに2番目以降のものを登録している場合でした。
たとえば、『四畳半神話大系』にはKindle版、単行本、文庫という3つのフォーマットがありますが、僕は3番目の文庫として登録していたので、Kindle版と底本の情報がマッチしなかったのです。

f:id:munieru_jp:20190930090125p:plain

購入済みのKindle本リストを作成するスクリプトにおいて、(Kindle版とAudible版を除く)最初のフォーマットを底本として扱っていることが原因でした。
スクリプトを修正して再度実行してもよかったのですが、数も少なかったので残りは手動でCSVファイルの値を書き換えてインポートしました。

以上の手順により、ブクログに登録している本のうち、Kindle版を持っている本を一気にKindle版に差し替えることができました。


四畳半神話大系 (角川文庫)

四畳半神話大系 (角川文庫)