Доброго времени суток, друзья. В сегодняшней статье мы попробуем решить следующую задачу при помощи языка Python: мы напишем с вами небольшой собственный модуль (я назвал его projectinfo.py), который будет решать задачу анализа структуры различных проектов, а в качестве экспериментального проекта мы выберем простой типовой проект на языке Java и посчитаем какие Java-классы объявлены в файлах проекта, имеющих расширение .java. В основу нашего модуля войдут небольшие классы, которые решают общую задачу сбора метаданных о файлах проекта, поэтому при необходимости вы без особого труда сможете адаптировать модуль для решения задач анализа других проектов - например, на языках C#, C++ и любых других (да и в целом, совсем не обязательно, чтобы проект представлял собой обязательно программу на каком-то языке программирования. Возможно, вы сможете найти применение наработкам из текущей статьи для анализа ваших проектов, имеющих иную природу и назначение).
Итак, начнём с полного текста нашего модуля projectinfo.py:
#!/usr/bin/env python
"""
[EN] Module allows you to aggregate information about some abstract project represented
in the form of folders structure containing some project files. Currently module
supports special classes for analyzing Java-based projects that contain files with the
*.java extension
[RU] Модуль позволяет вам собирать информацию о некотором абстрактном проекте, представленном
в форме структуры каталогов, содержащих какие-то проектные файлы. На текущий момент
модуль поддерживает специальные классы для анализа Java-проектов, содержащих файлы с расширением
*.java
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program. If not, see <http://www.gnu.org/licenses/>.
"""
__author__ = "allineed.ru"
__contact__ = "allineed.ru[at]gmail.com"
__copyright__ = "Copyright 2022, Allineed.Ru"
__date__ = "2022/06/13"
__deprecated__ = False
__email__ = "allineed.ru[at]gmail.com"
__license__ = "GPLv3"
__maintainer__ = "Max Damascus"
__status__ = "Alpha"
__version__ = "0.0.1"
import os
import re
from typing import List
class ResourceFileExtension:
"""
[EN] Class containing possible known extensions of the analyzed project
[RU] Класс, содержащий возможные известные расширения анализируемого проекта
"""
JAVA_FILE = '.java'
XML_FILE = '.xml'
GITIGNORE_FILE = '.gitignore'
IDEA_PROJECT_FILE = '.iml'
class ResourceFile:
"""
[EN] Class contains meta information about abstract resource file of the project
[RU] Класс содержит метаинформацию об абстрактном ресурсном файле проекта
"""
def __init__(self):
self.__init__('', '', '')
def __init__(self, name, path, extension):
self.__name = name
self.__path = path
self.__extension = extension
def get_name(self):
"""
[EN] Method gets the file name that corresponds to this resource
[RU] Метод получает имя файла, соответствующее данному ресурсу
"""
return self.__name
def get_path(self):
"""
[EN] Method gets the full path to the resource
[RU] Метод получает путь, по которому хранится данный ресурс
"""
return self.__path
def get_extension(self):
"""
[EN] Method gets the file extension corresponding to this resource
[RU] Метод получает расширение файла для данного ресурса
"""
return self.__extension
class ProjectResourceAnalyzer:
"""
[EN] Class helps to analyze the project and collect information about resource files included into the project
[RU] Этот класс является анализатором проекта и собирает информацию о входящих в него файлах ресурсов
"""
def __init__(self):
self.__resource_files: List[ResourceFile] = []
def __add_resource_file(self, source_file: ResourceFile):
"""
[EN] Private method that adds a resource file into the list of project resources
[RU] Приватный метод, добавляет файл ресурса в список файлов ресурса проекта
"""
self.__resource_files.append(source_file)
def get_resource_files(self, extension='') -> List[ResourceFile]:
"""
[EN] Method gets the list of ResourceFile elements holding all the resources included in the project
[RU] Метод получает список элементов ResourceFile, содержащий все файлы ресурсов, входящих в указанный проект
"""
if extension == '':
return self.__resource_files
result: List[ResourceFile] = []
for rf in self.__resource_files:
if rf.get_extension() == extension:
result.append(rf)
return result
def analyze(self, path):
"""
[EN] Method analyzes the project by the given path and aggregates meta information about its resource files
[RU] Метод анализирует проект по заданному пути и агрегирует метаинформацию о входящих в проект файлах ресуров
"""
for current_path, sub_folders, files in os.walk(path):
for file in files:
if isinstance(file, str):
str_file = str(file)
file_name, file_ext = os.path.splitext(str_file)
resource_file = ResourceFile(str_file, current_path, file_ext)
self.__add_resource_file(resource_file)
class JavaResourceFileMetaInfo:
"""
[EN] Class contains meta information about the resource file that has a .java extension.
This meta information includes:
* imports - all the imports that are used in this Java file
* classes - all the classes declared in this Java file
* package - the package name specified in this Java file (if any)
[RU] Класс, содержащий метаинформацию о файле ресурса проекта с расширением .java.
Эта метаинформация включает в себя:
* imports - все импорты, использованные в данном Java файле
* classes - все классы, объявленные в этом Java файле
* package - название пакета, указанное в данном Java файле (если оно присутствует)
"""
def __init__(self):
self.__imports: List[str] = []
self.__classes: List[str] = []
self.__package: str = ''
def classes(self):
return self.__classes
def imports(self):
return self.__imports
def set_package(self, package: str):
self.__package = package
def add_class(self, class_name: str):
self.__classes.append(class_name)
def add_import(self, imported_pkg_and_class: str):
self.__imports.append(imported_pkg_and_class)
def get_package(self):
return self.__package
class JavaMetaInfoAnalyzer:
"""
[EN] Class collects meta information about all resource files with the .java extension
[RU] Класс, собирающий метаинформацию о всех файлах ресурсов проекта с расширением .java
"""
def __init__(self, project_resource_analyzer: ProjectResourceAnalyzer):
self.__project_resource_analyzer = project_resource_analyzer
self.__java_resources: List[ResourceFile] = self.__project_resource_analyzer \
.get_resource_files(ResourceFileExtension.JAVA_FILE)
self.__java_classes_dict: dict[ResourceFile, JavaResourceFileMetaInfo] = {}
self.__is_multiline_comment_opened = False
def get_java_resources(self):
"""
[EN] Method gets the list of ResourceFile elements (files with .java extension)
[RU] Метод получает список элементов ResourceFile (файлов с расширением .java)
:return: [EN] the list of ResourceFile elements; [RU] список элементов ResourceFile
"""
return self.__java_resources
def get_java_classes_count(self):
"""
[EN] Method gets the number of Java classes found
[RU] Метод возвращает количество найденных Java классов
:return: [EN] the number of Java classes found; [RU] количество найденных Java классов
"""
return len(self.__java_classes_dict)
def get_java_classes(self):
"""
[EN] Method gets the dictionary that holds pairs <ResourceFile, JavaResourceFileMetaInfo>
[RU] Метод возвращает словарь, содержащий пары <ResourceFile, JavaResourceFileMetaInfo>
:return: [EN] dictionary containing metainfo about Java classes; [RU] словарь, содержащий метаданные о
Java-классах
"""
return self.__java_classes_dict
def __strip_comments(self, file_line: str):
"""
[EN] Private method that strips the Java-style comments from the current line that has been read from
the file
[RU] Приватный метод, который отрезает всё, что относится к комментариям в текущей строке, считанной из
файла
:param file_line: [EN] current line from the Java file being read; [RU] текущая строка из Java файл, который
считывается в настоящий момент
:return: [EN] part of the line that has no Java-comments parts; [RU] часть строки, не имеющая Java-комментариев
"""
single_line_comment_start_pos = file_line.find("//")
multi_line_comment_start_pos = file_line.find("/*")
multi_line_comment_end_pos = file_line.find("*/")
if multi_line_comment_start_pos == 0:
self.__is_multiline_comment_opened = multi_line_comment_end_pos < 0
return ''
elif multi_line_comment_start_pos > 0:
self.__is_multiline_comment_opened = multi_line_comment_end_pos < 0
return file_line[0:multi_line_comment_start_pos]
if multi_line_comment_end_pos >= 0:
self.__is_multiline_comment_opened = False
return file_line[multi_line_comment_end_pos + 2:]
if single_line_comment_start_pos == 0:
return ''
elif single_line_comment_start_pos > 0:
return file_line[0:single_line_comment_start_pos]
return file_line
def analyze_java_classes(self):
"""
[EN] Method starts Java classes analysis on the basis of project resources related to .java files
[RU] Метод начинает анализ Java-классов на основании проектных ресурсов, относящихся к файлам с расширением
.java
:return: None
"""
self.__is_multiline_comment_opened = False
java_class_pattern = re.compile('class\s+(?P<class_name>\w+)\s+')
java_import_pattern = re.compile('import\s+(?P<imported_package>[\w.]+);')
java_package_pattern = re.compile('package\s+(?P<package>[\w.]+);')
for java_resource in self.__java_resources:
full_path = java_resource.get_path() + os.sep + java_resource.get_name()
java_file = open(full_path, 'r')
java_file_lines = java_file.readlines()
java_resource_meta_info = JavaResourceFileMetaInfo()
for java_file_line in java_file_lines:
java_file_line = self.__strip_comments(java_file_line)
if len(java_file_line) == 0 or self.__is_multiline_comment_opened:
continue
pkg_results = java_package_pattern.findall(java_file_line)
if len(pkg_results) > 0:
java_resource_meta_info.set_package(pkg_results[0])
cls_results = java_class_pattern.findall(java_file_line)
if len(cls_results) > 0:
java_resource_meta_info.add_class(cls_results[0])
imp_results = java_import_pattern.findall(java_file_line)
if len(imp_results) > 0:
java_resource_meta_info.add_import(imp_results[0])
self.__java_classes_dict[java_resource] = java_resource_meta_info
java_file.close()
Сохранив представленный листинг в файле с именем projectinfo.py мы получаем отдельный модуль, предоставляющий возможности для анализа структуры проектов (пока что какую-то пользу он принесёт в части анализа Java-проектов). Чтобы воспользоваться функциональностью модуля, просто подключаем его к нашему скрипту (например, у вас это может быть основной скрипт/модуль main.py) посредством from projectinfo import *:
from projectinfo import *
if __name__ == '__main__':
pra = ProjectResourceAnalyzer()
pra.analyze('C:\\JavaProjects\\MyAmazingProject')
# Вывести на экран информацию обо всех файлах, входящих в проект:
print('ВСЕ ФАЙЛЫ ПРОЕКТА:')
for rs in pra.get_resource_files():
print(f'Имя файла: {rs.get_name()}, Путь к файлу: {rs.get_path()}, Расширение файла: {rs.get_extension()}')
print('ТОЛЬКО JAVA-ФАЙЛЫ ПРОЕКТА:')
for rs in pra.get_resource_files('.java'):
print(f'Имя файла: {rs.get_name()}, Путь к файлу: {rs.get_path()}, Расширение файла: {rs.get_extension()}')
print('ТОЛЬКО XML-ФАЙЛЫ ПРОЕКТА:')
for rs in pra.get_resource_files('.xml'):
print(f'Имя файла: {rs.get_name()}, Путь к файлу: {rs.get_path()}, Расширение файла: {rs.get_extension()}')
jmia = JavaMetaInfoAnalyzer(pra)
for jr in jmia.get_java_resources():
print(f'Файл: {jr.get_name()}, Путь к файлу: {jr.get_path()}')
jmia.analyze_java_classes()
print(f'Количество найденных Java-классов: {jmia.get_java_classes_count()}')
print(f'Информация о классах Java:')
for java_resource, java_resource_file_meta_info in jmia.get_java_classes().items():
print(f'============================')
print(f'Имя файла: {java_resource.get_name()}, Путь к файлу: {java_resource.get_path()}')
print(f'============================')
print(f'---> Пакет: {java_resource_file_meta_info.get_package()}')
print(f'---> Классы: {java_resource_file_meta_info.classes()}')
print(f'---> Импорты: {java_resource_file_meta_info.imports()}')
В моём случае по пути C:\JavaProjects\MyAmazingProject я создал простенький Java-проект, добавил в него пару-тройку классов и проверил работу нашего Python-модуля на нём. Ниже предоставляю листинг того, что вывел в моём случае основной скрипт main.py:
ВСЕ ФАЙЛЫ ПРОЕКТА:
Имя файла: MyAmazingProject.iml, Путь к файлу: C:\JavaProjects\MyAmazingProject, Расширение файла: .iml
Имя файла: pom.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject, Расширение файла: .xml
Имя файла: .gitignore, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла:
Имя файла: compiler.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: encodings.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: jarRepositories.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: misc.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: workspace.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: App.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\main\java\org\example, Расширение файла: .java
Имя файла: SomeClass.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\main\java\org\example, Расширение файла: .java
Имя файла: AppTest.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\test\java\org\example, Расширение файла: .java
ТОЛЬКО JAVA-ФАЙЛЫ ПРОЕКТА:
Имя файла: App.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\main\java\org\example, Расширение файла: .java
Имя файла: SomeClass.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\main\java\org\example, Расширение файла: .java
Имя файла: AppTest.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\test\java\org\example, Расширение файла: .java
ТОЛЬКО XML-ФАЙЛЫ ПРОЕКТА:
Имя файла: pom.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject, Расширение файла: .xml
Имя файла: compiler.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: encodings.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: jarRepositories.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: misc.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: workspace.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Файл: App.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\main\java\org\example
Файл: SomeClass.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\main\java\org\example
Файл: AppTest.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\test\java\org\example
Количество найденных Java-классов: 3
Информация о классах Java:
============================
Имя файла: App.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\main\java\org\example
============================
---> Пакет: org.example
---> Классы: ['App']
---> Импорты: ['java.lang.BigDecimal']
============================
Имя файла: SomeClass.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\main\java\org\example
============================
---> Пакет: org.example
---> Классы: ['SomeClass', 'SomeInnerClass', 'SomeOtherClassIncluded2']
---> Импорты: ['java.lang.String', 'java.lang.BigInteger', 'java.lang.BigDecimal']
============================
Имя файла: AppTest.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\test\java\org\example
============================
---> Пакет: org.example
---> Классы: ['AppTest']
---> Импорты: ['org.junit.Test']
Process finished with exit code 0
Думаю, что по выводу в консоли, а также приведённым листингам должно быть понятно, как работает наш новый модуль: он обходит рекурсивно заданный ему каталог файловой системы, где располагается наш проект, а также агрегирует информацию о входящих в проект ресурсах (т.е. файлах с различными расширениями). В модуле есть два удобных класса для анализа информации о Java-файлах:
- JavaResourceFileMetaInfo
- JavaMetaInfoAnalyzer
Все классы и методы я постарался снабдить документационными комментариями, так что вы без труда разберётесь в среде разработки PyCharm с их назначением при помощи всплывающих подсказок над классами/методами.
Дополню, что модуль является скорее тестовым, чем боевым, основная его цель показать возможный способ анализа структуры проекта и подход к агрегации метаинформации о файлах проекта.
Задавайте вопросы в комментариях, если что-то непонятно, а также делитесь мнением о том, насколько может пригодиться подобный модуль в решении ваших задач. Буду благодарен за обратную связь.
Ну а пока на этом всё, пробуйте модуль на своих проектах, делитесь мыслями, чего в нём не хватает и хотелось бы увидеть. Успехов!