Nyanpre commited on
Commit
847dbda
·
verified ·
1 Parent(s): 310f1f2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +72 -42
app.py CHANGED
@@ -29,77 +29,89 @@ def analyze_and_output(my_id, my_pw, target_id, progress=gr.Progress()):
29
  while len(posts_data) < max_limit:
30
  response = client.get_author_feed(actor=profile.did, limit=100, cursor=cursor)
31
  for feed_view in response.feed:
32
- # 安全策1: 投稿自体が存在するか
33
- if not feed_view.post or not hasattr(feed_view.post, 'author'):
34
  continue
35
 
36
  # リポストの分析
37
  if feed_view.reason and hasattr(feed_view.reason, 'by'):
38
- # 安全策2: リポスト元の投稿者が存在するか
39
- if hasattr(feed_view.post, 'author') and feed_view.post.author:
40
- orig_author = feed_view.post.author.handle
41
- if orig_author != target_handle:
42
- repost_users.append(orig_author)
43
  continue
44
 
45
  post = feed_view.post
46
  if post.author.handle == target_handle:
 
 
 
 
47
  rkey = post.uri.split('/')[-1]
48
  post_url = f"https://bsky.app/profile/{target_handle}/post/{rkey}"
49
 
50
- posts_data.append({
51
- 'text': post.record.text if hasattr(post.record, 'text') else "",
52
- 'created_at': pd.to_datetime(post.record.created_at),
53
- 'likes': post.like_count,
54
- 'reposts': post.repost_count,
55
- 'score': (post.like_count or 0) + (post.repost_count or 0),
56
- 'url': post_url
57
- })
58
 
59
- if hasattr(post.record, 'text'):
60
- hashtags.extend(re.findall(r'#(\w+)', post.record.text))
 
 
 
 
 
 
 
 
 
 
61
 
62
- # 安全策3: リプライ相手の存在チェック
63
  if feed_view.reply and feed_view.reply.parent:
64
  parent = feed_view.reply.parent
65
  if hasattr(parent, 'author') and parent.author:
66
- parent_author = parent.author.handle
67
- if parent_author != target_handle:
68
- reply_users.append(parent_author)
69
 
70
  cursor = response.cursor
71
  if not cursor or len(posts_data) >= max_limit: break
72
  progress(min(len(posts_data)/max_limit * 0.5, 0.5), desc=f"{len(posts_data)}件取得中...")
73
 
74
  # --- 2. いいね一覧の分析 ---
75
- progress(0.6, desc="いいねした相手を分析中...")
76
  like_cursor = None
77
- for _ in range(2):
78
- try:
79
  likes_resp = client.get_actor_likes(actor=profile.did, limit=100, cursor=like_cursor)
80
  for like_item in likes_resp.feed:
81
- # 安全策4: いいね先の投稿者が存在するか
82
  if like_item.post and hasattr(like_item.post, 'author') and like_item.post.author:
83
- l_author = like_item.post.author.handle
84
- if l_author != target_handle:
85
- like_users.append(l_author)
86
  like_cursor = likes_resp.cursor
87
  if not like_cursor: break
88
- except:
89
- break # いいね取得でエラーが出ても止まらないようにする
90
 
91
- if not posts_data: return "有効な投稿データが見つかりませんでした", "", "", "終了"
 
92
 
93
- # --- 3. HTML作成 ---
94
  df = pd.DataFrame(posts_data)
95
- df = df.sort_values('created_at', ascending=True) # 古い順にして初投稿を確定
96
  first_post = df.iloc[0]
97
- days_active = max((datetime.now().replace(tzinfo=None) - first_post['created_at'].replace(tzinfo=None)).days, 1)
 
 
 
98
 
99
  stats_html = f"""
100
  <div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 20px;'>
101
  <div style='background: #e3f2fd; padding: 15px; border-radius: 10px; text-align: center; border: 1px solid #90caf9;'>
102
- <div style='color: #1565c0; font-size: 0.8em;'>総ポスト</div><div style='font-size: 1.5em; font-weight: bold;'>{profile.posts_count}</div>
103
  </div>
104
  <div style='background: #f1f8e9; padding: 15px; border-radius: 10px; text-align: center; border: 1px solid #c5e1a5;'>
105
  <div style='color: #33691e; font-size: 0.8em;'>継続日数</div><div style='font-size: 1.5em; font-weight: bold;'>{days_active}日</div>
@@ -111,9 +123,10 @@ def analyze_and_output(my_id, my_pw, target_id, progress=gr.Progress()):
111
  """
112
 
113
  def make_rank_list(title, icon, counter_list):
114
- html = f"<b>{icon} {title}</b><div style='font-size: 0.85em; color: #444; margin: 5px 0 15px 0;'>"
115
  if not counter_list: return html + "データなし</div>"
116
- for name, count in Counter(counter_list).most_common(3):
 
117
  html += f"<div>@{name} ({count}回)</div>"
118
  return html + "</div>"
119
 
@@ -122,18 +135,35 @@ def analyze_and_output(my_id, my_pw, target_id, progress=gr.Progress()):
122
  {make_rank_list("よくリプライする相手", "💬", reply_users)}
123
  {make_rank_list("よくリポストする相手", "🔄", repost_users)}
124
  {make_rank_list("よくいいねする相手", "❤️", like_users)}
125
- <b>#️⃣ よく使うタグ</b><div style='display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px;'>
126
  {"".join([f"<span style='background: #eee; padding: 3px 8px; border-radius: 10px; font-size: 0.75em;'>#{t}</span>" for t, _ in Counter(hashtags).most_common(3)]) or "なし"}
127
  </div>
128
  </div>
129
  """
130
 
 
131
  top_3 = df.sort_values('score', ascending=False).head(3)
132
- posts_html = f"<h4>🌱 取得範囲内の初投稿 ({first_post['created_at'].strftime('%Y/%m/%d')})</h4><p style='font-size: 0.9em; background:#f9f9f9; padding:10px; border-radius:5px;'>{first_post['text']}</p><h4 style='color: #ff4081;'>🏆 ベストポスト Top 3</h4>"
 
 
 
 
 
 
 
133
  for i, (_, row) in enumerate(top_3.iterrows(), 1):
134
- posts_html += f"<div style='border: 1px solid #ffc107; padding: 10px; margin-bottom: 8px; border-radius: 8px; font-size: 0.9em;'><b>第{i}位</b> (❤️{row['likes']} 🔄{row['reposts']})<br>{row['text'][:80]}...<br><a href='{row['url']}' target='_blank' style='color:#ff4081;'>投稿を見る</a></div>"
 
 
 
 
 
 
 
 
 
135
 
136
- return stats_html, rank_html, posts_html, "解析完了!"
137
 
138
  except Exception as e:
139
- return f"エラー詳細: {str(e)}", "", "", "失敗"
 
29
  while len(posts_data) < max_limit:
30
  response = client.get_author_feed(actor=profile.did, limit=100, cursor=cursor)
31
  for feed_view in response.feed:
32
+ # 投稿と著者の存在チェック
33
+ if not feed_view.post or not hasattr(feed_view.post, 'author') or not feed_view.post.author:
34
  continue
35
 
36
  # リポストの分析
37
  if feed_view.reason and hasattr(feed_view.reason, 'by'):
38
+ orig_author = feed_view.post.author.handle
39
+ if orig_author != target_handle:
40
+ repost_users.append(orig_author)
 
 
41
  continue
42
 
43
  post = feed_view.post
44
  if post.author.handle == target_handle:
45
+ # recordの存在チェック
46
+ if not hasattr(post, 'record') or post.record is None:
47
+ continue
48
+
49
  rkey = post.uri.split('/')[-1]
50
  post_url = f"https://bsky.app/profile/{target_handle}/post/{rkey}"
51
 
52
+ # 各種カウント(None対策)
53
+ likes = getattr(post, 'like_count', 0) or 0
54
+ reposts = getattr(post, 'repost_count', 0) or 0
55
+ created_at_raw = getattr(post.record, 'created_at', None)
56
+ text = getattr(post.record, 'text', "") or ""
 
 
 
57
 
58
+ if created_at_raw:
59
+ posts_data.append({
60
+ 'text': text,
61
+ 'created_at': pd.to_datetime(created_at_raw),
62
+ 'likes': likes,
63
+ 'reposts': reposts,
64
+ 'score': likes + reposts,
65
+ 'url': post_url
66
+ })
67
+
68
+ if text:
69
+ hashtags.extend(re.findall(r'#(\w+)', text))
70
 
71
+ # リプライ相手
72
  if feed_view.reply and feed_view.reply.parent:
73
  parent = feed_view.reply.parent
74
  if hasattr(parent, 'author') and parent.author:
75
+ p_handle = parent.author.handle
76
+ if p_handle != target_handle:
77
+ reply_users.append(p_handle)
78
 
79
  cursor = response.cursor
80
  if not cursor or len(posts_data) >= max_limit: break
81
  progress(min(len(posts_data)/max_limit * 0.5, 0.5), desc=f"{len(posts_data)}件取得中...")
82
 
83
  # --- 2. いいね一覧の分析 ---
84
+ progress(0.6, desc="いいねを分析中...")
85
  like_cursor = None
86
+ try:
87
+ for _ in range(2):
88
  likes_resp = client.get_actor_likes(actor=profile.did, limit=100, cursor=like_cursor)
89
  for like_item in likes_resp.feed:
 
90
  if like_item.post and hasattr(like_item.post, 'author') and like_item.post.author:
91
+ l_handle = like_item.post.author.handle
92
+ if l_handle != target_handle:
93
+ like_users.append(l_handle)
94
  like_cursor = likes_resp.cursor
95
  if not like_cursor: break
96
+ except Exception:
97
+ pass # いいね取得できなくても続行
98
 
99
+ if not posts_data:
100
+ return "有効な投稿データが1件も見つかりませんでした。", "", "", "完了(データなし)"
101
 
102
+ # --- 3. 解析とHTML作成 ---
103
  df = pd.DataFrame(posts_data)
104
+ df = df.sort_values('created_at', ascending=True)
105
  first_post = df.iloc[0]
106
+
107
+ # 継続日数の計算
108
+ delta = datetime.now().replace(tzinfo=None) - first_post['created_at'].replace(tzinfo=None)
109
+ days_active = max(delta.days, 1)
110
 
111
  stats_html = f"""
112
  <div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 20px;'>
113
  <div style='background: #e3f2fd; padding: 15px; border-radius: 10px; text-align: center; border: 1px solid #90caf9;'>
114
+ <div style='color: #1565c0; font-size: 0.8em;'>総ポスト</div><div style='font-size: 1.5em; font-weight: bold;'>{getattr(profile, 'posts_count', 0)}</div>
115
  </div>
116
  <div style='background: #f1f8e9; padding: 15px; border-radius: 10px; text-align: center; border: 1px solid #c5e1a5;'>
117
  <div style='color: #33691e; font-size: 0.8em;'>継続日数</div><div style='font-size: 1.5em; font-weight: bold;'>{days_active}日</div>
 
123
  """
124
 
125
  def make_rank_list(title, icon, counter_list):
126
+ html = f"<b>{icon} {title}</b><div style='font-size: 0.85em; color: #444; margin: 5px 0 12px 0;'>"
127
  if not counter_list: return html + "データなし</div>"
128
+ items = Counter(counter_list).most_common(3)
129
+ for name, count in items:
130
  html += f"<div>@{name} ({count}回)</div>"
131
  return html + "</div>"
132
 
 
135
  {make_rank_list("よくリプライする相手", "💬", reply_users)}
136
  {make_rank_list("よくリポストする相手", "🔄", repost_users)}
137
  {make_rank_list("よくいいねする相手", "❤️", like_users)}
138
+ <b>#️⃣ よく使うタグ</b><div style='display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px;'>
139
  {"".join([f"<span style='background: #eee; padding: 3px 8px; border-radius: 10px; font-size: 0.75em;'>#{t}</span>" for t, _ in Counter(hashtags).most_common(3)]) or "なし"}
140
  </div>
141
  </div>
142
  """
143
 
144
+ # ベストポスト Top 3
145
  top_3 = df.sort_values('score', ascending=False).head(3)
146
+ posts_html = f"""
147
+ <div style='margin-bottom: 20px; padding: 12px; border-radius: 8px; background: #fff8e1; border: 1px solid #ffe082;'>
148
+ <small style='color: #ffa000; font-weight: bold;'>🌱 取得範囲内の初投稿 ({first_post['created_at'].strftime('%Y/%m/%d')})</small>
149
+ <p style='font-size: 0.9em; margin: 5px 0;'>{first_post['text']}</p>
150
+ <a href='{first_post['url']}' target='_blank' style='font-size: 0.8em; color: #ff8f00;'>投稿をみる</a>
151
+ </div>
152
+ <h4 style='color: #d81b60; margin-bottom: 10px;'>🏆 ベストポスト Top 3</h4>
153
+ """
154
  for i, (_, row) in enumerate(top_3.iterrows(), 1):
155
+ posts_html += f"""
156
+ <div style='border: 1px solid #f0f0f0; padding: 10px; margin-bottom: 10px; border-radius: 8px; background: white;'>
157
+ <div style='display: flex; justify-content: space-between;'>
158
+ <b style='color: #ffb300;'>第{i}位</b>
159
+ <span style='font-size: 0.8em; color: #999;'>❤️ {row['likes']} 🔄 {row['reposts']}</span>
160
+ </div>
161
+ <p style='font-size: 0.9em; margin: 8px 0;'>{row['text'][:100]}...</p>
162
+ <a href='{row['url']}' target='_blank' style='color: #d81b60; font-size: 0.85em;'>投稿をみる</a>
163
+ </div>
164
+ """
165
 
166
+ return stats_html, rank_html, posts_html, "すべての解析完了しました!"
167
 
168
  except Exception as e:
169
+ return f"実行エラー: {str(e)}", "", "", "解析に失敗しました"