在这里插入图片描述

👋 大家好,欢迎来到我的技术博客!
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕Linux这个话题展开,希望能为你带来一些启发或实用的参考。
🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!


🐚 Shell 脚本解析日志文件提取关键信息 —— 从基础到进阶实战(含 Java 代码示例)

在现代软件工程和运维体系中,日志文件是系统运行状态、用户行为轨迹、错误诊断依据的重要载体。面对动辄 GB 级别的日志数据,如何高效提取关键信息成为每个开发者和运维工程师的必备技能。本文将深入探讨使用 Shell 脚本解析日志文件的方法,并结合 Java 实现对比分析,帮助你构建灵活、可扩展的日志处理流水线。


📜 为什么需要解析日志?

在分布式系统、微服务架构或高并发 Web 应用中,日志记录着每一次请求、每一条异常、每一个性能瓶颈。原始日志通常格式杂乱、内容冗长,直接阅读效率极低。我们需要:

  • ✅ 快速定位错误发生时间与上下文
  • ✅ 统计高频访问接口或慢查询
  • ✅ 提取用户行为路径用于产品分析
  • ✅ 监控系统健康状况并触发告警
  • ✅ 合规审计与安全事件追踪

Shell 脚本因其轻量、快速、与 Linux 系统深度集成,成为日志处理的首选工具之一。而 Java 则在复杂逻辑、结构化输出、企业级集成方面更具优势。二者结合,可构建强大的日志分析生态。


🛠️ Shell 基础命令组合实战

🔍 grep:关键词搜索利器

grep 是最常用的文本搜索命令,支持正则表达式,能快速过滤包含特定模式的行。

# 搜索所有包含 "ERROR" 的日志行
grep "ERROR" /var/log/app.log

# 忽略大小写搜索 "warning"
grep -i "warning" /var/log/app.log

# 显示匹配行及其前后各2行上下文
grep -C 2 "NullPointerException" app.log

# 统计错误数量
grep "ERROR" app.log | wc -l

📊 awk:字段提取与统计大师

awk 是强大的文本处理语言,特别适合按列处理结构化日志(如空格、制表符分隔)。

假设日志格式如下:

2024-06-15T10:23:45Z INFO User login success, userId=12345, ip=192.168.1.10
2024-06-15T10:25:12Z ERROR Payment failed, orderId=98765, reason=InsufficientBalance

我们可以提取时间、级别、用户ID:

awk '{
    if ($3 == "ERROR") {
        split($5, arr, "=");
        print "时间:", $1, "错误订单:", arr[2];
    }
}' app.log

更复杂的统计:按小时统计请求数

awk '{
    gsub(/T/, " ", $1);           # 替换 T 为空格
    split($1, dt, " ");           # 分割日期时间
    split(dt[2], tm, ":");        # 分割时:分:秒
    hour = tm[1];                 # 获取小时
    count[hour]++;                # 计数
}
END {
    for (h in count) {
        print h ":00 - " h ":59 => " count[h] " 次请求";
    }
}' access.log

🔄 sed:流编辑器,批量替换与清洗

sed 可对日志进行预处理,比如脱敏、标准化格式、删除无用字段。

# 将所有手机号替换为 [PHONE]
sed 's/[0-9]\{11\}/[PHONE]/g' sensitive.log > cleaned.log

# 删除空白行
sed '/^$/d' messy.log > clean.log

# 在每行开头添加日志来源标记
sed 's/^/[APP-SERVER] /' raw.log > tagged.log

📁 find + xargs:批量处理多日志文件

生产环境往往有按天/小时分割的日志文件,需批量处理:

# 查找最近7天内所有 .log 文件并统计 ERROR 数量
find /var/log/app/ -name "*.log" -mtime -7 -print0 | \
xargs -0 grep "ERROR" | wc -l

# 对每个日志文件执行分析脚本
find /logs/ -name "access_*.log" -exec ./analyze.sh {} \;

🧩 构建完整 Shell 日志分析脚本

下面是一个完整的日志分析脚本 log_analyzer.sh,它会生成摘要报告:

#!/bin/bash

LOG_FILE=$1
REPORT_FILE="report_$(date +%Y%m%d_%H%M%S).txt"

if [ -z "$LOG_FILE" ]; then
    echo "Usage: $0 <logfile>"
    exit 1
fi

if [ ! -f "$LOG_FILE" ]; then
    echo "Error: Log file not found!"
    exit 1
fi

echo "📊 日志分析报告" > $REPORT_FILE
echo "========================" >> $REPORT_FILE
echo "📄 源文件: $LOG_FILE" >> $REPORT_FILE
echo "📅 生成时间: $(date)" >> $REPORT_FILE
echo "" >> $REPORT_FILE

# 总行数
TOTAL_LINES=$(wc -l < "$LOG_FILE")
echo "🔢 总日志行数: $TOTAL_LINES" >> $REPORT_FILE

# 错误数量
ERROR_COUNT=$(grep -c "ERROR" "$LOG_FILE")
echo "❌ 错误数量: $ERROR_COUNT" >> $REPORT_FILE

# 警告数量
WARN_COUNT=$(grep -c "WARN" "$LOG_FILE")
echo "⚠️  警告数量: $WARN_COUNT" >> $REPORT_FILE

# 按模块统计错误
echo "" >> $REPORT_FILE
echo "📂 按模块错误分布:" >> $REPORT_FILE
grep "ERROR" "$LOG_FILE" | awk '{
    for(i=1; i<=NF; i++) {
        if($i ~ /module=/) {
            split($i, arr, "=");
            module[arr[2]]++;
        }
    }
} END {
    for(m in module) {
        printf "   %s: %d\n", m, module[m]
    }
}' >> $REPORT_FILE

# 高频IP统计(假设日志含 ip=xxx 字段)
echo "" >> $REPORT_FILE
echo "🌐 高频访问IP Top 5:" >> $REPORT_FILE
grep -o "ip=[0-9.]*" "$LOG_FILE" | cut -d= -f2 | sort | uniq -c | sort -nr | head -5 >> $REPORT_FILE

# 最活跃时间段(按小时)
echo "" >> $REPORT_FILE
echo "⏰ 最活跃小时段 Top 3:" >> $REPORT_FILE
awk '{
    gsub(/T/, " ", $1);
    split($1, dt, " ");
    split(dt[2], tm, ":");
    hour = tm[1];
    count[hour]++;
} END {
    n = asorti(count, sorted, "@val_num_desc");
    for(i=1; i<=3 && i<=n; i++) {
        h = sorted[i];
        printf "   %02d:00-%02d:59 → %d 次\n", h, h, count[h];
    }
}' "$LOG_FILE" >> $REPORT_FILE

echo "" >> $REPORT_FILE
echo "✅ 报告生成完毕: $REPORT_FILE"
cat $REPORT_FILE

运行方式:

chmod +x log_analyzer.sh
./log_analyzer.sh /var/log/myapp/access.log

📈 数据可视化:用 mermaid 绘制趋势图

虽然 Shell 本身不支持绘图,但我们可以输出符合 mermaid 格式的数据,再粘贴到支持 mermaid 的编辑器(如 Typora、VSCode 插件、GitLab)中渲染。

以下脚本生成“每小时请求数”的柱状图代码:

#!/bin/bash

LOG_FILE=$1

if [ -z "$LOG_FILE" ]; then
    echo "Usage: $0 <logfile>"
    exit 1
fi

echo "```mermaid"
echo "barChart"
echo "    title 每小时请求数分布"
echo "    xAxis 小时"
echo "    yAxis 请求数"
echo "    series"

# 统计每小时请求数
awk '{
    gsub(/T/, " ", $1);
    split($1, dt, " ");
    split(dt[2], tm, ":");
    hour = tm[1];
    count[hour]++;
} END {
    for(h=0; h<=23; h++) {
        if(count[h] == "") count[h] = 0;
        printf "        \"%02d:00\" : %d\n", h, count[h];
    }
}' "$LOG_FILE"

echo "```"

运行后输出如下内容,可直接复制到 Markdown 编辑器:

渲染错误: Mermaid 渲染失败: No diagram type detected matching given configuration for text: barChart title 每小时请求数分布 xAxis 小时 yAxis 请求数 series "00:00" : 128 "01:00" : 97 "02:00" : 85 "03:00" : 76 "04:00" : 68 "05:00" : 150 "06:00" : 312 "07:00" : 890 "08:00" : 1567 "09:00" : 2345 "10:00" : 3120 "11:00" : 3890 "12:00" : 4200 "13:00" : 4100 "14:00" : 3980 "15:00" : 3760 "16:00" : 3540 "17:00" : 3320 "18:00" : 2980 "19:00" : 2650 "20:00" : 2300 "21:00" : 1980 "22:00" : 1650 "23:00" : 1320

该图表将在支持 mermaid 的环境中自动渲染为柱状图,直观展示流量高峰时段 👇

渲染错误: Mermaid 渲染失败: No diagram type detected matching given configuration for text: barChart title 每小时请求数分布 xAxis 小时 yAxis 请求数 series "00:00" : 128 "01:00" : 97 "02:00" : 85 "03:00" : 76 "04:00" : 68 "05:00" : 150 "06:00" : 312 "07:00" : 890 "08:00" : 1567 "09:00" : 2345 "10:00" : 3120 "11:00" : 3890 "12:00" : 4200 "13:00" : 4100 "14:00" : 3980 "15:00" : 3760 "16:00" : 3540 "17:00" : 3320 "18:00" : 2980 "19:00" : 2650 "20:00" : 2300 "21:00" : 1980 "22:00" : 1650 "23:00" : 1320

⚙️ Shell 脚本的局限性与 Java 的优势

虽然 Shell 脚本在简单文本处理上非常高效,但在以下场景中显得力不从心:

  • ❌ 复杂数据结构(嵌套对象、列表)
  • ❌ 多线程/异步处理海量日志
  • ❌ 与数据库、消息队列等外部系统交互
  • ❌ 输出 JSON/XML/Excel 等结构化格式
  • ❌ 单元测试与持续集成支持弱

此时,Java 凭借其丰富的类库、强类型系统、跨平台能力,成为更优选择。


☕ Java 实现日志解析器

下面是一个使用 Java 8+ Stream API 和正则表达式的日志解析器,功能对标上述 Shell 脚本,但更健壮、可测、可扩展。

Maven 依赖

<dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.12.0</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.2</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>2.0.7</version>
    </dependency>
</dependencies>

日志条目模型类

import java.time.LocalDateTime;
import java.util.Map;

public class LogEntry {
    private LocalDateTime timestamp;
    private String level;
    private String message;
    private Map<String, String> attributes;

    // 构造函数、getter、setter 省略...
    
    public LogEntry(LocalDateTime timestamp, String level, String message, Map<String, String> attributes) {
        this.timestamp = timestamp;
        this.level = level;
        this.message = message;
        this.attributes = attributes;
    }

    // Getters
    public LocalDateTime getTimestamp() { return timestamp; }
    public String getLevel() { return level; }
    public String getMessage() { return message; }
    public Map<String, String> getAttributes() { return attributes; }

    @Override
    public String toString() {
        return String.format("[%s] %s - %s %s", 
            timestamp, level, message, attributes);
    }
}

日志解析器核心类

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class LogAnalyzer {

    private static final DateTimeFormatter DATE_FORMATTER = 
        DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
    
    private static final Pattern LOG_PATTERN = Pattern.compile(
        "^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z)\\s+(\\w+)\\s+(.+)$"
    );

    public List<LogEntry> parseLogFile(String filePath) throws IOException {
        return Files.lines(Paths.get(filePath))
            .map(this::parseLogLine)
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
    }

    private LogEntry parseLogLine(String line) {
        Matcher matcher = LOG_PATTERN.matcher(line.trim());
        if (!matcher.matches()) {
            System.err.println("无法解析行: " + line);
            return null;
        }

        String timestampStr = matcher.group(1);
        String level = matcher.group(2);
        String message = matcher.group(3);

        LocalDateTime timestamp = LocalDateTime.parse(timestampStr, DATE_FORMATTER);
        
        // 解析 key=value 属性
        Map<String, String> attributes = extractAttributes(message);

        return new LogEntry(timestamp, level, message, attributes);
    }

    private Map<String, String> extractAttributes(String message) {
        Map<String, String> attrs = new HashMap<>();
        String[] parts = message.split(",\\s*");
        for (String part : parts) {
            if (part.contains("=")) {
                String[] kv = part.split("=", 2);
                if (kv.length == 2) {
                    attrs.put(kv[0].trim(), kv[1].trim());
                }
            }
        }
        return attrs;
    }

    public void generateReport(List<LogEntry> entries, String reportPath) throws IOException {
        try (PrintWriter writer = new PrintWriter(new FileWriter(reportPath))) {
            writer.println("📊 Java 日志分析报告");
            writer.println("========================");
            writer.println("📅 生成时间: " + LocalDateTime.now());
            writer.println("📄 条目总数: " + entries.size());
            writer.println();

            long errorCount = entries.stream()
                .filter(e -> "ERROR".equals(e.getLevel()))
                .count();
            writer.println("❌ 错误数量: " + errorCount);

            long warnCount = entries.stream()
                .filter(e -> "WARN".equals(e.getLevel()))
                .count();
            writer.println("⚠️  警告数量: " + warnCount);
            writer.println();

            // 按模块统计错误
            writer.println("📂 按模块错误分布:");
            entries.stream()
                .filter(e -> "ERROR".equals(e.getLevel()))
                .map(e -> e.getAttributes().get("module"))
                .filter(Objects::nonNull)
                .collect(Collectors.groupingBy(m -> m, Collectors.counting()))
                .entrySet().stream()
                .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
                .forEach(entry -> 
                    writer.printf("   %s: %d%n", entry.getKey(), entry.getValue())
                );
            writer.println();

            // 高频IP统计
            writer.println("🌐 高频访问IP Top 5:");
            entries.stream()
                .map(e -> e.getAttributes().get("ip"))
                .filter(Objects::nonNull)
                .collect(Collectors.groupingBy(ip -> ip, Collectors.counting()))
                .entrySet().stream()
                .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
                .limit(5)
                .forEach(entry ->
                    writer.printf("   %s → %d 次%n", entry.getKey(), entry.getValue())
                );
            writer.println();

            // 最活跃小时段
            writer.println("⏰ 最活跃小时段 Top 3:");
            entries.stream()
                .collect(Collectors.groupingBy(
                    e -> e.getTimestamp().getHour(),
                    Collectors.counting()
                ))
                .entrySet().stream()
                .sorted(Map.Entry.<Integer, Long>comparingByValue().reversed())
                .limit(3)
                .forEach(entry -> {
                    int hour = entry.getKey();
                    writer.printf("   %02d:00-%02d:59 → %d 次%n", hour, hour, entry.getValue());
                });
        }
    }

    // 新增:导出为 JSON
    public void exportToJson(List<LogEntry> entries, String jsonPath) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        List<Map<String, Object>> jsonList = entries.stream().map(entry -> {
            Map<String, Object> map = new HashMap<>();
            map.put("timestamp", entry.getTimestamp());
            map.put("level", entry.getLevel());
            map.put("message", entry.getMessage());
            map.put("attributes", entry.getAttributes());
            return map;
        }).collect(Collectors.toList());

        mapper.writeValue(new File(jsonPath), jsonList);
        System.out.println("✅ JSON 导出完成: " + jsonPath);
    }
}

主程序入口

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.databind.SerializationFeature;

import java.io.IOException;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        if (args.length == 0) {
            System.out.println("Usage: java Main <logfile>");
            return;
        }

        String logFile = args[0];
        String reportFile = "java_report_" + System.currentTimeMillis() + ".txt";
        String jsonFile = "exported_" + System.currentTimeMillis() + ".json";

        try {
            LogAnalyzer analyzer = new LogAnalyzer();
            
            System.out.println("🔍 正在解析日志文件: " + logFile);
            List<LogEntry> entries = analyzer.parseLogFile(logFile);
            
            System.out.println("📝 正在生成文本报告...");
            analyzer.generateReport(entries, reportFile);
            
            System.out.println("💾 正在导出 JSON 数据...");
            analyzer.exportToJson(entries, jsonFile);
            
            System.out.println("🎉 分析完成!");
            System.out.println("📄 报告文件: " + reportFile);
            System.out.println("📦 JSON 文件: " + jsonFile);

        } catch (IOException e) {
            System.err.println("❌ 处理失败: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

运行 Java 程序

编译并运行:

javac -cp ".:lib/*" Main.java LogAnalyzer.java LogEntry.java
java -cp ".:lib/*" Main /var/log/app/access.log

输出示例:

🔍 正在解析日志文件: /var/log/app/access.log
📝 正在生成文本报告...
💾 正在导出 JSON 数据...
🎉 分析完成!
📄 报告文件: java_report_1718456789123.txt
📦 JSON 文件: exported_1718456789123.json

🔄 Shell vs Java 对比总结

维度 Shell 脚本 Java 程序
执行速度 ⚡ 极快(原生命令) 🐢 较慢(JVM 启动开销)
学习曲线 📈 低(基础命令易学) 📉 高(需掌握语法、类库、构建工具)
复杂逻辑支持 ❌ 弱(无面向对象、异常处理弱) ✅ 强(完整 OOP、异常、泛型、Stream)
可维护性 🛠️ 中等(脚本膨胀后难维护) 🧩 高(模块化、单元测试、重构支持)
输出格式 📄 文本为主 📊 JSON/XML/Excel/数据库/图表
并发处理 🔄 有限(需外部工具如 GNU Parallel) 🚀 强大(线程池、CompletableFuture)
跨平台性 🐧 Linux 依赖 🌍 全平台(只要安装 JVM)
与企业系统集成 🔗 弱 💼 强(Spring Boot、Kafka、ES、DB)

💡 建议:简单任务用 Shell,复杂分析、长期维护、团队协作用 Java。二者也可结合 —— Shell 调用 Java 程序作为子模块。


🌐 实际应用场景与外部工具集成

1. 与 ELK Stack 集成

ELK Stack(Elasticsearch + Logstash + Kibana) 是业界标准的日志管理方案。Shell 脚本可作为 Logstash 的前置处理器:

# preprocess.sh
grep "ERROR" app.log | sed 's/^/PREPROCESSED: /' > preprocessed.log

然后配置 Logstash 读取 preprocessed.log

2. 与 Prometheus + Grafana 集成

通过 Java 程序暴露 metrics 端点,供 Prometheus 抓取:

// 使用 Micrometer 暴露指标
Counter errorCounter = Counter.builder("app.errors.total")
    .description("Total number of errors")
    .register(Metrics.globalRegistry);

// 在解析时递增
if ("ERROR".equals(entry.getLevel())) {
    errorCounter.increment();
}

再在 Grafana 中创建仪表盘,实现可视化监控。

3. 定时任务与告警

使用 cron 定时执行 Shell 脚本,并通过邮件或钉钉机器人发送告警:

# crontab -e
# 每天凌晨2点分析日志,如有超过100个错误则发邮件
0 2 * * * /opt/scripts/log_analyzer.sh /var/log/app.log | grep "错误数量: [1-9][0-9][0-9]" && echo "🚨 错误过多!" | mail -s "日志告警" admin@company.com

🧪 测试你的日志解析脚本

无论使用 Shell 还是 Java,都应编写测试用例确保准确性。

Shell 测试示例

创建 test.log

2024-06-15T08:30:00Z INFO User login, userId=1001, ip=192.168.1.1
2024-06-15T08:31:00Z ERROR Payment failed, orderId=2001, reason=Timeout, module=payment
2024-06-15T08:32:00Z WARN Cache miss, key=user:1001, module=cache
2024-06-15T08:33:00Z ERROR DB connection lost, module=database

运行脚本并验证输出是否包含:

  • 总行数:4
  • 错误数:2
  • 模块分布:payment=1, database=1
  • IP 统计:192.168.1.1 → 1 次

Java 单元测试示例

import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;

class LogAnalyzerTest {

    @Test
    void testParseLogLine() throws IOException {
        // 创建临时测试文件
        Path tempFile = Files.createTempFile("test", ".log");
        Files.write(tempFile, List.of(
            "2024-06-15T08:30:00Z INFO User login, userId=1001, ip=192.168.1.1",
            "2024-06-15T08:31:00Z ERROR Payment failed, orderId=2001, reason=Timeout, module=payment"
        ));

        LogAnalyzer analyzer = new LogAnalyzer();
        List<LogEntry> entries = analyzer.parseLogFile(tempFile.toString());

        assertEquals(2, entries.size());
        assertEquals("INFO", entries.get(0).getLevel());
        assertEquals("payment", entries.get(1).getAttributes().get("module"));

        Files.delete(tempFile);
    }
}

🧭 最佳实践与避坑指南

✅ Shell 脚本最佳实践

  1. 始终检查参数和文件是否存在
  2. 使用 set -euo pipefail 避免静默失败
  3. 避免硬编码路径,使用变量或配置文件
  4. 大型日志使用 zcatbzcat 处理压缩文件
  5. 敏感信息脱敏后再处理
#!/bin/bash
set -euo pipefail

LOG=${1:-"/var/log/app/default.log"}
[[ ! -f "$LOG" ]] && { echo "❌ 日志文件不存在"; exit 1; }

✅ Java 最佳实践

  1. 使用 try-with-resources 自动关闭流
  2. 合理分页或流式处理避免 OOM
  3. 使用 SLF4J 而非 System.out 进行日志输出
  4. 配置线程池控制并发度
  5. 支持配置文件(如 application.properties)

🚫 常见错误

  • Shell: 忘记转义特殊字符、管道中断未捕获、未处理多字节字符
  • Java: 正则表达式贪婪匹配、未处理编码问题、内存泄漏、未关闭文件句柄

🌟 结语:选择合适的工具,构建智能日志分析体系

日志分析不是一次性任务,而是持续优化的过程。Shell 脚本如同锋利的瑞士军刀,适合快速切割;Java 程序则是精密的数控机床,适合批量生产。根据你的需求、团队技能、系统规模,灵活选择或组合使用。

随着 AI 和大语言模型的发展,未来我们甚至可以训练模型自动识别异常模式、预测故障、生成修复建议。但现在,掌握扎实的 Shell 与 Java 日志处理技能,依然是每位工程师的立身之本。

📚 推荐延伸阅读:

愿你的日志不再沉默,愿你的系统永无黑盒。💻🔍📈


🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐