ふと、普段働いているところのイベントをなんとなく知りたいと思いました。ホームページには一覧が公開されていました。
この自治体です。
ただ、情報の共有という観点からするとちょっとどうよというような内容のフォーマット。
オープン化を進めるならRSS
とかXML
とかCSV
で共有してくれてもいいじゃないの?とか思ったりしなかったり。つまりHTML
ファイルでしか公開がされていないという不満です。入力データの形式は揃っていそうなので、入力側にはCMSが入っているんだろうな~と推測していますが、なんとなくデータの設計も微妙というか詰めきれていないような感じなんですよね。
というのも、内容を見てみると、「日付、曜日、URL、イベント名、ジャンル」という形式。ただ、せっかくのイベント情報を公開しているのにどこで行われいるのかわかるフィールドがないとかいうのは気になります。エリアが広いため、開催地が一箇所ではないこともあるからこんな感じにしたのかなと推測しましたけど、場所を軸にして検索ができないのはちょっと残念かな~。
せっかくの情報もこれだと使い道がない!俺がなんとかする!とかいう高尚な考えは持っていないですけど、うまく使えればもっと助かる人もいるんじゃないかなと思います。
今回はPython
のBeautifulSoup
とrequests
のライブラリを使用してこのHTMLファイルをスクレイピングしてCSVファイルにする練習をするという内容を書いてみようかなと思います。
公開されている情報の中身をみて、どういうデータか考えてみる
まずは公開されている表示とHTMLのソースを眺めてみます。
【表示部分】
この部分のソースは以下のような感じになっています。
【HTMLファイルの抜粋】
(略) <table class="calendar_month"> <caption> イベント情報 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)とします。
あとはプログラムをどうするか考えることになります。
プログラムの方針を考える
方針は以下のような手順になるかなと思います。
- コマンドラインで引数となるデータを取得するURLと出力するCSVファイル名を指定
- コマンドライン引数で与えられたURLからHTMLファイルを取得
- 取得したHTMLをBeautifulSoupでスクレイピングしてイベント情報を取得
- イベントをフォーマットに合わせてCSVファイルに格納
ではこの順で考えていくことにしていきます。
コマンドラインで引数となるデータを取得するURLと出力するCSVファイル名を指定
コマンドラインから引数を取得するのはargparseライブラリ
を使用します。
また、この処理に関してはメインの処理とは別にすることで処理の独立性を高めます。
詳細に関してはは以下を見ていただければ思いますが、いわゆるコマンドラインから実行されたら引数を確認するという部分を入れる形になります。
よく見かけるこんな形式です。
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.url
、arg.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ファイルへの変換をrequests
とBeautifulSoup
のライブラリを使用して行ってみました。久々にBeautifulSoup
を使ったので結構忘れていましたが、なんとな形になりました。このあたりデータ形式が全く統一されないのはなんでなんなのでしょうか。
念のためGitHub
でソースコード公開しておきます。