repository_reader / core /file_scanner.py
DeL-TaiseiOzaki's picture
Update core/file_scanner.py
e50fc98 verified
# core/file_scanner.py
import chardet
from pathlib import Path
from typing import List, Optional, Set
from dataclasses import dataclass
@dataclass
class FileInfo:
path: Path
size: int
extension: str
content: Optional[str] = None
encoding: Optional[str] = None
@property
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))