阿部寛さんのHPをスクレイピングしてリンク切れを検知する

この記事は[阿部寛 Advent Calendar](https://qiita.com/advent-calendar/2017/abehiroshi)19日目の記事です。
※Webの高速化[^1]は専門外なのでできません。ご了承ください。

## 阿部寛さんのHPを見て思ったこと
ご存知の通り[^2]、[阿部寛さんのホームページ](http://abehiroshi.la.coocan.jp/)は、高速に画面遷移するホームページです。サクサク動くのでストレスを感じません。ユーザ体験として素晴らしいです。

ですが、阿部寛さんのホームページで１つだけ残念だと思ったことがありました。それは**リンク切れ**です。
阿部寛さんのホームページでは、サイトのいたるところに過去の出演作品ページへの外部リンクが貼られています。しかし、番組や映画が終わって一定期間が経つと、リンク先のページが閉鎖になってしまいます。そのため、自然とリンク切れが増えていってしまいます。
「このドラマ懐かしいなー」と思ってクリックしたリンク先が「404 Not Found」とか表示されると、ちょっと残念ですよね。これはユーザ体験としてはよろしくないわけです。

## スクレイピングしてリンク切れをチェックする
今回は阿部寛さんのホームページをスクレイピングすることで、リンク切れになっているリンクを探してみることにしました。
Pythonでのスクレイピングでは`requests`ライブラリが便利なので、こちらを使ってスクレイピングしていきます。
## 手順
### 1. 仮想環境の作成(任意)
`requests`ライブラリをインストールするため、環境を汚したくない方はPythonの仮想環境を作成した方が良いと思います。仮想環境の作成方法はここでは割愛します。

### 2. ライブラリのインストール
#### requestsライブラリ
今回使用する`requests`ライブラリは標準では使用できませんので`pip`でインストールする必要があります。

```bash:
$ pip install requests
```

#### lxmlライブラリ
htmlのパースに使用する`lxml`ライブラリも`pip`でインストールします。

```bash:
$ pip install lxml
```

#### libxml2,libxsltのインストール(Macの場合)
`lxml`ライブラリの内部で使用しているモジュールをインストールします。Macの場合はhomebrewでインストールできました。

```bash:
$ brew install libxml2 libxslt
```

### 3. スクレイピングするコードの作成

#### 3-0. 完成品
完成したソースは[こちら](https://gist.github.com/acnaman/85716ca7e0b4ca75a85a91625c181e75)になります。

こちらを実行すると以下のような出力が得られます。

```bash:出力結果
$ python checklink.py
(前略)
リンク切れしているURLがあります
http://www.tbs.co.jp/e-housoku/
http://www.tbs.co.jp/mayonaka/
http://www.tbs.co.jp/mlc/
http://www.bunkamura.co.jp/cocoon/lineup/shosai_09_coast.html
http://www.bunkamura.co.jp/cocoon/lineup/shosai_08_dogen.html
http://www.tsuka.co.jp/kitaku/butai/monte2002/poster.htm
http://www.rlsm.bias.ne.jp/%7Esong/shashin/shashin-kan.htm
http://www.tsuka.co.jp/kitaku/butai/monte2002/poster3.html
http://www.tsuka.co.jp/kitaku/butai/monte2002/arasuji.html
http://www.s-woman.net/bebunko/016/index.html
```

ライブラリの使い方を中心に説明します。

#### 3-1. ページの取得
`requests.get(url)`関数を使用することでページ情報を取得できます。
今回はホームページ内をクローリングする関係で、Sessionオブジェクトのgetメソッドを使用しています[^3]が、取得できる内容は同じです。

```py:
session = requests.Session()
response = session.get(start_url)
```

#### 3-2. 取得情報のパース
`get(url)`で取得した内容をパースします。htmlの情報は`response.content`に記録されていますので、これをhtmlとして解釈し、パースします。

```py:
root = lxml.html.fromstring(response.content)
```

#### 3-3. 相対URLの絶対URL化
`requests.get(url)`(または`session.get(url)`)の引数にはスキームから始まる絶対URLを指定する必要があります。
今回はページ内のリンクをクローリングするので、ホームページ内遷移を記述した相対URLに対してもページの取得を行います。そのために相対URLを絶対URLに変換します。
絶対URLへの変換はパースして得られたオブジェクトの`make_links_absolute()`メソッドを使うと簡単にできます。

```py:
root.make_links_absolute(response.url)
```

#### 3-4. 要素・属性を取得する
パースして得られたオブジェクトでは、CSS使われるセレクタでHTML要素を取得できます。また、HTML要素のオブジェクトでは`.属性名`で属性にアクセスすることができます。このあたりの書き方はjQueryやJavaScriptの書き方に似ており理解しやすいです。
今回はページ内のリンクを全て取得したいため、セレクタ`a[href]`に該当するオブジェクトの`href`属性を取得する処理を書きました。

```py:
# href属性を持つ<a>要素の取得
for atag in root.cssselect('a[href]'):
    # href属性の取得
    url = atag.get('href')
```

#### 3-5. ステータスコードのチェック
`requests.get(url)`で得られたオブジェクト内の`status_code`をチェックすることで、HTTPステータスコードを確認できます。
今回はとりあえず400以上を対象にしました。

```py:
if res.status_code >= 400:
    # リンク切れだった場合の処理を記述
    yield url
```

#### 3-6. レスポンススピードのチェック(おまけ)
阿部寛さんのHPが爆速なのに、外部リンクのレスポンスが遅いとストレスを感じかねないので、レスポンスが遅いページも一応チェックしておきます。[^4]
レスポンスの速度はレスポンスオブジェクトの`elapsed.total_seconds()`で確認できます。

```py:
elif res.elapsed.total_seconds() > 1:
    # レスポンスが遅い場合の処理を記述
    print("このリンク先は遅い：" + url)
```

## 感想
`requests`ライブラリが便利で助かりました。スクレイピングでは非常に使いやすいライブラリだと思います。
外部リンクのリンク切れはレガシーなHPに限らず発生しうる問題ですが、リンクをいちいちクリックしてチェックするのは効率が悪いです。コード化する、専用のツール[^5]を使うなどして管理の手間を省くのが大事だと思いました。

## 参考
- [阿部寛のホームページ](http://abehiroshi.la.coocan.jp/)
- (書籍)[Pythonクローリング＆スクレイピング ―データ収集・解析のための実践開発ガイド―](http://gihyo.jp/book/2017/978-4-7741-8367-1)


[^1]: 阿部寛 Advent Calendarの説明に「阿部寛さんのHPをはじめとするレガシーなHPを今の技術でどのように高速化するかに関するアドベントカレンダーです。」とあります。
[^2]: ご存知ない方は[こちら](https://twitter.com/shoudai_2736/status/930670180509810688)を参照。
[^3]: Sessionオブジェクトを使用すると一度確立したTCPコネクションを使い回すのでサーバ負荷を軽減することができるそうです。
[^4]: 出力結果で「(前略)」と書いた箇所にページ取得の時間が1秒を超えたページを表示するようになっています。
[^5]: リンク切れのチェックだけであれば、もっと簡単にチェックできるツールが多分あります。Googleのサーチコンソールとか。