はじめに
どうも皆さんこんにちわ新人のOです。
今回はGoogle Books API(以下APIと呼ぶ)を使って、書籍のタイトルからISBN(※1)を取得するプログラムを作成しました。(プログラミング言語はJavaを使用しています。)
※1 書籍ごとに付与される国際標準の識別番号のこと
作成した理由は、社内用の図書管理システム作成にあたり、会社にある本の情報をDBに登録する必要があるためです。
現在、本の管理をしているExcel(以下書籍リストと呼ぶ)にはISBNの記載がないので、書籍リストにある本のタイトルをAPIにリクエストしてISBNを取得しようと考えました。しかし、何の工夫もなくAPIを使うだけだと、大量のレスポンスが帰ってきて、どれが正しい本かわからないという問題がありました。また、レスポンスの中にISBNが無かったり、そもそもリクエストした本の情報が無かったりもしました。
なので、Javaでレスポンスを絞り込んで、できるだけ正しいISBNを取得できるように、工夫を加えました。このプログラムでISBNを取得できなかったものはバーコードリーダを使って手動で取得してくる必要があり手間がかかるので、できるだけ多く取得できるようにも工夫しています。
やったこと
まず、APIのレスポンスをマッピングするクラス等、最初に必要なものは上司が用意してくれました。
私がやったことは、主に以下の3つです。
- 書籍リストを読み込んで動的に処理させる
- APIレスポンスを絞り込んで書籍リストのタイトルに一番近い本のISBNを一つ取得する
- 取得したISBNなどの情報を書き込み用のExcel(以下取得リストと呼ぶ)に書き込む
手順
1. 書籍リストを読み込む
2. 書籍のタイトルをAPIにリクエストする
3. ISBNを取得する
4. 取得リストに書き込む
使用したライブラリ
- Poi
- jackson
- commons.text(レーベンシュタイン距離)
作成したコード
作成したコードをGitHubに公開しました。下でメソッドの説明はありますが、全体をみたい方は↓から見ていただけます。
GetISBNForGoogleBooksAPIs
1. 書籍リストを読み込む
書籍リストには、本の種類、タイトル、著者、出版社の情報が記録されています。
本の種類には雑誌と単行本がありますが、雑誌にはISBNがないので単行本のタイトルだけを取得しています。
Excelの読み込み、書き込みにはPoiというライブラリを使用して行いました。
String filePath = path; // 読み込むExcelファイルのパス
List<String> list = new ArrayList<String>();
try (FileInputStream fis = new FileInputStream(new File(filePath)); Workbook workbook = new XSSFWorkbook(fis)) {
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
Sheet sheet = workbook.getSheetAt(i);
int count = 0;
for (Row row : sheet) {
if (count > 0) {
Cell janruCell = row.getCell(0);
Cell titleCell = row.getCell(1);
if (janruCell == null || titleCell == null) {
continue;
}
String janru = janruCell.getStringCellValue();
if ("雑誌".equals(janru)) {
continue;
}
String title = titleCell.getStringCellValue();
list.add(title);
}
count++;
}
}
return list;
}
2. 書籍のタイトルをAPIにリクエストする
ISBNを取得するために、最初はリクエスト時の検索条件でできるだけ絞り込む方法を検討しました。具体的には、タイトルに加えて著者名や出版社名もクエリに含めれば、より正確な検索ができると考えました。しかし、書籍リストでは著者名がカタカナ表記である一方API側では英語表記で登録されているケースがあり、うまくヒットしないことがありました。また、出版社名がAPI側に登録されていない場合もあって、検索結果が少なくなってしまう問題が発生しました。
さらに、関連度の高い順にレスポンスが返ってくると公式ドキュメントに書いてあったため、レスポンスの最大取得件数を指定して最上位の1件だけを取得する方法も試しました。しかし実際には期待どおりの順序で結果が返ってこないケースがあり、この方法も安定した手段とは言えませんでした。
これらの検討結果を踏まえて、最終的にはタイトルのみを指定してリクエストを送り、その後の絞り込み処理をJava側で行う方式が最も確実であると判断しました。
BookVolumes result = null;
var target = CLIENT.target("https://www.googleapis.com/books/v1/volumes").queryParam("q", encodetitle);
try (Response apiResponse = target.request().get()) {
result = apiResponse.readEntity(BookVolumes.class);
}
return result;
3. ISBNを取得する
APIから複数の書籍情報が返ってくることを想定し、なるべく正確なISBNを取得するために、書籍リストのタイトルとAPIのタイトルの一致度をレーベンシュタイン距離で比較して絞り込む方針を採用しました。
レーベンシュタイン距離で絞り込みをする方法には、主に次の2つの問題がありました。
1. サブタイトルの有無による一致度の低下
書籍リストにはサブタイトルが含まれている一方、APIのレスポンスにはサブタイトルが含まれないケースがありました。その結果、実際には同じ本であっても一致度が大きく下がってしまう。
2. 正規化による本来区別すべきタイトルの誤一致
一致度計算前に記号除去や小文字化といった正規化を行っていたため、「C#」と「C」のように区別すべきタイトルが同一と判定され、誤ったISBNを取得してしまう。
//正規化
String normInput = conversionStr(inputTitle);
String normApi = conversionStr(apiTitle);
LevenshteinDistance distance = new LevenshteinDistance();
int dist = distance.apply(normInput, normApi);
int maxLength = Math.max(normInput.length(), normApi.length());
if (maxLength == 0) {
return 0.0;
}
return 1.0 - ((double) dist / maxLength);
これらの問題を踏まえ、最終的には全候補タイトルとの一致度を計算し、その中から最も一致度が高いISBNを採用する方式にしました。この方法では、たとえ一致度が低くても候補の中で一番一致度が高ければ出力されてしまうため、出力リストを目視で確認する必要があります。手間はかかりますが、より多くのISBNを取得できるうえ、「C#」と「C」といった微妙な差異による誤判定にも気づきやすいという利点があります。
なお、一致度での絞り込みはあくまで “リクエストに一番近い本のISBNを取得する仕組み” であり、必ずしも正しいタイトルからISBNを取得できているとは限りません。そのため、最終的な目視確認は誤取得を防ぐうえでも重要な工程となります。
for (var item : result.getItems()) {
var volumeInfo = item.getVolumeInfo();
if (volumeInfo == null) {
continue;
}
apiTitle = volumeInfo.getTitle();
double similarity = calculateSimilarity(title, apiTitle);
// 最良一致を更新
if (similarity > bestSimilarity) {
bestSimilarity = similarity;
bestVolumeInfos.clear();
bestVolumeInfos.add(volumeInfo);
} else if (similarity == bestSimilarity) {
bestVolumeInfos.add(volumeInfo);
}
for (var bestVolumeInfo : bestVolumeInfos) {
finalTitle = bestVolumeInfo.getTitle();
}
}
// ISBNを取得
for (var volumeInfo : bestVolumeInfos) {
finalTitle = volumeInfo.getTitle();
var industryIdentifiers = volumeInfo.getIndustryIdentifiers();
if (checkIsbn(industryIdentifiers)) {
for (var identifier : industryIdentifiers) {
if (identifier.getIdentifier() != null && !identifier.getIdentifier().isEmpty()) {
isbn.setIsbn(identifier.getIdentifier());
}
}
return new TemporaryDataRecord(title, finalTitle, isbn, bestSimilarity);
}
}
return new TemporaryDataRecord(title, finalTitle, isbn, bestSimilarity);
4. 取得リストに書き込む
取得したISBNなどの情報を、取得リストに出力しています。取得リストでは、先頭行にヘッダー情報を記載するため、データの書き込みは2行目以降から行っています。
また、リクエスト時のタイトルと、取得したISBNのタイトルを横に並べて配置することで、両者を比較しやすいように工夫しました。
try (FileInputStream fis = new FileInputStream(path); Workbook workbook = new XSSFWorkbook(fis)) {
Sheet sheet = workbook.getSheetAt(1); // 書き込みたいシート番号を入れる
int count = 1;
// データ行(1行目から書く。0行目はヘッダー)
for (TemporaryDataRecord data : dataList) {
Row row = sheet.getRow(count);
if (row == null) {
row = sheet.createRow(count);
}
String excelTitle = data.excelTitle();
String apiTitle = data.apiTitle();
String isbn13 = "";
String isbn10 = "";
if (data.isbn().getIsbn13() != null) {
isbn13 = data.isbn().getIsbn13();
}
if (data.isbn().getIsbn10() != null) {
isbn10 = data.isbn().getIsbn10();
}
double percent = data.agreementPercent();
row.createCell(EXCELTITLECOL).setCellValue(excelTitle);
row.createCell(APITITLECOL).setCellValue(apiTitle);
row.createCell(ISBN13COL).setCellValue(isbn13);
row.createCell(ISBN10COL).setCellValue(isbn10);
row.createCell(PERCENTCOL).setCellValue(percent);
count++;
}
try (FileOutputStream fos = new FileOutputStream(path)) {
workbook.write(fos);
}
}
最後に
今回はGoogle Books APIを使用してISBNを取得するだけでなく、精度をできるだけ高められるような工夫を施したのが少し大変でした。様々な技術に触れることで、自分の成長につながりました。これからも技術を磨いていきます。ありがとうございました。