SpringBoot项目路由信息自动化提取脚本

news2024/11/13 10:12:53

文章目录

  • 前言
  • 工具开发
    • 1.1 ChatGPT初探
    • 1.2 初版代码效果
  • WebGoat适配
    • 2.1 识别常量路由
    • 2.2 适配跨行定义
  • 进阶功能优化
    • 3.1 识别请求类型
    • 3.2 识别上下文值
  • 总结

前言

最近工作上遇到一个需求:提取 SpringBoot 项目中的所有路由信息,本来想着这是一个再普通不过的任务了,本着白嫖党 “拿来主义” 的一贯作风,马上到 Github 上搜索相关工具,结果发现居然没有能够有效满足我需求的开源项目……那就自己动手丰衣足食吧!

工具开发

本文的目标是通过自动化脚本一键识别、提取 Java SpringBoot 项目的所有路由信息,方便识别、梳理代码审计的工作量,并统计审计进度和覆盖率。

在 Java Web 代码审计中,寻找和识别路由是很关键的部分,路由信息直接反映了一个系统对外暴露的攻击入口。而 Controller 作为 MVC 架构中的一个组件,可以说是每个用户交互的入口点,我们可以通过 Controller 定位系统注册的路由。

一般在代码审计时都会逐个分析每个 Controller 对应的对外 API 实现,通过梳理对应的路由接口并检查对应的业务实现,能帮助我们快速的检索代码中存在的漏洞缺陷,发现潜在的业务风险。

SpringMVC 框架中注册路由的常见注解如下:

@Controller
@RestController
@RequestMapping
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping

1.1 ChatGPT初探

一开始还是想偷懒,看看 ChatGPT 能不能帮我完成这项任务,结果 ChatGPT 提供了很简洁的代码,但是存在缺陷:无法识别 @Controller 类级别的父级路由并并自动拼接出完整路由,同时会导致提取的部分函数信息错乱。

import os
import re
import pandas as pd

# 正则表达式来匹配Spring的路由注解、方法返回类型、方法名称和参数
mapping_pattern = re.compile(r'@(?:Path|RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\((.*?)\)')
method_pattern = re.compile(r'(public|private|protected)\s+(\w[\w\.\<\>]*)\s+(\w+)\((.*?)\)\s*{')
value_pattern = re.compile(r'value\s*=\s*"(.*?)"')  # 只提取value字段中的路径值,可能包含的格式 value = "/xmlReader/sec", method = RequestMethod.POST


def extract_routes_from_file_01(file_path):
    """
    当前缺陷:无法识别@Controller类级别的父级路由并并自动拼接出完整路由,同时会导致提取的部分函数信息错乱,比如XXE(函数乱序)、xlsxStreamerXXE类(路由错误)
    为数不多的开源参考项目也存在同样的问题:https://github.com/charlpcronje/Java-Class-Component-Endpoint-Extractor
    """
    routes = []
    with open(file_path, 'r', encoding='utf-8') as file:
        content = file.read()
        # 找到所有路由注解
        mappings = mapping_pattern.findall(content)
        methods = method_pattern.findall(content)
        # 配对路由和方法
        for mapping, method in zip(mappings, methods):
            # 使用正则表达式提取出value字段的值
            value_match = value_pattern.search(mapping)
            route = value_match.group(1).strip() if value_match else mapping.strip()
            route = route.strip('"')  # 去除路径中的引号
            route_info = {
                'route': route,
                'return_type': method[1].strip(),
                'method_name': method[2].strip(),
                'parameters': method[3].strip(),
                'file_path': file_path,
            }
            routes.append(route_info)
    return routes


def scan_project_directory(directory):
    all_routes = []
    for root, _, files in os.walk(directory):
        for file in files:
            if file.endswith('.java'):
                file_path = os.path.join(root, file)
                routes = extract_routes_from_file_01(file_path)
                if routes:
                    all_routes.extend(routes)
    return all_routes


def write_routes_to_xlsx(all_data_list):
    data = {
        "Route": [item['route'] for item in all_data_list],
        "Return Type": [item['return_type'] for item in all_data_list],
        "Method Name": [item['method_name'] for item in all_data_list],
        "Parameters": [item['parameters'] for item in all_data_list],
        "File Path": [item['file_path'] for item in all_data_list],
    }
    writer = pd.ExcelWriter('Data.xlsx')
    dataFrame = pd.DataFrame(data)
    dataFrame.to_excel(writer, sheet_name="password")
    writer.close()
    print(f"[*] Successfully saved data to xlsx")


if __name__ == '__main__':
    # project_directory = input("Enter the path to your Spring Boot project: ")
    project_directory = r'D:\Code\Java\Github\java-sec-code-master'
    routes_info = scan_project_directory(project_directory)
    write_routes_to_xlsx(routes_info)

1.2 初版代码效果

偷懒是没戏了,那就自己动手吧。。

实验代码:https://github.com/JoyChou93/java-sec-code。

脚本实现:

import os
import re
import pandas as pd
from colorama import Fore, init

# 配置colorama颜色自动重置,否则得手动设置Style.RESET_ALL
init(autoreset=True)

# 统计路由数量的全局变量
route_num = 1
# 正则表达式来匹配Spring的路由注解、方法返回类型、方法名称和参数
mapping_pattern = re.compile(r'@(Path|(Request|Get|Post|Put|Delete|Patch)Mapping)\(')


def write_routes_to_xlsx(all_data_list):
    """
    将路由信息写入Excel文件
    """
    data = {
        "Parent Route": [item['parent_route'] for item in all_data_list],
        "Route": [item['route'] for item in all_data_list],
        "Return Type": [item['return_type'] for item in all_data_list],
        "Method Name": [item['method_name'] for item in all_data_list],
        "Parameters": [item['parameters'] for item in all_data_list],
        "File Path": [item['file_path'] for item in all_data_list],
    }
    writer = pd.ExcelWriter('Data.xlsx')
    dataFrame = pd.DataFrame(data)
    dataFrame.to_excel(writer, sheet_name="password")
    writer.close()
    print(Fore.BLUE + "[*] Successfully saved data to xlsx")


def extract_request_mapping_value(s):
    """
    提取类开头的父级路由,通过@RequestMapping注解中的value字段的值,可能出现括号中携带除了value之外的字段,比如 method = RequestMethod.POST
    """
    pattern = r'@RequestMapping\((.*?)\)|@RequestMapping\(value\s*=\s*"(.*?)"'
    match = re.search(pattern, s)
    if match:
        if match.group(1):
            return match.group(1).strip('"')
        else:
            return match.group(2)
    else:
        return None


def get_class_parent_route(content):
    """
    提取类级别的父级路由
    注意有可能会返回None,比如java-sec-code-master里的CommandInject.java
    """
    parent_route = None
    content_lines = content.split('\n')
    public_class_line = None
    # 遍历每一行,找到 "public class" 所在的行
    for line_number, line in enumerate(content_lines, start=1):
        if re.search(r'public class', line):
            public_class_line = line_number
            break
    if public_class_line is not None:
        # 提取 "public class" 之前的行
        content_before_public_class = content_lines[:public_class_line]
        for line in content_before_public_class:
            if re.search(r'@RequestMapping\(', line):
                parent_route = extract_request_mapping_value(line)
    return parent_route, public_class_line


def extract_value_between_quotes(line):
    """
    提取字符串中第一个""中间的值,目的是提取@GetMapping("/upload")格式中的路由值(尚待解决的是部分项目的路由值是通过一个常量类集中定义的)
    """
    pattern = r'"(.*?)"'
    match = re.search(pattern, line)
    if match:
        value = match.group(1)
        return value
    else:
        return None


def extract_function_details(function_def):
    """
    从函数定义的行级代码,解析并返回一个函数的详细信息,包括返回类型、函数名、参数等
    """
    pattern = re.compile(
        r'public\s+(?:static\s+)?(\w+)\s+(\w+)\s*\((.*)\)'
    )
    # 匹配函数签名
    match = pattern.search(function_def)
    if match:
        return_type = match.group(1)  # 返回类型
        function_name = match.group(2)  # 函数名
        parameters = match.group(3)  # 参数
        return return_type, function_name, parameters
    else:
        return None, None, None


def extract_routes_from_file(file_path):
    routes = []
    with open(file_path, 'r', encoding='utf-8') as file:
        content = file.read()
        # 找到Controller注解对应的Controller类
        if re.search('@(?!(ControllerAdvice))(|Rest)Controller', content):
            parent_route, public_class_line = get_class_parent_route(content)
            content_lines = content.split('\n')
            # 提取类名定义所在行后的所有代码
            content_after_public_class = content_lines[public_class_line:]
            global route_num
            for i, line in enumerate(content_after_public_class):
                if re.search(mapping_pattern, line):
                    # 获取完整的一条路由信息
                    route = extract_value_between_quotes(line)
                    if parent_route is not None and route is not None:
                        route = parent_route + route
                    # 向下遍历找到第一行不以 @ 开头的代码,因为一个函数的定义可能包含多个注解,比如 @GetMapping("/upload") @ResponseBody
                    j = i + 1
                    while j < len(content_after_public_class) and content_after_public_class[j].strip().startswith('@'):
                        j += 1
                    method_line = content_after_public_class[j].strip()
                    # print(route)
                    # print(method_line)
                    return_type, function_name, parameters = extract_function_details(method_line)
                    # print(parameters)
                    route_info = {
                        'parent_route': parent_route,
                        'route': route,
                        'return_type': return_type,
                        'method_name': function_name,
                        'parameters': parameters,
                        'file_path': file_path,
                    }
                    routes.append(route_info)
                    print(Fore.GREEN + '[%s]' % str(route_num) + str(route_info))
                    route_num += 1
    return routes


def scan_project_directory(directory):
    all_routes = []
    for root, _, files in os.walk(directory):
        for file in files:
            if file.endswith('.java'):
                file_path = os.path.join(root, file)
                routes = extract_routes_from_file(file_path)
                if routes:
                    all_routes.extend(routes)
    return all_routes


if __name__ == '__main__':
    # project_directory = input("Enter the path to your Spring Boot project: ")
    project_directory1 = r'D:\Code\Java\Github\java-sec-code-master'
    project_directory2 = r'D:\Code\Java\Github\java-sec-code-master\src\main\java\org\joychou\controller\othervulns'
    routes_info = scan_project_directory(project_directory1)
    write_routes_to_xlsx(routes_info)

生成的 xlsx 统计表格效果:
在这里插入图片描述
在这里插入图片描述
以最后的java-sec-code-master\src\main\java\org\joychou\controller\othervulns\xlsxStreamerXXE.java为例对比下代码:
在这里插入图片描述
解析均无误,此项目测试完毕。同时已验证另外的开源项目测试也没有问题:https://github.com/yangzongzhuan/RuoYi。

WebGoat适配

开源项目:https://github.com/WebGoat/WebGoat,扫描此项目面临需要解决的问题有两个。

2.1 识别常量路由

路由信息由静态常量定义,而非直接通过字符串提供。
在这里插入图片描述
直接通过上述脚本扫描将出错:
在这里插入图片描述
核心是修改上述脚本的 extract_value_between_quotes 函数提取路由入口函数的路由值所对应的代码逻辑。

2.2 适配跨行定义

提取路由注解定义的代码,如果出现换行符,则会导致此注解的参数解析出现残缺,比如:
在这里插入图片描述
同时获取路由的入口函数的定义,暂未考虑函数定义逻辑通过多行完成,可能导致提取的函数参数缺失,同时如果注解是多行的情况下,代码是有Bug的,不能直接提取第一行非@开头的代码。

直接通过上述脚本扫描则将提取到的字段全为空。
在这里插入图片描述
核心是修改上述脚本的提取路由注解、入口函数定义所对应的代码逻辑。

需求新增的代码

……

def find_constant_value(folder_path, constant_name):
    """
    提取出路由的常量值
    """
    for root, dirs, files in os.walk(folder_path):
        for file in files:
            if file.endswith('.java'):
                file_path = os.path.join(root, file)
                with open(file_path, 'r', encoding='utf-8') as f:
                    content = f.read()
                    pattern = fr'static\s+final\s+String\s+{constant_name}\s*=\s*"(.*?)";'
                    match = re.search(pattern, content)
                    if match:
                        return match.group(1)
    return None


def get_path_value(line, directory):
    """
    提取出路由的值,适配通过字符串直接提供的路由值,或者通过常量提供的路由值,比如:
    @GetMapping(path = "/server-directory")、@GetMapping(path = URL_HINTS_MVC, produces = "application/json")、@GetMapping(value = "/server-directory")
    """
    pattern = r'\((?:path|value)\s*=\s*(?:"([^"]*)"|([A-Z_]+))'
    matches = re.findall(pattern, line)
    route = ''
    for match in matches:
        if match[0]:  # 提取出path为字符串的值
            route = match[0]
            # print(Fore.GREEN + route)
        elif match[1]:  # 提取出path为常量的值
            route = find_constant_value(directory, match[1])
            # print(Fore.BLUE + route)
    return route
    

def extract_routes_from_file(file_path, directory):
    routes = []
    with open(file_path, 'r', encoding='utf-8') as file:
        content = file.read()
        # 找到Controller注解对应的Controller类
        if re.search('@(?!(ControllerAdvice))(|Rest)Controller', content):
            parent_route, public_class_line = get_class_parent_route(content)
            content_lines = content.split('\n')
            # 提取类名定义所在行后的所有代码
            content_after_public_class = content_lines[public_class_line:]
            global route_num
            for i, line in enumerate(content_after_public_class):
                try:
                    if re.search(mapping_pattern, line):
                        route_define = line.strip()
                        # 如果路由映射的定义逻辑在一行代码中完全覆盖
                        if route_define.endswith(')'):
                            route_define = route_define
                        # 如果路由映射的定义逻辑在多行代码中才覆盖
                        else:
                            q = i + 1
                            while q < len(content_after_public_class) and not content_after_public_class[q].strip().endswith(')'):
                                route_define += '' + content_after_public_class[q].strip()
                                q += 1
                            route_define += '' + content_after_public_class[q].strip()
                        # print(Fore.RED + route_define)
                        # 判断下路由信息是通过字符串字节提供的,还是通过常量提供的,然后统一提取出字符串值
                        if re.search(r'\("', route_define):
                            route = extract_value_between_quotes(route_define)
                        else:
                            route = get_path_value(route_define, directory)
                        # 获取完整的一条路由信息
                        if parent_route is not None and route is not None:
                            route = parent_route + route
                        # 向下遍历找到函数的定义,此处考虑了路由注解下方可能还携带多个其它用途的注解
                        j = i + 1
                        while j < len(content_after_public_class) and not content_after_public_class[j].strip().startswith('public'):
                            j += 1
                        method_define = content_after_public_class[j].strip()
                        # 获取函数定义的行级代码,考虑函数定义可能跨越多行,需进行代码合并,获得完整的函数定义,否则可能导致函数参数提取残缺
                        q = j
                        while j < len(content_after_public_class) and not content_after_public_class[q].strip().endswith('{'):
                            q += 1
                            method_define = method_define + '' + content_after_public_class[q].strip()
                        # print(route)
                        # print(method_define)
                        return_type, function_name, parameters = extract_function_details(method_define)
                        route_info = {
                            'parent_route': parent_route,
                            'route': route,
                            'return_type': return_type,
                            'method_name': function_name,
                            'parameters': parameters,
                            'file_path': file_path,
                        }
                        routes.append(route_info)
                        print(Fore.GREEN + '[%s]' % str(route_num) + str(route_info))
                        route_num += 1
                except Exception as e:
                    print(Fore.RED + '[-]' + str(file) + ' ' + str(e))
                    continue
    return routes

扫描结果与验证:
在这里插入图片描述
在这里插入图片描述
同时已验证对于前面 java-sec-code 的项目扫描结果不影响。

进阶功能优化

3.1 识别请求类型

增加路由注解的类型识别逻辑,最终对表格增加一列,保存路由所对应的 HTTP 请求类型字段,比如 GET、POST。

为此增加了get_request_type(route_define)函数:

def get_request_type(route_define):
    """
    从路由定义的注解中,提取出API请求类型,比如GET、POST等
    """
    # print(route_define)
    if route_define.startswith('@RequestMapping'):
        # 提取@RequestMapping注解中的method字段的值
        if route_define.find('method =') > -1:
            request_type = (str(route_define.split('method =')[1]).split('}')[0].strip().replace('{', '').replace(')', '')).replace('RequestMethod.', '')
        # 未指定具体请求类型的RequestMapping注解,则默认为支持所有请求类型
        else:
            request_type = 'All'
    else:
        request_type = route_define.split('Mapping')[0][1:]
    return request_type

本扫描效果:
在这里插入图片描述

3.2 识别上下文值

在 Spring Boot 项目中,context 上下文配置主要用于设置应用程序的上下文路径、组件扫描路径、国际化配置、资源路径、环境变量等。这些配置通常可以在以下几个地方进行:

1、application.properties 或 application.yml 文件

这些是 Spring Boot 项目中最常用的配置文件,位于 src/main/resources 目录下,设置上下文路径:

# application.properties
server.servlet.context-path=/myapp

或者:

# application.yml
server:
  servlet:
    context-path: /myapp

2、使用环境变量或命令行参数

Spring Boot 支持通过环境变量或命令行参数覆盖配置文件中的配置,这样可以动态调整上下文配置。

此处暂时只考虑识别第一种情况,即配置文件中的上下文路径配置。

添加识别上下文的功能函数如下:

def extract_context_path(directory):
    """
    从application.properties或xxx.yml等Java项目配置文件中提取上下文路径
    """
    for dirPath, dirNames, fileNames in os.walk(directory):
        for filename in fileNames:
            if filename.endswith(".properties") or filename.endswith('.yml') or filename.endswith('.yaml'):
                file_path = os.path.join(dirPath, filename)
                with open(file_path, 'r', encoding='utf-8') as data:
                    data = data.readlines()
                    for line in data:
                        # 匹配 properties 文件
                        if line.startswith('server.servlet.context-path'):
                            context = line.split('=')[1].strip()
                            print(Fore.BLUE + "[*]Found context-path:" + context)
                            return context
                        # 匹配 yml 文件
                        elif line.find('context-path') > -1:
                            context = line.strip().split(':')[1].strip()
                            print(Fore.BLUE + "[*]Found context-path:" + context)
                            return context
                        else:
                            continue
    return None

最终扫描效果如下所示:
在这里插入图片描述
在这里插入图片描述

符合预期:
在这里插入图片描述
对若依项目的识别也是正确的:
在这里插入图片描述
在这里插入图片描述

总结

最后附上代码开源地址:https://github.com/Tr0e/RouteScanner。

本文实现了对 Java SpringBoot 项目一键自动化识别、统计路由信息,并生成可视化的统计表格,此类项目在 Github 上当前基本找不到开源参考代码仓,也算是为开源社区做点贡献了。当然了,初版代码因为当前的实验数据并不是很多,后期在其它 Java 源代码项目中也可能出现不适配的情况,后续有时间的话会持续优化、完善,欢迎提交 issues。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2086881.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【随笔】使用spring AI接入大语言模型

引言 随着人工智能的发展&#xff0c;越来越多的应用开始集成AI模型来增强用户体验。OpenAI提供的大语言模型是目前最受欢迎的自然语言处理模型之一&#xff0c;能够处理各种语言任务&#xff0c;如文本生成、对话理解等。在Java开发中&#xff0c;我们可以利用Spring AI框架轻…

android 离线的方式使用下载到本地的gradle

1、android studio在下载gradle的时候&#xff0c;特别慢&#xff0c;有的时候会下载不完的情况&#xff0c;这样我们就要离线使用了。 2、下载Gradle Gradle | Releases 或者 Releases gradle/gradle GitHub Gradle | Releases 这里我们下载8.10 complete版本&#xff0c…

Python GraphQL 库之graphene使用详解

概要 随着 Web 技术的发展,GraphQL 已成为 REST 的一种强有力替代方案,为客户端提供了更灵活的数据查询方式。Graphene 是一个用于构建 GraphQL API 的 Python 库,它使得开发者可以轻松地将复杂的数据模型暴露为 GraphQL API。通过 Graphene,开发者可以利用 Python 的面向…

【战略游戏】

题目 代码 #include <bits/stdc.h> using namespace std; const int N 1510, M N; int h[N], e[M], ne[M], idx; int f[N][2]; int n; bool st[N]; int root; void add(int a, int b) // 添加一条边a->b {e[idx] b, ne[idx] h[a], h[a] idx ; } void dfs(int …

Java设计模式之外观模式详细讲解和案例示范

1. 引言 在软件开发过程中&#xff0c;复杂的系统往往包含许多子系统和模块&#xff0c;随着系统功能的增加&#xff0c;模块之间的交互也变得更加复杂。这种复杂性可能会导致系统的可维护性和扩展性降低。外观模式&#xff08;Facade Pattern&#xff09;是一种结构型设计模式…

【鸿蒙学习】HarmonyOS应用开发者高级认证 - 认证通过(附题目)

学完时间&#xff1a;2024年8月29日 学完排名&#xff1a;第192546名 一、前言叨叨 经过几日的休整&#xff0c;我终于再次挑战高级认证&#xff0c;并以82分的成绩堪堪越过了及格线。然而&#xff0c;通过考试后我惊讶地发现&#xff0c;原来顺利过关的人数如此众多。我逐一…

cv2图像总结

我今天发现cv2读进来的图像是BRG格式的&#xff0c;和其他的方式不同 import cv2 import matplotlib.pyplot as plt image_path "./GSE240429_data/image/GEX_C73_A1_Merged.tiff" img1 cv2.imread(image_path) print(img1.shape) plt.imshow(img1, cmapgray) …

MariaDB VS MySQL

MariaDB和MySQL是两种流行的开源关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;它们在功能、性能、兼容性、开源性以及社区支持等方面各有特点。以下是对两者主要区别的详细分析&#xff1a; 1. 开发者与起源 MySQL&#xff1a;自1995年问世以来&#xff0c…

白银现货的两大指标,如何使用?

在白银现货交易的过程中&#xff0c;我们会借助大量的技术指标&#xff0c;对现货白银走势进行分析&#xff0c;找到买点和卖点&#xff0c;可以说&#xff0c;技术指标对我们的白银现货交易起到很好的辅助作用&#xff0c;也是我们阅读白银市场很好的工具。本文将和大家讨论一…

一个非常实用的Win系统瘦身项目,PowerShell脚本支持Windows 11跟10,非常轻量好用(附源码)

Win经常我们都经常用&#xff0c;但系统里总是预装了一些我们可能并不需要的应用程序。这些应用不仅占用了宝贵的存储空间&#xff0c;还可能拖慢了我们的电脑速度。特别是Windows 11&#xff0c;一些花里胡哨的功能和后台服务&#xff0c;让我们的电脑变得不那么“清爽”。 今…

N10 - NLP中的注意力机制

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 目录 1. 注意力机制是什么2. 注意力实现步骤0. 准备隐藏状态1. 获取每个编码器隐藏状态的分数2. 通过softmax层运行所有分数3. 通过softmax得分将每个编码器的…

elasticsearch之我不会的

elasticsearch之我不会的 如何安装&#xff0c;在此不谈&#xff0c;开门见山 1.概念理解 Relational DBelasticsearch说明表tableindex索引(index)&#xff0c;就是文档的集合&#xff0c;类似数据库的表(table)行rows文档documents文档&#xff08;Document&#xff09;&a…

51.x86游戏实战-XXX返回城镇的实现

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 工具下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1rEEJnt85npn7N38Ai0_F2Q?pwd6tw3 提…

使用智谱AI大模型翻译视频字幕

不久前&#xff0c;国内的头部大模型厂商智谱 AI &#xff0c;刚刚推出了 glm-4-0520 模型&#xff0c;该模型被认为是当前平台最先进的模型&#xff0c;具备 128k 的上下文长度&#xff0c;并且相较于前一代模型&#xff0c;指令遵从能力大幅提升 18.6%。可以看出&#xff0c;…

一键开启,精彩即现!极简设计录屏软件大盘点

如果你想要用一款小巧的录屏工具&#xff0c;第一时间是不是就想到了ocam录屏&#xff0c;现在这类的简便录屏工具越来越多了&#xff0c;如果你想要换一个不妨接着往下看吧。 1.福昕录屏大师 链接&#xff1a;www.foxitsoftware.cn/REC/ 这个软件的界面看起来就很好操作&am…

《HelloGitHub》第 101 期

兴趣是最好的老师&#xff0c;HelloGitHub 让你对编程感兴趣&#xff01; 简介 HelloGitHub 分享 GitHub 上有趣、入门级的开源项目。 github.com/521xueweihan/HelloGitHub 这里有实战项目、入门教程、黑科技、开源书籍、大厂开源项目等&#xff0c;涵盖多种编程语言 Python、…

测试 UDP 端口可达性的方法

前言&#xff1a; UDP (User Datagram Protocol) 是一种无连接的传输层协议&#xff0c;它不像 TCP 那样提供确认机制来保证数据包的可靠传输。因此&#xff0c;测试 UDP 端口的可达性通常需要一些特殊的方法&#xff0c;因为传统的端口扫描工具&#xff08;如 nmap&#xff0…

【开源 Mac 工具推荐之 5】tldr:简洁明了的命令行手册显示工具

简介 在大家日常在 macOS/Linux 上使用 Shell 的时候&#xff0c;常常会遇到一些不太熟悉的命令行指令&#xff0c;为此我们一般会查看一下该命令的使用手册&#xff08;指南&#xff09;。往往&#xff0c;大家都会使用 man <command> 这样一个非常传统的指令。但 man …

YOLOv8改进 | 融合改进 | C2f融合Faster-GELU模块提升检测速度【完整代码 + 主要代码解析】

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 专栏目录 &#xff1a;《YOLOv8改进有效…