Python: 複数タレントの複数エリアのテレビ番組の出演情報の一気取得 | むかし日記

むかし日記

僕の古い日記です。

【目的】

Gガイド番組表にて,東京と大阪エリアでのタレントのテレビ番組の出演情報を取得する。

 

【理由】 東京と大阪では似て非なるテレビ放送の状況にあるため。

- 微妙に放送時間が異なる番組があり,時間が長い方の番組を録画したい。例: 沸騰ワード10(大阪は短め)

- 東京のみ,大阪のみの番組がある。東京のみ: あざとくて何が悪いの?, 王様のブランチ,ノンストップ! 大阪のみ: おかべろ

 

【環境】

Windowsマシーンのwsl,Ubuntu 22.04

 

【作成】

gemini-2.5 にHTMLのソースを食べさせて,以下の指示

<gemini-2.5のチャット窓への入力>

タレントXの東京と大阪のテレビ出演予定の情報をhttps://bangumi.org/から取得するPythonコードを作成したい。

# HTML

https://bangumi.org/fetch_search_content/?q="タレント名"&area_code=n

例えば,タレントが「吉岡里帆」で,エリアが東京の場合,

「https://bangumi.org/fetch_search_content/?q="吉岡里帆"&area_code=26」のソースに番組情報が書き込まれている。

# メインプログラム

- Step 1 東京エリアの番組情報取得

「view-source:https://bangumi.org/fetch_search_content/?q="タレント名"&area_code=26」を取得する。

- Step 2 「番組情報の取得方法」のプログラムによって,東京エリアの番組情報(配列変数)を取得する。

- Step 3 大阪エリアの番組情報取得

「view-source:https://bangumi.org/fetch_search_content/?q="タレント名"&area_code=40」を取得する。

- Step 4 「番組情報の取得方法」のプログラムによって,大阪エリアの番組情報(配列変数)を取得する。

- Step 5 東京エリアの番組情報(配列変数)と大阪エリアの番組情報(配列変数)を結合して,同じタレント,同じ日時,同じ番組タイトルのデータが見つかった場合,東京エリアのデータを削除する。

- Step 6 Step 5で得られた番組情報(配列変数)をcsv出力する。ヘッダーは不要である。

# 番組情報の取得方法

- Step 1 文字列「<li class="block"」と文字列「</li>」に挟まれた文字列を取得する。

- Step 2 Step 1で得られた文字列の中から1番目に見つけられる「<p class="repletion">...</p>」の文字列「...」が番組タイトルである。番組タイトルには,🈖,🈑,🈓,🈡などの四角囲み文字が含まれる場合があるので,四角囲い文字はすべて削除する。

- Step 3 Step 1で得られた文字列の中から2番目に見つけられる「<p class="repletion">...</p>」の文字列「...」がスペース区切りで日付,曜日,時刻,放送局名の4つの文字列を含んだ文字列である。

- Step 4 Step 3で得られた文字列「...」を日付,曜日,時刻,放送局名にそれぞれ分割する。ここで注意すべき点は,文字列「...」はスペース区切りで5つ以上の文字列に分離される場合がある。その場合,4番目以降の文字列を分離せずにすべて放送局名とする。

- Step 5 Step 4で得られた日付,曜日,時刻の文字列を加工して,1つの文字列”日時”とする。例えば,「2月3日」,「水曜日」,「22:00」であるなら,「2月3日(水)22:00」とする。

- Step 6 タレント,日時,放送局,番組タイトルの情報を配列変数に入れる。

# 複数タレントでの実行

- Step 1 タレントを”有村架純”,”松本穂香”,”吉岡里帆”,”本田翼”,”奈緒”,”小芝風花”,”堀田真由”,”吉高由里子”,”川口春奈”,”浜辺美波”,”福本莉子”,”松本まりか”,”田中みな実”,”白石聖”,”桜田ひより”,”志田未来”,”畑芽育”,”高橋ひかる”の18名でメインプログラムを繰り返して実行する。

 

【準備】

bashで,pipのアップグレードと必要なライブラリのインストール

python3 -m pip install --upgrade pip

pip install requests beautifulsoup4 pandas

 

【Pythonの実行】

python3 Get_TV_programs.py

 

【出力CSVの表示】

cat talent_tv_schedule.csv

"有村架純","11月22日(土)9:30","NHK総合1・水戸","連続テレビ小説「ひよっこ」 総集編 後編"
"吉岡里帆","11月20日(木)1:25","NHK総合1・大阪","Dearにっぽん「馬と紡ぐ 僕らの夢〜北海道・厚真町〜」"
"吉岡里帆","11月22日(土)0:50","NHK総合1・大阪","【夜ドラ】ひらやすみ(10)"
"吉岡里帆","11月22日(土)21:00","映画・chNECO","怪物の木こり【亀梨和也 菜々緒 吉岡里帆出演】◆NECO初◆"
"奈緒","11月20日(木)0:36","テレビ大阪1","「推しが上司になりまして フルスロットル」#7 キスからのヲタバレ…告白!"
"奈緒","11月22日(土)0:00","WOWOWプライム","塀の中の美容室 #1 「第1話」"
"奈緒","11月22日(土)0:30","WOWOWプライム","塀の中の美容室 #2 「第2話」"
"奈緒","11月22日(土)1:00","WOWOWプライム","塀の中の美容室 #3 「第3話」"
"奈緒","11月22日(土)1:30","WOWOWプライム","塀の中の美容室 #4 「第4話」"
"奈緒","11月22日(土)2:00","WOWOWプライム","塀の中の美容室 #5 「第5話」"
"奈緒","11月22日(土)2:30","WOWOWプライム","塀の中の美容室 #6 「第6話」"
"奈緒","11月22日(土)3:00","WOWOWプライム","塀の中の美容室 #7 「最終話」"
"奈緒","11月22日(土)8:00","ABCテレビ1","朝だ!生です旅サラダ"
"奈緒","11月22日(土)9:30","NHKEテレ1大阪","ミニアニメ のりものまん モービルランドのカークン「カーフェリーで海の旅」"
"奈緒","11月23日(日)3:30","WOWOWシネマ","傲慢と善良"
"奈緒","11月23日(日)19:00","BSテレ東","脳科学弁護士 海堂梓 ダウト"
"奈緒","11月23日(日)21:00","NHKEテレ1大阪","クラシック音楽館 N響 第2047回定期公演"
"小芝風花","11月20日(木)1:59","関西テレビ1","転職の魔王様 #06 魔王と弟子VS転職王子 ゲスト:宮野真守"
"小芝風花","11月20日(木)13:50","フジテレビ","大奥 #09"
"小芝風花","11月20日(木)17:45","WOWOWプライム","ツイスターズ(吹替版)"
"小芝風花","11月20日(木)19:54","日テレ1","まもなくぐるナイ残り3戦秋の私服ゴチでAIがガチ採点20万超自腹で順位変動が?"
"小芝風花","11月20日(木)20:00","読売テレビ1","ぐるナイ残り3戦!秋の私服ゴチでAIがガチ採点!20万超自腹で順位変動が!?"
"小芝風花","11月21日(金)13:50","フジテレビ","大奥 #10"
"小芝風花","11月22日(土)20:00","NHK BS8K","青 金 緑 平山郁夫 色彩と人生"
"小芝風花","11月24日(月)13:50","フジテレビ","大奥 #11"
"小芝風花","11月24日(月)16:00","テレ朝チャンネル1","[新]モコミ〜彼女ちょっとヘンだけど〜 #1"
"小芝風花","11月25日(火)16:00","テレ朝チャンネル1","モコミ〜彼女ちょっとヘンだけど〜 #2"
"小芝風花","11月25日(火)19:25","映画・chNECO","新・ミナミの帝王18 バイトテロの誘惑(千原ジュニア主演)"
"小芝風花","11月26日(水)16:00","テレ朝チャンネル1","モコミ〜彼女ちょっとヘンだけど〜 #3"
"堀田真由","11月20日(木)11:30","日テレプラス","若草物語―恋する姉妹と恋せぬ私― #2"
"堀田真由","11月21日(金)2:15","フジテレビ","明日夜!さんまのまんま【笑福亭鶴瓶にさんまとサンドが大説教!堤真一・山田裕貴】"
"堀田真由","11月21日(金)11:30","日テレプラス","若草物語―恋する姉妹と恋せぬ私― #3"
"堀田真由","11月21日(金)19:00","関西テレビ1","坂上どうぶつ王国 凶暴かみつき犬が甘えん坊ワンコに!奇跡の施設と最強ワンコ軍団"
"堀田真由","11月22日(土)23:00","読売テレビ1","アナザースカイ山下智久が挑戦の地・フランスのマルセイユへ!初めての人生の夏休み"
"堀田真由","11月24日(月)11:30","日テレプラス","若草物語―恋する姉妹と恋せぬ私― #4"
"堀田真由","11月25日(火)11:30","日テレプラス","若草物語―恋する姉妹と恋せぬ私― #5"
"堀田真由","11月26日(水)11:30","日テレプラス","若草物語―恋する姉妹と恋せぬ私― #6"
"堀田真由","11月26日(水)21:05","フジテレビTWO","イチケイのカラス スペシャル   あの型破りな裁判官・入間みちおが帰ってきた!"
"吉高由里子","11月20日(木)22:55","ファミリー劇場","ガリレオ(2013)【連日】#1"
"吉高由里子","11月22日(土)2:15","フジテレビ","私が恋愛できない理由 #01"
"吉高由里子","11月22日(土)10:00","テレ朝チャンネル1","星降る夜に #4"
"吉高由里子","11月22日(土)11:00","テレ朝チャンネル1","星降る夜に #5"
"吉高由里子","11月23日(日)10:00","テレ朝チャンネル1","星降る夜に #6"
"吉高由里子","11月23日(日)11:00","テレ朝チャンネル1","星降る夜に #7"
"吉高由里子","11月23日(日)17:25","テレビ朝日","未来につなぐエール"
"吉高由里子","11月23日(日)18:54","BS朝日 4K","未来につなぐエール"
"吉高由里子","11月24日(月)22:55","ファミリー劇場","ガリレオ(2013)【連日】#2"
"吉高由里子","11月25日(火)22:55","ファミリー劇場","ガリレオ(2013)【連日】#3"
"吉高由里子","11月26日(水)22:55","ファミリー劇場","ガリレオ(2013)【連日】#4"
"川口春奈","11月23日(日)16:00","BS朝日 4K","大改造!!劇的ビフォーアフター スペシャル 「母親と同居できない家」"
"浜辺美波","11月19日(水)21:54","フジテレビ","もし楽8話放送直前!!怒涛の後半戦に向けて、クベとリカをまるっとふり返りSP!"
"浜辺美波","11月19日(水)22:00","フジテレビ","もしもこの世が舞台なら、楽屋はどこにあるのだろう「八分坂の対決」 #08"
"浜辺美波","11月19日(水)22:00","関西テレビ1","もしもこの世が舞台なら、楽屋はどこにあるのだろう #08「八分坂の対決」"
"浜辺美波","11月20日(木)12:55","テレ朝チャンネル1","アリバイ崩し承ります #6"
"浜辺美波","11月21日(金)12:40","テレ朝チャンネル1","アリバイ崩し承ります #7[終]"
"浜辺美波","11月26日(水)21:54","フジテレビ","放送直前!もし楽9話見どころ!!(仮)"
"浜辺美波","11月26日(水)22:00","関西テレビ1","もしもこの世が舞台なら、楽屋はどこにあるのだろう #09"
"福本莉子","11月21日(金)12:30","WOWOWプライム","ストロボ・エッジ  Season1 #3 「第3話」"
"福本莉子","11月21日(金)19:30","NHK BSP4K","【BS時代劇】小吉の女房2(4)「麟太郎、ナポレオンと出会う」"
"福本莉子","11月21日(金)23:00","WOWOWプライム","ストロボ・エッジ  Season1 #4 「第4話」"
"福本莉子","11月23日(日)18:45","NHK BS","【BS時代劇・選】小吉の女房2(5)「お信、娘義太夫になる」"
"松本まりか","11月20日(木)9:55","テレ朝チャンネル1","ドクターX〜外科医・大門未知子〜(2019) #8"
"松本まりか","11月21日(金)19:00","日テレ1","沸騰ワード芦田愛菜がガチ解説!イタリア世界遺産旅!ベネチアの謎に岡田将生も迫る"
"田中みな実","11月21日(金)22:00","MBS毎日放送","金曜ドラマ「フェイクマミー」第7話""偽りの母親がいる""怪文書と不穏なキャンプ"
"田中みな実","11月22日(土)18:36","NHK総合1・東京","悪女について 再放送PR"
"田中みな実","11月22日(土)22:50","NHK総合1・水戸","悪女について 再放送PR"
"田中みな実","11月23日(日)1:25","NHK総合1・大阪","悪女について 再放送PR"
"田中みな実","11月23日(日)14:58","NHK総合1・大阪","悪女について 再放送PR"
"田中みな実","11月23日(日)23:00","NHK総合1・東京","悪女について(前編)"
"田中みな実","11月24日(月)22:31","NHK総合1・大阪","悪女について 再放送PR"
"田中みな実","11月25日(火)1:50","NHK総合1・大阪","悪女について 再放送PR"
"田中みな実","11月26日(水)23:45","NHK総合1・大阪","悪女について 再放送PR"
"桜田ひより","11月19日(水)22:00","日テレ1","ESCAPE それは誘拐のはずだった#07 令嬢出生の秘密が明らかに!"
"桜田ひより","11月19日(水)22:00","読売テレビ1","ESCAPE それは誘拐のはずだった#07 令嬢出生の秘密が明らかに!"
"桜田ひより","11月19日(水)23:00","NHK総合1・大阪","LIFE!ヒットパレード ムロツヨシがトーク参戦 ムロ思い出の作品をたっぷりと"
"桜田ひより","11月23日(日)14:20","NHK総合1・大阪","LIFE!秋 1分PR"
"桜田ひより","11月24日(月)0:10","NHK総合1・大阪","LIFE!夏 江口のりこ3年ぶり登場!痛快にツッコミ倒す江口に内村大喜び"
"桜田ひより","11月24日(月)5:14","NHK総合1・水戸","LIFE!秋 1分PR"
"桜田ひより","11月24日(月)12:27","NHK総合1・大阪","LIFE!秋 1分PR"
"桜田ひより","11月24日(月)18:39","NHK総合1・大阪","LIFE!秋 1分PR"
"桜田ひより","11月24日(月)21:30","NHK総合1・大阪","LIFE!秋 ムロツヨシが1年ぶりに登場 高校生たちと一緒にコントを作る!"
"桜田ひより","11月26日(水)22:00","日テレ1","ESCAPE それは誘拐のはずだった#08 桜田ひより×佐野勇斗/北村一輝"
"桜田ひより","11月26日(水)22:00","読売テレビ1","ESCAPE それは誘拐のはずだった#08桜田ひより×佐野勇斗/北村一輝"
"志田未来","11月19日(水)22:00","日テレ1","ESCAPE それは誘拐のはずだった#07 令嬢出生の秘密が明らかに!"
"志田未来","11月19日(水)22:00","読売テレビ1","ESCAPE それは誘拐のはずだった#07 令嬢出生の秘密が明らかに!"
"志田未来","11月21日(金)5:00","テレ朝チャンネル1","[新]ゆりあ先生の赤い糸 #1"
"志田未来","11月22日(土)11:05","映画・chNECO","『監察医 朝顔(2020)』#10-14【一挙】◆上野樹里主演"
"志田未来","11月22日(土)16:00","映画・chNECO","『監察医 朝顔(2020)』#15-19【一挙】◆上野樹里主演"
"志田未来","11月23日(日)12:20","映画・chNECO","監察医 朝顔 2022SP【上野樹里主演】"
"志田未来","11月24日(月)5:00","テレ朝チャンネル1","ゆりあ先生の赤い糸 #2"
"志田未来","11月24日(月)10:15","映画・chNECO","堂場瞬一サスペンス ラストライン 刑事 岩倉剛"
"志田未来","11月25日(火)5:00","テレ朝チャンネル1","ゆりあ先生の赤い糸 #3"
"志田未来","11月26日(水)5:00","テレ朝チャンネル1","ゆりあ先生の赤い糸 #4"
"志田未来","11月26日(水)22:00","日テレ1","ESCAPE それは誘拐のはずだった#08 桜田ひより×佐野勇斗/北村一輝"
"志田未来","11月26日(水)22:00","読売テレビ1","ESCAPE それは誘拐のはずだった#08桜田ひより×佐野勇斗/北村一輝"
"畑芽育","11月19日(水)18:28","NHK BSP4K","【プレミアムドラマ】終活シェアハウス PR"
"畑芽育","11月20日(木)4:59","NHK BS","【プレミアムドラマ】終活シェアハウス PR"
"畑芽育","11月20日(木)11:30","日テレプラス","若草物語―恋する姉妹と恋せぬ私― #2"
"畑芽育","11月20日(木)21:57","NHK BSP4K","【プレミアムドラマ】終活シェアハウス PR"
"畑芽育","11月21日(金)11:30","日テレプラス","若草物語―恋する姉妹と恋せぬ私― #3"
"畑芽育","11月21日(金)21:04","NHK BSP4K","【プレミアムドラマ】終活シェアハウス PR"
"畑芽育","11月22日(土)23:30","NHK BS","ドラマ 終活シェアハウス(5)"
"畑芽育","11月23日(日)8:28","NHK BSP4K","【プレミアムドラマ】終活シェアハウス PR"
"畑芽育","11月23日(日)22:00","NHK BSP4K","ドラマ 終活シェアハウス(6)"
"畑芽育","11月24日(月)11:30","日テレプラス","若草物語―恋する姉妹と恋せぬ私― #4"
"畑芽育","11月25日(火)11:14","NHK BSP4K","【プレミアムドラマ】終活シェアハウス PR"
"畑芽育","11月25日(火)11:30","日テレプラス","若草物語―恋する姉妹と恋せぬ私― #5"
"畑芽育","11月26日(水)11:30","日テレプラス","若草物語―恋する姉妹と恋せぬ私― #6"
"畑芽育","11月26日(水)18:28","NHK BSP4K","【プレミアムドラマ】終活シェアハウス PR"
"高橋ひかる","11月23日(日)7:30","テレビ大阪1","ポケモンとどこいく!?【パンサー、ポルカ雫が登場!アニポケ新EDを生歌唱!】"

 

【Pythonコード(Get_TV_programs.py)】

# -*- coding: utf-8 -*-
import requests
from bs4 import BeautifulSoup
import re
import pandas as pd
import time
import csv 
from typing import List, Dict, Any
from datetime import datetime
import locale # 💡 追加: 日付のパースに必要

# ロケールを設定することで、strftimeの月や曜日の処理が日本語に対応
# Windows WSL/Linux環境の場合
try:
    locale.setlocale(locale.LC_TIME, 'ja_JP.UTF-8')
except locale.Error:
    # ロケール設定に失敗した場合(Windows環境など)
    try:
        locale.setlocale(locale.LC_TIME, 'Japanese_Japan.932')
    except locale.Error:
        print("⚠️ ロケール設定に失敗しました。日時ソートが不安定になる可能性があります。")


# ==============================================================================
# 設定 (変更なし)
# ==============================================================================
TALENTS = [
    "有村架純", "松本穂香", "吉岡里帆", "本田翼", "奈緒", "小芝風花", "堀田真由",
    "吉高由里子", "川口春奈", "浜辺美波", "福本莉子", "松本まりか", "田中みな実",
    "白石聖", "桜田ひより", "志田未来", "畑芽育", "高橋ひかる"
]

AREA_CODES = {
    "東京": "26",
    "大阪": "40"
}

BASE_URL = "https://bangumi.org/fetch_search_content/"

# ==============================================================================
# 番組情報の取得方法 (clean_title, parse_datetime_and_station, fetch_program_infoは変更なし)
# ==============================================================================

def clean_title(title: str) -> str:
    """
    番組タイトルから四角囲み文字や記号を削除し、タイトルがない場合は代替テキストを返す。
    """
    pattern = re.compile(
        r'[\u2460-\u24FF\u3200-\u32FF\U0001F100-\U0001F2FF\U0001F300-\U0001F5FF\U0001F700-\U0001FAFF\U0001F000-\U0001F02F🈖🈑🈓🈡🈞]',
        re.UNICODE
    )
    
    cleaned_title = pattern.sub('', title).strip()
    
    if not cleaned_title:
        return "(番組タイトル不明)"
        
    return cleaned_title

def parse_datetime_and_station(repletion_text: str) -> Dict[str, str]:
    """
    日付、曜日、時刻、放送局名を含む文字列を分割・整形する。
    """
    parts = repletion_text.split()
    
    if len(parts) < 4:
        return None

    date_str = parts[0]
    day_of_week = parts[1].replace('曜日', '').replace('曜', '') 
    time_str = parts[2]
    
    station_name = " ".join(parts[3:])

    # 日時を1つの文字列に加工 (ソート用にもこの形式を保持)
    datetime_str = f"{date_str}({day_of_week[0]}){time_str}" 

    return {
        "日時": datetime_str,
        "放送局": station_name
    }

def fetch_program_info(talent_name: str, area_name: str) -> List[Dict[str, str]]:
    """
    特定のタレントとエリアの番組情報を取得・整形する。
    (中略:ロジック変更なし)
    """
    area_code = AREA_CODES.get(area_name)
    if not area_code:
        print(f"⚠️ エリア名 '{area_name}' のエリアコードが見つかりません。")
        return []

    url = f'{BASE_URL}?q="{talent_name}"&area_code={area_code}'
    
    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status() 
        response.encoding = response.apparent_encoding
    except requests.exceptions.RequestException as e:
        print(f"❌ {area_name} のデータ取得中にエラーが発生しました: {e}")
        return []

    soup = BeautifulSoup(response.text, 'html.parser')
    program_list = []
    
    list_items = soup.select('#tv-content ul.list-style-1 li.block')
    
    for li in list_items:
        try:
            repletion_paragraphs = li.find_all('p', class_='repletion')
            if not repletion_paragraphs or len(repletion_paragraphs) < 2:
                continue

            raw_title = repletion_paragraphs[0].get_text()
            title = clean_title(raw_title)

            raw_datetime_station = repletion_paragraphs[1].get_text()
            
            parsed_data = parse_datetime_and_station(raw_datetime_station)
            
            if parsed_data:
                program_list.append({
                    "タレント名": talent_name,
                    "日時": parsed_data["日時"],
                    "放送局": parsed_data["放送局"],
                    "番組タイトル": title,
                    "エリア": area_name 
                })
        except Exception as e:
            continue

    print(f"✅ {talent_name} の {area_name} エリアで {len(program_list)} 件の番組情報を見つけました。")
    return program_list


# ==============================================================================
# ヘルパー関数: 日時文字列をパースする
# ==============================================================================

def parse_japanese_datetime(datetime_str: str) -> datetime | None:
    """
    'M月D日(W)H:MM' 形式の日本語日時文字列をdatetimeオブジェクトに変換する。
    ※ 年情報がないため、直近の未来の日付と仮定する必要があるが、今回は月日と時刻のみでソートする
       pandasの機能を使用してパースする。
    """
    # 形式例: 11月22日(土)9:30
    
    # 曜日を一時的に削除し、pandasのto_datetimeでパースしやすい形式に変換
    # 曜日部分 '(.)' を削除し、年として仮に現在の年を付与
    match = re.search(r'(\d+月\d+日)\(\D\)(\d{1,2}:\d{2})', datetime_str)
    if not match:
        return None
    
    date_part = match.group(1) # 例: 11月22日
    time_part = match.group(2) # 例: 9:30

    # 年がないため、データ取得時の年を付与してパースを試みる
    current_year = datetime.now().year
    # pandasのto_datetimeに処理を任せるため、一時的な文字列を作成
    date_time_temp_str = f"{current_year}年{date_part} {time_part}"
    
    try:
        # '年'、'月'、'日' を持つ日本語の日時形式をパース
        dt_object = pd.to_datetime(date_time_temp_str, format='%Y年%m月%d日 %H:%M')
        
        # 取得日が今日より大幅に未来の場合、前年の日付の可能性を考慮
        # (例: 12月に翌年1月の番組を取得した場合など)
        if dt_object > datetime.now() + pd.Timedelta(days=60):
             dt_object = pd.to_datetime(f"{current_year-1}年{date_part} {time_part}", format='%Y年%m月%d日 %H:%M')
        
        return dt_object
    except Exception:
        return None


# ==============================================================================
# メインプログラム (ソート処理を追加)
# ==============================================================================

def process_talent_data(talent_name: str) -> List[Dict[str, str]]:
    """
    特定のタレントについて、東京・大阪の番組情報を取得し、重複を排除し、日時でソートする。
    """
    print(f"--- {talent_name} の番組情報を処理中 ---")
    
    # Step 1-4: データ取得と結合 (変更なし)
    tokyo_programs = fetch_program_info(talent_name, "東京")
    time.sleep(1) 
    osaka_programs = fetch_program_info(talent_name, "大阪")

    all_programs_df = pd.DataFrame(tokyo_programs + osaka_programs)

    if all_programs_df.empty:
        print(f"💡 {talent_name} の番組情報は見つかりませんでした。")
        return []

    # Step 5-A: 重複排除のための準備
    all_programs_df['sort_key'] = all_programs_df['エリア'].apply(lambda x: 0 if x == '東京' else 1)
    all_programs_df = all_programs_df.sort_values(by='sort_key', ascending=True)

    # Step 5-B: 重複排除 (keep='last'で大阪のデータを優先)
    deduplicated_df = all_programs_df.drop_duplicates(
        subset=["日時", "番組タイトル"], 
        keep='last'
    ).copy() # SettingWithCopyWarningを避けるため.copy()を追加

    # 💡 修正点: 日時ソートのための新しい列を追加
    deduplicated_df['ソート日時'] = deduplicated_df['日時'].apply(parse_japanese_datetime)

    # 💡 修正点: 'ソート日時'列でソート
    deduplicated_df = deduplicated_df.sort_values(by='ソート日時', ascending=True)
    
    # 不要な列を削除
    final_df = deduplicated_df.drop(columns=['エリア', 'sort_key', 'ソート日時'])
    
    print(f"✨ {talent_name} の重複排除・ソート後、合計 {len(final_df)} 件の番組情報が確定しました。")
    return final_df.to_dict('records')


# ==============================================================================
# 実行部分 (変更なし)
# ==============================================================================

if __name__ == "__main__":
    
    all_final_programs: List[Dict[str, Any]] = []

    for talent in TALENTS:
        result = process_talent_data(talent)
        all_final_programs.extend(result)
        time.sleep(2) 

    # Step 6: CSV出力
    if all_final_programs:
        final_df = pd.DataFrame(all_final_programs)
        output_filename = "talent_tv_schedule.csv"
        
        final_df.to_csv(
            output_filename, 
            index=False, 
            header=False, 
            encoding='utf-8',
            quoting=csv.QUOTE_ALL
        )
        
        print("\n=================================================================")
        print(f"🎉 全てのタレントの番組情報を {output_filename} に出力しました。")
        print(f"総件数: {len(final_df)} 件")
        print("=================================================================")
    else:
        print("\n🚨 処理を完了しましたが、出力すべき番組情報はありませんでした。")