Spaces:
Running
Running
DeL-TaiseiOzaki
commited on
Commit
•
227e75d
1
Parent(s):
0b1e3e4
first commit
Browse files- README copy.md +96 -0
- app.py +130 -0
- config/__init__.py +0 -0
- config/__pycache__/__init__.cpython-310.pyc +0 -0
- config/__pycache__/setting.cpython-310.pyc +0 -0
- config/__pycache__/settings.cpython-310.pyc +0 -0
- config/settings.py +18 -0
- core/__init__.py +0 -0
- core/__pycache__/__init__.cpython-310.pyc +0 -0
- core/__pycache__/file_scanner.cpython-310.pyc +0 -0
- core/__pycache__/git_manager.cpython-310.pyc +0 -0
- core/file_scanner.py +60 -0
- core/git_manager.py +34 -0
- main.py +69 -0
- output/scan_result_20241030_210745.txt +242 -0
- rquirements.txt +4 -0
- scan.sh +49 -0
- services/llm_service.py +78 -0
- utils/__init__.py +0 -0
- utils/__pycache__/__init__.cpython-310.pyc +0 -0
- utils/__pycache__/content_exporter.cpython-310.pyc +0 -0
- utils/__pycache__/file_writer.cpython-310.pyc +0 -0
- utils/file_writer.py +24 -0
- utils/logger.py +26 -0
README copy.md
ADDED
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# get_repository_info_by_llm
|
2 |
+
|
3 |
+
プログラミング関連ファイルを再帰的にスキャンし、内容を単一のテキストファイルにエクスポートするツールです。GitHubリポジトリまたはローカルディレクトリに対応しています。
|
4 |
+
|
5 |
+
## 機能
|
6 |
+
|
7 |
+
- GitHubリポジトリのクローンとスキャン
|
8 |
+
- ローカルディレクトリのスキャン
|
9 |
+
- 再帰的なファイル検索
|
10 |
+
- 主要なプログラミング言語ファイルの検出
|
11 |
+
- UTF-8/CP932エンコーディングの自動検出
|
12 |
+
- 結果のテキストファイル出力
|
13 |
+
|
14 |
+
## 必要条件
|
15 |
+
|
16 |
+
- Python 3.7以上
|
17 |
+
- Git(GitHubリポジトリをスキャンする場合)
|
18 |
+
|
19 |
+
## インストール
|
20 |
+
|
21 |
+
1. リポジトリをクローン
|
22 |
+
```bash
|
23 |
+
git clone [このリポジトリのURL]
|
24 |
+
cd directory-scanner
|
25 |
+
```
|
26 |
+
|
27 |
+
2. 必要なディレクトリを作成
|
28 |
+
```bash
|
29 |
+
mkdir output
|
30 |
+
```
|
31 |
+
|
32 |
+
## 使用方法
|
33 |
+
|
34 |
+
### コマンドライン
|
35 |
+
```bash
|
36 |
+
# GitHubリポジトリをスキャン
|
37 |
+
python main.py https://github.com/username/repository.git
|
38 |
+
|
39 |
+
# ローカルディレクトリをスキャン
|
40 |
+
python main.py /path/to/directory
|
41 |
+
```
|
42 |
+
|
43 |
+
### シェルスクリプトを使用
|
44 |
+
```bash
|
45 |
+
# スクリプトに実行権限を付与
|
46 |
+
chmod +x scan.sh
|
47 |
+
|
48 |
+
# GitHubリポジトリをスキャン
|
49 |
+
./scan.sh https://github.com/username/repository.git
|
50 |
+
|
51 |
+
# ローカルディレクトリをスキャン
|
52 |
+
./scan.sh /path/to/directory
|
53 |
+
```
|
54 |
+
|
55 |
+
## 出力形式
|
56 |
+
|
57 |
+
スキャン結果は `output` ディレクトリに保存され、以下の形式で出力されます:
|
58 |
+
|
59 |
+
```
|
60 |
+
#ファイルパス
|
61 |
+
path/to/file.py
|
62 |
+
------------
|
63 |
+
ファイルの内容
|
64 |
+
```
|
65 |
+
|
66 |
+
## スキャン対象
|
67 |
+
|
68 |
+
### 対象となるファイル拡張子
|
69 |
+
- Python (.py)
|
70 |
+
- JavaScript (.js)
|
71 |
+
- Java (.java)
|
72 |
+
- C/C++ (.c, .h, .cpp, .hpp)
|
73 |
+
- Go (.go)
|
74 |
+
- Rust (.rs)
|
75 |
+
- PHP (.php)
|
76 |
+
- Ruby (.rb)
|
77 |
+
- TypeScript (.ts)
|
78 |
+
- その他 (.scala, .kt, .cs, .swift, .m, .sh, .pl, .r)
|
79 |
+
|
80 |
+
### 除外されるディレクトリ
|
81 |
+
- .git
|
82 |
+
- __pycache__
|
83 |
+
- node_modules
|
84 |
+
- venv
|
85 |
+
- .env
|
86 |
+
- build
|
87 |
+
- dist
|
88 |
+
- target
|
89 |
+
- bin
|
90 |
+
- obj
|
91 |
+
|
92 |
+
## 注意事項
|
93 |
+
|
94 |
+
- GitHubリポジトリをスキャンする場合、一時的にローカルにクローンされます
|
95 |
+
- スキャン完了後、クローンされたリポジトリは自動的に削除されます
|
96 |
+
- 大きなファイルや特殊なエンコーディングのファイルは読み取れない場合があります
|
app.py
ADDED
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import tempfile
|
3 |
+
import git
|
4 |
+
from pathlib import Path
|
5 |
+
from datetime import datetime
|
6 |
+
import time
|
7 |
+
from dotenv import load_dotenv
|
8 |
+
from core.file_scanner import FileScanner
|
9 |
+
from services.llm_service import LLMService
|
10 |
+
|
11 |
+
# 環境変数の読み込み
|
12 |
+
load_dotenv()
|
13 |
+
|
14 |
+
# ページ設定
|
15 |
+
st.set_page_config(
|
16 |
+
page_title="Repository Code Analysis",
|
17 |
+
page_icon="🔍",
|
18 |
+
layout="wide"
|
19 |
+
)
|
20 |
+
|
21 |
+
# カスタムCSS
|
22 |
+
st.markdown("""
|
23 |
+
<style>
|
24 |
+
.stAlert {
|
25 |
+
padding: 1rem;
|
26 |
+
margin: 1rem 0;
|
27 |
+
}
|
28 |
+
.css-1v0mbdj.ebxwdo61 {
|
29 |
+
width: 100%;
|
30 |
+
max-width: 800px;
|
31 |
+
}
|
32 |
+
</style>
|
33 |
+
""", unsafe_allow_html=True)
|
34 |
+
|
35 |
+
def clone_repository(repo_url: str) -> Path:
|
36 |
+
"""リポジトリをクローンして一時ディレクトリに保存"""
|
37 |
+
temp_dir = Path(tempfile.mkdtemp())
|
38 |
+
git.Repo.clone_from(repo_url, temp_dir)
|
39 |
+
return temp_dir
|
40 |
+
|
41 |
+
# セッション状態の初期化
|
42 |
+
if 'repo_content' not in st.session_state:
|
43 |
+
st.session_state.repo_content = None
|
44 |
+
if 'temp_dir' not in st.session_state:
|
45 |
+
st.session_state.temp_dir = None
|
46 |
+
if 'llm_service' not in st.session_state:
|
47 |
+
st.session_state.llm_service = None
|
48 |
+
|
49 |
+
# メインのUIレイアウト
|
50 |
+
st.title("🔍 リポジトリ解析・質問システム")
|
51 |
+
|
52 |
+
# OpenAI APIキーの設定
|
53 |
+
api_key = st.sidebar.text_input("OpenAI APIキー", type="password", key="api_key")
|
54 |
+
if api_key:
|
55 |
+
st.session_state.llm_service = LLMService(api_key)
|
56 |
+
|
57 |
+
# URLの入力
|
58 |
+
repo_url = st.text_input(
|
59 |
+
"GitHubリポジトリのURLを入力",
|
60 |
+
placeholder="https://github.com/username/repository.git"
|
61 |
+
)
|
62 |
+
|
63 |
+
# スキャン実行ボタン
|
64 |
+
if st.button("スキャン開始", disabled=not repo_url):
|
65 |
+
try:
|
66 |
+
with st.spinner('リポジトリをクローン中...'):
|
67 |
+
temp_dir = clone_repository(repo_url)
|
68 |
+
st.session_state.temp_dir = temp_dir
|
69 |
+
|
70 |
+
with st.spinner('ファイルをスキャン中...'):
|
71 |
+
scanner = FileScanner(temp_dir)
|
72 |
+
files_content = scanner.scan_files()
|
73 |
+
|
74 |
+
if st.session_state.llm_service:
|
75 |
+
st.session_state.repo_content = LLMService.format_code_content(files_content)
|
76 |
+
|
77 |
+
st.success(f"スキャン完了: {len(files_content)}個のファイルを検出")
|
78 |
+
|
79 |
+
except Exception as e:
|
80 |
+
st.error(f"エラーが発生しました: {str(e)}")
|
81 |
+
|
82 |
+
# スキャン完了後の質問セクション
|
83 |
+
if st.session_state.repo_content and st.session_state.llm_service:
|
84 |
+
st.divider()
|
85 |
+
st.subheader("💭 コードについて質問する")
|
86 |
+
|
87 |
+
query = st.text_area(
|
88 |
+
"質問を入力してください",
|
89 |
+
placeholder="例: このコードの主な機能は何ですか?"
|
90 |
+
)
|
91 |
+
|
92 |
+
if st.button("質問する", disabled=not query):
|
93 |
+
with st.spinner('回答を生成中...'):
|
94 |
+
response, error = st.session_state.llm_service.get_response(
|
95 |
+
st.session_state.repo_content,
|
96 |
+
query
|
97 |
+
)
|
98 |
+
|
99 |
+
if error:
|
100 |
+
st.error(error)
|
101 |
+
else:
|
102 |
+
st.markdown("### 回答:")
|
103 |
+
st.markdown(response)
|
104 |
+
|
105 |
+
# セッション終了時のクリーンアップ
|
106 |
+
if st.session_state.temp_dir and Path(st.session_state.temp_dir).exists():
|
107 |
+
try:
|
108 |
+
import shutil
|
109 |
+
shutil.rmtree(st.session_state.temp_dir)
|
110 |
+
except:
|
111 |
+
pass
|
112 |
+
|
113 |
+
# サイドバー情報
|
114 |
+
with st.sidebar:
|
115 |
+
st.subheader("📌 使い方")
|
116 |
+
st.markdown("""
|
117 |
+
1. OpenAI APIキーを入力
|
118 |
+
2. GitHubリポジトリのURLを入力
|
119 |
+
3. スキャンを実行
|
120 |
+
4. コードについて質問
|
121 |
+
""")
|
122 |
+
|
123 |
+
st.subheader("🔍 スキャン対象")
|
124 |
+
st.markdown("""
|
125 |
+
- Python (.py)
|
126 |
+
- JavaScript (.js)
|
127 |
+
- Java (.java)
|
128 |
+
- C/C++ (.c, .h, .cpp, .hpp)
|
129 |
+
- その他の主要なプログラミング言語
|
130 |
+
""")
|
config/__init__.py
ADDED
File without changes
|
config/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (153 Bytes). View file
|
|
config/__pycache__/setting.cpython-310.pyc
ADDED
Binary file (1.02 kB). View file
|
|
config/__pycache__/settings.cpython-310.pyc
ADDED
Binary file (1.03 kB). View file
|
|
config/settings.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pathlib import Path
|
2 |
+
from datetime import datetime
|
3 |
+
|
4 |
+
class Settings:
|
5 |
+
DEFAULT_OUTPUT_DIR = Path("output")
|
6 |
+
TIMESTAMP_FORMAT = "%Y%m%d_%H%M%S"
|
7 |
+
|
8 |
+
@classmethod
|
9 |
+
def get_timestamp(cls) -> str:
|
10 |
+
return datetime.now().strftime(cls.TIMESTAMP_FORMAT)
|
11 |
+
|
12 |
+
@classmethod
|
13 |
+
def get_clone_dir(cls, timestamp: str) -> Path:
|
14 |
+
return cls.DEFAULT_OUTPUT_DIR / f"repo_clone_{timestamp}"
|
15 |
+
|
16 |
+
@classmethod
|
17 |
+
def get_output_file(cls, timestamp: str) -> Path:
|
18 |
+
return cls.DEFAULT_OUTPUT_DIR / f"scan_result_{timestamp}.txt"
|
core/__init__.py
ADDED
File without changes
|
core/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (151 Bytes). View file
|
|
core/__pycache__/file_scanner.cpython-310.pyc
ADDED
Binary file (2.55 kB). View file
|
|
core/__pycache__/git_manager.cpython-310.pyc
ADDED
Binary file (1.3 kB). View file
|
|
core/file_scanner.py
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pathlib import Path
|
2 |
+
from typing import List, Dict, Optional
|
3 |
+
from dataclasses import dataclass
|
4 |
+
|
5 |
+
@dataclass
|
6 |
+
class FileInfo:
|
7 |
+
path: Path
|
8 |
+
content: Optional[str] = None
|
9 |
+
|
10 |
+
class FileScanner:
|
11 |
+
# スキャン対象の拡張子
|
12 |
+
TARGET_EXTENSIONS = {
|
13 |
+
'.py', '.js', '.java', '.cpp', '.hpp', '.c', '.h',
|
14 |
+
'.go', '.rs', '.php', '.rb', '.ts', '.scala', '.kt',
|
15 |
+
'.cs', '.swift', '.m', '.sh', '.pl', '.r'
|
16 |
+
}
|
17 |
+
|
18 |
+
# スキャン対象から除外するディレクトリ
|
19 |
+
EXCLUDED_DIRS = {
|
20 |
+
'.git', '__pycache__', 'node_modules', 'venv', '.env',
|
21 |
+
'build', 'dist', 'target', 'bin', 'obj'
|
22 |
+
}
|
23 |
+
|
24 |
+
def __init__(self, base_dir: Path):
|
25 |
+
self.base_dir = base_dir
|
26 |
+
|
27 |
+
def _should_scan_file(self, path: Path) -> bool:
|
28 |
+
if any(excluded in path.parts for excluded in self.EXCLUDED_DIRS):
|
29 |
+
return False
|
30 |
+
return path.suffix.lower() in self.TARGET_EXTENSIONS
|
31 |
+
|
32 |
+
def _read_file_content(self, file_path: Path) -> Optional[str]:
|
33 |
+
try:
|
34 |
+
# まずUTF-8で試す
|
35 |
+
try:
|
36 |
+
with file_path.open('r', encoding='utf-8') as f:
|
37 |
+
return f.read()
|
38 |
+
except UnicodeDecodeError:
|
39 |
+
# UTF-8で失敗したらcp932を試す
|
40 |
+
with file_path.open('r', encoding='cp932') as f:
|
41 |
+
return f.read()
|
42 |
+
except (OSError, UnicodeDecodeError):
|
43 |
+
return None
|
44 |
+
|
45 |
+
def scan_files(self) -> List[FileInfo]:
|
46 |
+
if not self.base_dir.exists():
|
47 |
+
raise FileNotFoundError(f"Directory not found: {self.base_dir}")
|
48 |
+
|
49 |
+
files = []
|
50 |
+
|
51 |
+
for entry in self.base_dir.rglob('*'):
|
52 |
+
if entry.is_file() and self._should_scan_file(entry):
|
53 |
+
content = self._read_file_content(entry)
|
54 |
+
if content is not None:
|
55 |
+
files.append(FileInfo(
|
56 |
+
path=entry.relative_to(self.base_dir),
|
57 |
+
content=content
|
58 |
+
))
|
59 |
+
|
60 |
+
return sorted(files, key=lambda x: str(x.path))
|
core/git_manager.py
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import subprocess
|
2 |
+
from pathlib import Path
|
3 |
+
|
4 |
+
class GitManager:
|
5 |
+
def __init__(self, repo_url: str, target_dir: Path):
|
6 |
+
self.repo_url = repo_url
|
7 |
+
self.target_dir = target_dir
|
8 |
+
|
9 |
+
def clone_repository(self) -> bool:
|
10 |
+
try:
|
11 |
+
if self.target_dir.exists():
|
12 |
+
raise FileExistsError(f"Directory already exists: {self.target_dir}")
|
13 |
+
|
14 |
+
self.target_dir.parent.mkdir(parents=True, exist_ok=True)
|
15 |
+
|
16 |
+
subprocess.run(
|
17 |
+
["git", "clone", self.repo_url, str(self.target_dir)],
|
18 |
+
check=True,
|
19 |
+
capture_output=True,
|
20 |
+
text=True
|
21 |
+
)
|
22 |
+
return True
|
23 |
+
|
24 |
+
except subprocess.CalledProcessError as e:
|
25 |
+
raise RuntimeError(f"Clone error: {e.stderr}")
|
26 |
+
|
27 |
+
def cleanup(self):
|
28 |
+
if self.target_dir.exists():
|
29 |
+
subprocess.run(
|
30 |
+
["rm", "-rf", str(self.target_dir)],
|
31 |
+
check=True,
|
32 |
+
capture_output=True,
|
33 |
+
text=True
|
34 |
+
)
|
main.py
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sys
|
2 |
+
from pathlib import Path
|
3 |
+
from config.settings import Settings
|
4 |
+
from core.git_manager import GitManager
|
5 |
+
from core.file_scanner import FileScanner
|
6 |
+
from utils.file_writer import FileWriter
|
7 |
+
|
8 |
+
def main():
|
9 |
+
# コマンドライン引数からパスを取得
|
10 |
+
if len(sys.argv) != 2:
|
11 |
+
print("Usage: python main.py <github_url or directory_path>")
|
12 |
+
return 1
|
13 |
+
|
14 |
+
target_path = sys.argv[1]
|
15 |
+
timestamp = Settings.get_timestamp()
|
16 |
+
output_file = Settings.get_output_file(timestamp)
|
17 |
+
|
18 |
+
# GitHubのURLかローカルパスかを判定
|
19 |
+
is_github = target_path.startswith(('http://', 'https://')) and 'github.com' in target_path
|
20 |
+
|
21 |
+
try:
|
22 |
+
if is_github:
|
23 |
+
# GitHubリポジトリの場合
|
24 |
+
clone_dir = Settings.get_clone_dir(timestamp)
|
25 |
+
print(f"Cloning repository: {target_path}")
|
26 |
+
|
27 |
+
git_manager = GitManager(target_path, clone_dir)
|
28 |
+
git_manager.clone_repository()
|
29 |
+
|
30 |
+
scanner = FileScanner(clone_dir)
|
31 |
+
cleanup_needed = True
|
32 |
+
else:
|
33 |
+
# ローカルディレクトリの場合
|
34 |
+
target_dir = Path(target_path)
|
35 |
+
if not target_dir.exists():
|
36 |
+
print(f"Error: Directory not found: {target_dir}")
|
37 |
+
return 1
|
38 |
+
|
39 |
+
scanner = FileScanner(target_dir)
|
40 |
+
cleanup_needed = False
|
41 |
+
|
42 |
+
# ファイルスキャンと保存
|
43 |
+
print("Scanning files...")
|
44 |
+
files = scanner.scan_files()
|
45 |
+
|
46 |
+
print(f"Writing contents to {output_file}")
|
47 |
+
writer = FileWriter(output_file)
|
48 |
+
writer.write_contents(files)
|
49 |
+
|
50 |
+
print(f"Found {len(files)} files")
|
51 |
+
print(f"Results saved to {output_file}")
|
52 |
+
|
53 |
+
except Exception as e:
|
54 |
+
print(f"Error: {e}")
|
55 |
+
return 1
|
56 |
+
|
57 |
+
finally:
|
58 |
+
# GitHubリポジトリの場合はクリーンアップ
|
59 |
+
if is_github and cleanup_needed and 'git_manager' in locals():
|
60 |
+
try:
|
61 |
+
git_manager.cleanup()
|
62 |
+
print("Cleanup completed")
|
63 |
+
except Exception as e:
|
64 |
+
print(f"Cleanup error: {e}")
|
65 |
+
|
66 |
+
return 0
|
67 |
+
|
68 |
+
if __name__ == "__main__":
|
69 |
+
exit(main())
|
output/scan_result_20241030_210745.txt
ADDED
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#ファイルパス
|
2 |
+
Get_URL_list/get_url_list.py
|
3 |
+
------------
|
4 |
+
import json
|
5 |
+
import requests
|
6 |
+
from bs4 import BeautifulSoup
|
7 |
+
|
8 |
+
# Load URLs from JSON file
|
9 |
+
with open('ideabte_scraping/Get_URL_list/URL_json_output/debate_urls.json', 'r') as f:
|
10 |
+
json_urls = json.load(f)
|
11 |
+
|
12 |
+
# Function to get sub-page URLs from a main theme URL
|
13 |
+
def get_debate_topic_urls(main_url):
|
14 |
+
response = requests.get(main_url)
|
15 |
+
soup = BeautifulSoup(response.text, 'html.parser')
|
16 |
+
|
17 |
+
# Extract all links from the main URL page
|
18 |
+
links = soup.find_all('a', href=True)
|
19 |
+
|
20 |
+
# Filter for links that are debate topics
|
21 |
+
topic_urls = [link['href'] for link in links if link['href'].startswith('/')]
|
22 |
+
|
23 |
+
# Make URLs absolute
|
24 |
+
full_urls = [f"https://idebate.net{url}" for url in topic_urls if "~b" in url]
|
25 |
+
|
26 |
+
return full_urls
|
27 |
+
|
28 |
+
# Dictionary to store all debate topic URLs for each main theme URL
|
29 |
+
all_debate_topic_urls = {}
|
30 |
+
for theme_url in json_urls:
|
31 |
+
theme_name = theme_url.split("/")[-2].replace("~", "_")
|
32 |
+
all_debate_topic_urls[theme_name] = get_debate_topic_urls(theme_url)
|
33 |
+
|
34 |
+
# Output the results
|
35 |
+
with open('ideabte_scraping/Get_URL_list/output/debate_topic_urls.json', 'w') as f:
|
36 |
+
json.dump(all_debate_topic_urls, f, indent=4)
|
37 |
+
|
38 |
+
print("Debate topic URLs have been saved to debate_topic_urls.json")
|
39 |
+
|
40 |
+
#ファイルパス
|
41 |
+
scraping_idebate/run_main.sh
|
42 |
+
------------
|
43 |
+
#!/bin/bash
|
44 |
+
|
45 |
+
# Set default paths
|
46 |
+
JSON_FILE="ideabte_scraping/Get_URL_list/output/debate_topic_urls.json"
|
47 |
+
OUTPUT_DIR="ideabte_scraping/scraping_idebate/output"
|
48 |
+
|
49 |
+
# Check if the JSON file exists
|
50 |
+
if [ ! -f "$JSON_FILE" ]; then
|
51 |
+
echo "Error: JSON file '$JSON_FILE' does not exist."
|
52 |
+
exit 1
|
53 |
+
fi
|
54 |
+
|
55 |
+
# Create the output directory if it doesn't exist
|
56 |
+
mkdir -p "$OUTPUT_DIR"
|
57 |
+
|
58 |
+
# Run the Python script
|
59 |
+
python3 ideabte_scraping/scraping_idebate/src/scraping.py "$JSON_FILE" "$OUTPUT_DIR"
|
60 |
+
|
61 |
+
echo "Scraping completed. Output files are stored in $OUTPUT_DIR"
|
62 |
+
|
63 |
+
#ファイルパス
|
64 |
+
scraping_idebate/src/scraping.py
|
65 |
+
------------
|
66 |
+
import requests
|
67 |
+
from bs4 import BeautifulSoup
|
68 |
+
import json
|
69 |
+
import os
|
70 |
+
import sys
|
71 |
+
from urllib.parse import urlparse
|
72 |
+
|
73 |
+
def scrape_url(url, output_dir):
|
74 |
+
response = requests.get(url)
|
75 |
+
response.raise_for_status()
|
76 |
+
|
77 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
78 |
+
topic = soup.find("h1", class_="blog-post__title").get_text(strip=True)
|
79 |
+
|
80 |
+
points_list = []
|
81 |
+
|
82 |
+
def extract_points(section, section_name):
|
83 |
+
accordion_items = section.find_next_sibling('div', class_='accordion').find_all('div', class_='accordion__item')
|
84 |
+
for item in accordion_items:
|
85 |
+
point_subtitle = item.find('h4', class_='accordion__subtitle').get_text().strip()
|
86 |
+
point_body = item.find('div', class_='accordion__body').find('p').get_text().strip()
|
87 |
+
points_list.append({
|
88 |
+
"topic": topic,
|
89 |
+
"section": section_name,
|
90 |
+
"context": f"**{point_subtitle}**\n{point_body}"
|
91 |
+
})
|
92 |
+
|
93 |
+
points_for_section = soup.find('div', class_='points-vote points-vote--for')
|
94 |
+
if points_for_section:
|
95 |
+
extract_points(points_for_section, "Points For")
|
96 |
+
|
97 |
+
points_against_section = soup.find('div', class_='points-vote points-vote--against')
|
98 |
+
if points_against_section:
|
99 |
+
extract_points(points_against_section, "Points Against")
|
100 |
+
|
101 |
+
# Generate a unique filename based on the URL
|
102 |
+
parsed_url = urlparse(url)
|
103 |
+
filename = f"{parsed_url.path.strip('/').replace('/', '_')}.json"
|
104 |
+
output_path = os.path.join(output_dir, filename)
|
105 |
+
|
106 |
+
with open(output_path, "w", encoding="utf-8") as f:
|
107 |
+
json.dump(points_list, f, ensure_ascii=False, indent=4)
|
108 |
+
|
109 |
+
print(f"Data saved to {output_path}")
|
110 |
+
|
111 |
+
if __name__ == "__main__":
|
112 |
+
if len(sys.argv) != 3:
|
113 |
+
print("Usage: python script.py <json_file> <output_dir>")
|
114 |
+
sys.exit(1)
|
115 |
+
|
116 |
+
json_file = sys.argv[1]
|
117 |
+
output_dir = sys.argv[2]
|
118 |
+
|
119 |
+
os.makedirs(output_dir, exist_ok=True)
|
120 |
+
|
121 |
+
with open(json_file, 'r') as f:
|
122 |
+
url_data = json.load(f)
|
123 |
+
|
124 |
+
for category, urls in url_data.items():
|
125 |
+
for url in urls:
|
126 |
+
try:
|
127 |
+
scrape_url(url, output_dir)
|
128 |
+
except Exception as e:
|
129 |
+
print(f"Error scraping {url}: {str(e)}")
|
130 |
+
|
131 |
+
#ファイルパス
|
132 |
+
scraping_idebate/src/scraping_test.py
|
133 |
+
------------
|
134 |
+
import requests
|
135 |
+
from bs4 import BeautifulSoup
|
136 |
+
|
137 |
+
url = "https://idebate.net/this-house-would-make-all-museums-free-of-charge~b641/"
|
138 |
+
|
139 |
+
# ウェブページを取得
|
140 |
+
response = requests.get(url)
|
141 |
+
response.raise_for_status() # エラーチェック
|
142 |
+
|
143 |
+
# HTMLを解析
|
144 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
145 |
+
|
146 |
+
# Points Forのdiv要素を取得
|
147 |
+
points_for_section = soup.find('div', class_='points-vote points-vote--for')
|
148 |
+
|
149 |
+
# ポイントを含むアコーディオン要素を取得
|
150 |
+
accordion_items = points_for_section.find_next_sibling('div', class_='accordion').find_all('div', class_='accordion__item')
|
151 |
+
|
152 |
+
# 各ポイントのテキストを抽出
|
153 |
+
points = []
|
154 |
+
for item in accordion_items:
|
155 |
+
point_subtitle = item.find('h4', class_='accordion__subtitle').get_text().strip()
|
156 |
+
point_body = item.find('div', class_='accordion__body').find('p').get_text().strip()
|
157 |
+
points.append(f"**{point_subtitle}**\n{point_body}")
|
158 |
+
|
159 |
+
# 抽出したポイントを出力
|
160 |
+
for point in points:
|
161 |
+
print(point)
|
162 |
+
print("-" * 20) # 区切り線
|
163 |
+
|
164 |
+
|
165 |
+
#ファイルパス
|
166 |
+
scraping_idebate/src/scraping_tqdm.py
|
167 |
+
------------
|
168 |
+
import requests
|
169 |
+
from bs4 import BeautifulSoup
|
170 |
+
import json
|
171 |
+
import os
|
172 |
+
import sys
|
173 |
+
from urllib.parse import urlparse
|
174 |
+
from tqdm import tqdm
|
175 |
+
|
176 |
+
def scrape_url(url, output_dir):
|
177 |
+
response = requests.get(url)
|
178 |
+
response.raise_for_status()
|
179 |
+
|
180 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
181 |
+
topic = soup.find("h1", class_="blog-post__title").get_text(strip=True)
|
182 |
+
|
183 |
+
points_list = []
|
184 |
+
|
185 |
+
def extract_points(section, section_name):
|
186 |
+
accordion_items = section.find_next_sibling('div', class_='accordion').find_all('div', class_='accordion__item')
|
187 |
+
for item in accordion_items:
|
188 |
+
point_subtitle = item.find('h4', class_='accordion__subtitle').get_text().strip()
|
189 |
+
point_body = item.find('div', class_='accordion__body').find('p').get_text().strip()
|
190 |
+
points_list.append({
|
191 |
+
"topic": topic,
|
192 |
+
"section": section_name,
|
193 |
+
"context": f"**{point_subtitle}**\n{point_body}"
|
194 |
+
})
|
195 |
+
|
196 |
+
points_for_section = soup.find('div', class_='points-vote points-vote--for')
|
197 |
+
if points_for_section:
|
198 |
+
extract_points(points_for_section, "Points For")
|
199 |
+
|
200 |
+
points_against_section = soup.find('div', class_='points-vote points-vote--against')
|
201 |
+
if points_against_section:
|
202 |
+
extract_points(points_against_section, "Points Against")
|
203 |
+
|
204 |
+
# Generate a unique filename based on the URL
|
205 |
+
parsed_url = urlparse(url)
|
206 |
+
filename = f"{parsed_url.path.strip('/').replace('/', '_')}.json"
|
207 |
+
output_path = os.path.join(output_dir, filename)
|
208 |
+
|
209 |
+
with open(output_path, "w", encoding="utf-8") as f:
|
210 |
+
json.dump(points_list, f, ensure_ascii=False, indent=4)
|
211 |
+
|
212 |
+
return output_path
|
213 |
+
|
214 |
+
if __name__ == "__main__":
|
215 |
+
if len(sys.argv) != 3:
|
216 |
+
print("Usage: python script.py <json_file> <output_dir>")
|
217 |
+
sys.exit(1)
|
218 |
+
|
219 |
+
json_file = sys.argv[1]
|
220 |
+
output_dir = sys.argv[2]
|
221 |
+
|
222 |
+
os.makedirs(output_dir, exist_ok=True)
|
223 |
+
|
224 |
+
with open(json_file, 'r') as f:
|
225 |
+
url_data = json.load(f)
|
226 |
+
|
227 |
+
total_urls = sum(len(urls) for urls in url_data.values())
|
228 |
+
|
229 |
+
with tqdm(total=total_urls, desc="Scraping Progress") as pbar:
|
230 |
+
for category, urls in url_data.items():
|
231 |
+
for url in urls:
|
232 |
+
try:
|
233 |
+
output_path = scrape_url(url, output_dir)
|
234 |
+
pbar.set_postfix_str(f"Saved: {output_path}")
|
235 |
+
pbar.update(1)
|
236 |
+
except Exception as e:
|
237 |
+
pbar.set_postfix_str(f"Error: {url}")
|
238 |
+
print(f"\nError scraping {url}: {str(e)}")
|
239 |
+
pbar.update(1)
|
240 |
+
|
241 |
+
print("\nScraping completed. All data saved to the output directory.")
|
242 |
+
|
rquirements.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
streamlit>=1.24.0
|
2 |
+
openai>=0.27.0
|
3 |
+
pathlib>=1.0.1
|
4 |
+
chardet>=4.0.0
|
scan.sh
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/bin/bash
|
2 |
+
|
3 |
+
# エラーが発生した場合に停止
|
4 |
+
set -e
|
5 |
+
|
6 |
+
# デフォルトのターゲットパスを設定
|
7 |
+
# ここを変更することで対象を変更できます
|
8 |
+
TARGET_PATH="https://github.com/DeL-TaiseiOzaki/idebate_scraping.git" # 例: Linuxカーネル
|
9 |
+
# TARGET_PATH="/path/to/your/directory" # ローカルディレクトリの例
|
10 |
+
|
11 |
+
# 必要なディレクトリの存在確認
|
12 |
+
if [ ! -d "output" ]; then
|
13 |
+
mkdir output
|
14 |
+
fi
|
15 |
+
|
16 |
+
# Pythonの存在確認
|
17 |
+
if ! command -v python3 &> /dev/null; then
|
18 |
+
echo "Error: Python3 is not installed"
|
19 |
+
exit 1
|
20 |
+
fi
|
21 |
+
|
22 |
+
# GitHubリポジトリの場合、Gitの存在確認
|
23 |
+
if [[ $TARGET_PATH == http* ]] && [[ $TARGET_PATH == *github.com* ]]; then
|
24 |
+
if ! command -v git &> /dev/null; then
|
25 |
+
echo "Error: Git is not installed"
|
26 |
+
exit 1
|
27 |
+
fi
|
28 |
+
echo "Scanning GitHub repository: $TARGET_PATH"
|
29 |
+
else
|
30 |
+
if [ ! -d "$TARGET_PATH" ]; then
|
31 |
+
echo "Error: Directory not found: $TARGET_PATH"
|
32 |
+
exit 1
|
33 |
+
fi
|
34 |
+
echo "Scanning local directory: $TARGET_PATH"
|
35 |
+
fi
|
36 |
+
|
37 |
+
# スキャンの実行
|
38 |
+
echo "Starting directory scan..."
|
39 |
+
python3 main.py "$TARGET_PATH"
|
40 |
+
|
41 |
+
exit_code=$?
|
42 |
+
|
43 |
+
if [ $exit_code -eq 0 ]; then
|
44 |
+
echo "Scan completed successfully!"
|
45 |
+
echo "Results are saved in the 'output' directory"
|
46 |
+
else
|
47 |
+
echo "Scan failed with exit code: $exit_code"
|
48 |
+
exit $exit_code
|
49 |
+
fi
|
services/llm_service.py
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Optional
|
2 |
+
import openai
|
3 |
+
from pathlib import Path
|
4 |
+
|
5 |
+
class LLMService:
|
6 |
+
def __init__(self, api_key: str):
|
7 |
+
"""
|
8 |
+
LLMサービスの初期化
|
9 |
+
Args:
|
10 |
+
api_key: OpenAI APIキー
|
11 |
+
"""
|
12 |
+
self.api_key = api_key
|
13 |
+
openai.api_key = api_key
|
14 |
+
|
15 |
+
def create_prompt(self, content: str, query: str) -> str:
|
16 |
+
"""
|
17 |
+
プロンプトを生成
|
18 |
+
Args:
|
19 |
+
content: コードの内容
|
20 |
+
query: ユーザーからの質問
|
21 |
+
Returns:
|
22 |
+
生成されたプロンプト
|
23 |
+
"""
|
24 |
+
return f"""以下はGitHubリポジトリのコード解析結果です。このコードについて質問に答えてください。
|
25 |
+
|
26 |
+
コード解析結果:
|
27 |
+
{content}
|
28 |
+
|
29 |
+
質問: {query}
|
30 |
+
|
31 |
+
できるだけ具体的に、コードの内容を参照しながら回答してください。"""
|
32 |
+
|
33 |
+
def get_response(self, content: str, query: str) -> tuple[str, Optional[str]]:
|
34 |
+
"""
|
35 |
+
LLMを使用して回答を生成
|
36 |
+
Args:
|
37 |
+
content: コードの内容
|
38 |
+
query: ユーザーからの質問
|
39 |
+
Returns:
|
40 |
+
(回答, エラーメッセージ)のタプル
|
41 |
+
"""
|
42 |
+
try:
|
43 |
+
prompt = self.create_prompt(content, query)
|
44 |
+
|
45 |
+
response = openai.ChatCompletion.create(
|
46 |
+
model="gpt-3.5-turbo-16k",
|
47 |
+
messages=[
|
48 |
+
{
|
49 |
+
"role": "system",
|
50 |
+
"content": "あなたはコードアナリストとして、リポジトリの解析と質問への回答を行います。"
|
51 |
+
},
|
52 |
+
{
|
53 |
+
"role": "user",
|
54 |
+
"content": prompt
|
55 |
+
}
|
56 |
+
]
|
57 |
+
)
|
58 |
+
|
59 |
+
return response.choices[0].message.content, None
|
60 |
+
|
61 |
+
except Exception as e:
|
62 |
+
return None, f"エラーが発生しました: {str(e)}"
|
63 |
+
|
64 |
+
@staticmethod
|
65 |
+
def format_code_content(files_content: dict) -> str:
|
66 |
+
"""
|
67 |
+
ファイル内容をプロンプト用にフォーマット
|
68 |
+
Args:
|
69 |
+
files_content: ファイルパスと内容の辞書
|
70 |
+
Returns:
|
71 |
+
フォーマットされたテキスト
|
72 |
+
"""
|
73 |
+
formatted_content = []
|
74 |
+
for file_path, content in files_content.items():
|
75 |
+
formatted_content.append(
|
76 |
+
f"#ファイルパス\n{file_path}\n------------\n{content}\n"
|
77 |
+
)
|
78 |
+
return "\n".join(formatted_content)
|
utils/__init__.py
ADDED
File without changes
|
utils/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (152 Bytes). View file
|
|
utils/__pycache__/content_exporter.cpython-310.pyc
ADDED
Binary file (2.36 kB). View file
|
|
utils/__pycache__/file_writer.cpython-310.pyc
ADDED
Binary file (1.13 kB). View file
|
|
utils/file_writer.py
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pathlib import Path
|
2 |
+
from typing import List
|
3 |
+
from core.file_scanner import FileInfo
|
4 |
+
|
5 |
+
class FileWriter:
|
6 |
+
def __init__(self, output_file: Path):
|
7 |
+
self.output_file = output_file
|
8 |
+
|
9 |
+
def write_contents(self, files: List[FileInfo]) -> None:
|
10 |
+
self.output_file.parent.mkdir(parents=True, exist_ok=True)
|
11 |
+
|
12 |
+
with self.output_file.open('w', encoding='utf-8') as f:
|
13 |
+
for file_info in files:
|
14 |
+
# ファイルパスのセクション
|
15 |
+
f.write("#ファイルパス\n")
|
16 |
+
f.write(str(file_info.path))
|
17 |
+
f.write("\n------------\n")
|
18 |
+
|
19 |
+
# ファイル内容
|
20 |
+
if file_info.content is not None:
|
21 |
+
f.write(file_info.content)
|
22 |
+
else:
|
23 |
+
f.write("# Failed to read content")
|
24 |
+
f.write("\n\n")
|
utils/logger.py
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pathlib import Path
|
2 |
+
from typing import List
|
3 |
+
from datetime import datetime
|
4 |
+
from core.file_scanner import FileInfo
|
5 |
+
|
6 |
+
class ScanLogger:
|
7 |
+
def __init__(self, log_file: Path):
|
8 |
+
self.log_file = log_file
|
9 |
+
|
10 |
+
def write_log(self, repo_url: str, files: List[FileInfo], stats: dict):
|
11 |
+
"""スキャン結果をログファイルに書き込みます"""
|
12 |
+
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
13 |
+
|
14 |
+
with self.log_file.open('w', encoding='utf-8') as f:
|
15 |
+
f.write(f"スキャン日時: {datetime.now()}\n")
|
16 |
+
f.write(f"リポジトリ: {repo_url}\n")
|
17 |
+
f.write(f"ファイル数: {len(files)}\n\n")
|
18 |
+
|
19 |
+
f.write("=== ファイル種類の統計 ===\n")
|
20 |
+
for ext, count in stats.items():
|
21 |
+
f.write(f"{ext}: {count}個\n")
|
22 |
+
f.write("\n")
|
23 |
+
|
24 |
+
f.write("=== ファイルパス一覧 ===\n")
|
25 |
+
for file_info in files:
|
26 |
+
f.write(f"{file_info.path} ({file_info.formatted_size})\n")
|