BioErrorLog Tech Blog

試行錯誤の記録

はじめてのWebスクレイピング | ブログ投稿曜日を分析する

はじめてのWebスクレイピングに挑戦しました。今回は手始めに、自分のブログの曜日ごとの投稿数を分析します。


はじめに

こんにちは、@bioerrorlogです。

インターネットの登場によって、人間と情報の関係はすっかり変化しました。 私が今1秒もかからずにアクセスできる情報は、つい数十年前の資産家が何億円を積み上げても手に入れることのできない情報量でしょう。 これが無料のサービスとして誰でも利用できるのですから、えらいことです1

インターネットがなかった時代、手の届く情報が少なかった時代には、いかに多くの情報を知っているか、が大切でした。 しかし、膨大な情報がそこら中で踊る今では、いかに情報を収集し、抽出し、利用するかが武器となります。


というわけで、まったくの初心者ながら、Webデータ収集に手を出していきます。 どうやらこの分野にはいろいろな呼ばれ方の手法があるようです。 Webスクレイピング、クローリング、データマイニング、テキストマイニング、機械学習...とにかく、どれも有用な情報を引き出そうとする態度は同じでしょう。

今回は第一歩として、Wgetコマンドを用いて自分のこのホームページの情報を取得し、簡単にスクレイピングしてみます。


作業環境

Ubuntu18.04.1 LTS を
Windows10の上に、VMwareによって構築した仮想環境で起動しています。
www.bioerrorlog.work


ブログ投稿曜日を分析する

Wgetコマンドとは

まずは、Wgetコマンドの利用方法をマニュアルから把握します。

$ man wget
~
NAME
       Wget - The non-interactive network downloader.

SYNOPSIS
       wget [option]... [URL]...

DESCRIPTION
       GNU Wget is a free utility for non-interactive download of files from the Web.  It supports HTTP, HTTPS, and FTP protocols, as well as retrieval through HTTP proxies.
~

Wgetは、HTTP, HTTPS, FTPプロトコルなどをサポートするWebダウンローダーのようです。

使い方は単純で、wget [オプション]... [URL]...とします。

それでは、自分のこのホームページ情報を取得するのに必要そうなオプションを揃えていきます。

-r オプション | 再帰的にリンクをダウンロードする

このホームページのURLhttps://www.bioerrorlog.work/を網羅的にダウンロードするには、再帰的にリンクをたどる必要があります。 そこで、再帰検索オプション-rを用います。
マニュアルより、

-r
--recursive
    Turn on recursive retrieving.    The default maximum depth is 5.

デフォルトでは深さ5で、再帰的にリンクを辿ってダウンロードできるようです。

-w オプション | ダウンロード間隔をあける

あまり頻繁にダウンロードを要求するとサーバに負荷がかかってしまうため、ダウンロードに間隔を設けることは重要です。 -wは、ダウンロード間隔を指定することができます。
マニュアルより、

-w seconds
--wait=seconds
    Wait the specified number of seconds between the retrievals.  Use of this option is recommended, as it lightens the server load by making the requests less frequent.  Instead of in
    seconds, the time can be specified in minutes using the "m" suffix, in hours using "h" suffix, or in days using "d" suffix.

デフォルトでは秒でダウンロード間隔を指定でき、mをつけると分間隔で、hをつけると時間間隔で、dをつけると日間隔で指定できるようです。

-l オプション | リンクを辿る深さを指定する

果てしなくリンクを辿り続けることのないよう、-lで深さを指定します。

-l depth
--level=depth
    Specify recursion maximum depth level depth.

-np オプション | 親ディレクトリを辿らない

指定したURLの下位ディレクトリのみを取得するには、-npで親ディレクトリに登ることを禁じる必要があります。

-np
--no-parent
    Do not ever ascend to the parent directory when retrieving recursively.  This is a useful option, since it guarantees that only the files below a certain hierarchy will be downloaded.


必要なオプションはとりあえずこんなところでしょう。

自分のホームページ情報を取得する

それではさっそくこれらオプションを用いて、深さ1、ダウンロード間隔1秒で、このブログのトップページhttps://www.bioerrorlog.work/をWgetでクローリングしてみます。

$ wget -r -w 1 -l 1 -np https://www.bioerrorlog.work/

粛々とクローリングが始まりました。 ダウンロードが終了すると、カレントディレクトリに次のようなディレクトリが作成されました。 ディレクトリを木構造で示すtreeコマンドで確認します。

$ tree
.
└── www.bioerrorlog.work
    ├── about
    ├── archive
    │   ├── 2019
    │   │   ├── 01
    │   │   │   ├── 13
    │   │   │   ├── 15
    │   │   │   ├── 18
    │   │   │   ├── 20
    │   │   │   ├── 22
    │   │   │   ├── 24
    │   │   │   ├── 26
    │   │   │   ├── 28
    │   │   │   └── 31
    │   │   └── 02
    │   │       ├── 02
    │   │       ├── 06
    │   │       ├── 11
    │   │       ├── 16
    │   │       └── 24
    │   └── category
    │       ├── セキュリティ
    │       ├── プログラミング
    │       ├── C
    │       ├── gdb
    │       ├── Linux
    │       ├── Python
    │       ├── radare2
    │       └── その他
    ├── entry
    │   ├── auto-change-resolution
    │   ├── change-resolution
    │   ├── first-c-language-linux
    │   ├── first-python-windows
    │   ├── gdb-default-intel
    │   ├── how-to-man-command
    │   ├── linux-commands
    │   ├── reverse-engineering-ep1
    │   ├── reverse-engineering-ep2-disassemble
    │   ├── reverse-engineering-ep3-access
    │   ├── reverse-engineering-ep4-techniques
    │   ├── reverse-engineering-ep5-radare2
    │   ├── reverse-engineering-ep6-password-crypt
    │   ├── reverse-engineering-ep7-python-keygen
    │   └── what-is-file-permissions
    ├── feed
    ├── index.html
    ├── index.html?page=1547345313
    ├── robots.txt
    └── rss

こいつはなかなか面白いものが見れました。 はてなブログを用いてこのブログを運営しているのですが、改めてサイト構造を見るとなかなか新鮮です。 "category"が”archive"の下に格納されていたことなどは気付きませんでした。
また、下の方にある"index.html"と、"index.html?page=1547345313"も気になります。 何を指しているのでしょうか。 htmlファイルなので、firefoxコマンドで中身を見てみます。

$ firefox index.html index.html?page=1547345313

ブラウザが立ち上がり、html内容が表現されると、まさにどちらもブログトップの内容を表していました。 逆に1547345313という数字が何を指しているのか、よく分かりません。


とりあえず、Webページ情報を取得する方法はおおまかに把握できました。 次は何か情報を抽出してみます。

ブログ投稿曜日を分析する

さて、何の情報を抽出しましょうか。 はじめてなので、簡単そうなものにしたいです。

ブログの投稿日を抽出し、曜日を分析してみるというのはいかがでしょう。 手順としては次のようなものを考えています。

1. ブログ投稿日についてのhtml記述を特定
2. ブログ投稿日についての情報を抽出
3. 曜日を算出・分析

それでは、とにかくやってみます。

投稿日についてのhtml記述を特定する | ブラウザ開発者ツール

まずは、htmlファイルをfirefoxで確認し、どこの部分からブログ投稿日を取得できるかを特定します2

$ firefox index.html

先程のようにfirefoxで開くと、飾り気のないhtmlがブラウザで表示されます。 ページを見ていくと、ブログの投稿日を示している部分が簡単に見つかりました。

しかし、この部分を記述するhtml部分を目視で探し出すのはなかなか困難です。 そこで、この部分を右クリックして"要素を調査 (Inspect Element)"をクリックし、ブラウザの開発者ツールでhtmlの該当部分を特定させます。

<time datetime="2019-02-24" title="2019-02-24">
        <span class="date-year">2019</span><span class="hyphen">-</span><span class="date-month">02</span><span class="hyphen">-</span><span class="date-day">24</span>
</time>

これで、htmlファイルのどの部分にブログ投稿日が記述されているのかが特定できました。

つぎは、正規表現を利用して目的の情報を抽出します。

ブログ投稿日を抽出する | 正規表現

投稿日が記述されている行は

<time datetime="2019-02-24" title="2019-02-24">

となっているので、文字列<time datetime=に着目してgrepコマンドでindex.htmlから検索をかけます3

$ cat index.html | grep '<time datetime='
      <time datetime="2019-02-24" title="2019-02-24">
      <time datetime="2019-02-24" title="2019-02-24">
      <time datetime="2019-02-16" title="2019-02-16">
      <time datetime="2019-02-11" title="2019-02-11">
      <time datetime="2019-02-06" title="2019-02-06">
      <time datetime="2019-02-02" title="2019-02-02">
      <time datetime="2019-01-31" title="2019-01-31">
      <time datetime="2019-01-28" title="2019-01-28">
      <time datetime="2019-01-26" title="2019-01-26">
      <time datetime="2019-01-24" title="2019-01-24">
      <time datetime="2019-01-22" title="2019-01-22">
      <time datetime="2019-01-20" title="2019-01-20">
      <time datetime="2019-01-18" title="2019-01-18">
      <time datetime="2019-01-15" title="2019-01-15">
      <time datetime="2019-01-13" title="2019-01-13">

15行がgrep検索によって抽出されました。 今回情報を取得したトップページには15個のブログが掲載されているので、そのすべての投稿日を取得できた訳です。

しかし、このままのフォーマットでは解析に使えません。 日付部分のみを抽出する必要があります。 そのためにはsedコマンドと正規表現を用いて、必要な部分を切り抜く必要があります。

$ cat index.html | grep '<time datetime=' | sed -E 's/.*([0-9]{4}-[0-9]{2}-[0-9]{2}).*/\1/'
2019-02-24
2019-02-24
2019-02-16
2019-02-11
2019-02-06
2019-02-02
2019-01-31
2019-01-28
2019-01-26
2019-01-24
2019-01-22
2019-01-20
2019-01-18
2019-01-15
2019-01-13

みごと日付部分が抽出できましたが、sedコマンドに渡した正規表現が暗号のようになっているので、順に説明します。
sedコマンドの部分は、

sed -E 's/.*([0-9]{4}-[0-9]{2}-[0-9]{2}).*/\1/'

となっています。

まず-Eは、正規表現を記述するためのオプションです。

次の正規表現部分の大枠は、s/. <正規表現> / <文字列> / で、正規表現にマッチした部分が、指定した文字列で置換されます。 つまり今回は、正規表現.*([0-9]{4}-[0-9]{2}-[0-9]{2}).*にマッチする部分を\1に置換する、という操作になります。

ここで面白いのは、\1は正規表現の()内に記述された部分を参照するということです。 正規表現.*([0-9]{4}-[0-9]{2}-[0-9]{2}).*は、頭尾に任意の文字を任意回繰り返す.*を持つため、行全体を示しています。 よって操作としては、()内に記述された内容を切り出す、ということになるわけです。

()内の記述[0-9]{4}-[0-9]{2}-[0-9]{2}は、日付部分を示しています。 2019-02-24のように、0から9の数字[0-9]が、{4}回あるいは{2}回繰り返される塊が、 - で繋がれている文字列を表現しています。

このようにして、目的の情報が抽出できました。

結果をテキストファイル"datetime.txt"に出力し、Pythonでの曜日算出に移ります。

#txtファイルに出力
$ cat index.html | grep '<time datetime=' | sed -E 's/.*([0-9]{4}-[0-9]{2}-[0-9]{2}).*/\1/' > datetime.txt

日付から曜日投稿数を分析する | Python

最後に、日付情報をPythonで処理し、曜日を算出・分析します。

書いたPythonコードは次のようなものです。 本当はもっときれいなやり方があるのかもしれませんが4、とりあえず曜日の算出という目的は果たせたので良しとします。

目的は、投稿日から曜日を算出し、各曜日の累積投稿数を算出することです。

import sys
import datetime

#引数の取得
args = sys.argv

#引数として渡されたファイルを開いて行ごとに読み込む
datetimes = open(args[1])
dts = datetimes.readlines()

#各曜日累計投稿数を格納する箱を初期化
day_sum = {'Sun':0,'Mon':0,'Tue':0,'Wed':0,'Thu':0,'Fri':0,'Sat':0}

#各日付ごとに処理
for d in dts:
    #日付から曜日を算出
    dt = datetime.date(int(d[0:4]),int(d[5:7]),int(d[8:10]))
    youbi = dt.strftime('%w')

 #曜日に合わせて結果を加算(0→6: 日曜→土曜)
    if youbi=='0':
        day_sum['Sun'] += 1
    elif youbi=='1':
        day_sum['Mon'] += 1
    elif youbi=='2':
        day_sum['Tue'] += 1
    elif youbi=='3':
        day_sum['Wed'] += 1
    elif youbi=='4':
        day_sum['Thu'] += 1
    elif youbi=='5':
        day_sum['Fri'] += 1
    elif youbi=='6':
        day_sum['Sat'] += 1

#結果を出力
print('Sun: %d' % day_sum['Sun'])
print('Mon: %d' % day_sum['Mon'])
print('Tue: %d' % day_sum['Tue'])
print('Wed: %d' % day_sum['Wed'])
print('Thu: %d' % day_sum['Thu'])
print('Fri: %d' % day_sum['Fri'])
print('Sat: %d' % day_sum['Sat'])

処理内容は、コメントに書いたとおりです。
曜日の算出には、datetimeライブラリ5を利用しました。

すこし戸惑ったのは、ループ内の次の処理です。

dt = datetime.date(int(d[0:4]),int(d[5:7]),int(d[8:10]))

datetime.date()には、(<年>, <月>, <日>)の形式で引数を与えなくてはなりませんが、dに格納されるのはstring型の2019-02-24のような日付です。 これを引数として使うために、(int(d[0:4]),int(d[5:7]),int(d[8:10])としました。

それではこのPythonコードに、上述の通り"datetime.txt"として抽出した日付を渡して、コードを実行します。

#datetime.txt
2019-02-24
2019-02-24
2019-02-16
2019-02-11
2019-02-06
2019-02-02
2019-01-31
2019-01-28
2019-01-26
2019-01-24
2019-01-22
2019-01-20
2019-01-18
2019-01-15
2019-01-13
$ python3 datetime_cul.py datetime.txt
Sun: 4
Mon: 2
Tue: 2
Wed: 1
Thu: 2
Fri: 1
Sat: 3

ついに、曜日ごとの記事投稿数を導くことができました。


おわりに

今回は、LinuxコマンドとPythonを用いてWebスクレイピングを行い、自分のブログの曜日ごとの投稿数を分析しました。

正規表現などは今まで全く触れたこともなく、最初はなかなか難解に感じましたが、自分で触ったりサンプルを見たりしているうちに少しずつ慣れてきた気がします。 使いこなすことができれば、非常に強力でしょう。

なお分析結果について見てみると、私は土日に記事を投稿することが多いようです。 たしかに、パソコンをいじれる自由時間が多いのは当然土日ですので、この結果は納得できます。

今回は練習としてとりあえず手を動かしてみましたが、なかなかこれは有効な情報収集の手段になりそうです。 次からは、もっと面白く有益な情報をスクレイピングしていけたらと思います。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

参考


  1. もっとも、いま世間を賑わせているように、このような無料サービスの恩恵は個人情報との引き換えによって成り立っている訳です。

  2. もちろん、実際のWebページにアクセスしても同様の分析ができます。

  3. Linuxコマンドの使い方は別にまとめています:Linuxコマンドまとめ | 使用例とマニュアル - BioErrorLog Tech Blog

  4. お作法的なものもよく分かりませんので、あしからず。

  5. datetime --- 基本的な日付型および時間型 — Python 3.10.6 ドキュメント