Spaces:
Sleeping
Sleeping
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}")
|