Spring Batch读取平面文件导入至数据库

目录

一、Spring Batch简介

二、业务场景

三、基础知识

3.1 基础架构

3.2 核心接口

四、代码示例

4.1 引入POM依赖

4.2 读取和写入实体类

4.2.1 文件读取实体类

4.2.2 文件写入实体类

4.2.3 二者区别

4.3 数据处理Processor

4.4 配置Job

4.4.1 新建配置类

4.4.2 配置ItemWriter

4.4.3 配置ItemReader

4.4.4 配置Processor

4.4.5 配置Step

4.4.6 配置Job

4.5 调用Job

五、接口测试

5.1 接口测试

5.2 通过postman测试

5.3 查看控制台日志

5.4 查看数据库记录

六、问题记录

七、示例代码地址


前言:项目中需要使用批量文件导入的功能,调研了Spring Batch对应的用法。本文介绍了Spring Batch的基本概念和示例用法,读取平面文件,对数据进行处理后保存至数据库中。更多Spring Batch的介绍和用法请参考官网:

https://spring.io/projects/spring-batch

https://docs.spring.io/spring-batch/docs/4.2.1.RELEASE/api

一、Spring Batch简介

Spring Batch是一个轻量级、全面的批处理框架,旨在支持开发健壮的批处理成用程序。Spring Batch建在Spring Framework特性的基础上,使开发人员能够轻松使用更高级的企业服务。

与调度框架不同,Spring Batch不是一个独立的调度程序。但Spring Batch旨在与调度程序结合使用,以实现批处理作业的处理和调度。通过结合使用Spring Batch和企业调度程序,可以创建功能强大的批处理系统,可以可靠地处理大量的数据和业务操作,提高系统的可靠性和稳定性。

二、业务场景

在当前的系统开发中存在一种场景:

185a81c6b3274788be13294454467e1c.png

SpringBatch支持以下业务场景:

定期提交批处理、并发处理作业、分阶段的企业消息驱动处理、大规模并行批处理、失败后手动或计划重启、相关步骤的顺序处理、部分处理(例如在回滚时跳过记录)和整批交易(适用于批量较小的情况)等场景。与调度程序结合使用,而不是替代调度程序。

三、基础知识

3.1 基础架构

a5ec4da2217d411da358d39fdb9db3e9.png

名称

作用

JobRepository

为所有的原型(Job、Joblnstance、Step)提供持久化的支持

JobLauncher

表示一个简单的接口,用于启动一个Job给定的集合JobParameters

Job

封装了整个批处理处理过程的实体

Step

一个域对象,封装了批处理作业的一个独立的顺序阶段

3.2 核心接口

  • ItemReader:表示一个Step的输入,一次批处理或一块条目。
  • ltemProcessor:表示一个条目的业务处理。
  • Itemwriter:表示一个Step的输出,一次批处理或一块条目。

大体流程为输入-->数据加工-->输出,一个Job包含一个或多个Step及处理流程,一个Step

通常涵盖 ltemReader、ltemProcessor和ltemWriter。

四、代码示例

以批处理文件,将数据处理后,写入数据库,以Student学生信息为例:

4.1 引入POM依赖

<!--spring batch 批处理-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-batch</artifactId>
   <version>2.3.12.RELEASE</version>
</dependency>

4.2 读取和写入实体类

4.2.1 文件读取实体类

在文件读取时,将字段值映射到实体类StudentFile中

@Data
@AllArgsConstructor
@NoArgsConstructor
public class StudentFile {
    /**
     * id 20
     */
    private String id;
    /**
     * 姓名  50
     */
    private String name;
    /**
     * 年龄 5
     */
    private int age;
    /**
     * 性别 5
     */
    private String sex;
    /**
     * 地址 100
     */
    private String address;

    /**
     * 联系电话 20
     */
    private String phone;
}

4.2.2 文件写入实体类

在数据处理完成之后,进行数据库写入时的实体类StudentEntity

@Data
@AllArgsConstructor
@NoArgsConstructor
public class StudentEntity {
    private static final long serialVersionUID = 1L;
    /**
     * id 20
     */
    private String id;
    /**
     * 姓名 50
     */
    private String name;
    /**
     * 年龄 5
     */
    private int age;
    /**
     * 性别 5
     */
    private String sex;
    /**
     * 地址 100
     */
    private String address;
    /**
     * 联系电话 20
     */
    private String phone;
    /**
     * 创建时间 20
     */
    private String createTime;
}

4.2.3 二者区别

根据ItemProcessor<I, O>的接口定义,文件数据处理时,映射泛型为I的实体,处理后,返回一个O类型的实体供写入操作。写入实体类与读取实体类也可以相同,具体看业务需求。

package org.springframework.batch.item;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
public interface ItemProcessor<I, O> {
    @Nullable
    O process(@NonNull I var1) throws Exception;
}

4.3 数据处理Processor

数据处理类,将读取的数据进行转换、筛选等操作。

@Slf4j
@Component
public class StudentFileProcessor implements ItemProcessor<StudentFile, StudentEntity> {

    private JobParameters jobParameters;

    @BeforeStep
    public void beforeStep(final StepExecution stepExecution) {
        jobParameters = stepExecution.getJobParameters();
    }

    //通过jobParameters接收参数
    public String getHandleDate() {
        return jobParameters.getString("createTime");
    }

    @Override
    public StudentEntity process(StudentFile student) throws Exception {
        if (Objects.isNull(student)) {
            //跳过空行
            return null;
        }

        //数据处理 此处简单赋值
        StudentEntity studentEntity = new StudentEntity();
        studentEntity.setId(UUID.randomUUID().toString().replace("-","").substring(0,20));
        studentEntity.setName(student.getName());
        studentEntity.setAge(student.getAge());
        studentEntity.setSex(student.getSex());
        studentEntity.setAddress(student.getAddress());
        studentEntity.setPhone(student.getPhone());

        studentEntity.setCreateTime(getHandleDate());

        return studentEntity;
    }
}

4.4 配置Job

4.4.1 新建配置类

新建配置类StudentFileStepConfig,配置Job、Step、ItemReader、ItemWriter等。

@Slf4j
@Configuration
public class StudentFileStepConfig {
    @Resource
    private JobBuilderFactory jobBuilderFactory;
    @Resource
    private StepBuilderFactory stepBuilderFactory;
    @Resource
    private StudentFileProcessor studentFileProcessor;
    @Resource
    private DataSource dataSource;
}

以下小节中的配置均在此配置类中。

4.4.2 配置ItemWriter

配置ItemWriter,通过JDBC将数据写入到数据库中。

/**
 * 配置ItemWriter
 *
 * @return ItemWriter
 */
@Bean
public JdbcBatchItemWriter studentJdbcBatchItemWriter() {
    //便用JdbcBatchItemWriter通过JDBC将数据写到数据库中
    JdbcBatchItemWriter writer = new JdbcBatchItemWriter();

    //设置数据源
    writer.setDataSource(dataSource);
    //设置插入更新的SQL,注意占位符的写法 :"属性名"
    writer.setSql("insert into student(id, name, age, sex, address, phone, createTime) values (:id, :name, :age, :sex, :address, :phone, :createTime)");
    writer.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider());
    return writer;
}

4.4.3 配置ItemReader

1、配置ItemReader,读取文件时,通常以FlatFileItemReader实现。

/**
 * 配置ItemReader
 *
 * @return ItemReader
 */
@Bean
@StepScope
public FlatFileItemReader<StudentFile> studentFileReader() {
    //FlatFileItemReader 是一个加载普通文件的 ItemReader
    FlatFileItemReader<StudentFile> reader = new FlatFileItemReader<>();
    //第一行为标题跳过,若没有标题,注释掉即可
    // reader.setLinesToSkip(1);
    String fileName = "studentinfo";
    String filePath = "C:\\test";
    String fullName = filePath + File.separator + fileName;
    FileSystemResource fileResource = new FileSystemResource(fullName);
    reader.setEncoding("GBK");
    reader.setResource(fileResource);
    reader.setLineMapper(new DefaultLineMapper<StudentFile>() {
        {
            setLineTokenizer(
                    //固定长度分割
                    studentFixedLengthTokenizer()
            );
            setFieldSetMapper(new BeanWrapperFieldSetMapper() {
                {
                    setTargetType(StudentFile.class);
                }
            });
        }
    });
    return reader;
}

2、配置tokenizer,文件格式为定长,使用FixedLengthTokenizer 。

(1)Range的范围截取需要按照文件的格式,从1开始计算,截取相应的值。Range数组顺序与Names的列一一对应。

(2)此处没有读取文件的全部列,只是截取了部分,所以需要设置tokenizer.setStrict(false)。

/**
 * 定长分割,文件一行长度固定,每个字段固定长度,长度不足的,数字前补0,字符串后补空格
 *
 * @return
 */
@Bean
public FixedLengthTokenizer studentFixedLengthTokenizer() {
    FixedLengthTokenizer tokenizer = new FixedLengthTokenizer();
    //设置文件中一共有多少列,分别是哪些列
    //需要读取的列
    tokenizer.setNames("name", "age", "sex", "address", "phone");
    tokenizer.setColumns(
            // new Range(1, 20),//id 20
            new Range(21, 70),//姓名 50
            new Range(71, 75),//年龄 5
            new Range(76, 80),//性别 5
            new Range(81, 180),//地址 100
            new Range(181, 200)//联系电话 20
    );

    /*
      是否严格匹配。设置读取一行的部分列时,必须设置为false。
      例如一行里有30个字段,但是只读取其中10个字段,其他字段忽略不需要读取。
      可以容忍标记比较少的行,并用空列填充,标记较多的行将被简单地截断。默认为true
    */
    tokenizer.setStrict(false);
    return tokenizer;
}

3、若文件格式为分隔符分割,则配置tokenizer时,需要使用DelimitedLineTokenizer ,并将全部的列一一列出。

4、按照文件格式的需要与第2步二选一即可。

/**
 * 分割符分割,文件一行长度不固定,字段之间使用指定的分隔符进行分割
 *
 * @return
 */
@Bean
public DelimitedLineTokenizer studentDelimitedLineTokenizer() {
    DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
    tokenizer.setNames("id", "name", "age", "sex", "address", "phone");
    tokenizer.setDelimiter(" ");
    return tokenizer;
}

4.4.4 配置Processor

在4.3数据处理中已经单独实现,此处通过注入的形式配置。

@Resource
private StudentFileProcessor studentFileProcessor;

4.4.5 配置Step

/**
 * 配置Step
 *
 * @return Step
 */
@Bean
public Step studentStep() {
    return stepBuilderFactory.get("studentStep")//通过get获取一个stepBuilder,参数为step的name
            .<StudentFile, StudentEntity>chunk(5000)//配置chunk,每读取5000条,就执行一次writer操作
            .reader(studentFileReader())//配置reader
            .processor(studentFileProcessor)//配置processor,进行数据的预处理
            .writer(studentJdbcBatchItemWriter())//配置writer
            .taskExecutor(new SimpleAsyncTaskExecutor())
            .build();
}

4.4.6 配置Job

一个Job可以包含多个Step。至此,一个完整的Job配置完成。

/**
 * 配置Job
 *
 * @return Job
 */
@Bean
public Job studentBatchJob() {
    return jobBuilderFactory.get("studentJob")//通过 jobBuilderFactory 构建一个 Job. get参数为 Job 的name
            .start(studentStep())//配置该Job的Step
            //.next(studentStep2())//多个Step时,通过next()进行配置
            .build();
}

4.5 调用Job

在Service的实现类中,使用jobLauncher.run(),通过Job的名称“studentBatchJob”调用指定的Job。

@Slf4j
@Service
public class StudentServiceImpl implements StudentService {

    @Resource
    private JobLauncher jobLauncher;

    @Resource
    private Job studentBatchJob;

    @Override
    public String importStudentFile() {
        log.info("导入学生信息开始...");
        try {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
            String createTime = sdf.format(new Date());

            JobParameters jobParameters = new JobParametersBuilder()
                    .addString("createTime", createTime)
                    .toJobParameters();

            jobLauncher.run(studentBatchJob, jobParameters);
        } catch (Exception e) {
          log.error("导入学生信息异常", e);
        }
        log.info("导入学生信息结束...");
        return "success";
    }
}

五、接口测试

5.1 接口测试

@Controller
@RequestMapping("/student")
public class StudentController {

    @Resource
    private StudentService studentService;

    @PostMapping("import")
    @ResponseBody
    public String importStudentFile() {
        return studentService.importStudentFile();
    }

}

5.2 通过postman测试

e209419ccbd24259b120c9ddc01d57fd.png

5.3 查看控制台日志

ee5f2c1093274ca1af6df4bfcc9c62ca.png

5.4 查看数据库记录

103ae971c4534fb69cb1c0eb43092b1e.png

六、问题记录

1、在processor中,如果要使用调用时传进来的参数,例如“createTime”,需要通过JobParameters 来传值和获取。详细见4.3数据处理Processor和4.5调用Job

2、如果文件中出现空行,在processor中进行判断,如果读取的对象属性全为null,直接return null即可。

3、在ItemWriter中,注意sql的写法,以及参数的占位符,需要用“:属性名”。

4、在配置tokenizer时,文件有定长和分隔符的区分,如果是定长文件,需要计算每个列的区间Range,字段较多时,可以使用Excel表格辅助计算。

5、定长文件的tokenizer,如果字段较多,但是只需要读取其中部分,例如一行里有30个字段,但是只读取其中10个字段,其他字段忽略不需要读取,需要设置 tokenizer.setStrict(false)。(一个小坑,最后通过官网的api解决)

七、示例代码地址

github:https://github.com/wgt7/spring-batch-file

gitee:https://gitee.com/no-one7/spring-batch-file

 

<think>我们正在讨论C语言中printf格式化输出时补空格的问题,特别是%m.nf格式说明符中的空格填充规则。 用户意图:了解补空格个数的判断方法 需要搜索的关键点:C语言 printf 格式说明符 空格填充 宽度 对齐 根据之前的讨论,我们知道: 在%m.nf中,m指定最小字段宽度,n指定小数点后的位数。 如果输出的数字位数(包括整数部分、小数点、小数部分)小于m,则会用空格填充到宽度m。 默认是右对齐(填充在左侧),如果使用-标志则是左对齐(填充在右侧)。 补空格个数的计算方法: 实际输出字符数 = 整数部分位数 + 1(小数点) + n(小数位数) 如果实际输出字符数小于m,则补的空格数 = m - 实际输出字符数 注意:整数部分位数如何计算?比如整数部分为0,则输出0,但注意负号也要占一位。 另外,当数值为负数时,负号也占一位。 我们可以用一个公式表示: 总字符数 = 符号位(如果是负数则为1,否则0) + 整数部分位数(注意:整数部分如果是0,则至少输出一个0) + 1(小数点) + n(小数位数) 补空格个数 = m - 总字符数 (当总字符数 < m 时) 但是,如果总字符数大于等于m,则不补空格。 另外,如果使用左对齐(%-m.nf),空格补在右侧。 例子: 输出:%8.2f,值为-12.345: 实际数值四舍五入为-12.35(保留两位小数) 整数部分:12(但因为是负数,所以整数部分包括负号,所以是3个字符:'-','1','2') 小数部分:两位(35) 小数点:1位 所以总字符数 = 3(整数部分包括负号) + 1 + 2 = 6 需要补空格数 = 8 - 6 = 2,因为默认右对齐,所以在左侧补两个空格,输出为" -12.35" 特殊情况:当整数部分为0时,例如0.1234,用%5.2f输出: 整数部分:0(1位) 小数点:1 小数部分:2位(四舍五入后为0.12,所以小数部分为12) 总字符数=1+1+2=4 补空格数=5-4=1,所以输出" 0.12"(注意前面有一个空格,因为总宽度5,右对齐) 然而,更复杂的是,当数值的绝对值小于1时,整数部分只输出一个0,然后小数点后输出n位。 但是,如果使用%g格式,情况会不同,但用户的问题集中在%f格式。 因此,我们可以总结为以下步骤: 步骤1:确定数值经过四舍五入保留n位小数后的字符串表示(包括整数部分、小数点和小数部分,以及符号位)。 注意:负号需要单独考虑。 步骤2:计算该字符串的字符个数(不包括任何填充的空格)。 例如:-12.35 的字符个数为6(负号1个,整数部分2个,小数点1个,小数部分2个)。 步骤3:比较计算出的字符个数与指定的宽度m: 如果字符个数小于m,则需要补空格,空格个数为m - 字符个数。 否则,不需要补空格(实际输出会突破m的限制)。 步骤4:根据对齐方式放置空格: 默认右对齐:空格在左侧; 使用'-'标志左对齐:空格在右侧。 注意:在计算整数部分位数时,要注意当整数部分为0时,输出0,所以至少1位。 另外,还有一种特殊情况:当数值较大时,整数部分位数可能很多,但小数部分固定n位。 我们可以用以下伪代码表示: ```c // 假设我们有一个浮点数value,格式为%m.nf // 首先,四舍五入到n位小数,然后转换为字符串(只考虑数值部分,不包括符号) // 但实际上,我们可以利用snprintf来模拟,但这里我们讨论的是如何计算空格数量。 // 实际输出字符串长度 = // 符号位(1表示负号,0表示正号,正数不显示符号) + // 整数部分位数(计算整数部分位数:对整数部分取以10为底的对数并向下取整加1,但注意0的情况) + // 1(小数点) + // n(小数位数) // 但是,整数部分位数计算比较复杂,我们可以用以下方法: // 将数值的绝对值的整数部分取出,然后计算其位数(注意0的情况,位数为1) // 然后加上符号位(如果是负数则加1,正数不加) // 然而,实际格式化输出时,printf的行为是: // 负数的符号总是会输出,正数不输出符号(除非使用+标志)。 // 因此,为了准确,我们考虑符号位s: // s = (value < 0) ? 1 : 0; // 整数部分:如果value的绝对值的整数部分为0,则整数部分位数为1(输出0),否则为 floor(log10(|value|)) + 1,但注意当value在0和1之间时,整数部分为0,输出0。 // 但是,由于浮点数可能存在精度问题,直接计算对数可能不准确,所以更可靠的方法是模拟printf的输出。 // 实际上,我们可以通过一次格式化到足够大的缓冲区(用%.nf)然后去掉符号和整数部分?这样反而复杂。 // 因此,我们建议的方法是基于实际输出字符串的长度来计算。我们可以这样模拟: // 先用snprintf以%.nf格式输出(但这样会丢失宽度信息),然后我们得到四舍五入后的字符串,再计算字符串长度(包括负号和小数点),最后与m比较。 // 但是,用户的问题是如何判断补空格个数,所以我们可以给出计算方法: 空格个数 = m - [符号位 + 整数部分位数 + 1 + n] 其中,整数部分位数 = (value == 0) ? 1 : floor(log10(|value|)) + 1 (注意,这是绝对值整数部分) 但是,注意:当value为负数时,整数部分位数是绝对值整数部分的位数,但符号位要加1。 然而,这个公式在value的整数部分为0时(比如0.123)是成立的:整数部分位数=1(输出0),即使绝对值小于1。 但是,当value的整数部分非0时,同样适用。 因此,总字符数 = (value < 0 ? 1 : 0) + (整数部分位数) + 1 + n 但是,请注意,如果用户使用了+标志(%+m.nf),那么正数也会显示一个正号,此时符号位为1(正数情况)。 所以,符号位实际上取决于: 如果value为负数,则符号位为1; 如果value为正数且有+标志,则符号位为1; 否则为0。 因此,我们还需要考虑标志。 由于用户问题中没有提到+标志,我们先默认不考虑,只考虑负号。 所以,在默认情况下: 总字符数 = (value<0?1:0) + 整数部分位数 + 1 + n 整数部分位数计算: 令 intPart = (int)floor(fabs(value)); // 取整数部分(绝对值) 如果intPart为0,则整数部分位数为1(因为要输出0)。 否则,整数部分位数 = (int)floor(log10(intPart)) + 1 (注意,当intPart为0时不能用log10,因为log10(0)未定义) 但是,注意:当整数部分很大时,比如1000,那么整数部分位数是4。 但是,这种方法有个问题:当整数部分为0时,我们直接取1;当整数部分大于0时,用对数计算位数。但是,对数计算可能会因为浮点精度问题而不准确。 因此,更稳妥的方法是通过整数转换成字符串来获取位数,但题目要求公式,我们可以用对数方法,但要提醒用户注意精度问题。 然而,实际上在printf中,整数部分不会用科学计数法表示,所以整数部分位数就是整数部分十进制表示的位数。 所以,我们可以这样计算整数部分位数: 如果 |value| 的整数部分等于0,则整数部分位数为1。 否则,整数部分位数 = floor(log10(整数部分)) + 1 (其中整数部分取绝对值) 但是,当value的整数部分非常大(比如超过10^15)时,log10计算可能会有误差。 因此,我们建议用户在实际编程中,如果要计算输出后的空格数,可以直接使用snprintf将数值格式化为一个临时字符串(使用%.nf),然后计算该字符串的长度(不包括终止符),再与m比较。 不过,用户的问题是如何手动判断,所以我们给出手动计算的公式: 总字符数 = (符号位) + (整数部分位数) + 1(小数点) + n 空格数 = max(0, m - 总字符数) 注意:四舍五入可能影响整数部分进位?例如,9.999保留2位小数四舍五入后会变成10.00,这时整数部分变成两位数(10),所以整数部分位数变化。 因此,严格来说,我们需要先对数值进行四舍五入到小数点后n位,然后判断进位后的整数部分。 例子:9.999,%m.2f -> 10.00,整数部分为10,整数部分位数=2(而原来是1)。 所以,步骤应该是: 1. 对value进行四舍五入到小数点后n位,得到新的值rounded_value。 2. 取rounded_value的整数部分的绝对值:int_part = (int)floor(fabs(rounded_value)); 注意,如果进位后整数部分可能为0(例如0.999四舍五入后为1.0,整数部分为1)。 3. 计算整数部分位数:如果int_part为0,则位数为1,否则为floor(log10(int_part)) + 1。 4. 计算符号位:如果rounded_value < 0,或者用户指定了+符号并且rounded_value>=0,则符号位为1;否则为0(默认情况不考虑+号则只有负数有符号位)。 5. 总字符数 = 符号位 + 整数部分位数 + 1 + n (小数部分固定n位,即使四舍五入进位后小数部分仍为n位,但注意整数部分进位后小数部分可能为0,但位数还是n,包括后面的0?) 注意:小数部分会输出n位,即使后面是0也要输出。 但是,注意:当四舍五入后整数部分进位,比如9.995保留两位小数变成10.00,那么小数部分仍然是两位(00),所以总字符数:符号位0(假设正数),整数部分2,小数点1,小数部分2,共5个字符。 因此,我们可以总结为: 空格数 = m - [ (sign) + (整数部分位数) + 1 + n ] 如果这个值小于0,则空格数为0。 但是,上述计算比较复杂,而且存在浮点数精度问题,所以在实际编程中不推荐手动计算,而是用snprintf模拟。 然而,用户的问题是如何判断(可能是手动判断),所以我们可以忽略浮点精度问题,只给出数学公式。 所以,回答可以这样组织: 补空格个数的判断方法主要基于C语言printf函数的格式化规则,具体步骤如下: 1. **确定数值的符号位占用字符数**: 负数始终占用1个符号位(输出负号"-")。 正数默认不显示符号(符号位为0),但如果使用`+`标志(如`%+m.nf`),则占用1个符号位(输出"+")。 2. **计算四舍五入后的整数部分位数**: 将浮点数四舍五入到小数点后$n$位。 取整数部分的绝对值(四舍五入后): - 若整数部分为0,则位数计为1(输出"0")。 - 若整数部分非0,则位数为$\lfloor \log_{10}(\text{整数部分绝对值}) \rfloor + 1$。 3. **计算总字符数**: 总字符数 = 符号位 + 整数部分位数 + 1(小数点) + $n$(小数位数)。 4. **计算补空格个数**: - 若总字符数 $\geq m$:不补空格(实际输出突破宽度$m$)。 - 若总字符数 $< m$:补空格个数 = $m - \text{总字符数}$。 - 对齐方式: - 默认右对齐:空格填充在左侧。 - 使用`-`标志(如`%-m.nf`):空格填充在右侧。 **示例计算**: - 示例1:`printf("%8.2f", -12.345);` - 四舍五入后:`-12.35` - 符号位:1(负号) - 整数部分:`|-12|`的整数部分为12,位数=$\lfloor \log_{10}(12) \rfloor + 1 = 1+1=2$(实际计算:$\log_{10}(12)\approx1.079$,取整后1,加1得2) - 总字符数 = $1 + 2 + 1 + 2 = 6$ - 空格数 = $8 - 6 = 2$(右对齐,左侧补2空格,输出` -12.35`)。 - 示例2:`printf("%5.2f", 0.126);` - 四舍五入后:`0.13` - 符号位:0 - 整数部分:0,位数=1 - 总字符数 = $0 + 1 + 1 + 2 = 4$ - 空格数 = $5 - 4 = 1$(右对齐,左侧补1空格,输出` 0.13`,注意前面有一个空格)。 **特殊情况说明**: - 若四舍五入导致整数部分进位(如`9.995`保留2位小数变为`10.00`): - 整数部分为10,位数=$\lfloor \log_{10}(10) \rfloor + 1 = 1+1=2$。 - 总字符数 = $0 + 2 + 1 + 2 = 5$(无符号位)。 - 注意:浮点数精度问题可能导致手动计算误差,实际应用建议用`snprintf`模拟输出后直接测量字符串长度。 最后,生成相关问题: §§相关问题§§: 1. 在C语言中如何用代码计算printf格式化后的字符串长度? 2. %+m.nf格式说明符中的加号标志如何影响空格填充? 3. 当浮点数四舍五入后整数部分进位,如何避免手动计算位数错误? </think>以下是针对C语言`printf`格式化输出中补空格个数判断方法的详细说明,结合格式说明符`%m.nf`的规则($m$为总宽度,$n$为小数位数): ### 补空格个数的判断方法 **1. 计算格式化后的数值字符串长度** - 符号位:负数固定占1位(`-`),正数无符号位(除非用`+`标志)。 - 整数部分位数: - 若整数部分为0,固定占1位(输出`0`)。 - 非零时,位数为 $\lfloor \log_{10}(|\text{整数部分}|) \rfloor + 1$(例如 `123` 的位数为3)。 - 小数点固定占1位。 - 小数部分固定占 $n$ 位(不足补零,超则四舍五入)。 - **总长度公式**: $$ L = (\text{符号位}) + (\text{整数部分位数}) + 1 + n $$ **2. 判断补空格个数** - 若 $L \geq m$:不补空格(输出突破宽度限制)。 - 若 $L < m$:需补空格,个数为 $m - L$。 - 默认右对齐:空格在**左侧**(如 `%8.2f` 输出 `␣␣12.35`)。 - 左对齐(`-`标志):空格在**右侧**(如 `%-8.2f` 输出 `12.35␣␣`)。 **3. 关键示例** - **示例1**:`printf("%8.2f", -12.345)` - 四舍五入后:`-12.35` - $L = 1\ (\text{负号}) + 2\ (\text{整数位}) + 1\ (\text{小数点}) + 2\ (\text{小数位}) = 6$ - 补空格数:$8 - 6 = 2$(右对齐,左侧补2空格) → 输出 `␣␣-12.35`。 - **示例2**:`printf("%5.2f", 0.126)` - 四舍五入后:`0.13` - $L = 0\ (\text{无符号}) + 1\ (0) + 1 + 2 = 4$ - 补空格数:$5 - 4 = 1$(右对齐,左侧补1空格) → 输出 `␣0.13`。 **4. 特殊情况处理** - **四舍五入进位**: 若进位后整数部分位数变化(如 `9.995` 保留2位小数 → `10.00`),需重新计算 $L$: - $L = 0 + 2\ (\text{整数位变成10}) + 1 + 2 = 5$。 - **精度超宽度**: 若 $n > m$(如 `%3.4f` 输出 `12.3456`),忽略 $m$ 限制,不补空格。 **5. 验证代码** ```c #include <stdio.h> int main() { double values[] = {-12.345, 0.126, 9.995}; printf("右对齐,宽度8,保留2位小数:\n"); for (int i=0; i<3; i++) printf("|%8.2f|\n", values[i]); // 输出:|-12.35|, | 0.13|, | 10.00| printf("\n左对齐,宽度5,保留2位小数:\n"); for (int i=0; i<3; i++) printf("|%-5.2f|\n", values[i]); // 输出:|-12.35|, |0.13 |, |10.00| return 0; } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值