Spaces:
Sleeping
Sleeping
| # core/file_scanner.py | |
| import chardet | |
| from pathlib import Path | |
| from typing import List, Optional, Set | |
| from dataclasses import dataclass | |
| class FileInfo: | |
| path: Path | |
| size: int | |
| extension: str | |
| content: Optional[str] = None | |
| encoding: Optional[str] = None | |
| def formatted_size(self) -> str: | |
| """ファイルサイズを見やすい単位で表示""" | |
| if self.size < 1024: | |
| return f"{self.size} B" | |
| elif self.size < 1024 * 1024: | |
| return f"{self.size / 1024:.1f} KB" | |
| else: | |
| return f"{self.size / (1024 * 1024):.1f} MB" | |
| class FileScanner: | |
| """ | |
| 指定された拡張子のファイルだけを再帰的に検索し、ファイル内容を読み込むクラス。 | |
| """ | |
| EXCLUDED_DIRS = { | |
| '.git', '__pycache__', 'node_modules', 'venv', | |
| '.env', 'build', 'dist', 'target', 'bin', 'obj' | |
| } | |
| def __init__(self, base_dir: Path, target_extensions: Set[str]): | |
| """ | |
| base_dir: 解析を開始するディレクトリ(Path) | |
| target_extensions: 対象とする拡張子の集合 (例: {'.py', '.js', '.md'}) | |
| """ | |
| self.base_dir = base_dir | |
| # 大文字・小文字のブレを吸収するために小文字化して保持 | |
| self.target_extensions = {ext.lower() for ext in target_extensions} | |
| def _should_scan_file(self, path: Path) -> bool: | |
| """対象外フォルダ・拡張子を除外""" | |
| # 除外フォルダ判定 | |
| if any(excluded in path.parts for excluded in self.EXCLUDED_DIRS): | |
| return False | |
| # 拡張子チェック | |
| if path.suffix.lower() in self.target_extensions: | |
| return True | |
| return False | |
| def _read_file_content(self, file_path: Path) -> (Optional[str], Optional[str]): | |
| """ | |
| ファイル内容を読み込み、エンコーディングを判定して返す。 | |
| 先頭4096バイトをchardetで解析し、失敗時はcp932も試す。 | |
| """ | |
| try: | |
| with file_path.open('rb') as rb: | |
| raw_data = rb.read(4096) | |
| detect_result = chardet.detect(raw_data) | |
| encoding = detect_result['encoding'] if detect_result['confidence'] > 0.7 else 'utf-8' | |
| # 推定エンコーディングで読み込み | |
| try: | |
| with file_path.open('r', encoding=encoding) as f: | |
| return f.read(), encoding | |
| except UnicodeDecodeError: | |
| # cp932 を再試行 (Windows向け) | |
| with file_path.open('r', encoding='cp932') as f: | |
| return f.read(), 'cp932' | |
| except Exception: | |
| return None, None | |
| def scan_files(self) -> List[FileInfo]: | |
| """ | |
| 再帰的にファイルを探して、指定拡張子だけをFileInfoオブジェクトのリストとして返す。 | |
| """ | |
| if not self.base_dir.exists(): | |
| raise FileNotFoundError(f"指定ディレクトリが見つかりません: {self.base_dir}") | |
| collected_files = [] | |
| for entry in self.base_dir.glob("**/*"): | |
| if entry.is_file() and self._should_scan_file(entry): | |
| content, encoding = self._read_file_content(entry) | |
| file_info = FileInfo( | |
| path=entry.resolve(), | |
| size=entry.stat().st_size, | |
| extension=entry.suffix.lower(), | |
| content=content, | |
| encoding=encoding | |
| ) | |
| collected_files.append(file_info) | |
| # path の文字列表現でソート | |
| return sorted(collected_files, key=lambda x: str(x.path)) | |