データ共有の未来: 自治体のイベント情報をもっとプログラマーに優しく

ふと、普段働いているところのイベントをなんとなく知りたいと思いました。ホームページには一覧が公開されていました。

この自治体です。

www.city.nagoya.jp

ただ、情報の共有という観点からするとちょっとどうよというような内容のフォーマット。

オープン化を進めるならRSSとかXMLとかCSVで共有してくれてもいいじゃないの?とか思ったりしなかったり。つまりHTMLファイルでしか公開がされていないという不満です。入力データの形式は揃っていそうなので、入力側にはCMSが入っているんだろうな~と推測していますが、なんとなくデータの設計も微妙というか詰めきれていないような感じなんですよね。

というのも、内容を見てみると、「日付、曜日、URL、イベント名、ジャンル」という形式。ただ、せっかくのイベント情報を公開しているのにどこで行われいるのかわかるフィールドがないとかいうのは気になります。エリアが広いため、開催地が一箇所ではないこともあるからこんな感じにしたのかなと推測しましたけど、場所を軸にして検索ができないのはちょっと残念かな~。

せっかくの情報もこれだと使い道がない!俺がなんとかする!とかいう高尚な考えは持っていないですけど、うまく使えればもっと助かる人もいるんじゃないかなと思います。

今回はPythonBeautifulSouprequestsのライブラリを使用してこのHTMLファイルをスクレイピングしてCSVファイルにする練習をするという内容を書いてみようかなと思います。

公開されている情報の中身をみて、どういうデータか考えてみる

まずは公開されている表示とHTMLのソースを眺めてみます。

【表示部分】

この部分のソースは以下のような感じになっています。

【HTMLファイルの抜粋】

(略)
    <table class="calendar_month">
      <caption>
        イベント情報&nbsp;0月の詳細
      </caption>
              <tr class="cal_sun">
          <th scope="row" class="cal_date">
            <span class="cal_day"><span>1<span class="cal_day_s"></span><span class="cal_week">日曜日</span></span></span>
          </th>
          <td>
          <ul>
          <li>
          <a href="https://www.city.nagoya.jp/jutakutoshi/page/0000163367.html">名古屋まちなみデザインセレクション「まちなみデザイン20選 一覧マップ」</a>
          <span class="eve_cate14">自然・環境</span><span class="eve_cate16">文化・芸術</span><span class="eve_cate19">お出かけ・レジャー</span>
          </li>
          <li>
          <a href="https://www.city.nagoya.jp/jutakutoshi/page/0000163879.html">「NagoまちWalk」-名古屋まちなみデザインセレクションデジタルスタンプラリー-</a>
          <span class="eve_cate14">自然・環境</span><span class="eve_cate16">文化・芸術</span><span class="eve_cate19">お出かけ・レジャー</span>
          </li>
          <li>
          <a href="https://www.city.nagoya.jp/kankyo/page/0000047438.html">「生ごみ堆肥づくり講座」を開催します!</a>
          <span class="eve_cate14">自然・環境</span>
          <p>生ごみ堆肥づくり講座についてご案内します。</p>
          </li>
(略)

典型的なHTMLのtableタグを使用しています。これはBeautifulSoupで解析がしやすそうです。

先程の抜粋の中で以下の部分が日付の部分になります。

          <th scope="row" class="cal_date">
            <span class="cal_day"><span>1<span class="cal_day_s"></span><span class="cal_week">日曜日</span></span></span>
          </th>

そしてイベントはliタグで配置されています。その中にaタグhref属性でリンク先、アンカーテキストでイベント名を入れています。そのあとのspanタグでイベントのカテゴリーを格納しているようで、カテゴリーは複数あっても良いようです。

          <li>
          <a href="https://www.city.nagoya.jp/jutakutoshi/page/0000163367.html">名古屋まちなみデザインセレクション「まちなみデザイン20選 一覧マップ」</a>
          <span class="eve_cate14">自然・環境</span><span class="eve_cate16">文化・芸術</span><span class="eve_cate19">お出かけ・レジャー</span>
          </li>

ここまでで作成するCSVデータは以下の様にすることにします。上の例を使用するならば、このような結果でしょうか。

日付(曜日), イベントタイトル, イベントURL,カテゴリ

”1日(日)","名古屋まちなみデザインセレクション「まちなみデザイン20選 一覧マップ」","https://www.city.nagoya.jp/jutakutoshi/page/0000163367.html","自然・環境,文化・芸術,お出かけ・レジャー"
(以下略)

念のため、データはすべてダブルクォーテーションで囲むことにして、ジャンルに関してはカンマで区切ることにしました。文字コードUTF-8、改行コードはCRLF(0D0A)とします。

あとはプログラムをどうするか考えることになります。

プログラムの方針を考える

方針は以下のような手順になるかなと思います。

  1. コマンドラインで引数となるデータを取得するURLと出力するCSVファイル名を指定
  2. コマンドライン引数で与えられたURLからHTMLファイルを取得
  3. 取得したHTMLをBeautifulSoupでスクレイピングしてイベント情報を取得
  4. イベントをフォーマットに合わせてCSVファイルに格納

ではこの順で考えていくことにしていきます。

コマンドラインで引数となるデータを取得するURLと出力するCSVファイル名を指定

コマンドラインから引数を取得するのはargparseライブラリを使用します。

また、この処理に関してはメインの処理とは別にすることで処理の独立性を高めます。

詳細に関してはは以下を見ていただければ思いますが、いわゆるコマンドラインから実行されたら引数を確認するという部分を入れる形になります。

docs.python.org

よく見かけるこんな形式です。

if __name__ == '__main__':
    ...

この部分に引数解析部分を入れます。argparseはデフォルトで存在するライブラリなので別途インストールは不要です。

【tmp.py 作業過程】

import argparse

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="イベント情報を抽出してCSVファイルに保存します。")
    parser.add_argument('url', help="イベント情報が含まれるウェブページのURL")
    parser.add_argument('csv_file', help="抽出したイベント情報を保存するCSVファイルのパス")
    args = parser.parse_args()

最終行のargs = parser.parse_args()を行うことで、コマンド引数をarg.urlarg.csv_fileとして使用できるようになります。

これを実行すると以下の様になります。--helpとオプションをつけることでヘルプなどの処理が入れられるのでとても便利です。

$ python tmp.py 
usage: tmp.py [-h] url csv_file
tmp.py: error: the following arguments are required: url, csv_file

$ python tmp.py --help
usage: tmp.py [-h] url csv_file

イベント情報を抽出してCSVファイルに保存します。

positional arguments:
  url         イベント情報が含まれるウェブページのURL
  csv_file    抽出したイベント情報を保存するCSVファイルのパス

options:
  -h, --help  show this help message and exit

コマンドライン引数で与えられたURLからHTMLファイルを取得する

コマンドライン引数として指定されたURLがurlとして格納されているので、これを引数として処理を行っていきます。 URLからデータを取得する処理にはお馴染みのrequestsライブラリが便利です。

requestsライブラリはpipコマンドで事前にインストール必要あるので、以下のように実行しておきます。

$ pip install request

先ほどソースコードに処理に加えて以下のような感じになるでしょうか

【tmp.py 作業過程】

import argparse
import requests

def parse_html_from_url(url):
    """
    作業過程
    
    :param url: イベント情報が含まれるウェブページのURL
    :return: html_content(HTMLファイルのテキストデータ) 
    """
    response = requests.get(url)
    response.raise_for_status()
    response.encoding = 'utf-8'
    html_content = response.text.encode('utf-8')
    return html_content

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="イベント情報を抽出してCSVファイルに保存します。")
    parser.add_argument('url', help="イベント情報が含まれるウェブページのURL")
    parser.add_argument('csv_file', help="抽出したイベント情報を保存するCSVファイルのパス")
    args = parser.parse_args()

    print(parse_html_from_url(args.url))

parse_html_from_url()で指定されたURLからHTMLのコンテンツを取得しています。そのままだとUTF-8のデータが標準出力に出力されるので>を使用してファイルに出力をしてみます。

$ python tmp.py https://www.city.nagoya.jp/main/event2/curr.html tmp.csv > html.txt

$ cat html.txt
b'<?xml version="1.0" encoding="utf-8" ?>\n<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n<html lang="ja" xml:lang="ja" xmlns="http://www.w3.org/1999/xhtml">\n<head>\n<meta name="viewport" content="width=device-width, initial-scale=1.0" />\n<meta http-equiv="X-UA-Compatible" content="IE=edge" />\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n<meta http-equiv="Content-Language" content="ja" />

(以下略)

うまくHTMLファイルを取得できたようです。

取得したHTMLをBeautifulSoupスクレイピングしてイベント情報を取得

取得したHTMLファイルからBeautifulSoupでフィルタリングをしてイベント情報を取得行います。

BeautifulSoupライブラリはpipコマンドで事前にインストール必要あるので、以下のように実行しておきます。

$ pip install  beautifulsoup4

【tmp.py 作業過程】

import argparse
import requests
from bs4 import BeautifulSoup
from datetime import datetime
import re

def extract_event_info(date_tag):
    """
    HTMLの日付タグからイベント情報を抽出する関数。
    
    :param date_tag: BeautifulSoupオブジェクトの日付タグ
    :return: イベント情報のリスト
    """
    date_text = date_tag.get_text(strip=True)
    day, weekday = date_text[:-3], date_text[-3:]
    weekday_dict = {'月曜日': '月', '火曜日': '火', '水曜日': '水', '木曜日': '木', '金曜日': '金', '土曜日': '土', '日曜日': '日'}
    date = f"{day}({weekday_dict.get(weekday, '?')})"
    events = []
    events_td = date_tag.find_next_sibling('td')
    if events_td:
        for li in events_td.find_all('li'):
            event_title_tag = li.find('a')
            event_title = event_title_tag.get_text(strip=True) if event_title_tag else "タイトル不明"
            event_url = event_title_tag['href'] if event_title_tag and event_title_tag.has_attr('href') else "URL不明"
            event_categories = [span.text for span in li.find_all('span', class_=re.compile('eve_cate'))]
            events.append({
                '日付': date,
                'イベントタイトル': event_title,
                'イベントURL': event_url,
                'カテゴリ': ','.join(event_categories)
            })
    return events

def parse_html_from_url(url):
    """
    URLからHTMLを取得し、イベント情報を抽出する関数。
    
    :param url: イベント情報が含まれるウェブページのURL
    :return: イベント情報のリスト
    """
    response = requests.get(url)
    response.raise_for_status()
    response.encoding = 'utf-8'
    html_content = response.text.encode('utf-8')
    soup = BeautifulSoup(html_content, 'html.parser')
    date_tags = soup.find_all('th', {'scope': 'row', 'class': 'cal_date'})
    all_events = []
    for date_tag in date_tags:
        all_events.extend(extract_event_info(date_tag))
    return all_events

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="イベント情報を抽出してCSVファイルに保存します。")
    parser.add_argument('url', help="イベント情報が含まれるウェブページのURL")
    parser.add_argument('csv_file', help="抽出したイベント情報を保存するCSVファイルのパス")
    args = parser.parse_args()

    print(parse_html_from_url(args.url))

今回はイベントを含むタグの一覧をdate_tags = soup.find_all('th', {'scope': 'row', 'class': 'cal_date'})で取得し、その後でそのタグ情報を元にイベントの情報を取り出すという2段階の処理を行っています。

取得したイベント情報はextract_event_info()で解析してリスト化しています。途中、曜日の処理をdictを使用している点がかなり微妙ですが、 実行しているのがDocker上のコンテナだったのでlocale関連の処理でエラーがでたので仕方なくこのようにしています。GoogleColabでも設定していないとlocale関連の処理に失敗することがあるので我慢しました。

これを実行すると以下の様になります。

$ python tmp.py https://www.city.nagoya.jp/main/event2/curr.html tmp.csv
[{'日付': '1日(水)', 'イベントタイトル': 'なごや和菓子「和菓子の原点に還って、名古屋の素朴な和菓子」(やっとかめ文化祭DOORS 2023)(外部リンク)', 'イベントURL': 'https://yattokame.jp/2023/wagashi', 'カテゴリ': ''}, {'日付': '1日(水)', 'イベントタイトル': '福祉会館めぐり', 'イベントURL': 'https://www.city.nagoya.jp/kenkofukushi/page/0000167646.html', 'カテゴリ': ''},

(以下略)

かなり最終出力に近づいてきました。

イベントをフォーマットに合わせてCSVファイルに格納

あとは取得したデータをフォーマットに合わせてファイルに出力するだけです。今まではファイル名をtmp.pyとしていましたが 完成なのでファイル名もget_nagoya_city_events.pyに変更します。また、CSVファイルの出力はcsvライブラリを使用します。

get_nagoya_city_events.py

import argparse
import requests
from bs4 import BeautifulSoup
from datetime import datetime
import re
import csv

def extract_event_info(date_tag):
    """
    HTMLの日付タグからイベント情報を抽出する関数。
    
    :param date_tag: BeautifulSoupオブジェクトの日付タグ
    :return: イベント情報のリスト
    """
    date_text = date_tag.get_text(strip=True)
    day, weekday = date_text[:-3], date_text[-3:]
    weekday_dict = {'月曜日': '月', '火曜日': '火', '水曜日': '水', '木曜日': '木', '金曜日': '金', '土曜日': '土', '日曜日': '日'}
    date = f"{day}({weekday_dict.get(weekday, '?')})"
    events = []
    events_td = date_tag.find_next_sibling('td')
    if events_td:
        for li in events_td.find_all('li'):
            event_title_tag = li.find('a')
            event_title = event_title_tag.get_text(strip=True) if event_title_tag else "タイトル不明"
            event_url = event_title_tag['href'] if event_title_tag and event_title_tag.has_attr('href') else "URL不明"
            event_categories = [span.text for span in li.find_all('span', class_=re.compile('eve_cate'))]
            events.append({
                '日付': date,
                'イベントタイトル': event_title,
                'イベントURL': event_url,
                'カテゴリ': ','.join(event_categories)
            })
    return events

def parse_html_from_url(url):
    """
    URLからHTMLを取得し、イベント情報を抽出する関数。
    
    :param url: イベント情報が含まれるウェブページのURL
    :return: イベント情報のリスト
    """
    response = requests.get(url)
    response.raise_for_status()
    response.encoding = 'utf-8'
    html_content = response.text.encode('utf-8')
    soup = BeautifulSoup(html_content, 'html.parser')
    date_tags = soup.find_all('th', {'scope': 'row', 'class': 'cal_date'})
    all_events = []
    for date_tag in date_tags:
        all_events.extend(extract_event_info(date_tag))
    return all_events

def save_to_csv(events, output_path):
    """
    イベント情報をCSVファイルに保存する関数。
    
    :param events: イベント情報のリスト
    :param output_path: 出力するCSVファイルのパス
    """
    with open(output_path, 'w', newline='', encoding='utf-8') as csvfile:
        fieldnames = ['日付', 'イベントタイトル', 'イベントURL', 'カテゴリ']
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames, quoting=csv.QUOTE_ALL)
        writer.writeheader()
        for event in events:
            writer.writerow(event)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="イベント情報を抽出してCSVファイルに保存します。")
    parser.add_argument('url', help="イベント情報が含まれるウェブページのURL")
    parser.add_argument('csv_file', help="抽出したイベント情報を保存するCSVファイルのパス")
    args = parser.parse_args()

    events = parse_html_from_url(args.url)
    save_to_csv(events, args.csv_file)
    print(f"イベント情報を{args.csv_file}に保存しました。")

出力されるCSVファイルはライブラリを使用すれば簡単に出力をすることができます。以下の様に処理を行っています。

    with open(output_path, 'w', newline='', encoding='utf-8') as csvfile: # UTF-8での出力
        fieldnames = ['日付', 'イベントタイトル', 'イベントURL', 'カテゴリ']  # フィールドの定義
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames, quoting=csv.QUOTE_ALL) # フィールドの設定とデータにダブルクオーテーションをつける
        writer.writeheader() # ヘッダー行の出力
        for event in events:
            writer.writerow(event) # CSVデータの出力

今回できたプログラムを実行すると以下の様になります。

$ python get_nagoya_city_events.py https://www.city.nagoya.jp/main/event2/curr.html event.csv
イベント情報をevent.csvに保存しました。
$ cat event.csv 
"日付","イベントタイトル","イベントURL","カテゴリ"
"1日(水)","なごや和菓子「和菓子の原点に還って、名古屋の素朴な和菓子」(やっとかめ文化祭DOORS 2023)(外部リンク)","https://yattokame.jp/2023/wagashi","生活・福祉,文化・芸術,お出かけ・レジャー"
"1日(水)","福祉会館めぐり","https://www.city.nagoya.jp/kenkofukushi/page/0000167646.html","生活・福祉,健康・スポーツ"
"1日(水)","名古屋まちなみデザインセレクション「まちなみデザイン20選 一覧マップ」","https://www.city.nagoya.jp/jutakutoshi/page/0000163367.html","自然・環境,文化・芸術,お出かけ・レジャー"

(以下略)

これでCSVファイルへの変換が完了です。

おわりに

某市のイベント情報をHTMLファイルからCSVファイルへの変換をrequestsBeautifulSoupのライブラリを使用して行ってみました。久々にBeautifulSoupを使ったので結構忘れていましたが、なんとな形になりました。このあたりデータ形式が全く統一されないのはなんでなんなのでしょうか。

念のためGitHubソースコード公開しておきます。

github.com

/* -----codeの行番号----- */