Анализируем Java-проект при помощи Python

Доброго времени суток, друзья. В сегодняшней статье мы попробуем решить следующую задачу при помощи языка 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 с их назначением при помощи всплывающих подсказок над  классами/методами.

Дополню, что модуль является скорее тестовым, чем боевым, основная его цель показать возможный способ анализа структуры проекта и подход к агрегации метаинформации о файлах проекта.

Задавайте вопросы в комментариях, если что-то непонятно, а также делитесь мнением о том, насколько может пригодиться подобный модуль в решении ваших задач. Буду благодарен за обратную связь.

Ну а пока на этом всё, пробуйте модуль на своих проектах, делитесь мыслями, чего в нём не хватает и хотелось бы увидеть. Успехов!

Добавить комментарий

Работа со словарями в Python

В предыдущих статьях мы смотрели с вами, как работать со списками и кортежами в Python. В Python есть ещё одна примечательная встроенная структура данных, которая часто используется во многих скриптах - это словари (англ. dictionaries). Во многих других языках программирования словари ещё принято называть ассоциативными массивами. Словари в Python не являются последовательностями, в отличие от списков, строк и кортежей. Они индексируются при помощи ключей. В качестве ключа может выступать любой неизменяемый тип, например, строки или числа. Кортежи могут использоваться как ключи, но только если они содержат исключительно строки, числа или другие кортежи. Если кортеж содержит любой изменяемый объект, то он прямо или косвенно не может быть использован в качестве ключа для словаря. Также в качестве ключей нельзя использовать и списки, поскольку они могут быть изменены - к примеру, при замене элемента по индексу или операциями нарезки, либо методами append() и extend(). Лучше всего представить себе словарь как набор пар ключ: значение с тем требованием, что ключи должны быть всегда уникальны в пределах одного словаря.

Добавить комментарий

Как открыть страницу в браузере при помощи Python?

Добрый день, друзья. В этой статье мы рассмотрим пример того, как открыть заданную страницу (URL) в браузере при помощи языка Python. Для работы с браузерами в Python есть отдельный модуль webbrowser, который мы и будем использовать для этой цели. 

Первое, что нужно сделать - подключить модуль webbrowser в вашем скрипте при помощи оператора import:

import webbrowser

Также для нашего примера потребуется функция из модуля sys для получения информации о последнем исключении, которое может произойти в программе, поэтому также подключим его к нашему скрипту:

import sys

Дальше мы рассмотрим небольшой пример того, как можно открыть нужную вам страницу в нужном браузере, используя этот модуль. В примере мы откроем одну страницу в браузере Microsoft Edge, а вторую откроем в браузере, установленном в вашей системе по умолчанию. Перейдем сразу к коду нашего скрипта:

Добавить комментарий

Как работать с JSON в Python

В этой статье мы посмотрим, как можно работать с JSON в Python, а именно сериализовать объекты в строковое представление с JSON и десериализовать обратно в объекты Python из строки, содержащей JSON. Для работы с JSON в Python существует специальный модуль json, поэтому первое, что необходимо сделать - это подключить данный модуль к вашему скрипту/модулю:

import json

Далее нашему вниманию предстают две функции этого модуля:

  • dumps - преобразует Python-объекты (списки, словари, кортежи) в строку, содержащую JSON-представление этих объектов
  • loads - выполняет действие, обратное функции dumps: эта функция десериализует JSON из строкового представления обратно в объекты Python, к которым можно обращаться и получать данные, в соответствии с их типом

Посмотрим на следующий небольшой пример, в котором мы преобразуем в строки, содержащие JSON, различные объекты: списки, словари и кортежи:

Добавить комментарий

Модуль logging в Python. Логируем сообщения скрипта в файл

В этой статье мы познакомимся с возможностями, которые язык Python предоставляет разработчику в части логирования. Для этих целей в Python есть отдельный модуль logging, в котором есть множество полезных функций, позволяющих логировать сообщения вашего Python-скрипта на экран консоли или в отдельный файл.

Для начала работы нам потребуется подключить с помощью оператора import модуль логирования. Также в рамках нашего тестового примера мы будем использовать модуль datetime, который поставляет полезные функции для работы с датой и временем. В начале вашего скрипта/модуля (у меня это основной тестовый скрипт с именем main.py) поместите следующие строки:

import logging
import datetime
Добавить комментарий
Яндекс.Метрика