Skip to content

Commit 0e012d9

Browse files
✨ бновление: улучшен анализ статистики и график, добавлена сводка
1 parent 7e4871d commit 0e012d9

File tree

5 files changed

+183
-74
lines changed

5 files changed

+183
-74
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,5 @@ utils/
3434
external_libraries/
3535
external/
3636
libraries/
37+
/project_stats.csv
38+
/project_stats.html

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Changelog
2+
3+
## [v1.2.0] — 2025-05-02
4+
5+
### ✨ Новое:
6+
- Полностью переработано окно статистики:
7+
- Чистый и понятный график количества проектов по месяцам
8+
- Автоматическое удаление повреждённых или отсутствующих дат
9+
- Поддержка сгруппированного анализа по месяцам
10+
- Добавлена таблица со всеми проектами в формате CSV
11+
- Добавлена визуально отформатированная таблица, открываемая в браузере
12+
13+
### 📊 Улучшения:
14+
- Добавлен вывод анализа используемых библиотек по проектам
15+
- Убраны шумовые проекты без .py-файлов
16+
- Поддержка вложенных структур до 6 уровней глубины
17+
18+
### 🐞 Исправления:
19+
- Исправлена ошибка `Unresolved reference 'stats_canvas'`
20+
- Устранены ошибки при отсутствии поля `created` и повреждённых датах
21+
- Устранена проблема с пустыми данными при первом запуске
22+
23+
---

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
## 🔄 Последнее обновление — v1.2.0
2+
3+
> 📅 Дата релиза: 2025-05-02
4+
5+
- ✅ Новый модуль статистики
6+
- 📈 Графики по времени создания проектов
7+
- 📚 Анализ импортируемых библиотек
8+
- 🛡 Поддержка вложенных структур
9+
- 🐞 Исправления критических багов и повышена стабильность
10+
- 📄 project_stats.csv — таблица со всеми проектами в формате CSV
11+
- 🌐 project_stats.html — визуально отформатированная таблица, открываемая в браузере
12+
➡ Посмотреть полный [Changelog](CHANGELOG.md)
13+
14+
115
<a href="https://github.com/AlgorithmAlchemy/py-import-scanner/blob/main/README_en.md" style="text-decoration: none;">
216
<button style="background-color: #4CAF50; color: white; padding: 10px 20px; text-align: center; display: inline-block; font-size: 16px; margin: 4px 2px; cursor: pointer; border: none;">
317
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/b/be/Flag_of_England.svg/32px-Flag_of_England.svg.png" alt="English" style="vertical-align: middle; padding-right: 8px;" />

main.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,7 @@ def process_file(file_path):
164164
projects = find_projects(directory)
165165

166166
# Проводим анализ каждого проекта
167-
project_info = parse_python_files(directory)
168-
project_data = list(project_info.values())
167+
project_data = parse_python_files(directory)
169168

170169
# Переименование поля
171170
for item in project_data:
@@ -355,7 +354,6 @@ def on_closing():
355354
window.destroy() # Закрытие окна
356355

357356

358-
359357
window = Tk()
360358
window.title("Статистика импортов в проектах")
361359
window.geometry("1200x800")
@@ -428,4 +426,5 @@ def periodic_check():
428426

429427
# Запуск Tkinter
430428
periodic_check()
431-
window.mainloop()
429+
window.mainloop()
430+

stats_window.py

Lines changed: 141 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
from utils import read_gitignore, is_ignored, find_projects
1212

13+
IGNORED_DIRS = {'.git', '__pycache__', '.idea', '.vscode', '.venv', '.eggs'}
14+
1315

1416
def analyze_project_structure(directory, task_queue):
1517
ignored_paths = read_gitignore(directory)
@@ -50,14 +52,30 @@ def analyze_project_structure(directory, task_queue):
5052

5153

5254

53-
def parse_python_files(projects_dir):
55+
def parse_python_files(projects_dir, export=True, max_files=5000, max_depth=6):
56+
import os, ast, datetime
57+
import pandas as pd
58+
59+
IGNORED_DIRS = {'.git', '__pycache__', '.idea', '.vscode', 'venv', '.venv', 'env', '.env', '.mypy_cache'}
60+
5461
project_stats = {}
62+
scanned_files = 0
63+
64+
for root, dirs, files in os.walk(projects_dir):
65+
# Удаление игнорируемых директорий
66+
dirs[:] = [d for d in dirs if d not in IGNORED_DIRS]
67+
68+
# Ограничение глубины
69+
rel_root = os.path.relpath(root, projects_dir)
70+
depth = rel_root.count(os.sep)
71+
if depth > max_depth:
72+
continue
5573

56-
# Используем find_projects для поиска папок с проектами
57-
projects = find_projects(projects_dir)
74+
py_files = [f for f in files if f.endswith(".py")]
75+
if not py_files:
76+
continue
5877

59-
for project_dir in projects:
60-
project_name = os.path.relpath(project_dir, projects_dir).split(os.sep)[0]
78+
project_name = rel_root.replace(os.sep, " / ") if rel_root != "." else "ROOT"
6179

6280
if project_name not in project_stats:
6381
project_stats[project_name] = {
@@ -67,98 +85,151 @@ def parse_python_files(projects_dir):
6785
"dirs": set()
6886
}
6987

70-
# Склонение директорий с проектами
71-
for root, dirs, files in os.walk(project_dir):
72-
for file in files:
73-
if file.endswith(".py"):
74-
file_path = os.path.join(root, file)
75-
76-
# Обновляем счётчик Python файлов
77-
project_stats[project_name]["py_count"] += 1
78-
79-
# Обновляем дату создания проекта
80-
creation_time = os.path.getctime(file_path)
81-
creation_date = datetime.datetime.fromtimestamp(creation_time)
82-
83-
current_created = project_stats[project_name]["created"]
84-
if current_created is None or creation_date < current_created:
85-
project_stats[project_name]["created"] = creation_date
86-
87-
# Сбор директорий
88-
rel_dir = os.path.relpath(root, os.path.join(projects_dir, project_name))
89-
if rel_dir != ".":
90-
project_stats[project_name]["dirs"].add(rel_dir)
91-
92-
# Парсим файл для библиотек
93-
try:
94-
with open(file_path, "r", encoding="utf-8") as f:
95-
node = ast.parse(f.read(), filename=file_path)
96-
97-
for sub_node in ast.walk(node):
98-
if isinstance(sub_node, ast.Import):
99-
for alias in sub_node.names:
100-
project_stats[project_name]["libs"].add(alias.name.split('.')[0])
101-
elif isinstance(sub_node, ast.ImportFrom):
102-
if sub_node.module:
103-
project_stats[project_name]["libs"].add(sub_node.module.split('.')[0])
104-
except Exception:
105-
continue
106-
107-
# Приведение к нужному формату
108-
for proj in project_stats:
109-
project_stats[proj]["libs"] = sorted(project_stats[proj]["libs"])
110-
project_stats[proj]["dirs"] = sorted(project_stats[proj]["dirs"])
111-
if project_stats[proj]["created"]:
112-
project_stats[proj]["created"] = project_stats[proj]["created"].strftime("%Y-%m-%d %H:%M:%S")
113-
114-
return project_stats
88+
for file in py_files:
89+
"""if scanned_files >= max_files:
90+
print(f"⚠ Превышен лимит {max_files} файлов. Анализ остановлен.")
91+
break"""
92+
93+
file_path = os.path.join(root, file)
94+
scanned_files += 1
95+
project_stats[project_name]["py_count"] += 1
96+
97+
# Обработка даты
98+
try:
99+
creation_time = os.path.getctime(file_path)
100+
creation_date = datetime.datetime.fromtimestamp(creation_time)
101+
current_created = project_stats[project_name]["created"]
102+
if current_created is None or creation_date < current_created:
103+
project_stats[project_name]["created"] = creation_date
104+
except Exception:
105+
pass
106+
107+
# Добавление относительной директории
108+
rel_dir = os.path.relpath(root, projects_dir)
109+
if rel_dir != ".":
110+
project_stats[project_name]["dirs"].add(rel_dir)
111+
112+
# Парсинг импортов
113+
try:
114+
with open(file_path, "r", encoding="utf-8") as f:
115+
content = f.read()
116+
node = ast.parse(content, filename=file_path)
117+
for sub_node in ast.walk(node):
118+
if isinstance(sub_node, ast.Import):
119+
for alias in sub_node.names:
120+
project_stats[project_name]["libs"].add(alias.name.split('.')[0])
121+
elif isinstance(sub_node, ast.ImportFrom) and sub_node.module:
122+
project_stats[project_name]["libs"].add(sub_node.module.split('.')[0])
123+
except Exception:
124+
continue
125+
126+
print(f"[✓] {project_name}{len(py_files)} файлов")
127+
128+
# Финальная сборка
129+
result = []
130+
for proj, data in project_stats.items():
131+
date_str = data["created"].strftime("%Y-%m-%d %H:%M:%S") if data["created"] else None
132+
result.append({
133+
"name": proj,
134+
"stack": sorted(data["libs"]),
135+
"dirs": sorted(data["dirs"]),
136+
"date": date_str,
137+
"py_count": data["py_count"]
138+
})
139+
140+
if not result:
141+
print("⚠ Не найдено проектов с .py файлами.")
142+
return []
143+
144+
df = pd.DataFrame(result)
145+
df["date"] = pd.to_datetime(df["date"], errors="coerce")
146+
147+
if export:
148+
df.to_csv("project_stats.csv", index=False, encoding="utf-8-sig")
149+
df.to_html("project_stats.html", index=False)
150+
151+
print("==== Итог ====")
152+
print(df[["name", "date"]])
153+
return df.to_dict("records")
154+
155+
156+
115157

116158

117159
def open_stats_window(root, project_data: list[dict]):
118160
stats_win = tk.Toplevel(root)
119-
stats_win.title("Статистика проектов")
120-
stats_win.geometry("800x600")
161+
stats_win.title("📊 Статистика по проектам")
162+
stats_win.geometry("1000x700")
121163

122-
# Проверка наличия данных в project_data
123164
if not project_data:
124-
tk.Label(stats_win, text="Нет данных для отображения статистики.").pack(pady=20)
165+
tk.Label(stats_win, text="Нет данных для отображения.", font=("Arial", 12)).pack(pady=20)
125166
return
126167

127168
df = pd.DataFrame(project_data)
128169

129-
# Если данных в столбце 'date' нет, выводим сообщение
130170
if df.empty or 'date' not in df.columns:
131-
tk.Label(stats_win, text="Недостаточно данных для отображения статистики.").pack(pady=20)
171+
tk.Label(stats_win, text="Недостаточно данных для статистики.", font=("Arial", 12)).pack(pady=20)
132172
return
133173

134-
# Конвертируем 'date' в формат datetime для графиков
135174
df['date'] = pd.to_datetime(df['date'], errors='coerce')
136-
137-
# Убираем записи с некорректными датами
138175
df = df.dropna(subset=['date'])
139176

140-
stats_canvas = tk.Frame(stats_win)
141-
stats_canvas.pack(fill="both", expand=True)
177+
if df.empty:
178+
tk.Label(stats_win, text="❌ Все даты повреждены или отсутствуют.", font=("Arial", 12)).pack(pady=20)
179+
return
180+
181+
df['month'] = df['date'].dt.to_period('M')
182+
183+
# === Верхняя сводка ===
184+
summary_frame = tk.Frame(stats_win)
185+
summary_frame.pack(pady=10)
186+
187+
total_projects = len(df)
188+
total_files = df['py_count'].sum()
189+
earliest = df['date'].min().strftime("%Y-%m-%d")
190+
latest = df['date'].max().strftime("%Y-%m-%d")
191+
unique_months = df['month'].nunique()
142192

143-
# === График по дате создания ===
144-
fig, ax = plt.subplots(figsize=(6, 4))
145-
df_by_month = df.groupby(df['date'].dt.to_period('M')).size().sort_index()
146-
df_by_month.plot(kind='bar', ax=ax, title='Проекты по месяцам', rot=45)
193+
summary_text = (
194+
f"📦 Всего проектов: {total_projects} 🧠 Всего .py файлов: {total_files}\n"
195+
f"📆 Период: {earliest}{latest} 📊 Уникальных месяцев: {unique_months}"
196+
)
197+
tk.Label(summary_frame, text=summary_text, font=("Arial", 11), justify="left").pack()
198+
199+
# === График по месяцам ===
200+
chart_frame = tk.Frame(stats_win)
201+
chart_frame.pack(fill="both", expand=False, pady=5)
202+
203+
fig, ax = plt.subplots(figsize=(8, 4))
204+
df_by_month = df.groupby('month').size().sort_index()
205+
df_by_month.plot(kind='bar', ax=ax, color='skyblue', edgecolor='black')
206+
207+
ax.set_title('📅 Количество проектов по месяцам', fontsize=14)
208+
ax.set_ylabel('Проекты', fontsize=12)
209+
ax.set_xlabel('Месяц', fontsize=12)
210+
ax.grid(True, axis='y', linestyle='--', alpha=0.5)
147211
fig.tight_layout()
148212

149-
canvas = FigureCanvasTkAgg(fig, master=stats_canvas)
213+
canvas = FigureCanvasTkAgg(fig, master=chart_frame)
150214
canvas.draw()
151-
canvas.get_tk_widget().pack(pady=10)
215+
canvas.get_tk_widget().pack()
152216

153217
# === Анализ технологий ===
218+
tech_frame = tk.LabelFrame(stats_win, text="📚 Используемые библиотеки", font=("Arial", 12))
219+
tech_frame.pack(fill="both", expand=True, padx=10, pady=10)
220+
154221
if 'stack' in df.columns:
155222
all_stacks = sum(df['stack'].tolist(), [])
156223
stack_series = pd.Series(all_stacks).value_counts()
157-
stack_text = "\n".join(f"{lang}: {count}" for lang, count in stack_series.items())
224+
if not stack_series.empty:
225+
stack_text = "\n".join(f"• {lib:<20}{count}" for lib, count in stack_series.items())
226+
else:
227+
stack_text = "Нет данных о библиотеках."
158228
else:
159229
stack_text = "Нет данных по стеку технологий."
160230

161-
tk.Label(stats_canvas, text="Анализ технологий:\n" + stack_text, justify="left", font=("Arial", 12)).pack(pady=5)
231+
tk.Message(tech_frame, text=stack_text, font=("Courier New", 10), width=900, anchor="w", justify="left").pack()
162232

163233
# === Кнопка закрытия ===
164-
tk.Button(stats_canvas, text="Закрыть", command=stats_win.destroy).pack(pady=10)
234+
tk.Button(stats_win, text="Закрыть", command=stats_win.destroy).pack(pady=10)
235+

0 commit comments

Comments
 (0)