File size: 5,516 Bytes
fb05e78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
バッチスクレイピング処理モジュール
"""

import asyncio
import logging
import sys
from enum import Enum
from pathlib import Path
from typing import List, Tuple, Literal, Optional

from tqdm import tqdm

from src.scraping.exceptions import ArticleNotFoundError, FetchError
from src.scraping.pipeline import run as run_pipeline

# ロガーの設定
logging.basicConfig(level=logging.INFO, format='%(message)s')
logger = logging.getLogger(__name__)


class ScrapeStatus(Enum):
    """スクレイピング結果のステータス"""
    SUCCESS = "success"
    SKIPPED = "skipped"  # 記事が存在しない
    FAILED = "failed"    # その他のエラー


async def scrape_single_page(url: str, out_dir: Path) -> Tuple[str, ScrapeStatus, str]:
    """
    単一ページのスクレイピング
    
    Returns:
        (url, status, message) のタプル
    """
    try:
        path = await run_pipeline(url, out_dir)
        return (url, ScrapeStatus.SUCCESS, f"保存完了: {path}")
    except ArticleNotFoundError:
        return (url, ScrapeStatus.SKIPPED, "記事が見つかりません")
    except FetchError as e:
        return (url, ScrapeStatus.FAILED, f"取得エラー: {str(e)}")
    except Exception as e:
        return (url, ScrapeStatus.FAILED, f"エラー: {str(e)}")


async def batch_scrape(
    start_id: int,
    end_id: int,
    out_dir: Path,
    delay: float = 1.0,
    base_url: str = "https://ja.empatheme.org/potion",
    verbose: bool = False
) -> List[Tuple[str, ScrapeStatus, str]]:
    """
    指定範囲のIDでバッチスクレイピング実行
    
    Args:
        start_id: 開始ID
        end_id: 終了ID(含む)
        out_dir: 出力ディレクトリ
        delay: 各リクエスト間の待機時間(秒)
        base_url: ベースURL
        verbose: 詳細ログを表示するか
    
    Returns:
        各URLの処理結果のリスト
    """
    results = []
    total = end_id - start_id + 1
    
    logger.info(f"スクレイピング開始: ID {start_id} から {end_id} まで(計{total}件)")
    logger.info(f"出力先: {out_dir}")
    logger.info(f"待機時間: {delay}秒\n")
    
    # カウンター初期化
    success_count = 0
    skipped_count = 0
    failed_count = 0
    
    # プログレスバーの作成(単一行で更新)
    pbar = tqdm(
        total=total, 
        desc="処理中", 
        leave=True,
        ncols=80,
        bar_format='{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {postfix}]'
    )
    
    try:
        for page_id in range(start_id, end_id + 1):
            url = f"{base_url}/{page_id:03d}/"
            
            # スクレイピング実行
            result = await scrape_single_page(url, out_dir)
            results.append(result)
            
            # カウンター更新
            url, status, message = result
            if status == ScrapeStatus.SUCCESS:
                success_count += 1
            elif status == ScrapeStatus.SKIPPED:
                skipped_count += 1
            else:  # FAILED
                failed_count += 1
            
            # プログレスバーの説明を更新
            pbar.set_postfix({
                '成功': success_count,
                'スキップ': skipped_count,
                '失敗': failed_count
            })
            
            # verboseモードの場合は詳細ログも表示
            if verbose:
                # プログレスバーを一時的にクリアして詳細を表示
                pbar.clear()
                if status == ScrapeStatus.SUCCESS:
                    print(f"  ✓ {url}: {message}")
                elif status == ScrapeStatus.SKIPPED:
                    print(f"  ⊘ {url}: {message}")
                else:  # FAILED
                    print(f"  ✗ {url}: {message}")
                pbar.refresh()
            
            # プログレスバーを進める
            pbar.update(1)
            
            # 最後のページでなければ待機
            if page_id < end_id:
                await asyncio.sleep(delay)
    finally:
        pbar.close()
    
    return results


def print_summary(results: List[Tuple[str, ScrapeStatus, str]]) -> None:
    """処理結果のサマリーを表示"""
    total = len(results)
    success_count = sum(1 for _, status, _ in results if status == ScrapeStatus.SUCCESS)
    skipped_count = sum(1 for _, status, _ in results if status == ScrapeStatus.SKIPPED)
    failed_count = sum(1 for _, status, _ in results if status == ScrapeStatus.FAILED)
    
    logger.info("\n" + "="*50)
    logger.info("処理結果サマリー")
    logger.info("="*50)
    logger.info(f"合計: {total}件")
    logger.info(f"成功: {success_count}件")
    logger.info(f"スキップ(記事なし): {skipped_count}件")
    logger.info(f"失敗: {failed_count}件")
    
    # スキップしたURL(記事が存在しない)の表示
    if skipped_count > 0:
        logger.info("\nスキップしたURL(記事が存在しない):")
        for url, status, message in results:
            if status == ScrapeStatus.SKIPPED:
                logger.info(f"  ⊘ {url}")
    
    # 失敗したURLの詳細表示
    if failed_count > 0:
        logger.info("\n失敗したURL:")
        for url, status, message in results:
            if status == ScrapeStatus.FAILED:
                logger.info(f"  ✗ {url}: {message}")