7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【個人開発】何が何でも魚拓を見つけるChrome拡張を作った

Posted at

デモ

↓2019/03/02
キャプチャ4.PNG
↓1998/11/11
キャプチャ5.PNG

特徴

  • archive.is, archive.org, gyo.tc, googleのキャッシュ、と計四か所から魚拓のURLを探します!
  • vue.js製(やってみたかった)

なぜ作ったか

ハテブで魚拓コメがトップだったりして、需要を感じた。
後普通に自分が使いたかった。
そして、Qiitaユーザーの皆さんも必要性を感じてくださっている事と思います^^。

どう作ったか

Chorome拡張はHTML+CSS+javascriptの基本セットで作れます。
ファイル構成は以下のようになりました。

  • manifest.json(どの拡張でも必須)
  • popup.js
  • popup.html
  • popup.css
  • jquery
  • vue.js
  • icons(フォルダ。配下にicon16.png, icon48.png, icon128.png)

詳しく見ていきましょう。
manifest.jsonは、Chrome拡張のまとめ役みたいな感じ。

manifest.json
{
    "name": "FindGyotaku in Console",
    "version": "0.0.1",
    "manifest_version": 2,//2で固定する
    "permissions": ["tabs", "http://*/", "http://*/"],//tabsはアクティブなタブのURLを取得するため、http://*/,https://*/はChrome拡張からすべてのサイトへのアクセスを許可するために必要
    "description": "Show all Gyotaku",
    "icons": {
        "16": "icons/icon16.png",
        "48": "icons/icon48.png",
        "128": "icons/icon16.png"
    },
    "browser_action": {
        "default_popup": "popup.html"//ここでpopupのhtmlを指定
    }
}

そして、popup.jsが今回の大事ポイントです。

popup.js
let vue;
//https://gyo.tc/から
function findGyotakuByUrl(url) {
    return new Promise(resolve => {
        console.log("findGyotakuByUrl Started");
        $.get("https://gyo.tc/" + url, null,
            function (data) {
                console.log(data);
                let gyotakuList_ = new DOMParser()
                    .parseFromString(data, "text/html")
                    .querySelectorAll("[id^=fish]");
                resolve(gyotakuList_)
            },
            "html"
        );
    })
}
//https://archive.is/から
function findInternetArchiveByUrl(url) {
    return new Promise(resolve => {
        $.get(`https://archive.is/${url}`, function (data) {
            let rowsList =
                new DOMParser()
                    .parseFromString(data, "text/html")
                    .querySelectorAll("[id^=row]");
            let archivesList = [];
            rowsList.forEach(row => {
                archivesList.push(
                    Array.from(
                        row.querySelectorAll("[style^=text\\-decoration\\:none]")
                    )
                )
            });
            resolve(archivesList.flat());
            console.log(`flatten archivesList is ${archivesList.flat()}`);
        })
    })
}
//googleのwebcacheから
function findGoogleCache(url) {
    return new Promise(resolve => {
        $.get(`http://webcache.googleusercontent.com/search?q=cache:${url}`, function () {
            resolve(`http://webcache.googleusercontent.com/search?q=cache:${url}`);
        })
    })
}
//http://web.archive.orgから
function findWaybackMachineLatest(url) {
    return new Promise(resolve => {
        //limit=-1で最新
        $.get(`http://web.archive.org/cdx/search/cdx?limit=-1&fl=timestamp&url=${url}`, function (timestamp) {
            resolve(`https://web.archive.org/web/${timestamp}/${url}`);
            console.log(`waybackMachine response(latest) is ${timestamp}`);
        })
    })
}
//http://web.archive.orgから
function findWaybackMachineOldest(url) {
    return new Promise(resolve => {
        //limit=1で最古
        $.get(`http://web.archive.org/cdx/search/cdx?limit=1&fl=timestamp&url=${url}`, function (timestamp) {
            resolve(`https://web.archive.org/web/${timestamp}/${url}`);
            console.log(`waybackMachine response(latest) is ${timestamp}`);
        })
    })
}

console.log("popup.jsLoaded");

chrome.tabs.query({
    active: true,
    currentWindow: true
}, function (tabsArray) {
    console.log(`currentUrl is ${tabsArray[0].url}`);

    findGyotakuByUrl(tabsArray[0].url).then((gyotakuList) => {
        console.log(gyotakuList);
        gyotakuList.forEach(element => {
            vue.items.push(element)
        });
        vue.isLoadingGyotaku = false
    }).catch((err) => {
        console.log(err);
        vue.isLoadingGyotaku = false
    });
    findInternetArchiveByUrl(tabsArray[0].url).then((archivesList) => {
        console.log(archivesList);
        archivesList.forEach(element => {
            vue.items.push(element)
        });
        vue.isLoadingArchives = false
    }).catch((err) => {
        console.log(err);
        vue.isLoadingArchives = false
    });
    findGoogleCache(tabsArray[0].url).then((googleCacheUrl) => {
        console.log({
            href: googleCacheUrl
        });
        vue.items.push({
            href: googleCacheUrl
        });
        vue.isLoadingGoogleCache = false
    }).catch((err) => {
        console.log(err)
    });
    findWaybackMachineLatest(tabsArray[0].url).then((waybackMachineUrl) => {
        console.log({
            href: waybackMachineUrl
        });
        vue.items.push({
            href: waybackMachineUrl
        });
        vue.isLoadingWaybackMachine = false
    }).catch((err) => {
        console.log(err)
    });
    findWaybackMachineOldest(tabsArray[0].url).then((waybackMachineUrl) => {
        console.log({
            href: waybackMachineUrl
        });
        vue.items.push({
            href: waybackMachineUrl
        });
        vue.isLoadingWaybackMachine = false

    }).catch((err) => {
        console.log(err)
    });

});

$(function () {
    vue = new Vue({
        el: '#example-1',
        data: {
            items: [],
            isLoadingGyotaku: true,
            isLoadingArchives: true,
            isLoadingGoogleCache: true,
            isLoadingWaybackMachine: true,
        }
    })

});

分割して解説します。

function findGyotakuByUrl(url) {
    return new Promise(resolve => {
        $.get("https://gyo.tc/" + url, null,
            function (
                let gyotakuList_ = new DOMParser()
                    .parseFromString(data, "text/html")
                    .querySelectorAll("[id^=fish]");
                resolve(gyotakuList_)
            },
            "html"
        );
    })
}

↑要するにスクレイピングですね。javascriptではquerySelectorが便利です。
https://gyo.tc/ 以下にURLを投げると魚拓一覧が返るので、開発者ツールから頑張って解析しました。

function findInternetArchiveByUrl(url) {
    return new Promise(resolve => {
        $.get(`https://archive.is/${url}`, function (data) {
            let rowsList =
                new DOMParser()
                    .parseFromString(data, "text/html")
                    .querySelectorAll("[id^=row]");
            let archivesList = [];
            rowsList.forEach(row => {
                archivesList.push(
                    Array.from(
                        row.querySelectorAll("[style^=text\\-decoration\\:none]")
                    )
                )
            });
            resolve(archivesList.flat());
        })
    })
}

↑こちらもhttps://archive.is/ 以下にURLを投げると魚拓一覧が返るので、こちらも開発者ツールから頑張って解析しました。
Array.flat()はMDNで分かるように実験的なヤツで、配列を好きな次元に均せる関数です。デフォルトで一次元なのでそのまま使用しました。

function findGoogleCache(url) {
    return new Promise(resolve => {
        $.get(`http://webcache.googleusercontent.com/search?q=cache:${url}`, function () {
            resolve(`http://webcache.googleusercontent.com/search?q=cache:${url}`);
        })
    })
}

↑言うことなし。思ったより簡単。

function findWaybackMachineLatest(url) {
    return new Promise(resolve => {
        //limit=-1で最新
        $.get(`http://web.archive.org/cdx/search/cdx?limit=-1&fl=timestamp&url=${url}`, function (timestamp) {
            resolve(`https://web.archive.org/web/${timestamp}/${url}`);
        })
    })
}

function findWaybackMachineOldest(url) {
    return new Promise(resolve => {
        //limit=1で最古
        $.get(`http://web.archive.org/cdx/search/cdx?limit=1&fl=timestamp&url=${url}`, function (timestamp) {
            resolve(`https://web.archive.org/web/${timestamp}/${url}`);
            console.log(`waybackMachine response(latest) is ${timestamp}`);
        })
    })
}

↑apiが有って助かりました。
WaybackMachineのアーカイブは膨大なので最古&最新のみ取得しています。
limit=1で最古、limit=-1で最新を指定、fl=timestampでレスポンスをtimestampのみに限定します。
WaybackMachineはtimestampとURLを指定すると(珍しく)個別ページのURLが分かるので、timestampだけで十分なのです。

$(function () {
    vue = new Vue({
        el: '#example-1',
        data: {
            items: [],//以下デフォルト値を指定しています
            isLoadingGyotaku: true,
            isLoadingArchives: true,
            isLoadingGoogleCache: true,
            isLoadingWaybackMachine: true,
        }
    })

});
index.html
<p v-if="isLoadingGyotaku">
    魚拓から読み込んでいます
</p>

↑出ましたvue.js!読み込み中のメッセージを出しています。
vue.jsを使うと、HTMLとjs間でバインディングができます。
例えば、

HTMLのv-if属性にisLoadingGyotakuを指定

js側でvueインスタンス作成(vueのdata属性に、isLoadinGyotakuをメンバにしたオブジェクトを付けたもの)

vue.isLoadingGyotakuにアクセスできるように&
vue.isLoadingGyotakuがv-if="isLoadingGyotaku"とバインドされる

って感じ。
つまり、isLoadingGyotaku=trueならpタグが見えて、falseなら見えなくなる!
これは素晴らしいですね。本当に素晴らしい。
ほかにもいろいろな属性が有ります。だいたいv-が付く。

<ul>
    <li v-for="item in items">
        <a href="{{ item.href }}" target='_newtab'>
            {{ item.href }}
        </a>
    </li>
</ul>

↑v-forとか。見つかったURLを表示するために使っています。
{{}}はテンプレートリテラルみたいに使えます。
↓ラスト

chrome.tabs.query({
    active: true,
    currentWindow: true
}, function (tabsArray) {
    console.log(`currentUrl is ${tabsArray[0].url}`);

    findGyotakuByUrl(tabsArray[0].url).then((gyotakuList) => {
        console.log(gyotakuList);
        gyotakuList.forEach(element => {
            vue.items.push(element)

        });
        vue.isLoadingGyotaku = false
    }).catch((err) => {
        console.log(err);
        vue.isLoadingGyotaku = false
    });
    findInternetArchiveByUrl(tabsArray[0].url).then((archivesList) => {
        console.log(archivesList);
        archivesList.forEach(element => {
            vue.items.push(element)
        });
        vue.isLoadingArchives = false
    }).catch((err) => {
        console.log(err);
        vue.isLoadingArchives = false
    });
    findGoogleCache(tabsArray[0].url).then((googleCacheUrl) => {
        console.log({
            href: googleCacheUrl
        });
        vue.items.push({
            href: googleCacheUrl
        });
        vue.isLoadingGoogleCache = false
    }).catch((err) => {
        console.log(err);
        vue.isLoadingGoogleCache = false
    });
    findWaybackMachineLatest(tabsArray[0].url).then((waybackMachineUrl) => {
        console.log({
            href: waybackMachineUrl
        });
        vue.items.push({
            href: waybackMachineUrl
        });
        vue.isLoadingWaybackMachine = false
    }).catch((err) => {
        console.log(err)
        vue.isLoadingWaybackMachine = false
    });
    findWaybackMachineOldest(tabsArray[0].url).then((waybackMachineUrl) => {
        console.log({
            href: waybackMachineUrl
        });
        vue.items.push({
            href: waybackMachineUrl
        });
        vue.isLoadingWaybackMachine = false

    }).catch((err) => {
        console.log(err)
        vue.isLoadingWaybackMachine = false
    });

});

↑async/awaitで見やすくなるのは分かってるんだけどね……面倒なのでそのまま。
chrome.tabs.queryで今見てるタブのURLを取得後、非同期×5を回しています。
chrome.系はChrome拡張の時しか使えないapiで、ほかにも色々便利です。

はまったポイント

最初

let fuga = true
vue = new Vue({
        el: '#example-1',
        data: {
            isLoadingGyotaku: fuga,
        }
    })
fuga = false//バインドされてるはずなのに動かない!!!

って事が有って大変でしたね。本当は

vue = new Vue({
        el: '#example-1',
        data: {
            isLoadingGyotaku: true,
        }
    })
vue.isLoadingGyotaku = false//バインドされてて動く!!!

で良かったっていう。公式ドキュメントをちゃんと読みましょう。

後、動作確認にevent pagesを使ってしまって、popupのみで動くapiを動かせなかったりとか……
popupページで検証する方法を知らなかったとか……(左クリックでポップアップされたのを右クリ)

計何時間使ったでしょうかね。今はどうでも良いけど。

最後に

2020/04に大学生になる(浪人しなければ!)ので、良かったらインターンに誘ってください。
github:https://github.com/negitorogithub
gmail:bakeratta35@gmail.com

お読み頂きありがとうございました。 ~~旦⊂(・∀・ )

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?