書籍管理を楽にする（Chrome拡張）

## 動機

　以前、Google Apps Scriptを使って[書籍を管理する仕組みを作りました](https://qiita.com/dexia/items/6630f49636da0b945d37)。

　一応まだスプレッドシートで書籍を管理しているのですが、書籍の情報を追加するのが面倒に感じるようになりました。

　大体Amazonの商品ページから情報をとってきているのですが、情報が書かれている位置が離れていて手間でした。

　そこで何か楽になる手段を探していて、Chrome拡張がいいと思ったので試してみました。

## Chrome拡張作成

### Amazonの検索ページへのジャンプ

#### 仕様

　新規に書籍を購読する場合は大体ブログだとか、新刊一覧のサイトから入ることが多いです。

　なので、そういう文章情報からAmazonの商品ページを開けるとまず楽かなと思いました。

　テキストを選択して右クリックすると「Googleで検索」というようなメニューが出てくるので、まずはそれに近いものを目指しました。

#### 事前準備

　128×128のアイコン（png形式）があると捗るということで、適当にアイコンを用意します。個人用であればなくても、そこまで困らないと思います。

　自分はフリーの虫眼鏡のアイコンを用意しました。

#### フォルダを準備

　開発用のフォルダを準備しました。大体以下のようになっています。

```
├── assets
|   └── js
|       └── background.js
├── image
|   └── search.png
└── manifest.json 
```

#### マニフェストを準備する

　manifest.jsonは作る拡張がどういったものかを表すJSONです。

```json:manifest.json
{
    "manifest_version": 2,
    "version": "1.0.0",
    "name": "ToAmazon",
    "description": "Amazonの検索Windowを開きます",
    "icons": {
        "128": "/image/search.png"
    },
    "background": {
        "scripts": [
            "/assets/js/background.js"
        ]
    },
    "permissions": [
        "contextMenus",
        "tabs"
    ]
}
```

　大体見た通りですが、manifest_versionは必ず2にするというルールになっており、ない場合は怒られます。

　backgroundはDOMに関係なく実行されるスクリプトを指定するもので、今回の処理はここに書けば良さそうです。

　permissionsは使いたいChromeのAPI（chrome.xxx）の名前を書きます。これもAPIを使う場合は必須です。

#### 処理を書く

```javascript:background.js
chrome.contextMenus.create({
    title: "Amazonで検索",
    contexts: ["selection"],
    type: "normal",
    onclick: function (info) {
        var keyword = encodeURI(info.selectionText);
        chrome.tabs.create({
            url: "https://www.amazon.co.jp/s/?field-keywords=" + keyword
        });
    }
});　
```

　とてもシンプルですね。contextsはコンテキストメニューがいつ表示されるかを表します。
　今回は文字を選択したときだけでいいので、selectionを指定します。

　typeはメニューをラジオボタンにしたり、チェックボックスにしたりできるみたいですが、今回は特にいらないのでnormalです。

　あとはクリック時にAmazonの検索URLを指定して、タブを開くように書きます。

#### テスト

##### アップロード

　自作の拡張をアップする手段については下記が詳しいです。

　[Chrome拡張を簡単に作れるテンプレとライブラリ造ったので紹介](https://qiita.com/waterada/items/7f5729e066714284ff72)

##### コンテキストメニューをクリック

![toAmazon.png](https://qiita-image-store.s3.amazonaws.com/0/229574/fe35a725-79ac-f6e0-4e00-fdebdf8268bd.png)

　欲しい書籍を選択して、「Amazonで検索」をクリックします。

##### 実行結果

![to-amazon2.png](https://qiita-image-store.s3.amazonaws.com/0/229574/f3583419-02c7-9f02-ab35-b4063da2e74f.png)

　検索ページが表示されました。

### 書籍情報の取得

#### 仕様

　Amazonの商品ページから以下の情報を取得できればいいなと思いました。

+ タイトル
+ 作者
+ 出版日

　書籍管理のスプレッドシートに直接書き込めたら素敵かなと思ったのですが、思いのほかタイトルが自分の欲しい形と違うことが多かったので、そこまでは求めないことにしました。

　スプレッドシートに貼り付けできる形に情報を整形してクリップボードにコピーし、あとは手動で調整というところで納めました。

#### jQueryのインストール

　スクレイピングするためのライブラリとしてjQueryを使いました。他にあるのかもしれませんが、自分にとっては慣れているのでとりあえずこれで行きます。

　ファイルを物理的にコピーしてもいいのですが、一応NPM経由でインストールしました。

```
npm init
npm install --save jquery 
```

#### フォルダ構成

　結果的に次のようなフォルダ構成になりました

```
├── assets
|   └── js
|       └── background.js
|       └── content.js
├── image
|   └── search.png
├── node_modules
|   └── jquery
|       └── dist/jquery.min.js
└── manifest.json 
└── package.json
```

#### マニフェストの変更

```json:manifest.json
{
    "manifest_version": 2,
    "version": "1.0.0",
    "name": "ToAmazon",
    "description": "Amazonの操作を簡単にします。",
    "icons": {
        "128": "/image/search.png"
    },
    "background": {
        "scripts": [
            "/assets/js/background.js"
        ]
    },
    "content_scripts": [
        {
            "matches": [
                "https://www.amazon.co.jp/*"
            ],
            "js": [
                "/node_modules/jquery/dist/jquery.min.js",
                "/assets/js/content.js"
            ]
        }
    ],
    "permissions": [
        "clipboardWrite",
        "contextMenus",
        "tabs"
    ]
}
```

　DOMを操作し、かつ裏で動くようなスクリプトを書く場合はcontent_scriptsがよさそうだったので、それを追加しました。

　スクリプトを起動させるURLのパターンはmatchesで指定します。商品ページのURLが意外に複雑だったので、とりあえずAmazon全体を対象にしました。

　あとはclipboardWriteをpermissionsに追加します。

#### スクレイピング

##### 書籍情報の抜き取り

```javascript:content.js
function scrapeBookInfo() {
    var title = $("#productTitle").text();
    var authors =
        $("#bylineInfo > .author a.a-link-normal")
            .map(function () {
                return this.textContent;
            }).get().join(",");

    var published =
        $("#booksTitle .a-size-medium").map(function () {
            return this.textContent && this.textContent.match(/\d{4}\/\d{1,2}\/\d{1,2}/);
        })
            .get()
            .find(Boolean)
            .replace(/\//g, "-");
    return { title, authors, published };
}
```

　セレクタの指定は自信がないです。特に著者名はパターンが多いので、ある程度間違った名称をひっかけることが多いです。

　上から、タイトル、著者名、出版日を取得します。

##### クリップボードへのコピー

```javascript:content.js
// クリップボードへのテキストコピー
function copyToClipboard(text) {
    var input = document.createElement("input");
    input.style.position = "fixed";
    input.style.opacity = 0;
    input.value = text;
    document.body.appendChild(input);
    input.select();
    document.execCommand("Copy");
    document.body.removeChild(input);
};
```

　たぶん一般的なやり方だと思います。

##### 書籍情報のコピー

```javascript:content.js
// デフォルト値
var DEFAULT_LATEST = 1;
var DEFAULT_PERIOD = 5;
var DEFAULT_SUBSCRIBE = 1;
function copyBookInfo() {
    var bookInfo = scrapeBookInfo();
    var copiedText = [
        bookInfo.title,
        DEFAULT_LATEST,
        DEFAULT_PERIOD,
        DEFAULT_SUBSCRIBE,
        bookInfo.published,
        bookInfo.authors
    ].join("\t");
    copyToClipboard(copiedText);
}
```

　欲しい情報のために、整形しています。タブで連結させておけば、そのまま貼り付けられる形式になります。

##### アクションの追加

```javascript:content.js
// ロード時
$(function () {

    // ボタンの作成
    var copyButton = $("<button/>",
        {
            text: "Copy",
            click: function () {
                copyBookInfo();
            }
        });

    // ボタンの設置
    $("#productTitle").before(copyButton);

});
```

　タイトルの前にボタンを設置し、それをクリックすることでコピーするようにしました。デザインは凝ったほうがいいのかもしれないですね。

### テスト

![to-amazon3.png](https://qiita-image-store.s3.amazonaws.com/0/229574/8eb6f693-d3fe-837b-d642-a4f6dfc97032.png)

 こんな感じにでれば成功です。クリックすると、クリップボードに情報がコピーされています。

## 所感

　ほかのいつも見ているサイトをいじってみたり、あるいは開発のデバッグの際に何かできるんじゃないかという気がします。

　可能性がないかもう少し考えてみたいです。