Spaces:
Running
Running
| import os | |
| import time | |
| import json | |
| import requests | |
| import hashlib | |
| from datetime import datetime | |
| from bs4 import BeautifulSoup | |
| import xml.etree.ElementTree as ET | |
| # ===== Channel.io 設定 ===== | |
| CHANNEL_ID = "200605" | |
| GROUP_ID = "534457" | |
| POST_URL = f"https://desk-api.channel.io/desk/channels/{CHANNEL_ID}/groups/{GROUP_ID}/messages" | |
| X_ACCOUNT = os.getenv("channeliotokenbot2") | |
| if not X_ACCOUNT: | |
| raise RuntimeError("環境変数 channeliotokenbot2 が設定されていません") | |
| HEADERS_POST = { | |
| "accept": "application/json", | |
| "accept-language": "ja", | |
| "content-type": "application/json", | |
| "x-account": X_ACCOUNT, | |
| } | |
| # ===== RSS ===== | |
| RSS_URL = "https://www.nippon.com/ja/rss-all/" | |
| # ===== 設定 ===== | |
| SENT_LOG_FILE = "sent_nippon_news.json" | |
| LAST_RUN_FILE = "last_run.json" | |
| TARGET_TIMES = {"06:30", "18:30"} # 朝・夕方 | |
| # ===== Utils ===== | |
| def load_sent_log(): | |
| if not os.path.exists(SENT_LOG_FILE): | |
| return set() | |
| with open(SENT_LOG_FILE, "r", encoding="utf-8") as f: | |
| return set(json.load(f)) | |
| def save_sent_log(sent_set): | |
| with open(SENT_LOG_FILE, "w", encoding="utf-8") as f: | |
| json.dump(list(sent_set), f, ensure_ascii=False, indent=2) | |
| def load_last_run(): | |
| if not os.path.exists(LAST_RUN_FILE): | |
| return "" | |
| with open(LAST_RUN_FILE, "r", encoding="utf-8") as f: | |
| return json.load(f).get("last_run", "") | |
| def save_last_run(value): | |
| with open(LAST_RUN_FILE, "w", encoding="utf-8") as f: | |
| json.dump({"last_run": value}, f) | |
| def hash_link(link: str) -> str: | |
| return hashlib.sha256(link.encode("utf-8")).hexdigest() | |
| def send_to_channel(text): | |
| payload = { | |
| "requestId": f"desk-web-{int(time.time() * 1000)}", | |
| "blocks": [ | |
| {"type": "text", "value": text} | |
| ], | |
| } | |
| res = requests.post( | |
| POST_URL, | |
| headers=HEADERS_POST, | |
| data=json.dumps(payload), | |
| timeout=30 | |
| ) | |
| res.raise_for_status() | |
| def fetch_rss_items(): | |
| res = requests.get(RSS_URL, timeout=30) | |
| res.raise_for_status() | |
| root = ET.fromstring(res.content) | |
| channel = root.find("channel") | |
| if channel is None: | |
| return [] | |
| items = [] | |
| for item in channel.findall("item"): | |
| title = item.findtext("title", "").strip() | |
| link = item.findtext("link", "").strip() | |
| description_raw = item.findtext("description", "").strip() | |
| soup = BeautifulSoup(description_raw, "lxml") | |
| description = soup.get_text(strip=True) | |
| if title and link: | |
| items.append({ | |
| "title": title, | |
| "link": link, | |
| "description": description, | |
| }) | |
| return items | |
| # ===== Main ===== | |
| def run_job(): | |
| sent_log = load_sent_log() | |
| items = fetch_rss_items() | |
| new_count = 0 | |
| for item in items: | |
| link_hash = hash_link(item["link"]) | |
| if link_hash in sent_log: | |
| continue | |
| message = ( | |
| f"<link type=\"url\" value=\"{item['link']}\">" | |
| f"{item['title']}" | |
| f"</link>\n\n" | |
| f"{item['description']}" | |
| ) | |
| send_to_channel(message) | |
| sent_log.add(link_hash) | |
| new_count += 1 | |
| time.sleep(1) | |
| if new_count > 0: | |
| save_sent_log(sent_log) | |
| print(f"{new_count} 件のニュースを送信しました") | |
| else: | |
| print("新しいニュースはありません") | |
| def main(): | |
| last_run = load_last_run() | |
| while True: | |
| now = datetime.now() | |
| now_key = now.strftime("%Y-%m-%d %H:%M") | |
| now_time = now.strftime("%H:%M") | |
| if now_time in TARGET_TIMES and last_run != now_key: | |
| print(f"{now_time} 実行開始") | |
| try: | |
| run_job() | |
| save_last_run(now_key) | |
| except Exception as e: | |
| print("エラー:", e) | |
| time.sleep(60) # 1分ごとにチェック | |
| if __name__ == "__main__": | |
| main() | |