Repository navigation
Expand file tree
/
Copy pathselenium_facebook_scraper.py
More file actions
1216 lines (1049 loc) · 58.8 KB
/
Copy pathselenium_facebook_scraper.py
File metadata and controls
1216 lines (1049 loc) · 58.8 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
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
使用 Selenium 抓取 Facebook 貼文
支援載入 Cookies 並模擬真實瀏覽器行為
"""
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
import json
import time
import os
import sys
# 設定即時輸出(無緩衝)
sys.stdout.reconfigure(encoding='utf-8') if hasattr(sys.stdout, 'reconfigure') else None
sys.stderr.reconfigure(encoding='utf-8') if hasattr(sys.stderr, 'reconfigure') else None
def print_debug(msg, flush=True):
"""即時輸出 debug 訊息"""
print(msg, flush=flush)
def scroll_with_mouse(driver, scroll_amount=500, screenshot_name=None):
"""使用滑鼠滾輪滾動頁面(滑鼠在螢幕中間)"""
try:
# 獲取當前滾動位置
scroll_before = driver.execute_script("return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;")
print_debug(f" 滾動前位置: {scroll_before}px")
# 獲取視窗大小
window_size = driver.get_window_size()
center_x = window_size['width'] // 2
center_y = window_size['height'] // 2
print_debug(f" 視窗大小: {window_size['width']}x{window_size['height']}, 中心點: ({center_x}, {center_y})")
# 方法 1: 找到可滾動的元素(可能是 html 或 body)
scrollable_element = None
try:
# 嘗試找到 html 元素
html = driver.find_element(By.TAG_NAME, "html")
scrollable_element = html
except:
try:
body = driver.find_element(By.TAG_NAME, "body")
scrollable_element = body
except:
pass
if scrollable_element:
# 方法 1A: 使用 ActionChains 移動到元素中間並滾動
try:
actions = ActionChains(driver)
# 移動到元素
actions.move_to_element(scrollable_element)
# 移動到視窗中間(相對於元素中心)
actions.move_by_offset(0, 0)
# 滾動
actions.scroll_by_amount(0, scroll_amount)
actions.perform()
print_debug("✅ 使用 ActionChains 滾動成功")
except Exception as e1a:
print_debug(f"⚠️ 方法 1A 失敗: {e1a}")
# 方法 1B: 使用 send_keys 滾動
try:
scrollable_element.click() # 確保焦點
scrollable_element.send_keys(Keys.PAGE_DOWN)
scrollable_element.send_keys(Keys.PAGE_DOWN)
print_debug("✅ 使用 PAGE_DOWN 滾動成功")
except Exception as e1b:
print_debug(f"⚠️ 方法 1B 失敗: {e1b}")
# 方法 1C: 使用 JavaScript
driver.execute_script(f"window.scrollBy(0, {scroll_amount});")
print_debug("✅ 使用 JavaScript 滾動成功")
else:
# 方法 2: 直接使用 JavaScript 滾動
driver.execute_script(f"window.scrollBy(0, {scroll_amount});")
print_debug("✅ 使用 JavaScript 滾動(無元素)")
time.sleep(2)
# 獲取滾動後位置
scroll_after = driver.execute_script("return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;")
scroll_diff = scroll_after - scroll_before
print_debug(f" 滾動後位置: {scroll_after}px (移動了 {scroll_diff}px)")
# 如果還是沒滾動,嘗試滾動到底部
if scroll_diff == 0:
print_debug(" ⚠️ 滾動位置沒變化,嘗試滾動到底部...")
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(2)
scroll_after = driver.execute_script("return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;")
scroll_diff = scroll_after - scroll_before
print_debug(f" 滾動到底部後位置: {scroll_after}px (移動了 {scroll_diff}px)")
# 截圖確認
if screenshot_name:
screenshot_path = f'scroll_{screenshot_name}.png'
driver.save_screenshot(screenshot_path)
print_debug(f"✅ 截圖已儲存: {screenshot_path}")
return scroll_diff > 0 # 返回是否真的滾動了
except Exception as e:
print_debug(f"❌ 滾動失敗: {e}")
import traceback
traceback.print_exc()
return False
def extract_comments_from_page(driver):
"""從當前頁面提取留言"""
comments = []
processed_urls = set()
print_debug(" 開始尋找留言元素...")
# 嘗試多種選擇器來找到留言容器
comment_containers = []
# 方法 1: 使用 data-testid
comment_selectors = [
"[data-testid='UFI2Comment/root']",
"div[data-testid='UFI2Comment/root']",
"[data-testid*='UFI2Comment']",
"div[data-testid*='Comment']",
"[data-testid*='comment']",
"div[data-testid*='comment']",
]
for selector in comment_selectors:
try:
elements = driver.find_elements(By.CSS_SELECTOR, selector)
if elements:
comment_containers = elements
print_debug(f" ✅ 方法 1: 使用選擇器找到 {len(elements)} 個留言容器: {selector}")
break
except Exception as e:
print_debug(f" ⚠️ 選擇器 {selector} 失敗: {e}")
continue
# 方法 2: 如果沒找到,從用戶連結找(在頁面下方)
if not comment_containers:
print_debug(" 方法 2: 嘗試從用戶連結找到留言容器...")
try:
user_links = driver.find_elements(By.CSS_SELECTOR, "a[href*='/user/'], a[href*='/profile.php'], a[href*='facebook.com'][role='link']")
print_debug(f" 找到 {len(user_links)} 個用戶連結")
containers = set()
for link in user_links:
try:
location = link.location
# 過濾:只取在頁面下方(可能是留言)的連結
if location['y'] > 500: # 在頁面下方
# 向上查找父容器
try:
parent = link.find_element(By.XPATH, "./ancestor::div[contains(@data-testid, 'Comment') or contains(@class, 'comment') or @role='article'][1]")
except:
parent = link.find_element(By.XPATH, "./ancestor::div[1]")
if parent not in containers:
containers.add(parent)
except:
continue
comment_containers = list(containers)
if comment_containers:
print_debug(f" ✅ 方法 2: 從用戶連結找到 {len(comment_containers)} 個可能的留言容器")
except Exception as e:
print_debug(f" ⚠️ 方法 2 失敗: {e}")
# 方法 3: 使用更通用的方法 - 尋找所有包含用戶名稱和文字的區域
if not comment_containers:
print_debug(" 方法 3: 使用通用方法尋找留言...")
try:
# 尋找所有包含用戶連結的區域
user_links = driver.find_elements(By.CSS_SELECTOR, "a[href*='/user/'], a[href*='/profile.php'], a[role='link'][href*='facebook.com']")
print_debug(f" 找到 {len(user_links)} 個可能的用戶連結")
comment_containers = []
for link in user_links:
try:
location = link.location
# 過濾:只取在頁面下方(可能是留言)的連結
if location['y'] > 500:
# 找到父容器
parent = link.find_element(By.XPATH, "./ancestor::div[1]")
if parent not in comment_containers:
comment_containers.append(parent)
except:
continue
if comment_containers:
print_debug(f" ✅ 方法 3: 找到 {len(comment_containers)} 個可能的留言容器")
except Exception as e:
print_debug(f" ⚠️ 方法 3 失敗: {e}")
if not comment_containers:
print_debug(" ⚠️ 未找到任何留言容器")
return comments
# 提取留言資訊
print_debug(f" 開始提取 {len(comment_containers)} 個留言容器的資訊...")
for idx, comment_elem in enumerate(comment_containers, 1):
try:
comment_data = {
'comment_id': None,
'author_name': None,
'author_url': None,
'comment_text': None,
}
# 尋找留言者(改進版:使用更多選擇器和方法)
author_selectors = [
"a[href*='/user/']",
"a[href*='/profile.php']",
"a[role='link'][href*='facebook.com']",
"a[href*='facebook.com'][role='link']",
"strong a[href*='facebook.com']",
"h3 a[href*='facebook.com']",
"span a[href*='facebook.com']",
"div a[href*='facebook.com']",
]
# 方法 1: 使用 CSS 選擇器
for author_selector in author_selectors:
try:
author_links = comment_elem.find_elements(By.CSS_SELECTOR, author_selector)
print_debug(f" 留言 #{idx}: 選擇器 {author_selector} 找到 {len(author_links)} 個連結")
# 先收集所有有效的用戶連結
valid_links = []
for link_idx, author_link in enumerate(author_links):
try:
href = author_link.get_attribute('href')
name = author_link.text.strip()
print_debug(f" 連結 #{link_idx + 1}: href={href[:80] if href else None}, name={name[:30] if name else None}")
# 過濾:接受所有有效的用戶連結
if href and 'facebook.com' in href:
# 排除貼文連結和其他非用戶連結
if '/posts/' in href or '/photo/' in href or '/videos/' in href or '/groups/' in href or '/pages/' in href:
continue
# 從 URL 提取用戶名
username = None
if '/user/' in href:
username = href.split('/user/')[-1].split('/')[0].split('?')[0]
elif '/profile.php' in href:
if 'id=' in href:
username = href.split('id=')[-1].split('&')[0]
else:
# 格式:https://www.facebook.com/username
parts = href.split('facebook.com/')
if len(parts) > 1:
username = parts[1].split('?')[0].split('/')[0]
if username in ['www', 'm', 'mobile', 'web']:
username = None
if username:
base_url = href.split('?')[0]
valid_links.append({
'href': href,
'base_url': base_url,
'display_name': name if name else None,
'username': username
})
except:
continue
# 優先選擇有顯示名稱的連結
selected_link = None
for link in valid_links:
if link['display_name']:
selected_link = link
break
# 如果沒有有顯示名稱的連結,選擇第一個
if not selected_link and valid_links:
selected_link = valid_links[0]
# 使用選中的連結
if selected_link:
base_url = selected_link['base_url']
if base_url not in processed_urls:
final_name = selected_link['display_name'] if selected_link['display_name'] else selected_link['username']
comment_data['author_name'] = final_name if final_name else None
comment_data['author_url'] = base_url
processed_urls.add(base_url)
print_debug(f" 留言 #{idx}: ✅ 找到留言者 {final_name or '未知'} ({base_url})")
break
else:
print_debug(f" 留言 #{idx}: ⚠️ 連結已處理過,跳過")
if comment_data['author_name'] or comment_data['author_url']:
break
except Exception as e:
print_debug(f" 留言 #{idx}: ⚠️ 選擇器 {author_selector} 失敗: {e}")
continue
# 方法 2: 如果還沒找到,使用 XPath 尋找
if not comment_data['author_name'] and not comment_data['author_url']:
try:
# 尋找所有包含 facebook.com 的連結
xpath = ".//a[contains(@href, 'facebook.com') and (contains(@href, '/user/') or contains(@href, '/profile.php'))]"
author_links = comment_elem.find_elements(By.XPATH, xpath)
for author_link in author_links:
try:
href = author_link.get_attribute('href')
name = author_link.text.strip()
if href and 'facebook.com' in href:
# 排除貼文連結和其他非用戶連結
if '/posts/' in href or '/photo/' in href or '/videos/' in href or '/groups/' in href or '/pages/' in href:
continue
# 優先使用連結的文字內容(顯示名稱)
display_name = name if name else None
username = None
# 從 URL 提取用戶名(作為備用)
if '/user/' in href:
username = href.split('/user/')[-1].split('/')[0].split('?')[0]
elif '/profile.php' in href and 'id=' in href:
username = href.split('id=')[-1].split('&')[0]
else:
# 格式:https://www.facebook.com/username
parts = href.split('facebook.com/')
if len(parts) > 1:
username = parts[1].split('?')[0].split('/')[0]
if username in ['www', 'm', 'mobile', 'web']:
username = None
# 使用顯示名稱,如果沒有則使用用戶名
final_name = display_name if display_name else username
base_url = href.split('?')[0]
if base_url not in processed_urls:
comment_data['author_name'] = final_name if final_name else None
comment_data['author_url'] = base_url
processed_urls.add(base_url)
print_debug(f" 留言 #{idx}: ✅ 找到留言者 {final_name or '未知'} ({base_url}) [XPath]")
break
except:
continue
except:
pass
# 方法 3: 如果還是沒找到,嘗試從整個容器的文字中提取
if not comment_data['author_name'] and not comment_data['author_url']:
try:
# 尋找容器中第一個連結(可能是留言者)
first_link = comment_elem.find_element(By.XPATH, ".//a[contains(@href, 'facebook.com')][1]")
if first_link:
href = first_link.get_attribute('href')
name = first_link.text.strip()
if href and 'facebook.com' in href:
# 排除貼文連結和其他非用戶連結
if '/posts/' in href or '/photo/' in href or '/videos/' in href or '/groups/' in href or '/pages/' in href:
pass
else:
# 優先使用連結的文字內容(顯示名稱)
display_name = name if name else None
username = None
# 從 URL 提取用戶名(作為備用)
if '/user/' in href:
username = href.split('/user/')[-1].split('/')[0].split('?')[0]
elif '/profile.php' in href and 'id=' in href:
username = href.split('id=')[-1].split('&')[0]
else:
# 格式:https://www.facebook.com/username
parts = href.split('facebook.com/')
if len(parts) > 1:
username = parts[1].split('?')[0].split('/')[0]
if username in ['www', 'm', 'mobile', 'web']:
username = None
# 使用顯示名稱,如果沒有則使用用戶名
final_name = display_name if display_name else username
base_url = href.split('?')[0]
if base_url not in processed_urls:
comment_data['author_name'] = final_name if final_name else None
comment_data['author_url'] = base_url
processed_urls.add(base_url)
print_debug(f" 留言 #{idx}: ✅ 找到留言者 {final_name or '未知'} ({base_url}) [第一個連結]")
except:
pass
# 尋找留言內容
text_selectors = [
"div[data-testid='UFI2Comment/body']",
"div[data-testid*='Comment/body']",
"div[dir='auto']",
"span[dir='auto']",
"div[class*='text']",
"span[class*='text']",
]
for text_selector in text_selectors:
try:
text_elements = comment_elem.find_elements(By.CSS_SELECTOR, text_selector)
if text_elements:
texts = []
for elem in text_elements:
try:
text = elem.text.strip()
# 過濾:只接受有效的留言文字
if text and len(text) > 5:
# 排除按鈕文字
if text not in ['讚', '回覆', '分享', 'Like', 'Reply', 'Share', '查看更多', '查看回覆', '查看更多回覆']:
texts.append(text)
except:
continue
if texts:
# 選擇最長的文字(通常是留言內容)
comment_data['comment_text'] = max(texts, key=len)
print_debug(f" 留言 #{idx}: 找到留言內容 ({len(comment_data['comment_text'])} 字元)")
break
except:
continue
# 如果還是沒找到留言內容,嘗試獲取整個容器的文字
if not comment_data['comment_text']:
try:
full_text = comment_elem.text.strip()
# 移除用戶名稱
if comment_data['author_name']:
full_text = full_text.replace(comment_data['author_name'], '', 1).strip()
# 移除常見的按鈕文字
for btn_text in ['讚', '回覆', '分享', 'Like', 'Reply', 'Share', '查看更多', '查看回覆', '查看更多回覆', '·', '小時', '分鐘', '天']:
full_text = full_text.replace(btn_text, '')
comment_data['comment_text'] = full_text.strip()
if comment_data['comment_text'] and len(comment_data['comment_text']) > 3:
print_debug(f" 留言 #{idx}: 使用完整文字提取 ({len(comment_data['comment_text'])} 字元)")
except:
pass
# 只添加有內容的留言
if comment_data['comment_text'] and len(comment_data['comment_text']) > 3:
# 檢查是否重複(根據 URL 和內容)
is_duplicate = any(
c.get('author_url') == comment_data['author_url'] and
c.get('comment_text') == comment_data['comment_text']
for c in comments
)
if not is_duplicate:
comments.append(comment_data)
print_debug(f" ✅ 留言 #{idx}: {comment_data.get('author_name', '未知')} - {comment_data['comment_text'][:50]}...")
else:
print_debug(f" ⚠️ 留言 #{idx}: 沒有有效的留言內容")
except Exception as e:
print_debug(f" ⚠️ 處理留言 #{idx} 時發生錯誤: {e}")
continue
print_debug(f" 提取完成,共 {len(comments)} 則有效留言")
return comments
def scrape_comments(driver, max_comments=200, click_more_count=0):
"""抓取 Facebook 貼文的留言(改進版:先抓取初始留言,成功後再點擊「查看更多留言」)
Args:
driver: Selenium WebDriver 實例
max_comments: 最大留言數量(未使用,保留向後兼容)
click_more_count: 點擊「查看更多留言」的次數(預設 0,最多 100)
"""
all_comments = []
try:
# 步驟 1: 先滾動到頁面下方,確保留言區域可見
print_debug("步驟 1: 滾動到頁面下方,確保留言區域可見...")
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(2)
# 截圖確認當前狀態
driver.save_screenshot('initial_state.png')
print_debug("截圖已儲存: initial_state.png")
# 步驟 2: 先抓取初始可見的留言(在點擊「查看更多留言」之前)
print_debug("\n步驟 2: 先抓取初始可見的留言(在點擊「查看更多留言」之前)...")
initial_comments = extract_comments_from_page(driver)
print_debug(f"初始抓取到 {len(initial_comments)} 則留言")
if len(initial_comments) > 0:
# 成功抓到留言,加入到總列表
all_comments.extend(initial_comments)
print_debug(f"✅ 成功抓取初始留言,已加入總列表,目前總共 {len(all_comments)} 則留言")
# 顯示前幾則留言
for idx, comment in enumerate(initial_comments[:3], 1):
author = comment.get('author_name', '未知')
text_preview = comment.get('comment_text', '')[:50] + '...' if len(comment.get('comment_text', '')) > 50 else comment.get('comment_text', '')
print_debug(f" 初始留言 #{idx}: {author} - {text_preview}")
else:
print_debug("⚠️ 初始抓取未找到留言,但繼續嘗試點擊「查看更多留言」...")
# 限制點擊次數範圍
click_more_count = max(0, min(100, int(click_more_count)))
print_debug(f"\n設定點擊「查看更多留言」次數: {click_more_count} 次")
# 步驟 3-7: 循環點擊「查看更多留言」並抓取
if len(initial_comments) == 0:
print_debug("⚠️ 未抓到初始留言,跳過點擊「查看更多留言」")
elif click_more_count == 0:
print_debug("⚠️ 點擊次數設為 0,跳過點擊「查看更多留言」")
else:
# 循環點擊:點擊 → 等待 → 抓取
for cycle in range(click_more_count):
print_debug(f"\n--- 循環 {cycle + 1}/{click_more_count} ---")
print_debug(f"點擊「查看更多留言」按鈕(第 {cycle + 1}/{click_more_count} 次)...")
# 滾動到底部,確保留言區域可見
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(2)
# 尋找「查看更多留言」按鈕
more_comment_texts = ['查看更多留言', '查看所有留言', 'View all comments', 'See more comments', '查看更多', '查看']
clicked = False
for text in more_comment_texts:
try:
xpath = f"//span[contains(text(), '{text}')] | //a[contains(text(), '{text}')] | //div[contains(text(), '{text}')] | //button[contains(text(), '{text}')]"
elements = driver.find_elements(By.XPATH, xpath)
for element in elements:
try:
if element.is_displayed() and element.is_enabled():
# 滾動到元素可見
driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});", element)
time.sleep(1)
# 截圖確認
driver.save_screenshot(f'before_click_more_{cycle + 1}.png')
print_debug(f"找到「{text}」按鈕,準備點擊...")
# 點擊
driver.execute_script("arguments[0].click();", element)
print_debug(f"✅ 已點擊「{text}」按鈕(第 {cycle + 1} 次)")
clicked = True
break
except Exception as e:
print_debug(f"⚠️ 點擊元素時發生錯誤: {e}")
continue
if clicked:
break
except:
continue
if not clicked:
print_debug(f"⚠️ 第 {cycle + 1} 次未找到「查看更多留言」按鈕,可能已經全部載入")
break
# 等待留言載入(3 秒)
print_debug(f"\n等待留言載入(3 秒)...")
time.sleep(3)
print_debug("✅ 等待完成")
# 抓取點擊後的新留言
print_debug(f"\n抓取點擊「查看更多留言」後的新留言...")
new_comments = extract_comments_from_page(driver)
print_debug(f"本次抓取到 {len(new_comments)} 則留言")
# 合併新留言(去重)
new_count = 0
for comment in new_comments:
is_duplicate = any(
existing.get('author_url') == comment.get('author_url') and
existing.get('comment_text') == comment.get('comment_text')
for existing in all_comments
)
if not is_duplicate:
all_comments.append(comment)
new_count += 1
print_debug(f"新增 {new_count} 則留言,目前總共 {len(all_comments)} 則留言")
max_iterations = 3 # 測試時最多循環 3 次
no_new_data_count = 0
# 步驟 5: 可選的額外滾動循環(如果需要更多留言)
# 注意:這個循環現在是可選的,因為主要流程已經改為「先抓再點」的模式
# 如果還需要更多留言,可以在這裡添加滾動邏輯
print_debug(f"\n✅ 抓取完成!總共抓取到 {len(all_comments)} 則留言")
# 顯示前幾則留言
for idx, comment in enumerate(all_comments[:5], 1):
author = comment.get('author_name', '未知')
text_preview = comment.get('comment_text', '')[:50] + '...' if len(comment.get('comment_text', '')) > 50 else comment.get('comment_text', '')
print_debug(f" 留言 #{idx}: {author} - {text_preview}")
return all_comments
# 滾動回中間位置(留言通常在貼文下方)
print_debug("步驟 2: 調整滾動位置...")
driver.execute_script("window.scrollTo(0, document.body.scrollHeight * 0.7);")
time.sleep(2)
# 再次滾動到底部
print_debug("步驟 3: 再次滾動到底部...")
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(3)
# 嘗試點擊「查看所有留言」或「查看更多留言」按鈕
print_debug("步驟 4: 尋找並點擊「查看更多留言」按鈕...")
try:
# 使用 XPath 尋找包含特定文字的按鈕
more_comment_texts = ['查看所有留言', '查看更多留言', 'View all comments', 'See more comments', '查看', '更多']
clicked = False
for text in more_comment_texts:
try:
xpath = f"//span[contains(text(), '{text}')] | //a[contains(text(), '{text}')] | //div[contains(text(), '{text}')]"
elements = driver.find_elements(By.XPATH, xpath)
for element in elements:
try:
if element.is_displayed() and element.is_enabled():
print_debug(f"找到「{text}」按鈕,準備點擊...")
# 滾動到元素可見
driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});", element)
time.sleep(1)
# 點擊
driver.execute_script("arguments[0].click();", element)
print_debug(f"✅ 已點擊「{text}」按鈕")
time.sleep(4) # 等待留言載入
clicked = True
break
except Exception as e:
print_debug(f"⚠️ 點擊元素時發生錯誤: {e}")
continue
if clicked:
break
except:
continue
if not clicked:
print_debug("⚠️ 未找到「查看更多留言」按鈕,可能留言已經全部顯示")
except Exception as e:
print_debug(f"⚠️ 點擊「查看更多留言」時發生錯誤: {e}")
# 多次滾動以載入更多留言
print_debug("步驟 5: 多次滾動以載入更多留言...")
for i in range(5): # 增加滾動次數
print_debug(f" 滾動 #{i+1}/5...")
# 滾動到底部
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(2)
# 嘗試點擊「查看更多」按鈕(如果有的話)
try:
more_buttons = driver.find_elements(By.XPATH, "//span[contains(text(), '查看更多')] | //a[contains(text(), '查看更多')] | //span[contains(text(), 'See more')]")
for btn in more_buttons:
try:
if btn.is_displayed():
driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});", btn)
time.sleep(1)
driver.execute_script("arguments[0].click();", btn)
print_debug(f" 點擊「查看更多」按鈕 #{i+1}")
time.sleep(2)
break
except:
continue
except:
pass
# 稍微向上滾動一點,然後再向下(模擬人類行為)
driver.execute_script("window.scrollBy(0, -200);")
time.sleep(1)
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(2)
print_debug("步驟 6: 滾動完成,開始尋找留言元素...")
# 等待留言載入
print_debug("等待留言載入...")
time.sleep(3)
# 嘗試多種選擇器來找到留言容器
print_debug("步驟 7: 尋找留言元素...")
comment_containers = []
# 方法 1: 使用 data-testid(Facebook 常用的屬性)
comment_selectors = [
"[data-testid='UFI2Comment/root']",
"div[data-testid='UFI2Comment/root']",
"[data-testid*='UFI2Comment']",
"div[data-testid*='Comment']",
"[data-testid='UFI2Comment/root_level_0']",
]
for selector in comment_selectors:
try:
elements = driver.find_elements(By.CSS_SELECTOR, selector)
if elements:
comment_containers = elements
print_debug(f"✅ 方法 1: 使用選擇器找到 {len(elements)} 個留言容器: {selector}")
break
except Exception as e:
print_debug(f"⚠️ 選擇器 {selector} 失敗: {e}")
continue
# 方法 2: 如果沒找到,嘗試尋找包含用戶連結的區域(留言通常有用戶連結)
if not comment_containers:
print_debug("方法 2: 嘗試從用戶連結找到留言容器...")
try:
# 尋找所有用戶連結
user_links = driver.find_elements(By.CSS_SELECTOR, "a[href*='/user/'], a[href*='/profile.php']")
print_debug(f"找到 {len(user_links)} 個用戶連結")
# 找到這些連結的父容器(可能是留言容器)
containers = set()
for link in user_links:
try:
# 向上查找父元素,找到留言容器
# 留言通常在 role='article' 或包含特定 class 的 div 中
parent = link.find_element(By.XPATH, "./ancestor::div[contains(@data-testid, 'Comment') or contains(@class, 'comment') or @role='article'][1]")
if parent:
containers.add(parent)
except:
try:
# 嘗試其他父元素結構
parent = link.find_element(By.XPATH, "./ancestor::div[@role='article'][1]")
if parent:
containers.add(parent)
except:
pass
comment_containers = list(containers)
if comment_containers:
print_debug(f"✅ 方法 2: 從用戶連結找到 {len(comment_containers)} 個可能的留言容器")
except Exception as e:
print_debug(f"⚠️ 方法 2 失敗: {e}")
# 方法 3: 使用更通用的方法 - 尋找所有可能的留言區域
if not comment_containers:
print_debug("方法 3: 使用通用方法尋找留言...")
try:
# 尋找所有包含用戶名稱連結的區域
user_links = driver.find_elements(By.CSS_SELECTOR, "a[href*='/user/'], a[href*='/profile.php'], a[role='link'][href*='facebook.com']")
print_debug(f"找到 {len(user_links)} 個可能的用戶連結")
# 過濾掉貼文作者的連結(通常在頁面頂部)
# 留言通常在頁面下方
comment_containers = []
for link in user_links:
try:
# 檢查這個連結是否在頁面下方(可能是留言)
location = link.location
if location['y'] > 500: # 在頁面下方
parent = link.find_element(By.XPATH, "./ancestor::div[1]")
if parent not in comment_containers:
comment_containers.append(parent)
except:
continue
if comment_containers:
print_debug(f"✅ 方法 3: 找到 {len(comment_containers)} 個可能的留言容器")
except Exception as e:
print_debug(f"⚠️ 方法 3 失敗: {e}")
comment_elements = comment_containers
# 提取留言資訊
print_debug(f"步驟 8: 開始提取 {len(comment_elements)} 個留言容器的資訊...")
processed_urls = set() # 使用 URL 來去重
for idx, comment_elem in enumerate(comment_elements[:max_comments], 1):
try:
comment_data = {
'comment_id': None,
'author_name': None,
'author_url': None,
'comment_text': None,
}
# 獲取留言 ID(如果有的話)
comment_id = comment_elem.get_attribute('data-ufi-comment-id') or comment_elem.get_attribute('id') or comment_elem.get_attribute('data-testid')
comment_data['comment_id'] = comment_id
# 尋找留言者名稱和連結
try:
# 嘗試多種方式找到用戶名稱和連結
author_selectors = [
"a[role='link'][href*='facebook.com']",
"a[href*='/user/']",
"a[href*='/profile.php']",
"strong a",
"h3 a",
"span a[href*='facebook.com']",
]
for author_selector in author_selectors:
try:
author_links = comment_elem.find_elements(By.CSS_SELECTOR, author_selector)
for author_link in author_links:
href = author_link.get_attribute('href')
name = author_link.text.strip()
# 過濾:只接受有效的用戶連結(包含 /user/ 或 /profile.php)
if href and name and ('/user/' in href or '/profile.php' in href):
# 避免重複
if href not in processed_urls:
comment_data['author_name'] = name
comment_data['author_url'] = href
processed_urls.add(href)
print_debug(f" 找到留言者: {name}")
break
if comment_data['author_name']:
break
except:
continue
# 如果還是沒找到,嘗試從 aria-label 獲取
if not comment_data['author_name']:
try:
author_elem = comment_elem.find_element(By.CSS_SELECTOR, "a[role='link']")
comment_data['author_name'] = author_elem.text.strip()
comment_data['author_url'] = author_elem.get_attribute('href')
except:
pass
except Exception as e:
print_debug(f"⚠️ 提取留言者資訊時發生錯誤: {e}")
# 尋找留言內容
try:
# 嘗試多種選擇器來找到留言文字
text_selectors = [
"div[data-testid='UFI2Comment/body']",
"div[dir='auto']",
"span[dir='auto']",
"div[class*='text']",
"span[class*='text']",
]
for text_selector in text_selectors:
try:
text_elements = comment_elem.find_elements(By.CSS_SELECTOR, text_selector)
if text_elements:
# 過濾並選擇最長的文字內容
texts = []
for elem in text_elements:
text = elem.text.strip()
# 過濾掉太短的文字(可能是按鈕文字等)
if text and len(text) > 5 and text not in ['讚', '回覆', '分享', 'Like', 'Reply', 'Share', '查看更多']:
texts.append(text)
if texts:
# 選擇最長的文字(通常是留言內容)
comment_data['comment_text'] = max(texts, key=len)
print_debug(f" 找到留言內容: {comment_data['comment_text'][:50]}...")
break
except:
continue
# 如果還是沒找到,嘗試獲取整個容器的文字
if not comment_data['comment_text']:
full_text = comment_elem.text
# 移除用戶名稱和常見的按鈕文字
if comment_data['author_name']:
full_text = full_text.replace(comment_data['author_name'], '', 1)
for btn_text in ['讚', '回覆', '分享', 'Like', 'Reply', 'Share', '查看更多', '查看回覆']:
full_text = full_text.replace(btn_text, '')
comment_data['comment_text'] = full_text.strip()
except Exception as e:
print_debug(f"⚠️ 提取留言內容時發生錯誤: {e}")
# 只添加有內容的留言(至少要有留言文字)
if comment_data['comment_text'] and len(comment_data['comment_text']) > 3:
comments.append(comment_data)
author = comment_data['author_name'] or '未知'
text_preview = comment_data['comment_text'][:50] + '...' if len(comment_data['comment_text']) > 50 else comment_data['comment_text']
print_debug(f" ✅ 留言 #{len(comments)}: {author} - {text_preview}")
except Exception as e:
print_debug(f"⚠️ 處理留言 #{idx} 時發生錯誤: {e}")
continue
# 如果使用選擇器沒找到,嘗試從整個頁面尋找
if not comments:
print_debug("嘗試從整個頁面尋找留言...")
try:
# 尋找所有包含用戶名稱的區域
all_links = driver.find_elements(By.CSS_SELECTOR, "a[href*='facebook.com'][role='link']")
print_debug(f"找到 {len(all_links)} 個可能的用戶連結")
# 尋找留言文字區域
text_areas = driver.find_elements(By.CSS_SELECTOR, "div[dir='auto'], span[dir='auto']")
print_debug(f"找到 {len(text_areas)} 個文字區域")
except:
pass
except Exception as e:
print_debug(f"❌ 抓取留言時發生錯誤: {e}")
import traceback
traceback.print_exc()
return comments
def load_cookies_from_file(cookie_file='cookies.json'):
"""從檔案載入 Cookies"""
if not os.path.exists(cookie_file):
print_debug(f"❌ Cookies 檔案 {cookie_file} 不存在")
return None
try:
with open(cookie_file, 'r', encoding='utf-8') as f:
cookies_data = json.load(f)
print_debug(f"✅ 成功載入 Cookies 從 {cookie_file}")
return cookies_data
except Exception as e:
print_debug(f"❌ 載入 Cookies 時發生錯誤:{e}")
return None
def add_cookies_to_driver(driver, cookies_data):
"""將 Cookies 加入到瀏覽器中"""
try:
# 先訪問 Facebook 主頁面(必須先訪問才能設定 Cookies)
print_debug("正在訪問 Facebook 主頁面...")
driver.get("https://www.facebook.com")
time.sleep(2)
# 轉換 Cookies 格式
selenium_cookies = []
for cookie in cookies_data:
# 轉換為 Selenium 可用的格式
selenium_cookie = {
'name': cookie.get('name'),
'value': cookie.get('value'),
'domain': cookie.get('domain', '.facebook.com'),
'path': cookie.get('path', '/'),
}
# 添加可選屬性
if cookie.get('secure'):
selenium_cookie['secure'] = True
if cookie.get('httpOnly'):
selenium_cookie['httpOnly'] = True
if cookie.get('sameSite'):
same_site = cookie.get('sameSite')
if same_site == 'no_restriction':
selenium_cookie['sameSite'] = 'None'
elif same_site:
selenium_cookie['sameSite'] = same_site.capitalize()
try:
driver.add_cookie(selenium_cookie)
selenium_cookies.append(selenium_cookie['name'])
except Exception as e:
print_debug(f"⚠️ 無法添加 Cookie {selenium_cookie['name']}: {e}")
print_debug(f"✅ 成功添加 {len(selenium_cookies)} 個 Cookies: {', '.join(selenium_cookies[:5])}...")
return True
except Exception as e:
print_debug(f"❌ 添加 Cookies 時發生錯誤:{e}")
return False
def scrape_facebook_post(url, use_cookies=True, click_more_count=0):
"""使用 Selenium 抓取 Facebook 貼文
Args:
url: Facebook 貼文 URL
use_cookies: 是否使用 Cookies
click_more_count: 點擊「查看更多留言」的次數(預設 0,最多 100)
"""
print_debug("=" * 60)
print_debug("Selenium Facebook 貼文抓取工具")
print_debug("=" * 60)
print_debug(f"\n目標 URL: {url}")
print_debug(f"使用 Cookies: {'是' if use_cookies else '否'}")
print_debug(f"點擊「查看更多留言」次數: {click_more_count}\n")
# 設定 Chrome 選項
chrome_options = Options()
# chrome_options.add_argument('--headless') # 無頭模式(可選,註解掉可以看到瀏覽器)
chrome_options.add_argument('--disable-blink-features=AutomationControlled')
chrome_options.add_argument('--disable-dev-shm-usage')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument('--window-size=1920,1080')
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
chrome_options.add_experimental_option('useAutomationExtension', False)