はじめに
2025年9月からPythonの学習を始めました。
約1か月半たくさんのことを学び、できることが日に日に増えていくので楽しみながらpythonの学習をしています。
今回は学習内容のアウトプットとして、Slackへ公共工事の入札情報の自動通知+最新情報のcsv取得に挑戦しました。
なぜ公共工事の入札情報なのか
私は、現在電動工具メーカーの営業職として働いており、日々の仕事の中で施工会社へ訪問し営業活動をしております。その中で公共工事をよくやられるお客様もあり公共工事の入札情報が通知でわかれば便利なのではないかと思ったからです。
システムの内容
このシステムでできることは2つあります。
(本システムは広島県の入札情報を取得しています)
1. APIで公共工事入札案件の自動取得してslackへ自動通知
毎日11時に新しい入札情報があればその情報を通知、なければ「新しい入札情報はありません」と通知


2. ユーザーが「csvを送って」とメッセージすると最新情報のcsv取得
「csvを送って」のメッセージをトリガーに最新情報を100件取得しcsvファイルにして送られてきます。

csv取得のイメージgif
※gifファイルのため動画を短くしています。実際は取得まで少し時間がかかります。
事前準備
1-1.slackへ登録
1-2.ワークスペースの作成
1-3.ワークスペースの名前の設定
1-4.名前の設定
1-5.ワークスペースへ誘う人がいれば選択
1-5.新しいチャンネルの作成
2.BOT用アカウントの作成
2-1 slack_bolt入門ガイド
今回使用するslack_boltの入門ガイドを開く
https://docs.slack.dev/tools/bolt-python/ja-jp/getting-started/
slackアプリを作成をクリック

botの名前の設定とワークスペースの設定
名前は好きな名前を入力し、先ほど作成したワークスペースの選択

2-2.トークンの発行
左側のメニューよりOAuth & Permissionsを選択
・BOTに権限の付与
今回使う権限は以下の表です。
Add an OAuth Scope を押して追加していく
| 項目 | 説明 | 今回の用途 |
|---|---|---|
| channels:history | チャンネルの履歴を観覧 | 「csvを送って」のトリガーを読むため |
| chat:write | メッセージ送付 | 定期通知のメッセージを送信する |
| files:write | csvなどのファイルを送信 | csvを送信するため |

・AppHomeでdisplaynameの作成
Editをクリック

・2か所に名前を入力
Default usernameは小文字のみ(日本語、大文字不可)
入力してAddをクリック

・OAuth & Permissionsに戻って[Install to "ワークスペース名"]をクリック

BOT TOKENが発行される
この情報は外に漏れないように気を付けましょう!!
こちらは後程使用するので"BOT_TOKEN":"xxx\xxxx"この形でメモ

BOTをチャンネルに招待
チャンネルのメッセージ欄に/invite @あなたのBot名で送信

2-3.署名シークレットの設定
slackから来たリクエストのみ受け付けをする設定を行う
Signing Secretをコピー
こちらも後程使うので"SIGNING_SECRET":xxxxxxの形でメモ

以上でslackの設定は一旦終了
3.googlecloudの設定
3-1. googlecloudのアカウント作成
1 googlecloudで検索
2 無料で開始をクリック
3 Gmailを使ったログイン画面に行動
4 Gmailアドレスとクレジットカードの方法の入力

ログインが完了するとgooglecloudの画面に移動
最初の90日間は無料クレジットがもらえます

3-2.今回使用するサービス
| サービス名 | 説明 | 今回の用途 |
|---|---|---|
| cloud ran (functiouns) | クラウド上で関数が実行できる | 定期通知やcsvを送る処理のコードを記載 |
| Cloud Scheduler | ユーザーが指定した時間・間隔で自動でプログラムが実行できる | 定期通知のコードを毎日11時に実行する |
| Cloud Storage | クラウド上でファイルの保存ができる | APIより取得した入札案件情報を保存 |
| Gcloud CLI | googlecloudをコマンド操作できる | ローカルでのコードテストやコードのデプロイに使用 |
| Secret Manager | 機密情報を安全に保存・管理・アクセスするサービス | BOT_TOKENやSIGING_SECRETの保管や関数にアクセスする際に使用 |
| IAM | アクセス権限の管理システム | Cloud Schedulerやcloudstrageがアクセスできる権限を付与 |
3-3. cloud ran (functiouns)
・関数を作成から関数を作成することができる
今回はgcloud CLIで関数をデプロイするのでここでは作成しない

3-4. Cloud Scheduler
・画面上の検索バーよりCloud Schedulerで検索
Cloud Schedulerをクリック

3-5. Cloud Storage
・ナビゲーションメニューよりCloud Storageを選択

・バケットの名前の設定
"BUCKET_NAME":"バケットの名前"でメモ

・データの保管場所の選択
Regionを選択しasia-northeast1 (東京)を選択
あとはデフォルトでOK下にある作成をクリック

・バケットが作成されていればOK
3-6. gcloud CLI
Gcloud CLIリンク: https://cloud.google.com/sdk/docs/install?hl=ja
インストール画面で各OSに合わせてインストールする

3-7. Secret Manager
・検索バーにSecret Managerと入力しSecret Managerを選択
・名前を任意で入力
シークレット値今までメモしたBOT_TOKEN,SIGNING_SECRET,BUCKET_NAMEをjson形式で作成
jsonファイルを保存
{
"BOT_TOKEN":"xxx\xxxx",
"SIGNING_SECRET":"xxxxxx",
"BUCKET_NAME":"バケットネーム"
}
参照をクリックして作成したjsonファイルを選択
下にjsonの内容が表示されます
その後シークレットを作成をクリック

3-8. IAM
・ナビゲーションメニューのIAMと管理をクリック
サービスアカウントを作成をクリック
・サービスアカウント名を任意で入力
作成して続行をクリック
・権限を選択
Cloud Run サービス起動元、Secret Manager のシークレット アクセサー、Storage オブジェクト管理者を選択
アカウントが作成できていればOK
4. 環境構築
pyenvとpoetryを使って環境構築
4-1. 使用するライブラリ
| 使用目的 | 環境・ライブラリ | バージョン |
|---|---|---|
| 言語 | Python | 3.9.13 |
| APIを取得するため | requests | (>=2.32.5,<3.0.0) |
| APIの形式がxmlなので辞書型へ変換 | xmltodict | (>=1.0.2,<2.0.0) |
| slackを操作 | slack-bolt | (>=1.26.0,<2.0.0) |
| ローカル上でテストで使用 | functions-framework | (>=3.9.2,<4.0.0) |
| csvの作成に使用 | pandas | (>=2.3.3,<3.0.0) |
| cloudstorageへ保存やロードをする | google-cloud-storage | (>=3.4.1,<4.0.0) |
| ローカルからsecretmanagerへアクセス | google-cloud-secret-manager | (>=2.25.0,<3.0.0) |
※poetry add functions-frameworkの注意点
Because no versions of functions-framework match >3.9.2,<4.0.0
and functions-framework (3.9.2) requires Python <4,>=3.5, functions-framework is forbidden.
So, because qitta-cloud depends on functions-framework (^3.9.2), version solving failed.
* Check your dependencies Python requirement: The Python requirement can be specified via the `python` or `markers` properties
For functions-framework, a possible solution would be to set the `python` property to ">=3.9,<4"
https://python-poetry.org/docs/dependency-specification/#python-restricted-dependencies,
https://python-poetry.org/docs/dependency-specification/#using-environment-markers
私が、poetry add functions-frameworkをした際上記の内容でエラーがでました。
`python` property to ">=3.9,<4"
>=3.9,<4ここが原因みたいです。
readme = "README.md"
requires-python = ">=3.9"
dependencies = [
"pandas (>=2.3.3,<3.0.0)",
"requests (>=2.32.5,<3.0.0)",
"xmltodict (>=1.0.2,<2.0.0)",
"slack-bolt (>=1.26.0,<2.0.0)",
"google-cloud-storage (>=3.4.1,<4.0.0)",
"google-cloud-secret-manager (>=2.25.0,<3.0.0)"
]
解決策としては上記のpyproject.tomlファイルのrequires-python = ">=3.9"を
">=3.9,<4"に変更すると解決します
requires-python = ">=3.9,<4"
4-2. gclod CLIの初期化
gcloud initをpowersgellで実行
[2] Create a new configurationを選択
Welcome! This command will take you through the configuration of gcloud.
Settings from your current configuration [xxxxx] are:
accessibility:
screen_reader: 'False'
core:
account: xxxxxxxx@gmail.com
disable_usage_reporting: 'True'
project: xxxxxxxxxxxx
Pick configuration to use:
[1] Re-initialize this configuration [yamamoto1] with new settings
[2] Create a new configuration
[3] Switch to and re-initialize existing configuration: [default]
Please enter your numeric choice:
設定名の入力~小文字でスタート、使用できるのは小文字と数字のみ~
任意の名前を設定
Enter configuration name. Names start with a lower case letter and contain only lower case letters a-z, digits 0-9, and hyphens '-': test1
アカウントを選択する
最初は [1] xxxxxxxx@gmail.comは表示されないため
初めての方は
[2] Sign in with a new Google Accountを選択
Choose the account you want to use for this configuration.
To use a federated user account, exit this command and sign in to the gcloud CLI with your login configuration file, then run this command again.
Select an account:
[1] xxxxxxxx@gmail.com
[2] Sign in with a new Google Account
[3] Skip this step
使用したいgoogleアカウントを選択
許可を選択
powershellに戻るとプロジェクトの選択をします。
プロジェクトの確認方法は1個したに記載しています。
Pick cloud project to use:
[1] xxxxxxxxxxxxxxxxxxxx
[2] yyyyyyyyyyyyyyyyyyyy
[3] Enter a project ID
[4] Create a new project
Please enter numeric choice or text value (must exactly match list item):
googlecloudのページに移動して画像のプロジェクトのボタンを押す
この画像だとMy First Projectをクリック
プロジェクトを選択する画面が出ます
IDのところを確認して
powersgellに戻り任意のプロジェクトを選択
4-3. ローカルからSecret Managerへアクセスする準備
こちらのページへ移動https://cloud.google.com/secret-manager/regional-secrets/reference/libraries?hl=ja
画像のようなページに移動する
google-cloud-secret-managerがインストールされてない場合はインストール
poetry add google-cloud-secret-manager
gcloud auth application-default login
アカウントのログイン画面が出るのでgcloudを使っているアカウントを選択
続行をクリック
これでローカルでテストができるようになったのでOK
5. フォルダ構成
本プロジェクトは、APIで公共工事入札案件の自動取得してslackへ自動通知 と
ユーザーが「csvを送って」とメッセージすると最新情報のcsv取得 の2つを主軸としています。
📁slack-bids-project-cloudのフォルダ内に以下のフォルダがあります。
📦 scheduler_task/
APIで公共工事入札案件の自動取得してslackへ自動通知の処理をまとめたディレクトリ。
scheduler_task/
├── service-account/ ← サービスアカウントのJSONファイル
├── src/ ← 共通処理モジュール
│ ├── init.py
│ ├── bids_api.py ← 入札情報APIからデータ取得
│ ├── compare_utils.py ← 差分検出とCSV作成処理
│ ├── gcs_storage.py ← GCSへの保存・読み込み
│ └── local_storage.py ← ローカルストレージ操作
│
├── check_bids.py ← 新規入札データを検出しSlack通知
├── main.py ← Cloud Functions エントリーポイント
└── requirements.txt ← 使用パッケージ一覧
💬 slack_bot/
ユーザーが「csvを送って」とメッセージすると最新情報のcsv取得するディレクトリ。
slack_bot/
├── src/ ← scheduler_task と同一の共通モジュール
│ ├── init.py
│ ├── bids_api.py
│ ├── compare_utils.py
│ ├── gcs_storage.py
│ └── local_storage.py
│
├── slack_bot.py ← Slackコマンド受付(例:「csvを送って」)
├── main.py ← Slack用Cloud Function エントリーポイント
└── requirements.txt ← この関数の依存パッケージ
⚙️ その他設定ファイル
.env
pyproject.toml
poetry.lockなど
6. メインコード
6-1. APIで公共工事入札案件の自動取得してslackへ通知するコード
import json
from slack_bolt import App
from google.cloud import secretmanager_v1
import functions_framework
from src.bids_api import fetch_bids_data
from src.compare_utils import extract_new_bids_dict
from src.gcs_storage import save_bids_data, load_bids_data
#secretmanagerのURL
NAME='projects/1057750657085/secrets/slack-bot-credentials/versions/latest'
client = secretmanager_v1.SecretManagerServiceClient()
# BOT_TOKENの情報を取得
response = client.access_secret_version(request={"name":NAME})
payload = response.payload.data.decode("UTF-8")
payload_dict = json.loads(payload)
bot_token = payload_dict['BOT_TOKEN']
app = App(token=bot_token)
# 定期通知の処理
@functions_framework.http
def check_new_bids(request):
bids_dict = load_bids_data() # GCSに保存されているjsonファイルをpythonの辞書型で変数に格納
new_bids_dict = fetch_bids_data()# APIから取得したデータを成型した辞書を変数に格納{'':[]}
if bids_dict is None:# 初回はstrageにデータがないので保存のみして終了
save_bids_data(new_bids_dict)
return '初回のデータを保存', 200
unique_dict = extract_new_bids_dict(bids_dict,new_bids_dict)# 元データと新データの差分検出しユニークなデータの取得
if not unique_dict['工事ID']:# ユニークなデータ(新しい情報)がなければ入札方法なしと送信
app.client.chat_postMessage(channel='#all-python-workspace', text='新しい入札情報はありません')
return '新しい入札情報はありません', 200
blocks = []
MAX_BLOCKS = 40 #slackの通知は一度に50ブロックまでしか通知できなため上限の設定
for i in range(len(unique_dict['工事ID'])): # 通知の本文を数量分、リストにextend
blocks.extend([
{'type':'section', 'text':{'type':'mrkdwn','text':
f"*工事名:* {unique_dict['工事名'][i]}\n"
f"*公告日またはデータ取得日:* {unique_dict['公告日'][i]}\n"
f"*都道府県名:* {unique_dict['都道府県名'][i]}\n"
f"*市町村名:* {unique_dict['市町村名'][i]}\n"
f"*詳細:* {unique_dict['公告情報のURI'][i]}\n"
}},
{'type': 'divider'}
])
for i, start in enumerate(range(0, len(blocks), MAX_BLOCKS)): # ヘッダーの作成(通知が複数になった場合は各通知の一番上に表示)
chunk = blocks[start:start + MAX_BLOCKS] # 工事情報を最大20件で分割してリストに格納
# ヘッダー情報を格納
header_block = [
{"type":"section", "text":{"type":"mrkdwn","text":
(f"*📢新しい入札情報:({i+1} / {((len(blocks)//2 -1)//(MAX_BLOCKS//2))+1})📢*\n"
f"*✨新しい入札情報を {len(chunk)//2} 件見つけました!✨*\n")
}},{"type": "divider"}
]
# slackへ通知の実装
app.client.chat_postMessage(
channel='#all-python-workspace',
text=f"新しい入札情報を{len(chunk)//2} 件見つけました!",
blocks= header_block + chunk # ヘッダーと分割したブロックを結合
)
save_bids_data(new_bids_dict)# 新しい辞書データをcloudstrageへ保存
return "Slackへ新しい入札情報を送信しました。", 200
6-2. ユーザーが「csvを送って」とメッセージすると最新情報のcsv取得するコード
import json
from slack_bolt import App
from slack_bolt.adapter.flask import SlackRequestHandler
from google.cloud import secretmanager_v1
import functions_framework
from src.bids_api import fetch_bids_data
from src.compare_utils import create_csv
NAME='projects/1057750657085/secrets/slack-bot-credentials/versions/latest'
client = secretmanager_v1.SecretManagerServiceClient()
# BOT_TOKENとSIGING_SECRETの情報の取得
response = client.access_secret_version(request={"name":NAME})
payload = response.payload.data.decode("UTF-8")
payload_dict = json.loads(payload)
bot_token = payload_dict['BOT_TOKEN']
signing_secret = payload_dict['SIGNING_SECRET']
app = App(token=bot_token,signing_secret=signing_secret)
handler = SlackRequestHandler(app)
# csvを送ってもらうコード
@app.message('csvを送って')
def get_csv(message):
user = message['user']
channel = message['channel']
new_dict = fetch_bids_data() #最新の情報をAPIより取得
bids_csv = create_csv(new_dict)#csvを作成
# 取得したcsvをslackへ投稿
app.client.files_upload_v2(
channel=channel,
initial_comment=f'<@{user}> さん、直近100件の入札情報のCSVをお送りいたします。',
file=bids_csv,
filename='入札情報.csv',
title='入札情報.csv'
)
@functions_framework.http
def slack_csv(request):
return handler.handle(request)
7.メインコードでimportするモジュール
7-1. 官公需情報ポータルサイトAPIから必要情報を取り{'key':[xx,oo]}に成型する
APIの構造
APIの戻り値がXML形式のためxmltodictを使用して辞書型に変換する必要があった


import requests
import xmltodict
from datetime import datetime
#官公需情報ポータルサイトAPIのURL
URL = 'http://www.kkj.go.jp/api/?'
# リクエストの情報を辞書に格納
req_dict = {}
req_dict['Query'] = ''
req_dict['LG_Code'] = 34
req_dict['Category'] = 2
req_dict['Count'] = 100
# レスポンスのキーを辞書に格納
keys_map = {
'工事ID': 'Key',
'工事名': 'ProjectName',
'都道府県名': 'PrefectureName',
'市町村名': 'CityName',
'公告日': 'CftIssueDate',
'公告取得日時': 'Date',
'公告情報のURI': 'ExternalDocumentURI',
}
# iso文字列を'%Y-%m-%d'に変換する関数
def safe_date_convert(date_list):
new_list = []
for date in date_list:
try:
new_list.append(datetime.fromisoformat(date).strftime('%Y-%m-%d'))
except Exception:
new_list.append('')
return new_list
# メイン処理
def fetch_bids_data():
# rereq_dictを使用してリクエストを送るURLの作成
req_list = []
for key, value in req_dict.items():
req_list.append(f'{key}={value}')
endpoint = URL + '&'.join(req_list)
# rrequestsを使用して情報を取得
response = requests.get(endpoint)
response.encoding = 'utf-8'
data_dict = xmltodict.parse(response.text)# XMLで返ってくるので辞書型に変換
# 必要な情報がある階層まで行き複数の辞書があるリストを格納
bid_project = data_dict['Results']['SearchResults']['SearchResult']
# keys_mapを使用してbids_data作成{key:[]}
bids_data = {k:[] for k in list(keys_map.keys())}
# 辞書が入ったリストから1つずつ辞書を取り出し処理
for bid_dict in bid_project:
for jp_key, api_key in keys_map.items():
bids_data[jp_key].append(bid_dict.get(api_key))
# bbids_dataの日付がISO文字列になっているため変換の処理 ssafe_date_convert()関数を使用
bids_data['公告取得日時'] = safe_date_convert(bids_data['公告取得日時'])
bids_data['公告日'] = safe_date_convert(bids_data['公告日'])
return bids_data
if __name__=='__main__':
print(fetch_bids_data())
7-2. 差分検出しユニークなデータを辞書型で返す処理とcsvを作成しバイナリデータで返す処理
import io
import pandas as pd
# 新しく取得したデータと古いデータからユニークなデータを取得する関数
def extract_new_bids_dict(old_dict,new_dict):
unique_dict = {k:[] for k in new_dict.keys()}
for i , new_id in enumerate(new_dict['工事ID']):
if new_id not in old_dict['工事ID']:
for key in new_dict.keys():
unique_dict[key].append(new_dict[key][i])
return unique_dict
# bids_dictをcsvへ変換する関数
def create_csv(dict):
df = pd.DataFrame(dict)
#データをバイナリデータで処理
buffer = io.BytesIO()
df.to_csv(buffer,index=False,encoding='utf-8-sig')
buffer.seek(0)
return buffer
7-3. cloudstrageに保存やロードをする処理
import json
from google.cloud import storage
from google.cloud import secretmanager_v1
NAME='projects/1057750657085/secrets/slack-bot-credentials/versions/latest'
client = secretmanager_v1.SecretManagerServiceClient()
# Access the secret version.
response = client.access_secret_version(request={"name":NAME})
payload = response.payload.data.decode("UTF-8")
payload_dict = json.loads(payload)
buket_name = payload_dict['BUCKET_NAME']
# バケット名の取得
BUCKET_NAME = buket_name
# python辞書をjson形式で保存する関数
def save_bids_data(save_dict):
client = storage.Client()
bucket = client.bucket(BUCKET_NAME)# 使用するバケットの指定
blob = bucket.blob('bids_data.json')# アップロード先の指定
json_data = json.dumps(save_dict, ensure_ascii=False, indent=2)# python辞書をjson型に変更
blob.upload_from_string(json_data, content_type='application/json')# json文字列をアプロード
# 保存してあるjsonファイルを読み込む関数
def load_bids_data():
client = storage.Client()
bucket = client.bucket(BUCKET_NAME)# 使用するバケットの指定
blob = bucket.blob('bids_data.json')#読み込むファイルの指定
if not blob.exists():
return None
json_data = blob.download_as_text(encoding='utf-8')
return json.loads(json_data)
8. ローカルテスト
check_bids.py
functions-framework --target=check_new_bids --source=scheduler_task/main.py
※google cloudへデプロイ時の関係でmain.pyでcheck_bids.pyをimportしています
from check_bids import check_new_bids
9. googlecloudへデプロイ
requirements.txt作成
poetry export -f requirements.txt --without-hashes --output requirements.txt
以下コマンドでデプロイ
gcloud functions deploy google_cloud上の関数名 `
--region=asia-northeast1 `
--gen2 `
--runtime=python39 `
--source=main.pyがあるファイル `
--entry-point=check_new_bids(デプロイしたい関数) `
--trigger-http `
--service-account=サービスアカウント
10. cloudschedulerとslackAPIの設定
10-1. cloudscheduler設定
1 googlecloudのCloudschedulerを開く
2 ジョブを作成をクリック

1 任意の名前を入力
2 リージョンは**asia-northeast1 (東京)**を選択
3 頻度は、(0 11 * * *)これで毎日11時に定期実行
4 タイムゾーン 日本標準時(JST) を選択
5 続行をクリック

1 ターゲットタイプはHTTPを選択
2 URLは、APIで公共工事入札案件の自動取得してslackへ通知するコードのURLを貼り付け
3 HTTPメゾッドはPOSTを選択
4 AuthヘッダーはOIDC トークンを追加を追加
5 サービスアカウントは、以前作成したものを選択
6 続行を押して、作成をクリック

10-2.「cvsを送って」に反応できるように設定
1 SlackAPIの左のメニューよりEvent Subscriptionsを選択
2 右上をOnにする
3 Subscribe to bot eventsを選択
4 message.channelsを選択

Cloud RunのURLを確認
slack-send-scvの関数のURLの後ろにslack/eventsを足す
例https://asia-northeast1-tidal-guild-475507-k6.cloudfunctions.net/slack_send_csv/slack/events
Request URLへ貼り付け
save changesをクリック

11. 開発してみての振り返り
今回の開発で一番苦労した点は、SlackAPIの設定やgooglecloudの設定の部分です。
今までクラウドで開発したことなく、すべてローカルでの開発だったため、エラーは基本的にターミナル上に記載されるので、そのエラーを調べて解決していくやり方でやっていましたが、クラウドの設定や、SlackAPIの設定が原因でエラーが出た場合は、ターミナル上では真の原因を突き止めるのは難しく、クラウドであればログを確認、SlackAPIであれば教材動画を見直し確認しながらやっていきました。
特にログの部分は複雑すぎて読み取ることができなかったので、ログをjson形式でコピーしてchatGPTにペーストして、エラーの原因が何通りかあるとヒントをもらいながら進めていくことで何とか完成させることができました。
ログも読めるように日々トレーニングしていきたいです。
この度Qitta記事を書いたことで、クラウドやSlackAPIの設定のやり方を1つ1つ細切れにしてアウトプットできたので頭の中が整理されて理解が深まりました。
12. 今後の展望
現段階の本システムは、定期通知とwebhookの2つの機能となっており、特にwebhookに関しては、
「csvを送って」のメッセージで直近の100件をまとめてCSVを出力する形となっています。
今後は、以下のような機能を追加していけたらと考えています。
・100件で縛らずユーザーが指定する数を取得csvにして送る
・ユーザーが指定する任意の都道府県のcsvを送る
・ユーザーが指定した市町村でフィルターをかけた値のcsvを送る
などよりインタラクティブなシステムにしていきたいです。













































