|
では、「美しい未来は美しい心から」プロジェクト用の
✅ 完全な仕様書
✅ ロリポップ+Python用コード群
をまとめて一式お渡しします。
そのままエンジニアさんに渡してもらって大丈夫なレベルにしてあります。
1. システム概要仕様
1-1. 目的
-
X(旧Twitter)とInstagramに、
「美しい未来は美しい心から」をテーマにした文章を
365日 × 朝昼夕 で自動投稿する。
-
投稿内容はあらかじめ用意したCSV・画像群から取得。
-
日本語アカウントは X+Instagram同時投稿。
-
英語・中国語は Xのみ自動投稿。
1-2. 対象SNS・アカウント
-
X(旧Twitter)
-
日本語公式アカウント(必須)
-
英語公式アカウント(任意)
-
中国語公式アカウント(任意)
-
Instagram
1-3. 投稿タイミング(JST)
-
朝:07:30(「朝」スロット)
-
昼:12:15(「昼」スロット)
-
夕方:20:30(「夕方」スロット)
ロリポップの cron 機能を用いて、
これらの時刻に Python スクリプトを起動する。
2. ディレクトリ構成仕様
ロリポップのホームディレクトリ配下の例:
/home/users/xxxx/xxxx/web/beautiful_future/ config.py post_ja_x_ig.py # 日本語X + Instagram 同時投稿 post_en_x.py # 英語X投稿 post_zh_x.py # 中国語X投稿 data/ ja_posts.csv # 日本語+祝日版CSV multi_lang_posts.csv # 英語・中国語版CSV logs/ post.log # 共通ログ public_images/ # Web公開ディレクトリ配下に配置推奨 morning/ ig_morning_01_yellow_sunrise.png ... ig_morning_10_yellow_smile.png noon/ ig_noon_01_blue_sky.png ... ig_noon_10_yellow_focus.png night/ ig_night_01_navy_calm.png ... ig_night_10_purple_moonlight.png
※ public_images は Web 公開パスと紐付くように配置し、
https://your-domain.com/beautiful_future/public_images/... でアクセス可能にする。
3. データ仕様
3-1. 日本語投稿CSV(data/ja_posts.csv)
カラム仕様
-
day_of_year : 1〜365(任意)
-
month : 1〜12(任意)
-
date_label : "MM-DD" (例:"01-01")※日付判定に使用
-
time_slot : "朝" / "昼" / "夕方"
-
text_ja : 日本語本文
-
category : 任意(感謝 / 自己対話 / …)
-
tags : 日本語ハッシュタグ(スペース区切り)
選択ロジック
-
実行時のJST日付 → date_label(MM-DD)に変換
-
実行スロット(朝 / 昼 / 夕方)に一致する行を1件取得
-
tweet_text = text_ja + " " + tags を基本形式とする
3-2. 多言語CSV(data/multi_lang_posts.csv)
カラム仕様
選択ロジック
4. 背景画像仕様(Instagram)
4-1. フォルダ構成
public_images/ morning/ ig_morning_01_yellow_sunrise.png ... ig_morning_10_yellow_smile.png noon/ ig_noon_01_blue_sky.png ... ig_noon_10_yellow_focus.png night/ ig_night_01_navy_calm.png ... ig_night_10_purple_moonlight.png
4-2. ファイル命名規則
ig_{slot}_{seq}_{color}_{keyword}.png
-
{slot} : morning / noon / night
-
{seq} : 01〜10
-
{color} : yellow / skyblue / navy など支配色
-
{keyword} : sunrise, calm などテーマ
朝・昼・夕の世界観は先ほど決めたカラーパレットに準拠。
4-3. Instagram投稿側挙動
5. 投稿フロー仕様
5-1. X & Instagram(日本語)
-
cronにより post_ja_x_ig.py が起動
-
JST現在日時から「朝/昼/夕方」スロット判定
-
ja_posts.csv から対象行を取得
-
Tweetテキストを生成し、X API(POST /2/tweets)で投稿
-
同じテキストをキャプションとして、
Instagram Graph API で
-
ログを logs/post.log に記録
5-2. X(英語・中国語)
-
cronにより post_en_x.py / post_zh_x.py が起動
-
JST現在日時から "Morning" / "Noon" / "Evening" を判定
-
multi_lang_posts.csv から対象行を取得
-
Tweetテキスト生成 → X APIで投稿
-
ログを logs/post.log に記録
6. 設定ファイル仕様:config.py
各スクリプトが共通で読み込む設定ファイル。
7. ロリポップ運用仕様
7-1. ファイル権限
-
post_*.py : 700 か 755
-
logs/ : 書き込み可能(755 など)
-
data/ : 読み取り可能(755)
7-2. cron設定例(日本語X+Instagram)
ロリポップ管理画面の「cron設定」から:
英語・中国語も同様に post_en_x.py, post_zh_x.py を登録。
8. セキュリティ
9. コード群
ここから実際の Python ファイルをまとめます。
すべてUTF-8で保存してください。
9-1. config.py
# config.py # 共通設定ファイル import os from datetime import timedelta, timezone # ===== タイムゾーン ===== JST = timezone(timedelta(hours=9)) # ===== ベースディレクトリ ===== BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.path.join(BASE_DIR, "data") LOG_DIR = os.path.join(BASE_DIR, "logs") if not os.path.exists(LOG_DIR): os.makedirs(LOG_DIR) LOG_FILE = os.path.join(LOG_DIR, "post.log") # ===== CSVパス ===== JA_CSV_PATH = os.path.join(DATA_DIR, "ja_posts.csv") MULTI_CSV_PATH = os.path.join(DATA_DIR, "multi_lang_posts.csv") # ===== X (Twitter) API 設定 ===== # 実際の値を環境変数または直書きで設定 X_BEARER_TOKEN_JA = "YOUR_JA_ACCOUNT_BEARER_TOKEN" X_BEARER_TOKEN_EN = "YOUR_EN_ACCOUNT_BEARER_TOKEN" X_BEARER_TOKEN_ZH = "YOUR_ZH_ACCOUNT_BEARER_TOKEN" X_TWEET_ENDPOINT = "https://api.twitter.com/2/tweets" # ===== Instagram Graph API 設定 ===== IG_USER_ID = "YOUR_IG_USER_ID" # InstagramビジネスアカウントID IG_ACCESS_TOKEN = "YOUR_IG_ACCESS_TOKEN" # 長期有効アクセストークン IG_API_BASE = "https://graph.facebook.com/v21.0" # あなたのドメインに変更 BASE_PUBLIC_URL = "https://your-domain.com/beautiful_future" IG_IMAGE_URLS = { "朝": [ f"{BASE_PUBLIC_URL}/public_images/morning/ig_morning_01_yellow_sunrise.png", f"{BASE_PUBLIC_URL}/public_images/morning/ig_morning_02_orange_morninglight.png", f"{BASE_PUBLIC_URL}/public_images/morning/ig_morning_03_skyblue_clear.png", f"{BASE_PUBLIC_URL}/public_images/morning/ig_morning_04_lightblue_fresh.png", f"{BASE_PUBLIC_URL}/public_images/morning/ig_morning_05_white_minimal.png", f"{BASE_PUBLIC_URL}/public_images/morning/ig_morning_06_pink_softdawn.png", f"{BASE_PUBLIC_URL}/public_images/morning/ig_morning_07_gold_horizon.png", f"{BASE_PUBLIC_URL}/public_images/morning/ig_morning_08_skyblue_cloud.png", f"{BASE_PUBLIC_URL}/public_images/morning/ig_morning_09_green_morningdew.png", f"{BASE_PUBLIC_URL}/public_images/morning/ig_morning_10_yellow_smile.png", ], "昼": [ f"{BASE_PUBLIC_URL}/public_images/noon/ig_noon_01_blue_sky.png", f"{BASE_PUBLIC_URL}/public_images/noon/ig_noon_02_cyan_energy.png", f"{BASE_PUBLIC_URL}/public_images/noon/ig_noon_03_white_clouds.png", f"{BASE_PUBLIC_URL}/public_images/noon/ig_noon_04_orange_active.png", f"{BASE_PUBLIC_URL}/public_images/noon/ig_noon_05_green_nature.png", f"{BASE_PUBLIC_URL}/public_images/noon/ig_noon_06_blue_momentum.png", f"{BASE_PUBLIC_URL}/public_images/noon/ig_noon_07_turquoise_refresh.png", f"{BASE_PUBLIC_URL}/public_images/noon/ig_noon_08_lightblue_clearair.png", f"{BASE_PUBLIC_URL}/public_images/noon/ig_noon_09_sunshine_bright.png", f"{BASE_PUBLIC_URL}/public_images/noon/ig_noon_10_yellow_focus.png", ], "夕方": [ f"{BASE_PUBLIC_URL}/public_images/night/ig_night_01_navy_calm.png", f"{BASE_PUBLIC_URL}/public_images/night/ig_night_02_darkblue_silent.png", f"{BASE_PUBLIC_URL}/public_images/night/ig_night_03_purple_relax.png", f"{BASE_PUBLIC_URL}/public_images/night/ig_night_04_black_minimal.png", f"{BASE_PUBLIC_URL}/public_images/night/ig_night_05_orange_sunset.png", f"{BASE_PUBLIC_URL}/public_images/night/ig_night_06_red_sunsetwarm.png", f"{BASE_PUBLIC_URL}/public_images/night/ig_night_07_darkgreen_deepforest.png", f"{BASE_PUBLIC_URL}/public_images/night/ig_night_08_blue_twilight.png", f"{BASE_PUBLIC_URL}/public_images/night/ig_night_09_gold_nightlight.png", f"{BASE_PUBLIC_URL}/public_images/night/ig_night_10_purple_moonlight.png", ], } # ===== 時刻テーブル (JST) ===== JA_TIME_TABLE = { "朝": (7, 30), "昼": (12, 15), "夕方": (20, 30), } EN_TIME_TABLE = { "Morning": (7, 30), "Noon": (12, 15), "Evening": (20, 30), }
9-2. 共通ユーティリティ(任意):common_utils.py
(必須ではないですが、コード整理用に)
# common_utils.py import json import csv import random from datetime import datetime import urllib.request import urllib.parse from config import ( JST, LOG_FILE, JA_CSV_PATH, MULTI_CSV_PATH, X_TWEET_ENDPOINT, IG_API_BASE, IG_ACCESS_TOKEN, IG_USER_ID, IG_IMAGE_URLS, ) def log(message: str): now = datetime.now(JST) line = f"[{now.isoformat()}] {message}" try: with open(LOG_FILE, "a", encoding="utf-8") as f: f.write(line + "\n") except Exception: print(line) def get_now_jst(): return datetime.now(JST) # ===== X API 共通 ===== def post_to_x(bearer_token: str, tweet_text: str) -> bool: if not bearer_token: log("ERROR: X bearer token not set") return False payload = {"text": tweet_text} data_bytes = json.dumps(payload).encode("utf-8") req = urllib.request.Request( X_TWEET_ENDPOINT, data=data_bytes, method="POST", headers={ "Authorization": f"Bearer {bearer_token}", "Content-Type": "application/json", } ) try: with urllib.request.urlopen(req) as resp: body = resp.read().decode("utf-8") log(f"X API response: {body}") return True except Exception as e: log(f"ERROR posting to X: {e}") return False # ===== Instagram API 共通 ===== def choose_random_image_url(slot: str): if slot not in IG_IMAGE_URLS: all_urls = [] for urls in IG_IMAGE_URLS.values(): all_urls.extend(urls) return random.choice(all_urls) if all_urls else None urls = IG_IMAGE_URLS[slot] return random.choice(urls) if urls else None def ig_create_media(image_url: str, caption: str): endpoint = f"{IG_API_BASE}/{IG_USER_ID}/media" params = { "image_url": image_url, "caption": caption, "access_token": IG_ACCESS_TOKEN, } data_bytes = urllib.parse.urlencode(params).encode("utf-8") req = urllib.request.Request(endpoint, data=data_bytes, method="POST") try: with urllib.request.urlopen(req) as resp: body = resp.read().decode("utf-8") log(f"IG create media response: {body}") res = json.loads(body) return res.get("id") except Exception as e: log(f"ERROR ig_create_media: {e}") return None def ig_publish_media(creation_id: str) -> bool: endpoint = f"{IG_API_BASE}/{IG_USER_ID}/media_publish" params = { "creation_id": creation_id, "access_token": IG_ACCESS_TOKEN, } data_bytes = urllib.parse.urlencode(params).encode("utf-8") req = urllib.request.Request(endpoint, data=data_bytes, method="POST") try: with urllib.request.urlopen(req) as resp: body = resp.read().decode("utf-8") log(f"IG publish media response: {body}") return True except Exception as e: log(f"ERROR ig_publish_media: {e}") return False def post_to_instagram(caption: str, slot: str) -> bool: if not IG_ACCESS_TOKEN or not IG_USER_ID: log("ERROR: IG_ACCESS_TOKEN or IG_USER_ID not set") return False image_url = choose_random_image_url(slot) if not image_url: log(f"IG: no image_url found for slot={slot}") return False log(f"IG: chosen image_url={image_url}") creation_id = ig_create_media(image_url, caption) if not creation_id: log("IG: creation_id is None, abort.") return False success = ig_publish_media(creation_id) return success # ===== CSV読み込み ===== def load_ja_post_for_today(slot: str, now: datetime): date_label = now.strftime("%m-%d") try: with open(JA_CSV_PATH, "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: if row.get("date_label") == date_label and row.get("time_slot") == slot: text = row.get("text_ja", "").strip() tags = row.get("tags", "").strip() tweet_text = text if tags: tweet_text = f"{text} {tags}" return tweet_text except FileNotFoundError: log(f"JA_CSV_PATH not found: {JA_CSV_PATH}") return None log(f"No JA row found for date={date_label}, slot={slot}") return None def load_multi_post_for_today(slot: str, now: datetime, lang: str): date_label = now.strftime("%m-%d") key = "text_en" if lang == "en" else "text_cn" try: with open(MULTI_CSV_PATH, "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: if row.get("date") == date_label and row.get("time_slot") == slot: text = row.get(key, "").strip() return text except FileNotFoundError: log(f"MULTI_CSV_PATH not found: {MULTI_CSV_PATH}") return None log(f"No {lang} row found for date={date_label}, slot={slot}") return None
9-3. 日本語 X + Instagram:post_ja_x_ig.py
#!/usr/local/bin/python3.7 # -*- coding: utf-8 -*- from config import JA_TIME_TABLE, X_BEARER_TOKEN_JA from common_utils import ( log, get_now_jst, post_to_x, post_to_instagram, load_ja_post_for_today, ) def detect_time_slot(now): h = now.hour m = now.minute for slot, (sh, sm) in JA_TIME_TABLE.items(): if h == sh and m == sm: return slot return None def main(): now = get_now_jst() slot = detect_time_slot(now) if slot is None: log("JA: No time slot matched (not 朝/昼/夕方). Exiting.") return log(f"JA: Detected slot={slot}") tweet_text = load_ja_post_for_today(slot, now) if not tweet_text: log("JA: No tweet_text found. Exiting.") return if len(tweet_text) > 270: log("JA: Tweet too long, truncating for X.") tweet_text_for_x = tweet_text[:270] + "…" else: tweet_text_for_x = tweet_text # X 投稿 log(f"JA: Posting to X: {tweet_text_for_x}") ok_x = post_to_x(X_BEARER_TOKEN_JA, tweet_text_for_x) if ok_x: log("JA: Tweet posted successfully.") else: log("JA: Tweet failed.") # Instagram 投稿(captionは全文) caption = tweet_text log(f"JA: Posting to Instagram (slot={slot})") ok_ig = post_to_instagram(caption, slot) if ok_ig: log("JA: Instagram post succeeded.") else: log("JA: Instagram post failed.") if __name__ == "__main__": main()
9-4. 英語 X 用:post_en_x.py
#!/usr/local/bin/python3.7 # -*- coding: utf-8 -*- from config import EN_TIME_TABLE, X_BEARER_TOKEN_EN from common_utils import ( log, get_now_jst, post_to_x, load_multi_post_for_today, ) def detect_time_slot(now): h = now.hour m = now.minute for slot, (sh, sm) in EN_TIME_TABLE.items(): if h == sh and m == sm: return slot return None def main(): now = get_now_jst() slot = detect_time_slot(now) if slot is None: log("EN: No time slot matched (not Morning/Noon/Evening). Exiting.") return log(f"EN: Detected slot={slot}") text = load_multi_post_for_today(slot, now, lang="en") if not text: log("EN: No text found. Exiting.") return tweet_text = f"{text} #BeautifulFuture #BeautifulMind" if len(tweet_text) > 270: log("EN: Tweet too long, truncating.") tweet_text = tweet_text[:270] + "…" log(f"EN: Posting to X: {tweet_text}") ok = post_to_x(X_BEARER_TOKEN_EN, tweet_text) if ok: log("EN: Tweet posted successfully.") else: log("EN: Tweet failed.") if __name__ == "__main__": main()
9-5. 中国語 X 用:post_zh_x.py
#!/usr/local/bin/python3.7 # -*- coding: utf-8 -*- from config import EN_TIME_TABLE, X_BEARER_TOKEN_ZH from common_utils import ( log, get_now_jst, post_to_x, load_multi_post_for_today, ) def detect_time_slot(now): h = now.hour m = now.minute for slot, (sh, sm) in EN_TIME_TABLE.items(): if h == sh and m == sm: return slot return None def main(): now = get_now_jst() slot = detect_time_slot(now) if slot is None: log("ZH: No time slot matched (not Morning/Noon/Evening). Exiting.") return log(f"ZH: Detected slot={slot}") text = load_multi_post_for_today(slot, now, lang="zh") if not text: log("ZH: No text found. Exiting.") return tweet_text = f"{text} #美好未来 #美好心念" if len(tweet_text) > 270: log("ZH: Tweet too long, truncating.") tweet_text = tweet_text[:270] + "…" log(f"ZH: Posting to X: {tweet_text}") ok = post_to_x(X_BEARER_TOKEN_ZH, tweet_text) if ok: log("ZH: Tweet posted successfully.") else: log("ZH: Tweet failed.") if __name__ == "__main__": main()
ここまでで、
が揃いました。
あとは、
-
config.py のトークンやドメインを実環境に合わせて書き換え
-
ja_posts.csv / multi_lang_posts.csv を data/ に配置
-
画像30枚を命名規則どおり public_images/ に配置
-
ロリポップの cron に post_ja_x_ig.py, post_en_x.py, post_zh_x.py を登録
すれば、システムとして動きます。
|