你是資料分析專家。請在這個專案中實作評論者分類與可靠度評分系統。

專案路徑：C:\Users\User\Documents\GitHub\apify-scraper-practice

現有資料檔案：
- step2_reviews_20260129_143615.json（最新評論）
- step2b_lowest_reviews_20260129_144321.json（最差評論）

評論資料結構中已有：
- reviewerId: 評論者 ID
- reviewerUrl: 評論者連結
- name: 評論者名稱
- reviewerNumberOfReviews: 評論者的總評論數
- isLocalGuide: 是否是 Local Guide
- stars: 評分
- text: 評論內容
- publishedAtDate: 發布日期
- reviewImageUrls: 照片

請實作以下功能，建立檔案 `reviewer_analysis.py`：

1. 評論者分類系統（7 大類）
   - ORGANIC（分享型）：評論數 10-200、評分分布正常
   - INFLUENCER（網紅型）：專業照片、可能業配
   - RATING_ONLY（評分部隊）：80%+ 無文字
   - EXPERT（專業評論者）：長文、有深度
   - SUSPICIOUS（可疑帳號）：新帳號、單一評論、異常行為
   - LOCAL_GUIDE（在地嚮導）：有認證
   - INACTIVE（沉睡用戶）：低活躍

2. 評論者可靠度評分（RCS: 0-100）
   - 評論數量（鐘形曲線：50-150 最佳）
   - 文字率（有文字的評論佔比）
   - Local Guide 加分
   - 異常扣分

3. 異常行為偵測
   - 單一評論帳號
   - 五星偏差（90%+ 五星）
   - 無文字模式
   - 時間集中評論

4. 評論者結構分析報告
   - 各類型評論者的佔比
   - 可靠度分數分布
   - 異常評論者列表

5. 加權評分計算
   - 根據評論者可靠度加權計算「真實評分」

注意事項：
- Python 腳本開頭要加 `import sys; sys.stdout.reconfigure(encoding='utf-8')`
- 對每間店家都執行分析
- 輸出詳細的分析報告

請直接實作完整程式碼並測試執行。


我來分析專案結構並實作評論者分類與可靠度評分系統。


讓我讀取評論資料檔案來了解資料結構。


<tool_use_error>File content (3.5MB) exceeds maximum allowed size (256KB). Please use offset and limit parameters to read specific portions of the file, or use the GrepTool to search for specific content.</tool_use_error>

Error: File content (3.5MB) exceeds maximum allowed size (256KB). Please use offset and limit parameters to read specific portions of the file, or use the GrepTool to search for specific content.


<tool_use_error>File content (2.9MB) exceeds maximum allowed size (256KB). Please use offset and limit parameters to read specific portions of the file, or use the GrepTool to search for specific content.</tool_use_error>

Error: File content (2.9MB) exceeds maximum allowed size (256KB). Please use offset and limit parameters to read specific portions of the file, or use the GrepTool to search for specific content.


檔案較大，讓我讀取部分內容了解結構。


     1→[
     2→  {
     3→    "title": "今日魚市-南京店-原丼賞和食",
     4→    "subTitle": null,
     5→    "description": "色彩繽紛的自助式餐廳，供應丼飯、壽司與其他日式特色料理。",
     6→    "price": "$400-600",
     7→    "categoryName": "日本餐廳",
     8→    "address": "B1, No. 140號南京東路二段中山區臺北市台灣 10491",
     9→    "neighborhood": null,
    10→    "street": "B1, No. 140號南京東路二段",
    11→    "city": null,
    12→    "postalCode": "10491",
    13→    "state": "臺北市中山區",
    14→    "countryCode": "TW",
    15→    "website": "https://www.facebook.com/140B1/",
    16→    "phone": "+886 2 2517 5526",
    17→    "phoneUnformatted": "+886225175526",
    18→    "claimThisBusiness": false,
    19→    "location": {
    20→      "lat": 25.0517646,
    21→      "lng": 121.533861
    22→    },
    23→    "locatedIn": null,
    24→    "floor": "1 樓",
    25→    "plusCode": "3G2M+PG 朱園里 台灣臺北市中山區",
    26→    "menu": "https://www.facebook.com/photo/?fbid=993342986172685&set=ecnf.100064909770582&locale=zh_TW",
    27→    "totalScore": 4.7,
    28→    "permanentlyClosed": false,
    29→    "temporarilyClosed": false,
    30→    "placeId": "ChIJ9240AWGpQjQRYkqzSvuO47I",
    31→    "categories": [
    32→      "日本餐廳"
    33→    ],
    34→    "fid": "0x3442a96101346ef7:0xb2e38efb4ab34a62",
    35→    "cid": "12890303768405690978",
    36→    "reviewsCount": 14219,
    37→    "reviewsDistribution": {
    38→      "oneStar": 337,
    39→      "twoStar": 218,
    40→      "threeStar": 559,
    41→      "fourStar": 1179,
    42→      "fiveStar": 11926
    43→    },
    44→    "imagesCount": 18563,
    45→    "imageCategories": [
    46→      "全部",
    47→      "最新",
    48→      "影片",
    49→      "菜單",
    50→      "餐飲",
    51→      "氛圍",
    52→      "牡蠣",
    53→      "鯖魚",
    54→      "螃蟹",
    55→      "味噌湯",
    56→      "湯",
    57→      "丼物",
    58→      "日式烤雞串",
    59→      "壽司",
    60→      "業主精選圖片",
    61→      "街景服務和 360 度相片"
    62→    ],
    63→    "scrapedAt": "2026-01-29T06:33:37.333Z",
    64→    "reserveTableUrl": "https://www.google.com/maps/reserve/v/dine/c/w4F_tV2ME-4?source=pa&opi=79508299&hl=zh-TW&gei=P_96abfFOLPJptQP9rSz2As&sourceurl=https://www.google.com/maps/preview/place?authuser%3D0%26hl%3Dzh-TW%26gl%3Dus%26pb%3D!1m16!1s0x3442a96101346ef7:0xb2e38efb4ab34a62!3m8!1m3!1d3614.4784037037157!2d121.5289901!3d25.0517694!3m2!1i489!2i906!4f13.1!4m2!3d40.7606891!4d-73.9991868!15m2!1m1!4s/m/053lrp6!12m4!2m3!1i360!2i120!4i8!13m57!2m2!1i203!2i100!3m2!2i4!5b1!6m6!1m2!1i86!2i86!1m2!1i408!2i240!7m33!1m3!1e1!2b0!3e3!1m3!1e2!2b1!3e2!1m3!1e2!2b0!3e3!1m3!1e8!2b0!3e3!1m3!1e10!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e10!2b0!3e4!1m3!1e9!2b1!3e2!2b1!9b0!15m8!1m7!1m2!1m1!1e2!2m2!1i195!2i195!3i20!14m2!1sMcYNaceANdnbi-gPt5PjyQQ!7e81!15m110!1m31!13m9!2b1!3b1!4b1!6i1!8b1!9b1!14b1!20b1!25b1!18m20!3b1!4b1!5b1!6b1!9b1!13b1!14b1!17b1!20b1!21b1!22b1!27m1!1b0!28b0!30b1!32b1!33m1!1b1!34b1!36e2!10m1!8e3!11m1!3e1!14m1!3b0!17b1!20m2!1e3!1e6!24b1!25b1!26b1!27b1!29b1!30m1!2b1!36b1!37b1!39m3!2m2!2i1!3i1!43b1!52b1!54m1!1b1!55b1!56m1!1b1!61m2!1m1!1e1!65m5!3m4!1m3!1m2!1i224!2i298!72m22!1m8!2b1!5b1!7b1!12m4!1b1!2b1!4m1!1e1!4b1!8m10!1m6!4m1!1e1!4m1!1e3!4m1!1e4!3sother_user_google_review_posts__and__hotel_and_vr_partner_review_posts!6m1!1e1!9b1!89b1!98m3!1b1!2b1!3b1!103b1!113b1!114m3!1b1!2m1!1b1!117b1!122m1!1b1!126b1!127b1!21m0!22m1!1e81!30m6!3b1!6m1!2b1!7m1!2b1!9b1!34m5!7b1!10b1!14b1!15m1!1b0!37i757!39s%25E4%25BB%258A%25E6%2597%25A5%25E9%25AD%259A%25E5%25B8%2582-%25E5%258D%2597%25E4%25BA%25AC%25E5%25BA%2597-%25E5%258E%259F%25E4%25B8%25BC%25E8%25B3%259E%25E5%2592%258C%25E9%25A3%259F%26q%3D%25E4%25BB%258A%25E6%2597%25A5%25E9%25AD%259A%25E5%25B8%2582-%25E5%258D%2597%25E4%25BA%25AC%25E5%25BA%2597-%25E5%258E%259F%25E4%25B8%25BC%25E8%25B3%259E%25E5%2592%258C%25E9%25A3%259F",
    65→    "googleFoodUrl": null,
    66→    "hotelStars": null,
    67→    "hotelDescription": null,
    68→    "checkInDate": null,
    69→    "checkOutDate": null,
    70→    "similarHotelsNearby": null,
    71→    "hotelReviewSummary": null,
    72→    "hotelAds": [],
    73→    "popularTimesLiveText": "比平常略為繁忙",
    74→    "popularTimesLivePercent": 65,
    75→    "popularTimesHistogram": {
    76→      "Su": [
    77→        {
    78→          "hour": 6,
    79→          "occupancyPercent": 0
    80→        },
    81→        {
    82→          "hour": 7,
    83→          "occupancyPercent": 0
    84→        },
    85→        {
    86→          "hour": 8,
    87→          "occupancyPercent": 0
    88→        },
    89→        {
    90→          "hour": 9,
    91→          "occupancyPercent": 0
    92→        },
    93→        {
    94→          "hour": 10,
    95→          "occupancyPercent": 0
    96→        },
    97→        {
    98→          "hour": 11,
    99→          "occupancyPercent": 47
   100→        },
   101→        {
   102→          "hour": 12,
   103→          "occupancyPercent": 65
   104→        },
   105→        {
   106→          "hour": 13,
   107→          "occupancyPercent": 59
   108→        },
   109→        {
   110→          "hour": 14,
   111→          "occupancyPercent": 38
   112→        },
   113→        {
   114→          "hour": 15,
   115→          "occupancyPercent": 0
   116→        },
   117→        {
   118→          "hour": 16,
   119→          "occupancyPercent": 0
   120→        },
   121→        {
   122→          "hour": 17,
   123→          "occupancyPercent": 52
   124→        },
   125→        {
   126→          "hour": 18,
   127→          "occupancyPercent": 75
   128→        },
   129→        {
   130→          "hour": 19,
   131→          "occupancyPercent": 75
   132→        },
   133→        {
   134→          "hour": 20,
   135→          "occupancyPercent": 55
   136→        },
   137→        {
   138→          "hour": 21,
   139→          "occupancyPercent": 0
   140→        },
   141→        {
   142→          "hour": 22,
   143→          "occupancyPercent": 0
   144→        },
   145→        {
   146→          "hour": 23,
   147→          "occupancyPercent": 0
   148→        }
   149→      ],
   150→      "Mo": [
   151→        {
   152→          "hour": 6,
   153→          "occupancyPercent": 0
   154→        },
   155→        {
   156→          "hour": 7,
   157→          "occupancyPercent": 0
   158→        },
   159→        {
   160→          "hour": 8,
   161→          "occupancyPercent": 0
   162→        },
   163→        {
   164→          "hour": 9,
   165→          "occupancyPercent": 0
   166→        },
   167→        {
   168→          "hour": 10,
   169→          "occupancyPercent": 0
   170→        },
   171→        {
   172→          "hour": 11,
   173→          "occupancyPercent": 33
   174→        },
   175→        {
   176→          "hour": 12,
   177→          "occupancyPercent": 41
   178→        },
   179→        {
   180→          "hour": 13,
   181→          "occupancyPercent": 38
   182→        },
   183→        {
   184→          "hour": 14,
   185→          "occupancyPercent": 26
   186→        },
   187→        {
   188→          "hour": 15,
   189→          "occupancyPercent": 0
   190→        },
   191→        {
   192→          "hour": 16,
   193→          "occupancyPercent": 0
   194→        },
   195→        {
   196→          "hour": 17,
   197→          "occupancyPercent": 42
   198→        },
   199→        {
   200→          "hour": 18,

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→"""
     2→Google Maps 評論分析器 - MVP 版本
     3→用於分析店家評論的可信度與品質
     4→"""
     5→import sys
     6→sys.stdout.reconfigure(encoding='utf-8')
     7→
     8→import json
     9→import re
    10→from collections import Counter, defaultdict
    11→from datetime import datetime
    12→from pathlib import Path
    13→
    14→
    15→# 可疑評論關鍵字（可能是利益交換）
    16→SUSPICIOUS_KEYWORDS = [
    17→    '打卡', '送', '免費', '贈品', '贈送', '招待',
    18→    '活動', '優惠', '折扣', '抽獎', '好評',
    19→    '五星', '5星', '滿分', '給星'
    20→]
    21→
    22→# 低品質評論特徵
    23→LOW_QUALITY_PATTERNS = [
    24→    r'^.{0,10}$',  # 太短的評論（10字以下）
    25→    r'^[好讚棒優]+$',  # 只有單一讚美詞
    26→    r'^(很好|不錯|推薦|好吃|讚)$',  # 過於簡短的正面詞
    27→]
    28→
    29→
    30→def load_reviews(file_path):
    31→    """載入評論資料"""
    32→    with open(file_path, 'r', encoding='utf-8') as f:
    33→        return json.load(f)
    34→
    35→
    36→def analyze_rating_distribution(reviews):
    37→    """分析評分分布"""
    38→    ratings = [r.get('stars', r.get('rating', 0)) for r in reviews if r.get('stars') or r.get('rating')]
    39→    counter = Counter(ratings)
    40→    total = len(ratings)
    41→
    42→    distribution = {}
    43→    for star in range(1, 6):
    44→        count = counter.get(star, 0)
    45→        percentage = (count / total * 100) if total > 0 else 0
    46→        distribution[star] = {'count': count, 'percentage': round(percentage, 1)}
    47→
    48→    avg_rating = sum(ratings) / len(ratings) if ratings else 0
    49→    return distribution, round(avg_rating, 2)
    50→
    51→
    52→def analyze_review_quality(reviews):
    53→    """分析評論品質"""
    54→    with_text = 0
    55→    with_photos = 0
    56→    suspicious = []
    57→    low_quality = []
    58→
    59→    for r in reviews:
    60→        text = r.get('text', r.get('reviewText', '')) or ''
    61→
    62→        # 有文字評論
    63→        if text.strip():
    64→            with_text += 1
    65→
    66→        # 有照片
    67→        photos = r.get('reviewImageUrls', r.get('photos', []))
    68→        if photos:
    69→            with_photos += 1
    70→
    71→        # 可疑評論偵測
    72→        text_lower = text.lower()
    73→        for keyword in SUSPICIOUS_KEYWORDS:
    74→            if keyword in text_lower:
    75→                suspicious.append({
    76→                    'text': text[:100] + '...' if len(text) > 100 else text,
    77→                    'keyword': keyword,
    78→                    'rating': r.get('stars', r.get('rating', 'N/A'))
    79→                })
    80→                break
    81→
    82→        # 低品質評論偵測
    83→        for pattern in LOW_QUALITY_PATTERNS:
    84→            if re.match(pattern, text.strip()):
    85→                low_quality.append({
    86→                    'text': text,
    87→                    'rating': r.get('stars', r.get('rating', 'N/A'))
    88→                })
    89→                break
    90→
    91→    total = len(reviews)
    92→    return {
    93→        'total': total,
    94→        'with_text': with_text,
    95→        'with_text_ratio': round(with_text / total * 100, 1) if total > 0 else 0,
    96→        'with_photos': with_photos,
    97→        'with_photos_ratio': round(with_photos / total * 100, 1) if total > 0 else 0,
    98→        'suspicious_count': len(suspicious),
    99→        'suspicious_reviews': suspicious[:10],  # 只顯示前 10 個
   100→        'low_quality_count': len(low_quality),
   101→    }
   102→
   103→
   104→def analyze_time_series(reviews):
   105→    """時序分析"""
   106→    monthly_data = defaultdict(lambda: {'count': 0, 'ratings': [], 'negative': []})
   107→
   108→    for r in reviews:
   109→        # 嘗試解析日期
   110→        date_str = r.get('publishedAtDate', r.get('date', ''))
   111→        if not date_str:
   112→            continue
   113→
   114→        try:
   115→            if 'T' in str(date_str):
   116→                date = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
   117→            else:
   118→                date = datetime.strptime(str(date_str)[:10], '%Y-%m-%d')
   119→
   120→            month_key = date.strftime('%Y-%m')
   121→            rating = r.get('stars', r.get('rating', 0))
   122→            text = r.get('text', r.get('reviewText', '')) or ''
   123→
   124→            monthly_data[month_key]['count'] += 1
   125→            monthly_data[month_key]['ratings'].append(rating)
   126→
   127→            if rating <= 2:
   128→                monthly_data[month_key]['negative'].append({
   129→                    'rating': rating,
   130→                    'text': text[:100] + '...' if len(text) > 100 else text
   131→                })
   132→        except:
   133→            continue
   134→
   135→    # 計算每月平均評分
   136→    result = {}
   137→    for month, data in sorted(monthly_data.items(), reverse=True)[:12]:  # 最近 12 個月
   138→        avg = sum(data['ratings']) / len(data['ratings']) if data['ratings'] else 0
   139→        result[month] = {
   140→            'count': data['count'],
   141→            'avg_rating': round(avg, 2),
   142→            'negative_count': len(data['negative']),
   143→            'negative_samples': data['negative'][:3]  # 每月最多 3 個負評範例
   144→        }
   145→
   146→    return result
   147→
   148→
   149→def analyze_negative_reviews(reviews):
   150→    """負面評論分析"""
   151→    negative = []
   152→
   153→    for r in reviews:
   154→        rating = r.get('stars', r.get('rating', 0))
   155→        if rating <= 2:
   156→            text = r.get('text', r.get('reviewText', '')) or ''
   157→            response = r.get('responseFromOwnerText', r.get('ownerResponse', ''))
   158→
   159→            negative.append({
   160→                'rating': rating,
   161→                'text': text,
   162→                'has_owner_response': bool(response),
   163→                'owner_response': response[:200] if response else None,
   164→                'date': r.get('publishedAtDate', r.get('date', 'N/A'))
   165→            })
   166→
   167→    # 按評分排序（最低的在前）
   168→    negative.sort(key=lambda x: (x['rating'], x['date']))
   169→
   170→    # 店家回覆率
   171→    response_rate = sum(1 for n in negative if n['has_owner_response']) / len(negative) * 100 if negative else 0
   172→
   173→    return {
   174→        'total_negative': len(negative),
   175→        'owner_response_rate': round(response_rate, 1),
   176→        'samples': negative[:10]  # 前 10 個負評
   177→    }
   178→
   179→
   180→def calculate_credibility_score(distribution, quality, negative_analysis):
   181→    """計算可信度分數 (0-100)"""
   182→    score = 50  # 基礎分
   183→
   184→    # 評論數量加分 (最多 +15)
   185→    total = quality['total']
   186→    if total >= 500:
   187→        score += 15
   188→    elif total >= 100:
   189→        score += 10
   190→    elif total >= 50:
   191→        score += 5
   192→
   193→    # 文字評論比例加分 (最多 +15)
   194→    text_ratio = quality['with_text_ratio']
   195→    if text_ratio >= 50:
   196→        score += 15
   197→    elif text_ratio >= 30:
   198→        score += 10
   199→    elif text_ratio >= 15:
   200→        score += 5
   201→
   202→    # 可疑評論扣分 (最多 -20)
   203→    suspicious_ratio = quality['suspicious_count'] / total * 100 if total > 0 else 0
   204→    if suspicious_ratio >= 10:
   205→        score -= 20
   206→    elif suspicious_ratio >= 5:
   207→        score -= 10
   208→    elif suspicious_ratio >= 2:
   209→        score -= 5
   210→
   211→    # 店家回覆負評加分 (最多 +10)
   212→    response_rate = negative_analysis['owner_response_rate']
   213→    if response_rate >= 50:
   214→        score += 10
   215→    elif response_rate >= 25:
   216→        score += 5
   217→
   218→    # 評分分布健康度 (最多 +10)
   219→    # 健康分布：有適度的 1-2 星（5-15%），不是全部都 5 星
   220→    low_rating_ratio = distribution[1]['percentage'] + distribution[2]['percentage']
   221→    if 5 <= low_rating_ratio <= 20:
   222→        score += 10
   223→    elif low_rating_ratio < 5:
   224→        score -= 5  # 太完美可能有問題
   225→
   226→    return max(0, min(100, score))
   227→
   228→
   229→def generate_report(reviews, place_name="未知店家"):
   230→    """生成完整分析報告"""
   231→    print(f"\n{'='*60}")
   232→    print(f"  Google Maps 評論分析報告")
   233→    print(f"  店家：{place_name}")
   234→    print(f"  分析時間：{datetime.now().strftime('%Y-%m-%d %H:%M')}")
   235→    print(f"{'='*60}\n")
   236→
   237→    # 1. 評分分布
   238→    distribution, avg_rating = analyze_rating_distribution(reviews)
   239→    print("【評分分布】")
   240→    print(f"  平均評分：{avg_rating} / 5.0")
   241→    for star in range(5, 0, -1):
   242→        d = distribution[star]
   243→        bar = '█' * int(d['percentage'] / 5)
   244→        print(f"  {star}星: {bar} {d['percentage']}% ({d['count']}則)")
   245→    print()
   246→
   247→    # 2. 評論品質
   248→    quality = analyze_review_quality(reviews)
   249→    print("【評論品質分析】")
   250→    print(f"  總評論數：{quality['total']}")
   251→    print(f"  有文字評論：{quality['with_text']} ({quality['with_text_ratio']}%)")
   252→    print(f"  有上傳照片：{quality['with_photos']} ({quality['with_photos_ratio']}%)")
   253→    print(f"  可疑評論數：{quality['suspicious_count']}")
   254→    print(f"  低品質評論：{quality['low_quality_count']}")
   255→
   256→    if quality['suspicious_reviews']:
   257→        print("\n  可疑評論範例：")
   258→        for i, s in enumerate(quality['suspicious_reviews'][:5], 1):
   259→            print(f"    {i}. [{s['rating']}星] 關鍵字「{s['keyword']}」")
   260→            print(f"       {s['text'][:50]}...")
   261→    print()
   262→
   263→    # 3. 負面評論分析
   264→    negative = analyze_negative_reviews(reviews)
   265→    print("【負面評論分析】")
   266→    print(f"  1-2星評論數：{negative['total_negative']}")
   267→    print(f"  店家回覆率：{negative['owner_response_rate']}%")
   268→
   269→    if negative['samples']:
   270→        print("\n  負評範例：")
   271→        for i, n in enumerate(negative['samples'][:5], 1):
   272→            text_preview = n['text'][:80] + '...' if len(n['text']) > 80 else n['text']
   273→            response_mark = " [已回覆]" if n['has_owner_response'] else ""
   274→            print(f"    {i}. [{n['rating']}星]{response_mark} {text_preview}")
   275→    print()
   276→
   277→    # 4. 時序分析
   278→    time_series = analyze_time_series(reviews)
   279→    if time_series:
   280→        print("【近期趨勢（最近 6 個月）】")
   281→        for month, data in list(time_series.items())[:6]:
   282→            trend = "↑" if data['avg_rating'] >= avg_rating else "↓"
   283→            print(f"  {month}: {data['avg_rating']}分 {trend} ({data['count']}則, {data['negative_count']}負評)")
   284→    print()
   285→
   286→    # 5. 可信度評分
   287→    credibility = calculate_credibility_score(distribution, quality, negative)
   288→    print("【可信度評估】")
   289→    print(f"  可信度分數：{credibility}/100")
   290→
   291→    if credibility >= 80:
   292→        print("  評價：★★★★★ 高度可信")
   293→        print("  說明：評論數量充足、品質良好、無明顯異常")
   294→    elif credibility >= 60:
   295→        print("  評價：★★★★☆ 相對可信")
   296→        print("  說明：整體正常，但有少數需注意的地方")
   297→    elif credibility >= 40:
   298→        print("  評價：★★★☆☆ 需謹慎參考")
   299→        print("  說明：存在一些可疑跡象，建議多方比較")
   300→    else:
   301→        print("  評價：★★☆☆☆ 可信度存疑")
   302→        print("  說明：評論可能受到人為操作，請謹慎判斷")
   303→
   304→    print(f"\n{'='*60}\n")
   305→
   306→    return {
   307→        'place_name': place_name,
   308→        'avg_rating': avg_rating,
   309→        'distribution': distribution,
   310→        'quality': quality,
   311→        'negative': negative,
   312→        'time_series': time_series,
   313→        'credibility_score': credibility
   314→    }
   315→
   316→
   317→def main():
   318→    """主程式"""
   319→    import argparse
   320→
   321→    parser = argparse.ArgumentParser(description='Google Maps 評論分析器')
   322→    parser.add_argument('file', help='評論 JSON 檔案路徑')
   323→    parser.add_argument('-n', '--name', default='未知店家', help='店家名稱')
   324→    parser.add_argument('-o', '--output', help='輸出報告 JSON 檔案')
   325→
   326→    args = parser.parse_args()
   327→
   328→    # 載入評論
   329→    reviews = load_reviews(args.file)
   330→
   331→    # 如果是巢狀結構，嘗試提取評論陣列
   332→    if isinstance(reviews, dict):
   333→        reviews = reviews.get('reviews', reviews.get('data', [reviews]))
   334→    if not isinstance(reviews, list):
   335→        reviews = [reviews]
   336→
   337→    print(f"載入 {len(reviews)} 則評論")
   338→
   339→    # 生成報告
   340→    result = generate_report(reviews, args.name)
   341→
   342→    # 輸出 JSON
   343→    if args.output:
   344→        with open(args.output, 'w', encoding='utf-8') as f:
   345→            json.dump(result, f, ensure_ascii=False, indent=2)
   346→        print(f"報告已儲存至：{args.output}")
   347→
   348→
   349→if __name__ == "__main__":
   350→    main()
   351→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


讓我繼續讀取評論資料以了解評論者相關欄位的結構。


Bash

cd "C:\Users\User\Documents\GitHub\apify-scraper-practice" && python -X utf8 -c "
import sys
sys.stdout.reconfigure(encoding='utf-8')
import json

with open('step2_reviews_20260129_143615.json', 'r', encoding='utf-8') as f:
    data = json.load(f)

# 查看資料結構
print('資料類型:', type(data))
print('資料長度:', len(data))

# 如果是列表，看第一筆
if isinstance(data, list):
    place = data[0]
    print('\n店家資訊欄位:', list(place.keys())[:20])

    # 找評論欄位
    if 'reviews' in place:
        reviews = place['reviews']
        print('\n評論數:', len(reviews))
        if reviews:
            print('\n評論欄位:', list(reviews[0].keys()))
            print('\n範例評論:')
            import json
            print(json.dumps(reviews[0], ensure_ascii=False, indent=2))
"

讀取評論 JSON 檔案結構


資料類型: <class 'list'>
資料長度: 5

店家資訊欄位: ['title', 'subTitle', 'description', 'price', 'categoryName', 'address', 'neighborhood', 'street', 'city', 'postalCode', 'state', 'countryCode', 'website', 'phone', 'phoneUnformatted', 'claimThisBusiness', 'location', 'locatedIn', 'floor', 'plusCode']

評論數: 300

評論欄位: ['reviewerId', 'reviewerUrl', 'name', 'reviewerNumberOfReviews', 'isLocalGuide', 'reviewerPhotoUrl', 'text', 'textTranslated', 'publishAt', 'publishedAtDate', 'likesCount', 'reviewId', 'reviewUrl', 'reviewOrigin', 'stars', 'rating', 'responseFromOwnerDate', 'responseFromOwnerText', 'reviewImageUrls', 'reviewContext', 'reviewDetailedRating', 'visitedIn', 'originalLanguage', 'translatedLanguage']

範例評論:
{
  "reviewerId": "100734908245064780279",
  "reviewerUrl": "https://www.google.com/maps/contrib/100734908245064780279?hl=zh-TW",
  "name": "魏佩怡",
  "reviewerNumberOfReviews": 1,
  "isLocalGuide": false,
  "reviewerPhotoUrl": "https://lh3.googleusercontent.com/a/ACg8ocJ7tdIWW5Q62vG26M58Xw2V3VcqTR6em3btH178RNvKIO0wxQ=s1920-c-rp-mo-br100",
  "text": "店員很貼心，餐點很棒",
  "textTranslated": null,
  "publishAt": "2 小時前",
  "publishedAtDate": "2026-01-29T04:09:42.091Z",
  "likesCount": 0,
  "reviewId": "Ci9DQUlRQUNvZENodHljRjlvT2t4Q1RrSkdkMjlJUzJVMVlqTjNSV2sxUzBWUWRHYxAB",
  "reviewUrl": "https://www.google.com/maps/reviews/data=!4m8!14m7!1m6!2m5!1sCi9DQUlRQUNvZENodHljRjlvT2t4Q1RrSkdkMjlJUzJVMVlqTjNSV2sxUzBWUWRHYxAB!2m1!1s0x0:0xb2e38efb4ab34a62!3m1!1s2@1:CAIQACodChtycF9oOkxCTkJGd29IS2U1YjN3RWk1S0VQdGc%7C0dThq54ToE6%7C?hl=zh-TW",
  "reviewOrigin": "Google",
  "stars": 5,
  "rating": null,
  "responseFromOwnerDate": null,
  "responseFromOwnerText": null,
  "reviewImageUrls": [],
  "reviewContext": {
    "餐點類型": "午餐",
    "平均每人消費金額": "$400-600"
  },
  "reviewDetailedRating": {
    "餐點": 5,
    "服務": 5,
    "氣氛": 5
  },
  "visitedIn": null,
  "originalLanguage": "zh-Hant",
  "translatedLanguage": null
}


現在我了解資料結構了。讓我查看更多評論者資料，包括有高評論數和 Local Guide 的範例。


cd "C:\Users\User\Documents\GitHub\apify-scraper-practice" && python -X utf8 -c "
import sys
sys.stdout.reconfigure(encoding='utf-8')
import json

with open('step2_reviews_20260129_143615.json', 'r', encoding='utf-8') as f:
    data = json.load(f)

# 收集所有評論
all_reviews = []
for place in data:
    reviews = place.get('reviews', [])
    for r in reviews:
        r['_place_title'] = place.get('title', '')
    all_reviews.extend(reviews)

print(f'總評論數: {len(all_reviews)}')

# 分析評論者特徵
local_guides = [r for r in all_reviews if r.get('isLocalGuide')]
high_reviewers = [r for r in all_reviews if (r.get('reviewerNumberOfReviews') or 0) > 100]
single_reviewers = [r for r in all_reviews if (r.get('reviewerNumberOfReviews') or 0) == 1]
with_photos = [r for r in all_reviews if r.get('reviewImageUrls')]
no_text = [r for r in all_reviews if not (r.get('text') or '').strip()]

print(f'\\n評論者統計:')
print(f'  - Local Guide: {len(local_guides)} ({len(local_guides)/len(all_reviews)*100:.1f}%)')
print(f'  - 評論數 > 100: {len(high_reviewers)} ({len(high_reviewers)/len(all_reviews)*100:.1f}%)')
print(f'  - 單一評論帳號: {len(single_reviewers)} ({len(single_reviewers)/len(all_reviews)*100:.1f}%)')
print(f'  - 有附照片: {len(with_photos)} ({len(with_photos)/len(all_reviews)*100:.1f}%)')
print(f'  - 無文字: {len(no_text)} ({len(no_text)/len(all_reviews)*100:.1f}%)')

# 顯示幾個不同類型的評論者
print('\\n--- 範例 Local Guide 評論者 ---')
for r in local_guides[:2]:
    print(f\"  {r['name']}: {r.get('reviewerNumberOfReviews')} 則評論, {r['stars']} 星\")
    print(f\"    文字: {(r.get('text') or '')[:50]}...\")

print('\\n--- 範例高評論數評論者 ---')
for r in sorted(high_reviewers, key=lambda x: x.get('reviewerNumberOfReviews', 0), reverse=True)[:2]:
    print(f\"  {r['name']}: {r.get('reviewerNumberOfReviews')} 則評論, {r['stars']} 星\")
    print(f\"    文字: {(r.get('text') or '')[:50]}...\")

print('\\n--- 範例單一評論帳號 ---')
for r in single_reviewers[:2]:
    print(f\"  {r['name']}: {r.get('reviewerNumberOfReviews')} 則評論, {r['stars']} 星\")
    print(f\"    文字: {(r.get('text') or '')[:50]}...\")

# 評論數分布
review_counts = [r.get('reviewerNumberOfReviews', 0) for r in all_reviews]
print(f'\\n評論數統計:')
print(f'  最小: {min(review_counts)}, 最大: {max(review_counts)}, 平均: {sum(review_counts)/len(review_counts):.1f}')
"

分析評論者特徵統計


總評論數: 1407

評論者統計:
  - Local Guide: 1076 (76.5%)
  - 評論數 > 100: 417 (29.6%)
  - 單一評論帳號: 40 (2.8%)
  - 有附照片: 780 (55.4%)
  - 無文字: 201 (14.3%)

--- 範例 Local Guide 評論者 ---
  Allan Chen: 32 則評論, 4 星
    文字: 丼飯新鮮美味
手機點餐、機器買單...
  Ian: 55 則評論, 4 星
    文字: 非常美味 大推 生魚片新鮮 cp值很高 不過醬油很難吃 能換成日式的會更好 現在的醬油超鮮超台 和生...

--- 範例高評論數評論者 ---
  A-Kwon Fang: 3459 則評論, 4 星
    文字: 近個把月在松江南京東側的建國高架橋旁開上的新咖啡廳，有著自己獨棟的三層樓空間；除了一樓因櫃台和製餐區...
  Shiba eat U: 2730 則評論, 5 星
    文字: 吃點輕食，喝杯咖啡的好去處
可以逛逛質感小物、周邊
可惜飲品表現普普
二樓環境只要有人在聊天就會很轟...

--- 範例單一評論帳號 ---
  魏佩怡: 1 則評論, 5 星
    文字: 店員很貼心，餐點很棒...
  李漢蘋: 1 則評論, 5 星
    文字: ...

評論數統計:
  最小: 0, 最大: 3459, 平均: 154.8


現在我有足夠的資訊來實作完整的評論者分類與可靠度評分系統了。讓我建立 `reviewer_analysis.py`。


Write

"""
評論者分類與可靠度評分系統 (Reviewer Classification System)
=====================================================
分析 Google Maps 評論者的類型、可靠度，並計算加權真實評分

功能：
1. 評論者分類系統（7 大類）
2. 評論者可靠度評分 (RCS: 0-100)
3. 異常行為偵測
4. 評論者結構分析報告
5. 加權評分計算
"""
import sys
sys.stdout.reconfigure(encoding='utf-8')

import json
import math
from collections import Counter, defaultdict
from datetime import datetime, timedelta
from pathlib import Path
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
from enum import Enum

# =============================================================================
# 評論者分類定義
# =============================================================================

class ReviewerType(Enum):
    """評論者類型"""
    ORGANIC = "ORGANIC"           # 分享型：一般用戶，正常分享體驗
    INFLUENCER = "INFLUENCER"     # 網紅型：專業照片、可能業配
    RATING_ONLY = "RATING_ONLY"   # 評分部隊：主要只給評分，少文字
    EXPERT = "EXPERT"             # 專業評論者：長文、有深度分析
    SUSPICIOUS = "SUSPICIOUS"     # 可疑帳號：異常行為
    LOCAL_GUIDE = "LOCAL_GUIDE"   # 在地嚮導：有 Google 認證
    INACTIVE = "INACTIVE"         # 沉睡用戶：低活躍度

@dataclass
class ReviewerProfile:
    """評論者檔案"""
    reviewer_id: str
    name: str
    reviewer_url: str
    total_reviews: int
    is_local_guide: bool
    reviews: List[dict] = field(default_factory=list)

    # 計算屬性
    reviewer_type: Optional[ReviewerType] = None
    reliability_score: float = 0.0
    anomaly_flags: List[str] = field(default_factory=list)

    def add_review(self, review: dict):
        """新增評論"""
        self.reviews.append(review)

    @property
    def text_rate(self) -> float:
        """有文字評論的比例"""
        if not self.reviews:
            return 0.0
        with_text = sum(1 for r in self.reviews if (r.get('text') or '').strip())
        return with_text / len(self.reviews)

    @property
    def photo_rate(self) -> float:
        """有照片評論的比例"""
        if not self.reviews:
            return 0.0
        with_photo = sum(1 for r in self.reviews if r.get('reviewImageUrls'))
        return with_photo / len(self.reviews)

    @property
    def avg_rating(self) -> float:
        """平均評分"""
        if not self.reviews:
            return 0.0
        ratings = [r.get('stars', 0) for r in self.reviews if r.get('stars')]
        return sum(ratings) / len(ratings) if ratings else 0.0

    @property
    def five_star_rate(self) -> float:
        """五星評論比例"""
        if not self.reviews:
            return 0.0
        five_stars = sum(1 for r in self.reviews if r.get('stars') == 5)
        return five_stars / len(self.reviews)

    @property
    def avg_text_length(self) -> float:
        """平均文字長度"""
        texts = [(r.get('text') or '') for r in self.reviews]
        if not texts:
            return 0.0
        return sum(len(t) for t in texts) / len(texts)

# =============================================================================
# 評論者分類器
# =============================================================================

class ReviewerClassifier:
    """評論者分類器"""

    # 分類閾值設定
    THRESHOLDS = {
        'expert_min_reviews': 50,
        'expert_min_avg_text_length': 100,
        'influencer_min_photos': 0.7,
        'influencer_min_reviews': 20,
        'rating_only_no_text_rate': 0.8,
        'organic_min_reviews': 10,
        'organic_max_reviews': 200,
        'suspicious_max_reviews': 2,
        'inactive_max_reviews': 5,
    }

    def classify(self, profile: ReviewerProfile) -> ReviewerType:
        """
        分類評論者類型
        優先順序：SUSPICIOUS > RATING_ONLY > EXPERT > INFLUENCER > LOCAL_GUIDE > ORGANIC > INACTIVE
        """
        # 1. 檢查可疑帳號（優先）
        if self._is_suspicious(profile):
            return ReviewerType.SUSPICIOUS

        # 2. 檢查評分部隊（80%+ 無文字）
        if self._is_rating_only(profile):
            return ReviewerType.RATING_ONLY

        # 3. 檢查專業評論者（高評論數 + 長文）
        if self._is_expert(profile):
            return ReviewerType.EXPERT

        # 4. 檢查網紅型（高照片比例）
        if self._is_influencer(profile):
            return ReviewerType.INFLUENCER

        # 5. 檢查 Local Guide
        if profile.is_local_guide:
            # Local Guide 但評論數正常，歸類為 LOCAL_GUIDE
            if profile.total_reviews >= self.THRESHOLDS['organic_min_reviews']:
                return ReviewerType.LOCAL_GUIDE

        # 6. 檢查分享型（正常用戶）
        if self._is_organic(profile):
            return ReviewerType.ORGANIC

        # 7. 其他歸為沉睡用戶
        return ReviewerType.INACTIVE

    def _is_suspicious(self, profile: ReviewerProfile) -> bool:
        """判斷是否為可疑帳號"""
        # 單一評論或極少評論
        if profile.total_reviews <= self.THRESHOLDS['suspicious_max_reviews']:
            return True
        return False

    def _is_rating_only(self, profile: ReviewerProfile) -> bool:
        """判斷是否為評分部隊"""
        if profile.total_reviews >= 10:  # 至少要有一定數量才判斷
            if (1 - profile.text_rate) >= self.THRESHOLDS['rating_only_no_text_rate']:
                return True
        return False

    def _is_expert(self, profile: ReviewerProfile) -> bool:
        """判斷是否為專業評論者"""
        if (profile.total_reviews >= self.THRESHOLDS['expert_min_reviews'] and
            profile.avg_text_length >= self.THRESHOLDS['expert_min_avg_text_length']):
            return True
        return False

    def _is_influencer(self, profile: ReviewerProfile) -> bool:
        """判斷是否為網紅型"""
        if (profile.photo_rate >= self.THRESHOLDS['influencer_min_photos'] and
            profile.total_reviews >= self.THRESHOLDS['influencer_min_reviews']):
            return True
        return False

    def _is_organic(self, profile: ReviewerProfile) -> bool:
        """判斷是否為分享型"""
        return (self.THRESHOLDS['organic_min_reviews'] <=
                profile.total_reviews <=
                self.THRESHOLDS['organic_max_reviews'])

# =============================================================================
# 可靠度評分計算器
# =============================================================================

class ReliabilityScorer:
    """
    評論者可靠度評分 (Reviewer Credibility Score, RCS)
    範圍：0-100
    """

    def __init__(self):
        # 評論數量的理想範圍（鐘形曲線）
        self.ideal_review_count_center = 100  # 中心點
        self.ideal_review_count_sigma = 50    # 標準差

    def calculate_score(self, profile: ReviewerProfile) -> float:
        """計算可靠度分數"""
        score = 0.0

        # 1. 評論數量分數（鐘形曲線：50-150 最佳）- 最高 30 分
        review_count_score = self._review_count_score(profile.total_reviews)
        score += review_count_score * 30

        # 2. 文字率分數 - 最高 25 分
        text_rate_score = profile.text_rate
        score += text_rate_score * 25

        # 3. Local Guide 加分 - 最高 15 分
        if profile.is_local_guide:
            score += 15

        # 4. 照片率加分 - 最高 10 分
        photo_score = min(profile.photo_rate, 0.5) * 20  # 最多加到 50% 照片率
        score += photo_score

        # 5. 評分分布健康度 - 最高 10 分
        rating_health_score = self._rating_distribution_score(profile)
        score += rating_health_score * 10

        # 6. 異常扣分
        anomaly_penalty = self._calculate_anomaly_penalty(profile)
        score -= anomaly_penalty

        # 7. 類型調整
        type_adjustment = self._type_adjustment(profile.reviewer_type)
        score += type_adjustment

        return max(0, min(100, score))

    def _review_count_score(self, count: int) -> float:
        """
        評論數量分數（鐘形曲線）
        50-150 為最佳區間，過少或過多都會降分
        """
        if count == 0:
            return 0.0

        # 使用高斯函數
        center = self.ideal_review_count_center
        sigma = self.ideal_review_count_sigma

        # 對數轉換使分布更平滑
        log_count = math.log(count + 1)
        log_center = math.log(center + 1)
        log_sigma = 1.5  # 對數空間的標準差

        score = math.exp(-((log_count - log_center) ** 2) / (2 * log_sigma ** 2))
        return score

    def _rating_distribution_score(self, profile: ReviewerProfile) -> float:
        """評分分布健康度評估"""
        if not profile.reviews:
            return 0.5

        # 計算評分分布
        ratings = [r.get('stars', 0) for r in profile.reviews if r.get('stars')]
        if not ratings:
            return 0.5

        counter = Counter(ratings)
        total = len(ratings)

        # 計算五星比例
        five_star_rate = counter.get(5, 0) / total

        # 全是五星：可能不正常
        if five_star_rate >= 0.95:
            return 0.3
        # 大部分五星但有一些其他：略不正常
        elif five_star_rate >= 0.85:
            return 0.6
        # 正常分布
        elif 0.4 <= five_star_rate <= 0.8:
            return 1.0
        # 分布較平均
        else:
            return 0.8

    def _calculate_anomaly_penalty(self, profile: ReviewerProfile) -> float:
        """計算異常行為扣分"""
        penalty = 0.0

        # 1. 單一評論帳號：扣 20 分
        if profile.total_reviews <= 1:
            profile.anomaly_flags.append("SINGLE_REVIEW")
            penalty += 20

        # 2. 五星偏差（90%+ 五星）：扣 15 分
        if profile.five_star_rate >= 0.9:
            profile.anomaly_flags.append("FIVE_STAR_BIAS")
            penalty += 15

        # 3. 無文字模式（80%+ 無文字）：扣 10 分
        if profile.text_rate < 0.2:
            profile.anomaly_flags.append("NO_TEXT_PATTERN")
            penalty += 10

        # 4. 時間集中評論（檢查此店家的評論）
        if self._has_burst_reviews(profile):
            profile.anomaly_flags.append("BURST_REVIEWS")
            penalty += 15

        return penalty

    def _has_burst_reviews(self, profile: ReviewerProfile) -> bool:
        """檢查是否有時間集中評論"""
        if len(profile.reviews) < 3:
            return False

        # 解析評論日期
        dates = []
        for r in profile.reviews:
            date_str = r.get('publishedAtDate', '')
            if date_str:
                try:
                    if 'T' in str(date_str):
                        date = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
                        dates.append(date)
                except:
                    pass

        if len(dates) < 3:
            return False

        # 檢查是否在短時間內（1小時）有多則評論
        dates.sort()
        for i in range(len(dates) - 2):
            time_diff = dates[i + 2] - dates[i]
            if time_diff.total_seconds() < 3600:  # 1小時內3則評論
                return True

        return False

    def _type_adjustment(self, reviewer_type: Optional[ReviewerType]) -> float:
        """根據評論者類型調整分數"""
        adjustments = {
            ReviewerType.ORGANIC: 5,
            ReviewerType.LOCAL_GUIDE: 3,
            ReviewerType.EXPERT: 5,
            ReviewerType.INFLUENCER: 0,  # 中立
            ReviewerType.RATING_ONLY: -5,
            ReviewerType.SUSPICIOUS: -15,
            ReviewerType.INACTIVE: -5,
        }
        return adjustments.get(reviewer_type, 0)

# =============================================================================
# 主分析系統
# =============================================================================

class ReviewerAnalysisSystem:
    """評論者分析系統"""

    def __init__(self):
        self.classifier = ReviewerClassifier()
        self.scorer = ReliabilityScorer()
        self.profiles: Dict[str, ReviewerProfile] = {}
        self.place_profiles: Dict[str, Dict[str, ReviewerProfile]] = {}  # 每店家的評論者

    def load_reviews(self, file_path: str) -> List[dict]:
        """載入評論資料"""
        with open(file_path, 'r', encoding='utf-8') as f:
            data = json.load(f)
        return data if isinstance(data, list) else [data]

    def analyze_places(self, places: List[dict]) -> Dict:
        """分析多個店家"""
        results = {
            'places': [],
            'summary': {},
            'generated_at': datetime.now().isoformat()
        }

        all_reviewers = {}

        for place in places:
            place_title = place.get('title', '未知店家')
            reviews = place.get('reviews', [])

            if not reviews:
                continue

            print(f"\n分析店家: {place_title}")
            print(f"  評論數: {len(reviews)}")

            # 分析此店家的評論者
            place_result = self.analyze_place(place_title, reviews)
            results['places'].append(place_result)

            # 收集所有評論者
            for profile in place_result['profiles'].values():
                if profile.reviewer_id not in all_reviewers:
                    all_reviewers[profile.reviewer_id] = profile

        # 產生整體摘要
        results['summary'] = self._generate_summary(results['places'], all_reviewers)

        return results

    def analyze_place(self, place_title: str, reviews: List[dict]) -> dict:
        """分析單一店家"""
        profiles: Dict[str, ReviewerProfile] = {}

        # 建立評論者檔案
        for review in reviews:
            reviewer_id = review.get('reviewerId', '')
            if not reviewer_id:
                continue

            if reviewer_id not in profiles:
                profiles[reviewer_id] = ReviewerProfile(
                    reviewer_id=reviewer_id,
                    name=review.get('name', '匿名'),
                    reviewer_url=review.get('reviewerUrl', ''),
                    total_reviews=review.get('reviewerNumberOfReviews', 0) or 0,
                    is_local_guide=review.get('isLocalGuide', False)
                )

            profiles[reviewer_id].add_review(review)

        # 分類並計算可靠度
        type_distribution = Counter()
        reliability_scores = []
        anomaly_reviewers = []

        for profile in profiles.values():
            # 分類
            profile.reviewer_type = self.classifier.classify(profile)
            type_distribution[profile.reviewer_type.value] += 1

            # 計算可靠度
            profile.reliability_score = self.scorer.calculate_score(profile)
            reliability_scores.append(profile.reliability_score)

            # 收集異常評論者
            if profile.anomaly_flags:
                anomaly_reviewers.append({
                    'name': profile.name,
                    'reviewer_id': profile.reviewer_id,
                    'type': profile.reviewer_type.value,
                    'reliability_score': round(profile.reliability_score, 1),
                    'anomaly_flags': profile.anomaly_flags,
                    'total_reviews': profile.total_reviews,
                    'stars': [r.get('stars') for r in profile.reviews]
                })

        # 計算加權評分
        weighted_rating = self._calculate_weighted_rating(profiles, reviews)
        original_rating = sum(r.get('stars', 0) for r in reviews if r.get('stars')) / len([r for r in reviews if r.get('stars')]) if reviews else 0

        return {
            'place_title': place_title,
            'total_reviews': len(reviews),
            'unique_reviewers': len(profiles),
            'profiles': profiles,
            'type_distribution': dict(type_distribution),
            'reliability_stats': {
                'mean': round(sum(reliability_scores) / len(reliability_scores), 1) if reliability_scores else 0,
                'min': round(min(reliability_scores), 1) if reliability_scores else 0,
                'max': round(max(reliability_scores), 1) if reliability_scores else 0,
            },
            'anomaly_reviewers': anomaly_reviewers,
            'original_rating': round(original_rating, 2),
            'weighted_rating': round(weighted_rating, 2),
            'rating_diff': round(weighted_rating - original_rating, 2)
        }

    def _calculate_weighted_rating(self, profiles: Dict[str, ReviewerProfile], reviews: List[dict]) -> float:
        """計算加權評分"""
        weighted_sum = 0.0
        weight_total = 0.0

        for review in reviews:
            reviewer_id = review.get('reviewerId', '')
            stars = review.get('stars', 0)

            if not stars or not reviewer_id:
                continue

            # 取得評論者的可靠度分數作為權重
            profile = profiles.get(reviewer_id)
            if profile:
                weight = profile.reliability_score / 100.0  # 轉換為 0-1
                weight = max(0.1, weight)  # 最低權重 0.1
            else:
                weight = 0.5  # 預設權重

            weighted_sum += stars * weight
            weight_total += weight

        return weighted_sum / weight_total if weight_total > 0 else 0.0

    def _generate_summary(self, place_results: List[dict], all_reviewers: Dict) -> dict:
        """產生整體摘要"""
        total_reviews = sum(p['total_reviews'] for p in place_results)
        total_unique_reviewers = len(all_reviewers)

        # 合併類型分布
        combined_type_dist = Counter()
        for p in place_results:
            combined_type_dist.update(p['type_distribution'])

        # 計算各類型百分比
        type_percentages = {}
        for type_name, count in combined_type_dist.items():
            type_percentages[type_name] = {
                'count': count,
                'percentage': round(count / total_unique_reviewers * 100, 1) if total_unique_reviewers > 0 else 0
            }

        # 計算可靠度分布
        all_scores = [p.reliability_score for p in all_reviewers.values()]
        reliability_distribution = {
            'excellent': len([s for s in all_scores if s >= 80]),  # 80-100
            'good': len([s for s in all_scores if 60 <= s < 80]),  # 60-80
            'moderate': len([s for s in all_scores if 40 <= s < 60]),  # 40-60
            'poor': len([s for s in all_scores if 20 <= s < 40]),  # 20-40
            'very_poor': len([s for s in all_scores if s < 20]),  # 0-20
        }

        # 收集所有異常評論者
        all_anomalies = []
        for p in place_results:
            all_anomalies.extend(p['anomaly_reviewers'])

        return {
            'total_places': len(place_results),
            'total_reviews': total_reviews,
            'total_unique_reviewers': total_unique_reviewers,
            'type_distribution': type_percentages,
            'reliability_distribution': reliability_distribution,
            'avg_reliability': round(sum(all_scores) / len(all_scores), 1) if all_scores else 0,
            'total_anomaly_reviewers': len(all_anomalies)
        }

    def generate_report(self, results: dict):
        """產生分析報告"""
        print("\n" + "=" * 70)
        print("  評論者分類與可靠度評分分析報告")
        print("  Reviewer Classification & Reliability Score Report")
        print("=" * 70)
        print(f"  產生時間: {results['generated_at']}")

        summary = results['summary']
        print(f"\n【整體摘要】")
        print(f"  分析店家數: {summary['total_places']}")
        print(f"  總評論數: {summary['total_reviews']}")
        print(f"  不重複評論者: {summary['total_unique_reviewers']}")
        print(f"  平均可靠度: {summary['avg_reliability']}/100")

        # 類型分布
        print(f"\n【評論者類型分布】")
        type_display = {
            'ORGANIC': ('分享型', '一般用戶，正常分享體驗'),
            'INFLUENCER': ('網紅型', '專業照片、可能業配'),
            'RATING_ONLY': ('評分部隊', '80%+ 無文字評論'),
            'EXPERT': ('專業評論者', '高評論數、長文有深度'),
            'SUSPICIOUS': ('可疑帳號', '新帳號、單一評論'),
            'LOCAL_GUIDE': ('在地嚮導', 'Google 認證用戶'),
            'INACTIVE': ('沉睡用戶', '低活躍度'),
        }

        for type_name, info in summary['type_distribution'].items():
            display_name, desc = type_display.get(type_name, (type_name, ''))
            bar = '#' * int(info['percentage'] / 2)
            print(f"  {display_name:12} | {bar:50} {info['percentage']:5.1f}% ({info['count']})")

        # 可靠度分布
        print(f"\n【可靠度分數分布】")
        rel_dist = summary['reliability_distribution']
        rel_display = [
            ('優秀 (80-100)', rel_dist['excellent'], '##'),
            ('良好 (60-80)', rel_dist['good'], '##'),
            ('普通 (40-60)', rel_dist['moderate'], '=='),
            ('較差 (20-40)', rel_dist['poor'], '--'),
            ('很差 (0-20)', rel_dist['very_poor'], '..'),
        ]
        total = sum(rel_dist.values())
        for name, count, symbol in rel_display:
            pct = count / total * 100 if total > 0 else 0
            bar = symbol * int(pct / 2)
            print(f"  {name:14} | {bar:50} {pct:5.1f}% ({count})")

        # 各店家詳細分析
        print("\n" + "-" * 70)
        print("【各店家詳細分析】")

        for place in results['places']:
            print(f"\n  店家: {place['place_title']}")
            print(f"  " + "-" * 40)
            print(f"    評論數: {place['total_reviews']} | 評論者: {place['unique_reviewers']}")
            print(f"    原始評分: {place['original_rating']} -> 加權評分: {place['weighted_rating']} ({'+' if place['rating_diff'] >= 0 else ''}{place['rating_diff']})")
            print(f"    可靠度: 平均 {place['reliability_stats']['mean']} | 最低 {place['reliability_stats']['min']} | 最高 {place['reliability_stats']['max']}")

            # 類型分布
            print(f"    類型分布:")
            for type_name, count in sorted(place['type_distribution'].items(), key=lambda x: -x[1]):
                pct = count / place['unique_reviewers'] * 100 if place['unique_reviewers'] > 0 else 0
                display_name = type_display.get(type_name, (type_name, ''))[0]
                print(f"      - {display_name}: {count} ({pct:.1f}%)")

            # 異常評論者
            if place['anomaly_reviewers']:
                print(f"    異常評論者 ({len(place['anomaly_reviewers'])} 位):")
                for anomaly in place['anomaly_reviewers'][:5]:  # 最多顯示 5 位
                    flags = ', '.join(anomaly['anomaly_flags'])
                    print(f"      - {anomaly['name']} | RCS: {anomaly['reliability_score']} | {flags}")
                if len(place['anomaly_reviewers']) > 5:
                    print(f"      ... 還有 {len(place['anomaly_reviewers']) - 5} 位")

        # 異常行為統計
        print("\n" + "-" * 70)
        print("【異常行為偵測統計】")

        anomaly_counter = Counter()
        for place in results['places']:
            for anomaly in place['anomaly_reviewers']:
                for flag in anomaly['anomaly_flags']:
                    anomaly_counter[flag] += 1

        anomaly_display = {
            'SINGLE_REVIEW': '單一評論帳號',
            'FIVE_STAR_BIAS': '五星偏差 (90%+)',
            'NO_TEXT_PATTERN': '無文字模式 (80%+)',
            'BURST_REVIEWS': '時間集中評論',
        }

        print(f"  總異常評論者: {summary['total_anomaly_reviewers']}")
        for flag, count in anomaly_counter.most_common():
            display = anomaly_display.get(flag, flag)
            print(f"    - {display}: {count} 位")

        # 加權評分比較
        print("\n" + "-" * 70)
        print("【加權評分比較】")
        print("  (根據評論者可靠度加權計算的「真實評分」)")
        print()
        print(f"  {'店家名稱':<30} {'原始評分':>10} {'加權評分':>10} {'差異':>10}")
        print(f"  {'-' * 30} {'-' * 10} {'-' * 10} {'-' * 10}")

        for place in results['places']:
            name = place['place_title'][:28]
            diff = place['rating_diff']
            diff_str = f"+{diff}" if diff >= 0 else f"{diff}"
            print(f"  {name:<30} {place['original_rating']:>10} {place['weighted_rating']:>10} {diff_str:>10}")

        print("\n" + "=" * 70)
        print("  分析完成")
        print("=" * 70 + "\n")

    def export_json(self, results: dict, output_path: str):
        """匯出 JSON 報告"""
        # 轉換 profiles 為可序列化格式
        export_data = {
            'generated_at': results['generated_at'],
            'summary': results['summary'],
            'places': []
        }

        for place in results['places']:
            place_data = {
                'place_title': place['place_title'],
                'total_reviews': place['total_reviews'],
                'unique_reviewers': place['unique_reviewers'],
                'type_distribution': place['type_distribution'],
                'reliability_stats': place['reliability_stats'],
                'original_rating': place['original_rating'],
                'weighted_rating': place['weighted_rating'],
                'rating_diff': place['rating_diff'],
                'anomaly_reviewers': place['anomaly_reviewers'],
                'reviewer_details': []
            }

            # 轉換 profiles
            for profile in place['profiles'].values():
                place_data['reviewer_details'].append({
                    'reviewer_id': profile.reviewer_id,
                    'name': profile.name,
                    'reviewer_url': profile.reviewer_url,
                    'total_reviews': profile.total_reviews,
                    'is_local_guide': profile.is_local_guide,
                    'reviewer_type': profile.reviewer_type.value if profile.reviewer_type else None,
                    'reliability_score': round(profile.reliability_score, 1),
                    'anomaly_flags': profile.anomaly_flags,
                    'text_rate': round(profile.text_rate, 2),
                    'photo_rate': round(profile.photo_rate, 2),
                    'five_star_rate': round(profile.five_star_rate, 2),
                    'avg_text_length': round(profile.avg_text_length, 1),
                    'review_count_here': len(profile.reviews),
                    'stars_given': [r.get('stars') for r in profile.reviews]
                })

            export_data['places'].append(place_data)

        with open(output_path, 'w', encoding='utf-8') as f:
            json.dump(export_data, f, ensure_ascii=False, indent=2)

        print(f"JSON 報告已儲存: {output_path}")

# =============================================================================
# 主程式
# =============================================================================

def main():
    """主程式"""
    import argparse

    parser = argparse.ArgumentParser(description='評論者分類與可靠度評分系統')
    parser.add_argument('files', nargs='+', help='評論 JSON 檔案路徑')
    parser.add_argument('-o', '--output', help='輸出 JSON 報告路徑')

    args = parser.parse_args()

    # 初始化分析系統
    system = ReviewerAnalysisSystem()

    # 收集所有店家
    all_places = []
    for file_path in args.files:
        print(f"載入: {file_path}")
        places = system.load_reviews(file_path)
        all_places.extend(places)

    print(f"\n總共載入 {len(all_places)} 間店家")

    # 執行分析
    results = system.analyze_places(all_places)

    # 產生報告
    system.generate_report(results)

    # 匯出 JSON
    if args.output:
        system.export_json(results, args.output)
    else:
        # 預設輸出檔名
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        output_path = f"reviewer_analysis_{timestamp}.json"
        system.export_json(results, output_path)

if __name__ == "__main__":
    main()


File created successfully at: C:\Users\User\Documents\GitHub\apify-scraper-practice\reviewer_analysis.py


現在讓我執行測試，使用兩個評論資料檔案。


Bash

cd "C:\Users\User\Documents\GitHub\apify-scraper-practice" && python -X utf8 reviewer_analysis.py step2_reviews_20260129_143615.json step2b_lowest_reviews_20260129_144321.json

執行評論者分析系統測試


載入: step2_reviews_20260129_143615.json
載入: step2b_lowest_reviews_20260129_144321.json

總共載入 10 間店家

分析店家: 今日魚市-南京店-原丼賞和食
  評論數: 300

分析店家: 義大利米蘭手工窯烤披薩 台北中山店
  評論數: 300

分析店家: 曹料理
  評論數: 300

分析店家: 香色 XIANG SE
  評論數: 300

分析店家: TWATUTIAN Coffee & Co.
  評論數: 207

分析店家: 義大利米蘭手工窯烤披薩 台北中山店
  評論數: 300

分析店家: 今日魚市-南京店-原丼賞和食
  評論數: 300

分析店家: TWATUTIAN Coffee & Co.
  評論數: 206

分析店家: 香色 XIANG SE
  評論數: 300

分析店家: 曹料理
  評論數: 300

======================================================================
  評論者分類與可靠度評分分析報告
  Reviewer Classification & Reliability Score Report
======================================================================
  產生時間: 2026-01-29T15:46:28.807387

【整體摘要】
  分析店家數: 10
  總評論數: 2813
  不重複評論者: 2360
  平均可靠度: 52.9/100

【評論者類型分布】
  可疑帳號         | ###                                                  6.9% (164)
  網紅型          | ###############                                     30.2% (713)
  沉睡用戶         | #######                                             15.2% (358)
  在地嚮導         | ################                                    32.4% (765)
  分享型          | ##                                                   4.6% (109)
  評分部隊         | #####                                               11.7% (275)
  專業評論者        | #########                                           18.2% (429)

【可靠度分數分布】
  優秀 (80-100)    | ##############                                      14.5% (343)
  良好 (60-80)     | ####################################                36.8% (868)
  普通 (40-60)     | ==================                                  18.1% (426)
  較差 (20-40)     | ------------                                        13.9% (329)
  很差 (0-20)      | ................                                    16.7% (394)

----------------------------------------------------------------------
【各店家詳細分析】

  店家: 今日魚市-南京店-原丼賞和食
  ----------------------------------------
    評論數: 300 | 評論者: 300
    原始評分: 4.81 -> 加權評分: 4.67 (-0.14)
    可靠度: 平均 30.9 | 最低 0 | 最高 91.6
    類型分布:
      - 沉睡用戶: 96 (32.0%)
      - 在地嚮導: 53 (17.7%)
      - 網紅型: 52 (17.3%)
      - 可疑帳號: 51 (17.0%)
      - 分享型: 21 (7.0%)
      - 評分部隊: 18 (6.0%)
      - 專業評論者: 9 (3.0%)
    異常評論者 (282 位):
      - 魏佩怡 | RCS: 0 | SINGLE_REVIEW, FIVE_STAR_BIAS
      - Tong Lee | RCS: 0 | SINGLE_REVIEW, FIVE_STAR_BIAS, NO_TEXT_PATTERN
      - 徐秀英 | RCS: 0 | SINGLE_REVIEW, FIVE_STAR_BIAS, NO_TEXT_PATTERN
      - 李漢蘋 | RCS: 0 | SINGLE_REVIEW, FIVE_STAR_BIAS, NO_TEXT_PATTERN
      - Paul Lo | RCS: 0 | FIVE_STAR_BIAS, NO_TEXT_PATTERN
      ... 還有 277 位

  店家: 義大利米蘭手工窯烤披薩 台北中山店
  ----------------------------------------
    評論數: 300 | 評論者: 300
    原始評分: 4.01 -> 加權評分: 3.95 (-0.06)
    可靠度: 平均 52.7 | 最低 0 | 最高 92.8
    類型分布:
      - 在地嚮導: 81 (27.0%)
      - 網紅型: 81 (27.0%)
      - 專業評論者: 48 (16.0%)
      - 評分部隊: 34 (11.3%)
      - 沉睡用戶: 28 (9.3%)
      - 可疑帳號: 14 (4.7%)
      - 分享型: 14 (4.7%)
    異常評論者 (207 位):
      - 秘密客 | RCS: 27.3 | FIVE_STAR_BIAS, NO_TEXT_PATTERN
      - hsu tina | RCS: 15.2 | FIVE_STAR_BIAS
      - Andy Kung | RCS: 13.1 | NO_TEXT_PATTERN
      - 道琪 | RCS: 55.6 | FIVE_STAR_BIAS
      - jc shih | RCS: 8.8 | FIVE_STAR_BIAS, NO_TEXT_PATTERN
      ... 還有 202 位

  店家: 曹料理
  ----------------------------------------
    評論數: 300 | 評論者: 300
    原始評分: 4.73 -> 加權評分: 4.66 (-0.07)
    可靠度: 平均 55.0 | 最低 0 | 最高 93.0
    類型分布:
      - 網紅型: 115 (38.3%)
      - 在地嚮導: 77 (25.7%)
      - 專業評論者: 50 (16.7%)
      - 沉睡用戶: 22 (7.3%)
      - 評分部隊: 21 (7.0%)
      - 分享型: 8 (2.7%)
      - 可疑帳號: 7 (2.3%)
    異常評論者 (250 位):
      - zeng jia | RCS: 67.9 | FIVE_STAR_BIAS
      - 周冠廷 | RCS: 7.0 | FIVE_STAR_BIAS, NO_TEXT_PATTERN
      - 南爵訓練「早洩專業訓練、陰莖硬度強化訓練」 | RCS: 52.8 | FIVE_STAR_BIAS
      - Winston Chao | RCS: 56.8 | FIVE_STAR_BIAS
      - 宋黑妹 | RCS: 42.8 | FIVE_STAR_BIAS
      ... 還有 245 位

  店家: 香色 XIANG SE
  ----------------------------------------
    評論數: 300 | 評論者: 300
    原始評分: 4.61 -> 加權評分: 4.51 (-0.1)
    可靠度: 平均 52.0 | 最低 0 | 最高 89.6
    類型分布:
      - 網紅型: 102 (34.0%)
      - 在地嚮導: 60 (20.0%)
      - 專業評論者: 54 (18.0%)
      - 沉睡用戶: 36 (12.0%)
      - 評分部隊: 33 (11.0%)
      - 分享型: 9 (3.0%)
      - 可疑帳號: 6 (2.0%)
    異常評論者 (240 位):
      - Megan Lin | RCS: 0 | FIVE_STAR_BIAS, NO_TEXT_PATTERN
      - 黃詩詒 | RCS: 49.4 | FIVE_STAR_BIAS
      - Cindy | RCS: 58.2 | FIVE_STAR_BIAS
      - ERINA HO | RCS: 46.5 | FIVE_STAR_BIAS
      - Yunheng | RCS: 0 | FIVE_STAR_BIAS, NO_TEXT_PATTERN
      ... 還有 235 位

  店家: TWATUTIAN Coffee & Co.
  ----------------------------------------
    評論數: 207 | 評論者: 207
    原始評分: 4.54 -> 加權評分: 4.46 (-0.08)
    可靠度: 平均 55.0 | 最低 0 | 最高 92.9
    類型分布:
      - 網紅型: 81 (39.1%)
      - 專業評論者: 43 (20.8%)
      - 在地嚮導: 41 (19.8%)
      - 沉睡用戶: 18 (8.7%)
      - 評分部隊: 11 (5.3%)
      - 可疑帳號: 8 (3.9%)
      - 分享型: 5 (2.4%)
    異常評論者 (158 位):
      - 五小石 | RCS: 12.0 | NO_TEXT_PATTERN
      - Nina Chien | RCS: 49.4 | FIVE_STAR_BIAS
      - J M | RCS: 0 | FIVE_STAR_BIAS
      - KC Wong | RCS: 72.6 | FIVE_STAR_BIAS
      - Lucie Fang | RCS: 45.9 | FIVE_STAR_BIAS
      ... 還有 153 位

  店家: 義大利米蘭手工窯烤披薩 台北中山店
  ----------------------------------------
    評論數: 300 | 評論者: 300
    原始評分: 1.9 -> 加權評分: 2.03 (+0.13)
    可靠度: 平均 56.9 | 最低 0 | 最高 93.0
    類型分布:
      - 在地嚮導: 112 (37.3%)
      - 評分部隊: 47 (15.7%)
      - 專業評論者: 40 (13.3%)
      - 沉睡用戶: 36 (12.0%)
      - 網紅型: 31 (10.3%)
      - 可疑帳號: 20 (6.7%)
      - 分享型: 14 (4.7%)
    異常評論者 (77 位):
      - Rick Lai | RCS: 0 | SINGLE_REVIEW
      - 許蘋茹 | RCS: 0 | SINGLE_REVIEW
      - sheng-hui ni | RCS: 0 | SINGLE_REVIEW
      - cheng Yu | RCS: 0 | SINGLE_REVIEW
      - Sysh Shh | RCS: 0 | SINGLE_REVIEW
      ... 還有 72 位

  店家: 今日魚市-南京店-原丼賞和食
  ----------------------------------------
    評論數: 300 | 評論者: 300
    原始評分: 1.11 -> 加權評分: 1.15 (+0.04)
    可靠度: 平均 59.6 | 最低 0 | 最高 93.0
    類型分布:
      - 在地嚮導: 102 (34.0%)
      - 沉睡用戶: 48 (16.0%)
      - 專業評論者: 43 (14.3%)
      - 網紅型: 41 (13.7%)
      - 評分部隊: 28 (9.3%)
      - 可疑帳號: 19 (6.3%)
      - 分享型: 19 (6.3%)
    異常評論者 (47 位):
      - 林明華 | RCS: 0 | SINGLE_REVIEW
      - Scott | RCS: 0 | SINGLE_REVIEW
      - 美食小站 | RCS: 0 | SINGLE_REVIEW
      - hun yang | RCS: 37.1 | NO_TEXT_PATTERN
      - Amber Lu | RCS: 0 | NO_TEXT_PATTERN
      ... 還有 42 位

  店家: TWATUTIAN Coffee & Co.
  ----------------------------------------
    評論數: 206 | 評論者: 206
    原始評分: 4.54 -> 加權評分: 4.46 (-0.08)
    可靠度: 平均 55.0 | 最低 0 | 最高 92.9
    類型分布:
      - 網紅型: 80 (38.8%)
      - 專業評論者: 43 (20.9%)
      - 在地嚮導: 41 (19.9%)
      - 沉睡用戶: 18 (8.7%)
      - 評分部隊: 11 (5.3%)
      - 可疑帳號: 8 (3.9%)
      - 分享型: 5 (2.4%)
    異常評論者 (157 位):
      - yang | RCS: 0 | SINGLE_REVIEW
      - Min Zhen | RCS: 32.3 | NO_TEXT_PATTERN
      - 陳君文 | RCS: 37.9 | NO_TEXT_PATTERN
      - Kerry Kao | RCS: 42.8 | NO_TEXT_PATTERN
      - 五小石 | RCS: 12.0 | NO_TEXT_PATTERN
      ... 還有 152 位

  店家: 香色 XIANG SE
  ----------------------------------------
    評論數: 300 | 評論者: 300
    原始評分: 2.13 -> 加權評分: 2.19 (+0.06)
    可靠度: 平均 60.3 | 最低 0 | 最高 93.0
    類型分布:
      - 在地嚮導: 122 (40.7%)
      - 評分部隊: 40 (13.3%)
      - 網紅型: 39 (13.0%)
      - 專業評論者: 37 (12.3%)
      - 沉睡用戶: 28 (9.3%)
      - 可疑帳號: 22 (7.3%)
      - 分享型: 12 (4.0%)
    異常評論者 (64 位):
      - 邱若語Haru | RCS: 0 | SINGLE_REVIEW
      - 陳新進 | RCS: 0 | SINGLE_REVIEW
      - 流河 | RCS: 0 | SINGLE_REVIEW
      - C hloe | RCS: 36.4 | NO_TEXT_PATTERN
      - Hendrik Kühl | RCS: 37.6 | NO_TEXT_PATTERN
      ... 還有 59 位

  店家: 曹料理
  ----------------------------------------
    評論數: 300 | 評論者: 300
    原始評分: 3.8 -> 加權評分: 3.79 (-0.01)
    可靠度: 平均 62.2 | 最低 0 | 最高 93.0
    類型分布:
      - 網紅型: 91 (30.3%)
      - 在地嚮導: 76 (25.3%)
      - 專業評論者: 62 (20.7%)
      - 評分部隊: 32 (10.7%)
      - 沉睡用戶: 28 (9.3%)
      - 可疑帳號: 9 (3.0%)
      - 分享型: 2 (0.7%)
    異常評論者 (122 位):
      - 鋼泫如 | RCS: 18.9 | NO_TEXT_PATTERN
      - flower wang | RCS: 36.9 | NO_TEXT_PATTERN
      - 陳國哲 | RCS: 14.2 | NO_TEXT_PATTERN
      - Cindy Yen | RCS: 24.1 | NO_TEXT_PATTERN
      - Wendy | RCS: 37.5 | NO_TEXT_PATTERN
      ... 還有 117 位

----------------------------------------------------------------------
【異常行為偵測統計】
  總異常評論者: 1604
    - 五星偏差 (90%+): 1302 位
    - 無文字模式 (80%+): 446 位
    - 單一評論帳號: 96 位

----------------------------------------------------------------------
【加權評分比較】
  (根據評論者可靠度加權計算的「真實評分」)

  店家名稱                                 原始評分       加權評分         差異
  ------------------------------ ---------- ---------- ----------
  今日魚市-南京店-原丼賞和食                       4.81       4.67      -0.14
  義大利米蘭手工窯烤披薩 台北中山店                    4.01       3.95      -0.06
  曹料理                                  4.73       4.66      -0.07
  香色 XIANG SE                          4.61       4.51       -0.1
  TWATUTIAN Coffee & Co.               4.54       4.46      -0.08
  義大利米蘭手工窯烤披薩 台北中山店                     1.9       2.03      +0.13
  今日魚市-南京店-原丼賞和食                       1.11       1.15      +0.04
  TWATUTIAN Coffee & Co.               4.54       4.46      -0.08
  香色 XIANG SE                          2.13       2.19      +0.06
  曹料理                                   3.8       3.79      -0.01

======================================================================
  分析完成
======================================================================

JSON 報告已儲存: reviewer_analysis_20260129_154628.json


分析系統運作成功。我注意到有一些重複的店家（因為兩個檔案都有相同店家的資料）。讓我優化程式碼，加入去重功能並增強一些分析。


Edit

    def analyze_places(self, places: List[dict]) -> Dict:
        """分析多個店家"""
        results = {
            'places': [],
            'summary': {},
            'generated_at': datetime.now().isoformat()
        }

        all_reviewers = {}

        for place in places:
            place_title = place.get('title', '未知店家')
            reviews = place.get('reviews', [])

            if not reviews:
                continue

            print(f"\n分析店家: {place_title}")
            print(f"  評論數: {len(reviews)}")

            # 分析此店家的評論者
            place_result = self.analyze_place(place_title, reviews)
            results['places'].append(place_result)

            # 收集所有評論者
            for profile in place_result['profiles'].values():
                if profile.reviewer_id not in all_reviewers:
                    all_reviewers[profile.reviewer_id] = profile

        # 產生整體摘要
        results['summary'] = self._generate_summary(results['places'], all_reviewers)

        return results

    def analyze_places(self, places: List[dict], merge_duplicates: bool = True) -> Dict:
        """
        分析多個店家

        Args:
            places: 店家資料列表
            merge_duplicates: 是否合併重複店家的評論
        """
        results = {
            'places': [],
            'summary': {},
            'generated_at': datetime.now().isoformat()
        }

        all_reviewers = {}

        # 合併重複店家
        if merge_duplicates:
            place_map = {}
            for place in places:
                place_id = place.get('placeId') or place.get('title', '')
                if place_id in place_map:
                    # 合併評論（去重）
                    existing_reviews = place_map[place_id].get('reviews', [])
                    new_reviews = place.get('reviews', [])
                    existing_ids = {r.get('reviewId') for r in existing_reviews}
                    for r in new_reviews:
                        if r.get('reviewId') not in existing_ids:
                            existing_reviews.append(r)
                    place_map[place_id]['reviews'] = existing_reviews
                else:
                    place_map[place_id] = place.copy()
            places = list(place_map.values())
            print(f"合併後店家數: {len(places)}")

        for place in places:
            place_title = place.get('title', '未知店家')
            reviews = place.get('reviews', [])

            if not reviews:
                continue

            print(f"\n分析店家: {place_title}")
            print(f"  評論數: {len(reviews)}")

            # 分析此店家的評論者
            place_result = self.analyze_place(place_title, reviews)
            results['places'].append(place_result)

            # 收集所有評論者
            for profile in place_result['profiles'].values():
                if profile.reviewer_id not in all_reviewers:
                    all_reviewers[profile.reviewer_id] = profile

        # 產生整體摘要
        results['summary'] = self._generate_summary(results['places'], all_reviewers)

        return results


The file C:\Users\User\Documents\GitHub\apify-scraper-practice\reviewer_analysis.py has been updated successfully.


現在讓我再次執行測試，看看合併後的結果。


Bash

cd "C:\Users\User\Documents\GitHub\apify-scraper-practice" && python -X utf8 reviewer_analysis.py step2_reviews_20260129_143615.json step2b_lowest_reviews_20260129_144321.json -o reviewer_analysis_report.json

執行評論者分析系統測試（含合併重複店家）


載入: step2_reviews_20260129_143615.json
載入: step2b_lowest_reviews_20260129_144321.json

總共載入 10 間店家
合併後店家數: 5

分析店家: 今日魚市-南京店-原丼賞和食
  評論數: 590

分析店家: 義大利米蘭手工窯烤披薩 台北中山店
  評論數: 525

分析店家: 曹料理
  評論數: 481

分析店家: 香色 XIANG SE
  評論數: 571

分析店家: TWATUTIAN Coffee & Co.
  評論數: 207

======================================================================
  評論者分類與可靠度評分分析報告
  Reviewer Classification & Reliability Score Report
======================================================================
  產生時間: 2026-01-29T15:46:50.731380

【整體摘要】
  分析店家數: 5
  總評論數: 2374
  不重複評論者: 2360
  平均可靠度: 53.0/100

【評論者類型分布】
  可疑帳號         | ###                                                  6.3% (149)
  網紅型          | ############                                        24.1% (568)
  沉睡用戶         | ######                                              13.5% (319)
  在地嚮導         | ##############                                      28.1% (664)
  分享型          | ##                                                   4.2% (98)
  評分部隊         | #####                                               10.3% (242)
  專業評論者        | #######                                             14.2% (334)

【可靠度分數分布】
  優秀 (80-100)    | ##############                                      14.6% (344)
  良好 (60-80)     | ####################################                36.8% (869)
  普通 (40-60)     | ==================                                  18.0% (425)
  較差 (20-40)     | ------------                                        13.9% (329)
  很差 (0-20)      | ................                                    16.7% (393)

----------------------------------------------------------------------
【各店家詳細分析】

  店家: 今日魚市-南京店-原丼賞和食
  ----------------------------------------
    評論數: 590 | 評論者: 590
    原始評分: 2.99 -> 加權評分: 2.43 (-0.56)
    可靠度: 平均 45.1 | 最低 0 | 最高 93.0
    類型分布:
      - 在地嚮導: 150 (25.4%)
      - 沉睡用戶: 143 (24.2%)
      - 網紅型: 91 (15.4%)
      - 可疑帳號: 68 (11.5%)
      - 專業評論者: 52 (8.8%)
      - 評分部隊: 46 (7.8%)
      - 分享型: 40 (6.8%)
    異常評論者 (327 位):
      - 魏佩怡 | RCS: 0 | SINGLE_REVIEW, FIVE_STAR_BIAS
      - Tong Lee | RCS: 0 | SINGLE_REVIEW, FIVE_STAR_BIAS, NO_TEXT_PATTERN
      - 徐秀英 | RCS: 0 | SINGLE_REVIEW, FIVE_STAR_BIAS, NO_TEXT_PATTERN
      - 李漢蘋 | RCS: 0 | SINGLE_REVIEW, FIVE_STAR_BIAS, NO_TEXT_PATTERN
      - Paul Lo | RCS: 0 | FIVE_STAR_BIAS, NO_TEXT_PATTERN
      ... 還有 322 位

  店家: 義大利米蘭手工窯烤披薩 台北中山店
  ----------------------------------------
    評論數: 525 | 評論者: 525
    原始評分: 3.14 -> 加權評分: 3.12 (-0.02)
    可靠度: 平均 54.6 | 最低 0 | 最高 93.0
    類型分布:
      - 在地嚮導: 169 (32.2%)
      - 網紅型: 103 (19.6%)
      - 專業評論者: 78 (14.9%)
      - 評分部隊: 66 (12.6%)
      - 沉睡用戶: 55 (10.5%)
      - 可疑帳號: 30 (5.7%)
      - 分享型: 24 (4.6%)
    異常評論者 (264 位):
      - 秘密客 | RCS: 27.3 | FIVE_STAR_BIAS, NO_TEXT_PATTERN
      - hsu tina | RCS: 15.2 | FIVE_STAR_BIAS
      - Andy Kung | RCS: 13.1 | NO_TEXT_PATTERN
      - 道琪 | RCS: 55.6 | FIVE_STAR_BIAS
      - jc shih | RCS: 8.8 | FIVE_STAR_BIAS, NO_TEXT_PATTERN
      ... 還有 259 位

  店家: 曹料理
  ----------------------------------------
    評論數: 481 | 評論者: 481
    原始評分: 4.25 -> 加權評分: 4.18 (-0.07)
    可靠度: 平均 56.9 | 最低 0 | 最高 93.0
    類型分布:
      - 網紅型: 154 (32.0%)
      - 在地嚮導: 133 (27.7%)
      - 專業評論者: 76 (15.8%)
      - 評分部隊: 49 (10.2%)
      - 沉睡用戶: 44 (9.1%)
      - 可疑帳號: 15 (3.1%)
      - 分享型: 10 (2.1%)
    異常評論者 (303 位):
      - zeng jia | RCS: 67.9 | FIVE_STAR_BIAS
      - 周冠廷 | RCS: 7.0 | FIVE_STAR_BIAS, NO_TEXT_PATTERN
      - 南爵訓練「早洩專業訓練、陰莖硬度強化訓練」 | RCS: 52.8 | FIVE_STAR_BIAS
      - Winston Chao | RCS: 56.8 | FIVE_STAR_BIAS
      - 宋黑妹 | RCS: 42.8 | FIVE_STAR_BIAS
      ... 還有 298 位

  店家: 香色 XIANG SE
  ----------------------------------------
    評論數: 571 | 評論者: 571
    原始評分: 3.42 -> 加權評分: 3.32 (-0.11)
    可靠度: 平均 55.7 | 最低 0 | 最高 93.0
    類型分布:
      - 在地嚮導: 171 (29.9%)
      - 網紅型: 139 (24.3%)
      - 專業評論者: 85 (14.9%)
      - 評分部隊: 70 (12.3%)
      - 沉睡用戶: 59 (10.3%)
      - 可疑帳號: 28 (4.9%)
      - 分享型: 19 (3.3%)
    異常評論者 (301 位):
      - Megan Lin | RCS: 0 | FIVE_STAR_BIAS, NO_TEXT_PATTERN
      - 黃詩詒 | RCS: 49.4 | FIVE_STAR_BIAS
      - Cindy | RCS: 58.2 | FIVE_STAR_BIAS
      - ERINA HO | RCS: 46.5 | FIVE_STAR_BIAS
      - Yunheng | RCS: 0 | FIVE_STAR_BIAS, NO_TEXT_PATTERN
      ... 還有 296 位

  店家: TWATUTIAN Coffee & Co.
  ----------------------------------------
    評論數: 207 | 評論者: 207
    原始評分: 4.54 -> 加權評分: 4.46 (-0.08)
    可靠度: 平均 55.0 | 最低 0 | 最高 92.9
    類型分布:
      - 網紅型: 81 (39.1%)
      - 專業評論者: 43 (20.8%)
      - 在地嚮導: 41 (19.8%)
      - 沉睡用戶: 18 (8.7%)
      - 評分部隊: 11 (5.3%)
      - 可疑帳號: 8 (3.9%)
      - 分享型: 5 (2.4%)
    異常評論者 (158 位):
      - 五小石 | RCS: 12.0 | NO_TEXT_PATTERN
      - Nina Chien | RCS: 49.4 | FIVE_STAR_BIAS
      - J M | RCS: 0 | FIVE_STAR_BIAS
      - KC Wong | RCS: 72.6 | FIVE_STAR_BIAS
      - Lucie Fang | RCS: 45.9 | FIVE_STAR_BIAS
      ... 還有 153 位

----------------------------------------------------------------------
【異常行為偵測統計】
  總異常評論者: 1353
    - 五星偏差 (90%+): 1088 位
    - 無文字模式 (80%+): 400 位
    - 單一評論帳號: 88 位

----------------------------------------------------------------------
【加權評分比較】
  (根據評論者可靠度加權計算的「真實評分」)

  店家名稱                                 原始評分       加權評分         差異
  ------------------------------ ---------- ---------- ----------
  今日魚市-南京店-原丼賞和食                       2.99       2.43      -0.56
  義大利米蘭手工窯烤披薩 台北中山店                    3.14       3.12      -0.02
  曹料理                                  4.25       4.18      -0.07
  香色 XIANG SE                          3.42       3.32      -0.11
  TWATUTIAN Coffee & Co.               4.54       4.46      -0.08

======================================================================
  分析完成
======================================================================

JSON 報告已儲存: reviewer_analysis_report.json


分析系統運作正常，已成功合併重複店家。讓我檢查輸出的 JSON 報告。


     1→{
     2→  "generated_at": "2026-01-29T15:46:50.731380",
     3→  "summary": {
     4→    "total_places": 5,
     5→    "total_reviews": 2374,
     6→    "total_unique_reviewers": 2360,
     7→    "type_distribution": {
     8→      "SUSPICIOUS": {
     9→        "count": 149,
    10→        "percentage": 6.3
    11→      },
    12→      "INFLUENCER": {
    13→        "count": 568,
    14→        "percentage": 24.1
    15→      },
    16→      "INACTIVE": {
    17→        "count": 319,
    18→        "percentage": 13.5
    19→      },
    20→      "LOCAL_GUIDE": {
    21→        "count": 664,
    22→        "percentage": 28.1
    23→      },
    24→      "ORGANIC": {
    25→        "count": 98,
    26→        "percentage": 4.2
    27→      },
    28→      "RATING_ONLY": {
    29→        "count": 242,
    30→        "percentage": 10.3
    31→      },
    32→      "EXPERT": {
    33→        "count": 334,
    34→        "percentage": 14.2
    35→      }
    36→    },
    37→    "reliability_distribution": {
    38→      "excellent": 344,
    39→      "good": 869,
    40→      "moderate": 425,
    41→      "poor": 329,
    42→      "very_poor": 393
    43→    },
    44→    "avg_reliability": 53.0,
    45→    "total_anomaly_reviewers": 1353
    46→  },
    47→  "places": [
    48→    {
    49→      "place_title": "今日魚市-南京店-原丼賞和食",
    50→      "total_reviews": 590,
    51→      "unique_reviewers": 590,
    52→      "type_distribution": {
    53→        "SUSPICIOUS": 68,
    54→        "INFLUENCER": 91,
    55→        "INACTIVE": 143,
    56→        "LOCAL_GUIDE": 150,
    57→        "ORGANIC": 40,
    58→        "RATING_ONLY": 46,
    59→        "EXPERT": 52
    60→      },
    61→      "reliability_stats": {
    62→        "mean": 45.1,
    63→        "min": 0,
    64→        "max": 93.0
    65→      },
    66→      "original_rating": 2.99,
    67→      "weighted_rating": 2.43,
    68→      "rating_diff": -0.56,
    69→      "anomaly_reviewers": [
    70→        {
    71→          "name": "魏佩怡",
    72→          "reviewer_id": "100734908245064780279",
    73→          "type": "SUSPICIOUS",
    74→          "reliability_score": 0,
    75→          "anomaly_flags": [
    76→            "SINGLE_REVIEW",
    77→            "FIVE_STAR_BIAS"
    78→          ],
    79→          "total_reviews": 1,
    80→          "stars": [
    81→            5
    82→          ]
    83→        },
    84→        {
    85→          "name": "Tong Lee",
    86→          "reviewer_id": "118340581967193083223",
    87→          "type": "SUSPICIOUS",
    88→          "reliability_score": 0,
    89→          "anomaly_flags": [
    90→            "SINGLE_REVIEW",
    91→            "FIVE_STAR_BIAS",
    92→            "NO_TEXT_PATTERN"
    93→          ],
    94→          "total_reviews": 0,
    95→          "stars": [
    96→            5
    97→          ]
    98→        },
    99→        {
   100→          "name": "徐秀英",
   101→          "reviewer_id": "107399920523828861266",
   102→          "type": "SUSPICIOUS",
   103→          "reliability_score": 0,
   104→          "anomaly_flags": [
   105→            "SINGLE_REVIEW",
   106→            "FIVE_STAR_BIAS",
   107→            "NO_TEXT_PATTERN"
   108→          ],
   109→          "total_reviews": 0,
   110→          "stars": [
   111→            5
   112→          ]
   113→        },
   114→        {
   115→          "name": "李漢蘋",
   116→          "reviewer_id": "117106659860932463386",
   117→          "type": "SUSPICIOUS",
   118→          "reliability_score": 0,
   119→          "anomaly_flags": [
   120→            "SINGLE_REVIEW",
   121→            "FIVE_STAR_BIAS",
   122→            "NO_TEXT_PATTERN"
   123→          ],
   124→          "total_reviews": 1,
   125→          "stars": [
   126→            5
   127→          ]
   128→        },
   129→        {
   130→          "name": "Paul Lo",
   131→          "reviewer_id": "104254133758391323530",
   132→          "type": "SUSPICIOUS",
   133→          "reliability_score": 0,
   134→          "anomaly_flags": [
   135→            "FIVE_STAR_BIAS",
   136→            "NO_TEXT_PATTERN"
   137→          ],
   138→          "total_reviews": 2,
   139→          "stars": [
   140→            5
   141→          ]
   142→        },
   143→        {
   144→          "name": "Brielle A",
   145→          "reviewer_id": "116559275360920793482",
   146→          "type": "SUSPICIOUS",
   147→          "reliability_score": 0,
   148→          "anomaly_flags": [
   149→            "SINGLE_REVIEW",
   150→            "FIVE_STAR_BIAS"

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


JSON 報告格式正確。現在讓我檢視完整的程式碼，確認所有功能都已實作完成。