ttttwr

导航

软件构造实验三——基于语法的数据读入

在实验三的结尾,我们需要实现一个解析器,它可以解析配置文件并将其转换为对象。一个配置文件的示例如下:

Employee{ //多个员工信息
  ZhangSan{Manger,139-0451-0000} //{}前是员工姓名,大小写字母构成
  //不同的行内的员工名字是唯一的,不能重复
  LiSi{Secretary,151-0101-0000} //{}内第一个分量是职务,大小写字母和空格
  WangWu{Associate Dean,177-2021-0301}
  //{}内第二个分量是中国境内合法的手机号码
  //共 11 位,分为三段(3-4-4),用“-”相连
}
Period{2021-01-10,2021-03-06} //表示排班的开始日期和结束日期
Roster{ //表示排班计划
  ZhangSan{2021-01-10,2021-01-31} //分别为姓名、开始日期、结束日期
  LiSi{2021-02-01,2021-02-28} //上述所有日期的格式为 yyyy-MM-dd
  WangWu{2021-03-01,2021-03-10}
  //出现在 Roster 内的员工,必须在 Employee 中已有定义,否则不合法
}

其中 Employee、Period、Roster 的顺序是可变的,排班表中的员工必须是 Employee 中已经定义的。
在这里的难点是如何解析语法。在这里,我有一些简单的思路:

# 正则表达式

  • 其实我们可以发现,样例展示的配置文件格式其实比较单一,可以寻找到一个固定的模式来匹配配置文件的内容,再以此为依据读取配置文件的内容。
  • 例如匹配 Period 字段,可以先判断行的开头是不是 Period,如果是,则使用正则表达式:\{\d{4}-\d{2}-\d{2},\d{4}-\d{2}-\d{2}\} 来匹配。
  • 但是这样的匹配方式显然很有局限性,例如无法处理空格和换行等操作,虽然我们可以修改匹配的代码,例如改为 \{\\d{4}-\\d{2}-\\d{2}[ \n\r]*,[ \n\r]*\\d{4}-\\d{2}-\\d{2}\} 这样正则代码就可以匹配在中间出现空格和换行的情况了。但是如果我们需要可以使用注释,或是一些更多的功能,正则表达式可能就难以照顾全局或是说我们就难以编写出可以匹配所有情况的表达式了,并且即使写出了这样的表达式,其长度和复杂程度将极高,难以阅读理解和维护。

# 编译原理的思想

  • 编译的过程大致可以分为词法分析、语法分析、语义分析、中间代码生成、代码优化和目标代码生成几个过程,其中前三个部分的任务就是将文本形式的代码转换为编译器中的数据结构,其中词法分析可以将源代码解析标记为词素(即一个个有意义的单元),语法分析可以解析源代码的结构,语义分析可以进行语法的检查。
  • 这么看,读入配置文件与编译的前三个步骤是十分相似的,那么也许我们能够将其应用于读入配置文件的任务中。
  • 由于编译器自然是可以处理例如注释和空格这类的问题的,那么理论上来说,我们使用类似的原理编写解析器,也可以实现类似的任务。

最终效果

  • 依据编译原理的思想,我编写了一个可以完成类似语法分析和语义分析的解析器。当然,这与真实的编译器还是有很大的区别,但是它能够实现解析一个具有单行、多行注释的,不限制使用空格的配置文件。
  • 示例的配置文件如下,可以看到其格式还是很自由的:
Employee{ 
 ZhangSan{Manger,139-0451-0000}
 LiSi{Secretary,151-0101-0000// 可以在除了子项的{}内的任何地方添加注释
}
 WangWu{Associate Dean,177-2021-0301}
 ZhaoLiua{Professor,138-1920-3912}
 ZhaoLiub{Lecturer,138-1921-3912}
 ZhaoLiuc{
Professor,138-1922-3912} // 格式自由
            ZhaoLiud{      Lecturer,
198-1920-3912}
 ZhaoLiue{Professor,178-1920-3912}// 可以在除了子项的{}内的任何地方添加注释
 ZhaoLiuf{Lecturer,138-1929-3912}
 ZhaoLiug{Professor,138-1920-0000}
/* 这是多行注释
fjiurhgg
gdgds
nnn

b*/ZhaoLiuh{AssciateProfessor,138-1929-0000}
 ZhaoLiui{Professor,138-1920-0200}
 ZhaoLiuj{AssciateProfessor,138-1920-0044}
 ZhaoLiuk{Professor,188-1920-0000}
}Period

{
 2021-01-10,            2021-03-06
}
// 可以在除了子项的{}内的任何地方添加注释
Roster{ // 可以在除了子项的{}内的任何地方添加注释
 ZhangSan{2021-01-10,2021-01-11}
 LiSi{2021-01-12,2021-01-20}
 WangWu{2021-01-21,2021-01-21}
 ZhaoLiua{

// 可以在除了子项的{}内的任何地方添加注释
               2021-01-22,2021-01-22   }
 ZhaoLiub{2021-01-23,  2021-01-29}
 ZhaoLiuc{2021-01-30,2021-01-31}
 ZhaoLiud{2021-02-01    ,2021-02-08}ZhaoLiue{2021-02-09,2021-02-15}ZhaoLiuf{2021-02-16,2021-02-24}
 ZhaoLiug{2021-02-25
,
2021-02-28}
 ZhaoLiuh{2021-03-01,          2021-03-01} // 可以在除了子项的{}内的任何地方添加注释
 ZhaoLiui{2021-03-02,2021-03-04}
 ZhaoLiuj{2021-03-05,2021-03-05}
 ZhaoLiuk{2021-03-06,2021-03-06}
}
  • 读入效果
当前读入的排班信息:
Duty Schedule: From 2021-01-10 to 2021-03-06
LiSi(电话: 151-0101-0000): 2021-01-12 - 2021-01-20
ZhangSan(电话: 139-0451-0000): 2021-01-10 - 2021-01-11
ZhaoLiuf(电话: 138-1929-3912): 2021-02-16 - 2021-02-24
ZhaoLiuh(电话: 138-1929-0000): 2021-03-01 - 2021-03-01
ZhaoLiuc(电话: 138-1922-3912): 2021-01-30 - 2021-01-31
ZhaoLiub(电话: 138-1921-3912): 2021-01-23 - 2021-01-29
ZhaoLiuj(电话: 138-1920-0044): 2021-03-05 - 2021-03-05
ZhaoLiue(电话: 178-1920-3912): 2021-02-09 - 2021-02-15
ZhaoLiua(电话: 138-1920-3912): 2021-01-22 - 2021-01-22
ZhaoLiui(电话: 138-1920-0200): 2021-03-02 - 2021-03-04
ZhaoLiud(电话: 198-1920-3912): 2021-02-01 - 2021-02-08
WangWu(电话: 177-2021-0301): 2021-01-21 - 2021-01-21
ZhaoLiuk(电话: 188-1920-0000): 2021-03-06 - 2021-03-06
ZhaoLiug(电话: 138-1920-0000): 2021-02-25 - 2021-02-28
  • 具体代码(可能并不完善,或是存在错误,仅作为一个示例)
package App.Duty;

import Entity.Employee;
import IntervalSet.IntervalSet;

import java.io.FileReader;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;


public class DataParser {
    public Data parse(String filePath){
        Data data = new Data();
        try (FileReader fileReader = new FileReader(filePath)) {

            // 只有三个属性都解析完毕才返回
            boolean hasEmployees = false, hasDuty = false, hasPeriod = false;
            // 用于中转的IntervalSet(从IntervalSet<String>最后填为IntervalSet<Employee>)
            IntervalSet<String> dutyIntervalSet = null;
            StringBuilder sb = new StringBuilder();
            boolean endParse = false;
            int next;
            while(!endParse && (next = fileReader.read()) != -1){
                char c = (char)next;

                switch(c){
                    case '/':
                        handleAnnotation(fileReader);
                        break;
                    case '\n':
                    case '\r':
                    case ' ':
                        break;
                    case '{':
                        String tmp = sb.toString();
                        sb.delete(0, sb.length());
                        switch (tmp) {
                            case "Employee":
                                if (hasEmployees) {
                                    throw new IllegalArgumentException("非法输入");
                                }
                                var employees = parseEmployee(fileReader);
                                if (employees == null) {
                                    throw new IllegalArgumentException("非法输入");
                                }
                                data.setEmployeeMap(new HashMap<>());
                                for (var employee : employees) {
                                    data.getEmployeeMap().put(employee, false);
                                }
                                hasEmployees = true;
                                break;
                            case "Roster":
                                if (hasDuty) {
                                    throw new IllegalArgumentException("非法输入");
                                }
                                var duty = parseDuty(fileReader);
                                if (duty == null) {
                                    throw new IllegalArgumentException("非法输入");
                                }
                                dutyIntervalSet = duty;
                                hasDuty = true;
                                break;
                            case "Period":
                                if (hasPeriod) {
                                    throw new IllegalArgumentException("非法输入");
                                }
                                var dates = parsePeriod(fileReader);
                                if (dates == null) {
                                    throw new IllegalArgumentException("非法输入");
                                }
                                data.setStartDate(dates[0]);
                                data.setEndDate(dates[1]);
                                hasPeriod = true;
                                break;
                            default:
                                throw new IllegalArgumentException("非法输入");
                        }
                        break;
                    case '}':
                        endParse = true;
                        break;
                    default:
                        sb.append(c);
                }
            }
            if (!hasEmployees || !hasDuty || !hasPeriod) {
                throw new IllegalArgumentException("非法输入");
            }
            IntervalSet<Employee> employeeIntervalSet = IntervalSet.empty();
            var employeeMap = data.getEmployeeMap();
            for (var label : dutyIntervalSet.labels()) {
                boolean match = false;
                for (var employee : employeeMap.keySet()) {
                    if (employee.getName().equals(label)) {
                        employeeIntervalSet.insert(dutyIntervalSet.start(label), dutyIntervalSet.end(label), employee);
                        match = true;
                        break;
                    }
                }
                if (!match) {
                    throw new IllegalArgumentException("未知员工");
                }
            }
            data.setDutyIntervalSet(employeeIntervalSet);
            // 设置员工是否已排班
            for (var label : employeeIntervalSet.labels()) {
                employeeMap.put(label, true);
            }

            return data;
        } catch (IOException e) {
            throw new IllegalArgumentException("文件读取错误");
        }
    }

    private List<Employee> parseEmployee(FileReader fr) throws IOException {
        StringBuilder sb = new StringBuilder();
        List<Employee> employees = new ArrayList<>();

        String name = null;
        String tmp;
        boolean inBrace = false;
        boolean endParse = false;
        int next;
        while(!endParse && (next = fr.read()) != -1){ // 注意顺序不可交换,否否则在最后endParse为true时会吞掉最后一个字符
            var c = (char)next;
            switch(c){
                case '/':
                    handleAnnotation(fr);
                    break;
                case '\n':
                case '\r':
                    break;
                case ' ':
                    if (inBrace) {
                        sb.append(c);
                    }
                    break;
                case '{':
                    if (inBrace) {
                        throw new IllegalArgumentException("非法输入");
                    }
                    name = sb.toString().strip();
                    // 清空sb
                    sb.delete(0, sb.length());
                    if (name.isEmpty()) {
                        throw new IllegalArgumentException("非法输入");
                    }
                    String finalName = name;
                    if(employees.stream().anyMatch(e -> e.getName().equals(finalName))){
                        throw new IllegalArgumentException("重复的员工名");
                    }
                    
                    inBrace = true;
                    break;
                case '}':
                    if (!inBrace) {
                        endParse = true;
                        break;
                    }

                    tmp = sb.toString().strip();
                    sb.delete(0, sb.length());
                    if(!tmp.matches("[^,]+[ \n\r]*,[ \n\r]*[0-9]{3}-[0-9]{4}-[0-9]{4}")){
                        throw new IllegalArgumentException("非法输入");
                    }
                    String[] split = tmp.split("[ \n\r]*,[ \n\r]*");
                    employees.add(new Employee(name, split[0].strip(), split[1].strip()));

                    inBrace = false;
                    break;
                default:
                    sb.append(c);
            }
        }
        return endParse ? employees : null;
    }

    private LocalDateTime[] parsePeriod(FileReader fr) throws IOException {
        StringBuilder sb = new StringBuilder();
        LocalDateTime[] dates = new LocalDateTime[2];
        boolean endParse = false;
        int next;
        while((next = fr.read()) != -1){
            var c = (char)next;
            switch(c){
                case '/':
                    handleAnnotation(fr);
                    break;
                case '\n':
                case '\r':
                case ' ':
                    break;
                case '{':
                    throw new IllegalArgumentException("非法输入");
                case '}':
                    endParse = true;
                    break;
                default:
                    sb.append(c);
            }
            if (endParse) {
                String tmp = sb.toString().strip();
                sb.delete(0, sb.length());
                if(tmp.matches("\\d{4}-\\d{2}-\\d{2}[ \n\r]*,[ \n\r]*\\d{4}-\\d{2}-\\d{2}")){
                    String[] split = tmp.split("[ \n\r]*,[ \n\r]*");
                    dates[0] = Utils.parseDate(split[0].strip());
                    dates[1] = Utils.parseDate(split[1].strip());
                    if (dates[0] == null || dates[1] == null) {
                        throw new IllegalArgumentException("非法输入");
                    }
                    dates[1] = dates[1].plusDays(1);
                } else {
                    throw new IllegalArgumentException("非法输入");
                }
                break;
            }
        }
        return endParse ? dates : null;
    }

    private IntervalSet<String> parseDuty(FileReader fr) throws IOException {
        StringBuilder sb = new StringBuilder();
        IntervalSet<String> duty = IntervalSet.empty();

        String name;
        boolean endParse = false;
        int next;
        while(!endParse && (next = fr.read()) != -1){
            var c = (char)next;
            switch(c){
                case '/':
                    handleAnnotation(fr);
                    break;
                case '\n':
                case '\r':
                    break;
                case '{':
                    name = sb.toString().strip();
                    sb.delete(0, sb.length());
                    var period = parsePeriod(fr);
                    if (period == null) {
                        throw new IllegalArgumentException("非法输入");
                    }
                    duty.insert(Utils.getEpochSecond(period[0]), Utils.getEpochSecond(period[1]), name);
                    break;
                case '}':
                    endParse = true;
                    break;
                default:
                    sb.append(c);
            }
        }
        return endParse ? duty : null;
    }

    private void handleAnnotation(FileReader fr) throws IOException {
        int next;
        next = fr.read();
        char c = (char)next;
        if(c == '/') {
            while ((next = fr.read()) != -1) {
                c = (char) next;
                if (c == '\n') {
                    break;
                }
            }
        } else if(c == '*') {
            boolean endAnnotation = false;
            while ((next = fr.read()) != -1) {
                c = (char) next;
                if (c == '*') {
                    if ((next = fr.read()) != -1) {
                        c = (char) next;
                        if (c == '/') {
                            endAnnotation = true;
                            break;
                        }
                    }
                }
            }
            if (!endAnnotation) {
                throw new IllegalArgumentException("注释未闭合");
            }
        } else {
            throw new IllegalArgumentException("非法输入");
        }
    }

}

posted on 2024-04-29 18:18  TTTTWR  阅读(23)  评论(0)    收藏  举报