背景
ある企業のプレスリリースページを RSS を使って Teams に自動投稿していた。
ある時から、RSS 説明ページはそのままに、通知が停止してしまった。
半年近く待っても復活しなかったので、暫定対処してみた記録
結果的に、
今回は Azure Functions (Python) で JSON API を直接パース→新着差分を検出→Teams に Adaptive Card で投稿する構成を採用した。
概要
- RSS フィード調査(結果: 利用不可)
- SNS 系の通知を充実させたので、RSS は不要って判断されたのかもしれない。だが、説明ページは残ってるのが解せない
- SNS 系の通知を充実させたので、RSS は不要って判断されたのかもしれない。だが、説明ページは残ってるのが解せない
- 代替案として Azure Functions でスクレイピング実装
- HTML 構造を解析し、最新ニュースを JSON 化
- Blob Storage で既読管理し、新着のみ通知
- Logic Apps 経由で Teams にカード送信
作業の全体フロー
1. RSS フィード調査と方針決定
- 対象サイト(https://example.com/news/)に RSS/Atom リンクは存在せず
- 旧 RSS URL も現在アクセス不可
結論: HTML スクレイピングで最新ニュース一覧を取得し、差分検出で新着を判定
RSS が復活したら、以前のRSS方式の方が楽なので、今回のはゴミ箱行き
2. Azure Functions でスクレイピング実装
- News Page を調査したところ
requestsで JSON API を直接取得 - ページ HTML 内の
data-js-prod-newsjsonpath属性から JSON エンドポイントを特定 - JSON の
news配列からタイトル・日付・URL・カテゴリー・概要を抽出 - Blob Storage に過去取得済み ID を保存し、新着のみを通知対象とする
# shared/scraper_simple.py(抜粋)
class NewsScraperSimple:
def fetch_releases(self, max_results=10):
# JSON API を直接取得
json_url = 'https://example.com/.../news-article.json'
response = requests.get(json_url)
json_data = response.json()
news_items = json_data.get('news', [])
# タイトル・日付・URL を抽出
return releases
3. Timer Trigger で定期実行
- 平日(月〜金)午前 9 時(JST)に実行
-
function_app.pyのpr_checkerが起動し、スクレイパー→差分検出→Teams 投稿を実行
4. Logic Apps 経由で Teams に投稿
- Azure Functions から HTTP POST で Logic Apps を呼び出し
- Logic Apps が Office 365 Connector で Teams チャネルに Adaptive Card を投稿
- カードには日時・タイトル・本文・リンクボタンを含む
Adaptive Card のサンプル:
{
"type": "AdaptiveCard",
"version": "1.5",
"msteams": {"width": "Full"},
"body": [
{
"type": "TextBlock",
"text": "🔔 企業ニュース",
"weight": "Bolder",
"size": "Large"
},
{
"type": "TextBlock",
"text": "2025年12月11日 10:00",
"isSubtle": true,
"spacing": "None"
},
{
"type": "Container",
"separator": true,
"items": [
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": "auto",
"items": [
{
"type": "TextBlock",
"text": "📰",
"size": "Large"
}
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "TextBlock",
"text": "2025年12月11日",
"weight": "Bolder",
"color": "Accent"
},
{
"type": "TextBlock",
"text": "新製品発表のお知らせ - 次世代製品シリーズの開発完了に伴い、2025年第1四半期より販売を開始いたします。",
"wrap": true,
"spacing": "Small"
}
]
}
]
}
],
"selectAction": {
"type": "Action.OpenUrl",
"url": "https://example.com/news/2025/1211/001.html"
}
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json"
}
デプロイと検証の詳細
1. zip パッケージ作成
Windows 端末から Linux ホストへ出すのでリモートビルドを必須に。zip は素直に Compress-Archive で作成。
# ルート: AzureProjects\{project-name}
Remove-Item function_app.zip -ErrorAction SilentlyContinue
Compress-Archive -Path .\function_app\* -DestinationPath function_app.zip
Microsoft Learn でも、Windows で開発した Linux アプリはリモートビルドを使えと明記されている。「Windows コンピューター上で開発された Linux ベースの関数アプリに…デプロイしている。これは一般的に Python アプリ開発の場合です。」(Azure Functions のデプロイ テクノロジ)
2. config-zip デプロイ
.zip を config-zip で投入。--build-remote true でビルドをクラウド側に任せる。
az functionapp deployment source config-zip \
--name {function-app-name} \
--resource-group {resource-group-name} \
--src function_app.zip \
--build-remote true \
--timeout 600
Zip デプロイは消費/EHP/専用プランで「既定かつおすすめのデプロイ テクノロジ」と公式に書かれている (Azure Functions のデプロイ テクノロジ).
3. HTTP トリガーで動作確認
test_trigger を叩いて Teams 送信を確認。キーはクエリ文字列 ?code= かヘッダー x-functions-key に入れる。
Invoke-RestMethod -Method Get `
"https://{function-app-name}.azurewebsites.net/api/test_trigger?code={function-key}"
レスポンスは「pr_checker 実行完了。Teams チャネルを確認してください。」で返ってきたので、チャネル側のカード時刻と本文を確認して完了。
関数キーは平文で扱わない。ローカルではキー検証が無効になるが、Azure 上では必須(HTTP トリガーの承認レベル)。
システム構成
┌─────────────────────────────────────────────┐
│ Timer Trigger (平日 9:00 JST) │
└────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Azure Functions (Python 3.11) │
│ ┌───────────────────────────────────────┐ │
│ │ 1. JSON API パース │ │
│ │ - requests で JSON 取得 │ │
│ │ - 最新ニュース一覧を抽出 │ │
│ └───────────────────────────────────────┘ │
│ ┌───────────────────────────────────────┐ │
│ │ 2. 新着判定 │ │
│ │ - Blob Storage から履歴取得 │ │
│ │ - 差分検出 │ │
│ └───────────────────────────────────────┘ │
│ ┌───────────────────────────────────────┐ │
│ │ 3. Adaptive Card 生成 │ │
│ │ - Logic Apps に HTTP POST │ │
│ └───────────────────────────────────────┘ │
└────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Logic Apps (Consumption) │
│ - Office 365 Connector で Teams 投稿 │
└─────────────────────────────────────────────┘
ハマりどころメモ
1. 静的 HTML スクレイピングでは取れない問題(BeautifulSoup 不要だった)
症状: 当初 BeautifulSoup で HTML を解析したが、ニュースリンクが 0 件または言語切替リンク(English, Português)のみ取得
原因: ページが JavaScript で動的にニュース一覧を生成していた。<ul class="news-list" data-js-component="..."> に data-js-newsjsonpath 属性があり、JSON API から非同期でデータを読み込む仕様だった。
解決策:
- HTML ソース内に埋め込まれた JSON API エンドポイントを発見(
data-js-prod-newsjsonpath属性) -
https://example.com/global/common/.../news-article.jsonを直接 GET して JSON をパース -
news配列からtitle,url,date,categories,descriptionを取得
# 試行1: BeautifulSoup(失敗)
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, 'html.parser')
links = soup.select('ul.news-list li a') # → 空配列(動的生成のため取得不可)
# 最終実装: JSON API 直接取得(成功)
json_url = 'https://example.com/.../news-article.json'
json_data = requests.get(json_url).json()
news_items = json_data.get('news', []) # ← BeautifulSoup 不要
動的レンダリングサイトでは、まず DevTools の Network タブで API 通信を確認すると早い。静的スクレイピングで「リンクが取れない」「件数 0」になったら、まず JSON API の存在を疑う。
2. Playwright のインストール制限
症状: 当初 Playwright(ヘッドレス Chrome)で動的レンダリング対応を試みたが、Azure Functions Consumption プランでは Chromium バイナリサイズが大きくタイムアウト/デプロイ失敗
解決策: JSON API を見つけたため Playwright 不要に。もし将来 API が消えたら、以下の代替手段を検討:
- Selenium Wire(軽量)
- 外部スクレイピング API(ScrapingBee, Apify)
- Azure Container Instances で Playwright 実行
3. ローカル HTTP テストができない問題
症状: ローカル開発環境(func start)では、HTTP トリガーの認証が無効化されるため、本番と同じアクセスキー検証ができない
対策:
- ローカルでは
http://localhost:7071/api/{function-name}で直接叩く(キー不要) - 本番デプロイ後に
Invoke-RestMethodでキー付き URL をテスト - 本番環境での初回テストは
test_trigger関数を別途用意し、そこでpr_checkerをプログラムから呼び出す方式を採用
Azure Functions の HTTP トリガー には「機能をローカルで実行する場合、指定された認可レベルの設定に関係なく、許可は無効になります」と明記されている。
4. WEBSITE_RUN_FROM_PACKAGE 設定の競合
症状: az functionapp deployment source config-zip 実行時に以下のエラーが発生
Error: Conflict (409): The requested operation cannot be performed
because the site is configured to run from package.
原因の詳細:
WEBSITE_RUN_FROM_PACKAGE は ZIP パッケージからアプリを直接実行するための設定。この設定には 2 種類の値がある:
-
WEBSITE_RUN_FROM_PACKAGE=1:wwwrootを読み取り専用にして、ZIP から直接実行 -
WEBSITE_RUN_FROM_PACKAGE=<URL>: 外部 Blob URL から ZIP を取得して実行
この設定が有効な状態で config-zip デプロイを実行すると、以下の競合が発生:
-
config-zip の動作: ZIP を
D:\home\data\SitePackagesに配置し、展開してwwwrootにファイルを配置しようとする - WEBSITE_RUN_FROM_PACKAGE=1 の動作: ZIP を展開せず、そのまま読み取り専用でマウントする
- 結果: デプロイシステムが「展開すべきか、マウントすべきか」で競合し、409 エラーを返す
リモートビルド (--build-remote true) を使う場合は WEBSITE_RUN_FROM_PACKAGE を削除する
公式ドキュメント (Remote builds) には「To enable the same build processes that you get with continuous integration, add SCM_DO_BUILD_DURING_DEPLOYMENT=true to your application settings in your deployment code and remove the WEBSITE_RUN_FROM_PACKAGE entirely.」と明記されている。
リモートビルドは Linux 向けパッケージ(Python, Node.js など)を Azure 側でビルドするため、ローカル ZIP とは互換性がない。
解決策:
# 1. 競合する設定を削除
az functionapp config appsettings delete \
--name {function-app-name} \
--resource-group {resource-group-name} \
--setting-names WEBSITE_RUN_FROM_PACKAGE
# 2. リモートビルド付き zip デプロイを実行
az functionapp deployment source config-zip \
--name {function-app-name} \
--resource-group {resource-group-name} \
--src function_app.zip \
--build-remote true
結果: デプロイ成功後、Azure 側で以下が自動設定される:
-
SCM_DO_BUILD_DURING_DEPLOYMENT=1(ビルド有効化) -
ENABLE_ORYX_BUILD=true(Linux ビルドエンジン有効化) -
WEBSITE_RUN_FROM_PACKAGEは設定されない(wwwroot に直接展開)
5. 通知方式の選択(Webhook vs Graph API)
当初: Teams Incoming Webhook で簡単実装を試みる
問題点:
- 投稿者が常に「Incoming Webhook」になり、誰が投稿したか不明
- Webhook URL 流出リスク(誰でも投稿可能)
- 環境ごとに Webhook URL を管理する手間
最終方針: Microsoft Graph API + Managed Identity に変更
- 投稿者を User ID で指定可能
- Azure AD ログで監査可能
- 環境変数で投稿先を動的変更
- セキュリティ向上(URL 流出リスクなし)
6. その他の注意点
-
HTML 構造変更リスク: サイトリニューアルで CSS クラス名や JSON API URL が変わったら・・
- 230 秒タイムアウト: HTTP 要求は 230 秒で 502 を返すので、長時間処理は非同期パターンに逃がす(Azure Functions の制限事項より)
-
トリガー同期: config-zip では自動同期だが、外部パッケージ URL 方式では
syncfunctiontriggersAPI/ポータル再起動が必要
あとがき
RSS 停止という問題に対し、スクレイピング→差分検出→Teams 投稿の全フローを Azure Functions + Logic Apps で構築した。デプロイは zip 再生成→config-zip→HTTP テスト の 3 コマンドで完結するので、CI/CD にも載せやすい形になった。
最終的に、これを IaC 化して、社内展開できるようにしておいた。
なぜなら、クラウド勉強会用の題材にしようかなと思った為である ![]()
これこそが今回のやる気の源泉
将来 RSS が復活したら Power Automate に戻せば済むので、それまではスクレイピングでしのぐ、ってことで。