ぐりこ製作所日報

趣味の話を中心に。

Pythonでスクレイピング(運行情報編)

LED行先表示器の製作記と並行して、Pythonに関する記事も書いていきたいと思います。初回は、スクレイピングの基礎として、Webサイトから運行情報を取得するためのスクリプトを紹介します。使用するPythonのバージョンは3系とします。

はじめに

今回は、Webサイトから運行情報を抽出し、駅の発車標に表示されているような「メッセージ」(下図)を出力するスクリプトを作成していきます。

f:id:pumpkinism113:20180125005633p:plain
メッセージの例

このスクリプトを使うと、運行情報をリアルタイムでLEDパネルに表示する、といったことができるようになります。

Webサイトの選定

運行情報を提供するWebサイトやAPIは数多くありますが、その中でも発車標に比較的近い構成となっているのが以下のサイトです。

関東の運行情報 - goo路線

これはとある日の運行情報の例です。左から路線と運行状況、詳細が記載されており、特に詳細は発車標とほぼ同じ文体であることがわかります*1。これをうまくスクレイピングすることで、メッセージを生成することができそうです。

f:id:pumpkinism113:20180124001751p:plain
運行情報一覧の例

f:id:pumpkinism113:20180124001938p:plain
運行情報詳細の例

スクレイピング

利用するWebサイトが決まったら、次はHTMLを眺めてみます。すると、このような文書構造を見つけることができます。

<ul class="traininfo">
<li>
<p class="time">23:00</p>
<p class="name"><a href="https://transit.goo.ne.jp/unkou/train/kantou/%E6%A8%AA%E9%A0%88%E8%B3%80%E7%B7%9A/6409351.html">横須賀線</a></p>
<p class="status att">列車遅延</p>
<p class="description">18:58頃、中央総武線(各停)内で発生した人身事故の影響で、現在も一部列車に遅れが出ています。</p>
</li>
<li>
<p class="time">23:00</p>
<p class="name"><a href="https://transit.goo.ne.jp/unkou/train/kantou/%E7%B7%8F%E6%AD%A6%E6%9C%AC%E7%B7%9A%5B%E5%8D%83%E8%91%89%EF%BD%9E%E9%8A%9A%E5%AD%90%5D/6404101.html">総武本線[千葉~銚子]</a></p>
<p class="status">平常運転</p>
<p class="description">大雪の影響で、一部列車に遅れや運休が出ていましたが、23:00現在、ほぼ平常通り運転しています。</p>
</li>
...
</ul>

…非常に抽出しやすい形でまとまっていますね*2。 笑

運行情報は箇条書き(<ul>~</ul>)で構成されており、その各項目(<li>~</li>)に以下のclass名で段落分け(<p>~</p>)がされていることがわかります。

  • time: 更新時刻
  • name: 路線
  • status: 運転状況
  • description: 詳細

PythonでHTMLをスクレイピング(抽出)するのに、今回はBeautiful Soupを使用します。詳細は割愛しますが、例えば上記の箇条書き構造全体を抽出する処理は、以下のように記述することができます。

from bs4 import BeautifulSoup
...

soup = BeautifulSoup(html, 'html.parser')
ul = soup.find('ul', {'class': 'traininfo'})

ここからさらに、各項目の段落に含まれるテキストを抽出する処理は以下のように書けます。

info = {}
for li in ul.findAll('li'):
    n = {}
    for p in li.findAll('p'):
        classname = p.get('class')[0]
        n[classname] = p.text.strip()

一連のコードを実行すると、nには各ループ毎に以下のような値が格納されます。ここまでできれば、あとは各変数の値をうまく加工していくだけです。

インデックスキー
timenamestatusdescription
123:00横須賀線列車遅延18:58頃、中央総武線(各停)内で発生した人身事故の影響で、現在も一部列車に遅れが出ています。
223:00総武本線[千葉~銚子]平常運転大雪の影響で、一部列車に遅れや運休が出ていましたが、23:00現在、ほぼ平常通り運転しています。
:::::

メッセージの生成

次に、上の処理で得られた値を用いてメッセージを生成していきます。ここでは、冒頭に示した「運転状況」「路線」「詳細」の各要素について、それぞれ生成方法を説明します。

運転状況

運転状況はstatusキーの値から生成します。 ただし、「列車遅延」→「遅延」のように表現がやや異なる箇所や、「平常運転」のように、発車標では表示されない情報も含まれます。

これを踏まえて、statusキーの値に対する出力値を以下のとおり定めました。

statusキーの値運転状況の出力値
運転見合わせ運転見合わせ
運転再開運転再開
列車遅延遅延
運転情報
上記以外(平常運転など)なし ※メッセージそのものを出力しない

路線

路線はnameキーの値が対応しますが、運転状況と同様に、下の例に示すような表現の差異が存在します。

基本的には該当する文字列を置換していくのですが、中には共通点を見出すことのできる表現(区間が角括弧に囲まれている、など)もあります。そのため、以下のような正規表現を用いた置換テーブルを作成して、効率良く処理していきます。

パターン 置換値 備考
\[[\s\S]+?\] (空文字列) 角括弧で囲まれた区間を削除
東海道本線 東海道線
総武線\(快速\) 総武線快速電車
... ... ...

詳細

詳細にはdescriptionキーの値が対応します。ここでも路線と同様に、置換テーブルを適用して表現を修正していきます。なお、路線も文中に登場することがあるため、上に示した路線に対する置換テーブルも同時に適用します。

パターン 置換値 備考
^[0-9]{1,2}:[0-9]{1,2}頃、 (空文字列) 文頭の時刻を削除
出ています でています
... ... ...

メッセージ全体

最後に、各要素を以下のように連結してメッセージを生成します。

運転状況路線は、詳細

ソースコード

一連の処理を行うスクリプトは以下のように書けます。 核となるのはget()関数で、処理の流れは概ね以下のとおりとなっています。

  1. HTMLデータの読み込み
  2. スクレイピング
  3. メッセージの生成

なお、表現の差異については調査しきれていないため、まだまだ漏れがあるものと思います。ご了承ください。

import re
from urllib import request
from bs4 import BeautifulSoup

# 置換テーブル
regs = {}
regs['name'] = []
regs['description'] = []

regs['name'].append((r'\[[\s\S]+?\]', r''))
regs['name'].append((r'東海道本線', r'東海道線'))
regs['name'].append((r'総武線\(快速\)', r'総武線快速電車'))
regs['name'].append((r'中央総武線\(各停\)', r'中央・総武線各駅停車'))
regs['name'].append((r'埼京川越線', r'埼京線'))
regs['name'].append((r'京浜東北根岸線', r'京浜東北線'))
regs['name'].append((r'小田急小田原線', r'小田急線'))

regs['description'].append((r'〜', r'~'))
regs['description'].append((r'^[0-9]{1,2}:[0-9]{1,2}頃、', r''))
regs['description'].append(('出ています', 'でています'))
regs['description'].extend(regs['name'])

def get(html):
    soup = BeautifulSoup(html, 'html.parser')
    ul = soup.find('ul', {'class': 'traininfo'})
    
    result = []
    # info = []    
    
    # 各路線の処理
    for li in ul.findAll('li'):
        n = {}

        for p in li.findAll('p'):
            # class名をキーとした辞書型配列に<P>内の各文字列を格納
            classname = p.get('class')[0]
            n[classname] = p.text.strip()
            if (classname in regs.keys()):
                # <P>内の各文字列に置換テーブル適用
                for reg in regs[classname]:
                    n[classname] = re.sub(reg[0], reg[1], n[classname])
        
        if (n.keys() < {'status', 'name', 'description'}):
            continue
        
        # 運転状況判定
        if (n['status'] in {'運転見合わせ', '運転再開'}):
            status = n['status']
        elif (n['status'] in {'列車遅延', '運転状況'}):
            status = '遅延'
        else:
            status = ''
    
        # メッセージ生成
        if (status != ''):
            result.append('【%s】%sは、%s' % (status, n['name'], n['description']))
    
    return result

# メイン処理
if __name__ == '__main__':
    # HTML取得
    url = r'https://transit.goo.ne.jp/unkou/train/kantou/'
    html = request.urlopen(url, timeout = 10).read()
    
    # 運行情報を抽出して表示
    trafficinfo = get(html)
    for r in trafficinfo:
        print(r)

出力例

「とある日の運行情報」からメッセージを生成した結果を以下に示します。

  • 【遅延】横須賀線は、中央総武線各駅停車内で発生した人身事故の影響で、現在も一部列車に遅れがでています。
  • 【遅延】埼京線は、指扇~南古谷駅間で線路内点検を行った影響で、現在も一部列車に遅れがでています。なお、りんかい線との直通運転を中止しています。
  • 【遅延】りんかい線は、JR埼京線内で線路内点検を行った影響で、JR埼京線との直通運転を中止しています。
  • 【遅延】内房線は、強風の影響で、現在も君津~安房鴨川駅間の一部列車に遅れがでています。
  • 【遅延】京成押上線は、青砥駅で車両点検を行った影響で、現在も列車に遅れがでています。
  • 【遅延】京成本線は、青砥駅で車両点検を行った影響で、現在も列車に遅れがでています。

目的はおおむね達成できたと思いますが、実際には、「京成押上線」と「京成本線」はまとめて表示されているような気がするので、そのあたりの処理も追加したいところですね*3

*1:表示上は途中で省略されていますが、HTML内には全文記載されています

*2:静的なHTMLなのも嬉しいところです

*3:実際の表示を確認したところ、両者は特にまとめられていはいないようです (2018/04/17追記)