Repository navigation
Expand file tree
/
Copy pathapp.py
More file actions
355 lines (297 loc) · 12.1 KB
/
Copy pathapp.py
File metadata and controls
355 lines (297 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Flask Web UI for Facebook Scraper
提供前端界面來執行 Facebook 貼文抓取
"""
from flask import Flask, render_template, request, jsonify, Response, stream_with_context
from flask_cors import CORS
import json
import threading
import queue
import time
from selenium_facebook_scraper import scrape_facebook_post
from analyze_friends import analyze_commenters_friends
app = Flask(__name__)
CORS(app)
# 全局變量:用於存儲日誌和結果
log_queue = queue.Queue()
scraping_result = None
scraping_status = "idle" # idle, running, completed, error
scraping_thread = None
analysis_result = None
analysis_status = "idle" # idle, running, completed, error
analysis_thread = None
def log_callback(message):
"""日誌回調函數,將日誌放入隊列"""
log_queue.put({
'type': 'log',
'message': message,
'timestamp': time.time()
})
def run_scraping(url, use_cookies=True, click_more_count=0):
"""在後台線程中執行抓取任務
Args:
url: Facebook 貼文 URL
use_cookies: 是否使用 Cookies
click_more_count: 點擊「查看更多留言」的次數(預設 0,最多 100)
"""
global scraping_result, scraping_status
scraping_status = "running"
# 重定向 print_debug 到 log_callback
# 我們需要修改 selenium_facebook_scraper 的 print_debug 函數
# 但為了不破壞原有代碼,我們使用 monkey patching
# 保存原始的 print_debug
import selenium_facebook_scraper as scraper_module
original_print_debug = scraper_module.print_debug
# 創建新的 print_debug,同時輸出到原始函數和回調
def new_print_debug(msg, flush=True):
original_print_debug(msg, flush)
log_callback(msg)
# 替換 print_debug
scraper_module.print_debug = new_print_debug
try:
log_callback("=" * 60)
log_callback("開始執行抓取任務...")
log_callback(f"目標 URL: {url}")
log_callback(f"使用 Cookies: {'是' if use_cookies else '否'}")
log_callback(f"點擊「查看更多留言」次數: {click_more_count}")
log_callback("=" * 60)
result = scrape_facebook_post(url, use_cookies=use_cookies, click_more_count=click_more_count)
if result:
scraping_result = result
scraping_status = "completed"
log_callback("✅ 抓取完成!")
else:
scraping_status = "error"
log_callback("❌ 抓取失敗")
except Exception as e:
scraping_status = "error"
log_callback(f"❌ 發生錯誤:{str(e)}")
import traceback
log_callback(traceback.format_exc())
finally:
# 恢復原始的 print_debug
scraper_module.print_debug = original_print_debug
@app.route('/')
def index():
"""主頁"""
return render_template('index.html')
@app.route('/api/start', methods=['POST'])
def start_scraping():
"""開始抓取任務"""
global scraping_status, scraping_thread, scraping_result
if scraping_status == "running":
return jsonify({'error': '抓取任務正在執行中'}), 400
data = request.get_json()
url = data.get('url', '')
use_cookies = data.get('use_cookies', True)
click_more_count = data.get('click_more_count', 0)
debug_mode = data.get('debug', 0)
# DEBUG mode: 直接使用示例資料
if debug_mode == 1:
# 清空之前的結果和日誌
scraping_result = None
while not log_queue.empty():
try:
log_queue.get_nowait()
except:
pass
log_callback("=" * 60)
log_callback("🔧 DEBUG MODE 已啟用")
log_callback("使用示例資料: facebook_scraper_example.json")
log_callback("=" * 60)
try:
with open('facebook_scraper_example.json', 'r', encoding='utf-8') as f:
scraping_result = json.load(f)
scraping_status = "completed"
log_callback("✅ DEBUG: 已載入示例資料")
log_callback(f"📊 載入了 {len(scraping_result.get('comments', []))} 筆留言")
return jsonify({'status': 'completed', 'message': 'DEBUG: 已載入示例資料,可直接顯示結果'})
except Exception as e:
scraping_status = "error"
log_callback(f"❌ DEBUG: 載入示例資料失敗:{str(e)}")
return jsonify({'error': f'載入示例資料失敗:{str(e)}'}), 500
if not url:
return jsonify({'error': '請提供 URL'}), 400
# 驗證點擊次數範圍
try:
click_more_count = int(click_more_count)
click_more_count = max(0, min(100, click_more_count))
except (ValueError, TypeError):
click_more_count = 0
# 清空之前的結果和日誌
scraping_result = None
while not log_queue.empty():
try:
log_queue.get_nowait()
except:
pass
# 在後台線程中執行抓取
scraping_thread = threading.Thread(target=run_scraping, args=(url, use_cookies, click_more_count))
scraping_thread.daemon = True
scraping_thread.start()
log_callback("✅ 抓取任務已開始")
return jsonify({'status': 'started', 'message': '抓取任務已開始'})
@app.route('/api/status')
def get_status():
"""獲取抓取狀態"""
return jsonify({
'status': scraping_status,
'has_result': scraping_result is not None
})
@app.route('/api/result')
def get_result():
"""獲取抓取結果"""
if scraping_result:
return jsonify(scraping_result)
else:
return jsonify({'error': '尚無結果'}), 404
def run_analysis(use_cookies=True, max_workers=3):
"""在後台線程中執行分析任務
Args:
use_cookies: 是否使用 Cookies
max_workers: 並行處理的最大 Chrome 實例數,預設為 3
"""
global analysis_result, analysis_status
analysis_status = "running"
# 重定向 print_debug 到 log_callback
import analyze_friends as analyze_module
original_print_debug = analyze_module.print_debug
def new_print_debug(msg, flush=True):
original_print_debug(msg, flush)
log_callback(msg)
analyze_module.print_debug = new_print_debug
try:
if not scraping_result or not scraping_result.get('comments'):
log_callback("⚠️ 尚無留言資料,無法進行分析")
analysis_status = "error"
return
# 提取所有留言者(去重)
commenters = []
seen_urls = set()
for comment in scraping_result['comments']:
author_url = comment.get('author_url')
if author_url and author_url not in seen_urls:
commenters.append({
'author_url': author_url,
'author_name': comment.get('author_name', '未知')
})
seen_urls.add(author_url)
log_callback(f"找到 {len(commenters)} 位不同的留言者")
log_callback(f"將使用 {max_workers} 個 Chrome 實例進行並行分析")
log_callback("開始分析朋友數量...")
# 執行分析
results = analyze_commenters_friends(commenters, use_cookies=use_cookies, log_callback=log_callback, max_workers=max_workers)
# 儲存結果為 JSON
output_file = 'friend_analysis.json'
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(results, f, indent=2, ensure_ascii=False)
analysis_result = results
analysis_status = "completed"
log_callback(f"\n✅ 分析完成!結果已儲存至: {output_file}")
except Exception as e:
analysis_status = "error"
log_callback(f"❌ 分析時發生錯誤:{str(e)}")
import traceback
log_callback(traceback.format_exc())
finally:
analyze_module.print_debug = original_print_debug
@app.route('/api/analyze', methods=['POST'])
def start_analysis():
"""開始分析任務"""
global analysis_status, analysis_thread, analysis_result
if analysis_status == "running":
return jsonify({'error': '分析任務正在執行中'}), 400
data = request.get_json() or {}
use_cookies = data.get('use_cookies', True)
debug_mode = data.get('debug', 0)
max_workers = data.get('max_workers', 3)
# 驗證 max_workers 範圍
try:
max_workers = int(max_workers)
max_workers = max(1, min(15, max_workers)) # 限制在 1-15 之間
except (ValueError, TypeError):
max_workers = 3
# DEBUG mode: 直接使用示例資料,不執行實際分析
if debug_mode == 1:
# 清空之前的結果和日誌
analysis_result = None
while not log_queue.empty():
try:
log_queue.get_nowait()
except:
pass
log_callback("=" * 60)
log_callback("🔧 DEBUG MODE 已啟用")
log_callback("偵測到 DEBUG mode 開啟,直接使用示例資料畫圖")
log_callback("跳過實際分析流程")
log_callback("=" * 60)
try:
with open('friend_analysis_example.json', 'r', encoding='utf-8') as f:
analysis_result = json.load(f)
analysis_status = "completed"
success_count = sum(1 for v in analysis_result.values() if v is not None)
log_callback("✅ DEBUG: 已載入示例資料")
log_callback(f"📊 載入了 {success_count}/{len(analysis_result)} 位用戶的朋友數量")
log_callback("✅ DEBUG: 準備繪製圖表")
return jsonify({'status': 'completed', 'message': 'DEBUG: 已載入示例資料,可直接繪圖'})
except Exception as e:
analysis_status = "error"
log_callback(f"❌ DEBUG: 載入示例資料失敗:{str(e)}")
return jsonify({'error': f'載入示例資料失敗:{str(e)}'}), 500
if not scraping_result or not scraping_result.get('comments'):
return jsonify({'error': '尚無留言資料,請先完成抓取'}), 400
# 清空之前的結果和日誌
analysis_result = None
# 在後台線程中執行分析
analysis_thread = threading.Thread(target=run_analysis, args=(use_cookies, max_workers))
analysis_thread.daemon = True
analysis_thread.start()
log_callback("✅ 分析任務已開始")
return jsonify({'status': 'started', 'message': '分析任務已開始'})
@app.route('/api/analysis_status')
def get_analysis_status():
"""獲取分析狀態"""
return jsonify({
'status': analysis_status,
'has_result': analysis_result is not None
})
@app.route('/api/analysis_result')
def get_analysis_result():
"""獲取分析結果"""
if analysis_result:
return jsonify(analysis_result)
else:
return jsonify({'error': '尚無結果'}), 404
@app.route('/api/logs')
def stream_logs():
"""使用 Server-Sent Events 實時傳輸日誌"""
def generate():
while True:
try:
# 從隊列中獲取日誌(超時 1 秒)
log_data = log_queue.get(timeout=1)
yield f"data: {json.dumps(log_data, ensure_ascii=False)}\n\n"
except queue.Empty:
# 發送心跳以保持連接
yield f"data: {json.dumps({'type': 'heartbeat'})}\n\n"
except Exception as e:
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
break
return Response(
stream_with_context(generate()),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no'
}
)
if __name__ == '__main__':
print("=" * 60)
print("Facebook Scraper Web UI")
print("=" * 60)
print("正在啟動伺服器...")
print("請在瀏覽器中訪問: http://localhost:5003")
print("=" * 60)
app.run(debug=True, host='0.0.0.0', port=5003, threaded=True)