AQL Subset的实现是我们学校软件工程编译原理课程的期末项目,其目的在于通过实现一个简单的编译器来加深学生对编译原理,如语言和文法、词法分析、语法分析等过程的理解,可以帮助学生深入认识编程的思想,编译器的底层实现。本文用于总结整个项目过程中遇到的难点,以及一点点个人经验。
什么是AQL
- 全称:Annotation Query Language
- 用于Text Analytics,可以从非结构化或半结构化的文本中提取结构化信息的语言
- 语法类似SQL
什么是AQL Subset
- AQL语法复杂,功能强大,实现的难度较高
- AQL子集具有AQL的主要特点
名词定义
- Token:以字母或者数字组成的无符号分隔的字符串,或者单纯的特殊符号,不包含空白符(blank)
- eg:I am strong, and I don’t like eating light-blue lollipop.
- eg:I am strong, and I don’t like eating light-blue lollipop.
- Span:由正则表达式提取出来的具有位置信息的字符串,可以是完整的Token,也可以是不完整的(半个或多个)
- View:类似于数据库的Table,可以分为多个属性列,列中的行单元为Span
AQL子集
- 主要操作:
- create view (创建view)
- extract regex (利用正则从文本中提取Span)
- extract pattern (利用正则和view在原文本中匹配符合的模式)
- select (在view中选列)
- output view (格式化打印view)
- create view (创建view)
AQL Subset 示例
- 任务:将人名和人名对应的地名从文本中提取出来
- 人名:大写字母开头的Token
- 地名:两个以大写字母开头的Tokens,并且中间以逗号分隔,其中第二个单词是美国的州名
- 文本:(下划线表示空格,会被忽略)
Carter_from_Plains,Georgia,_Washington_from
Westmoreland,_Virginia - 输入:
General Method
从文本中提取出大写字母开头的单词(左闭右开)
Eg:
Carter(1,7)
Plains (13,19)
Georgia (21,28)
Washington (30,40)
Westmoreland (46,58)
Virginia (60,68)
从文本中提取出美国的州名
Eg:
Georgia (21,28)
Washington (30,40)
Virginia (60,68)
按照中间只隔了一个逗号,且后一个单词为州名即为一个地名的规则,拼接上述两步操作得到的字符串列表
Eg:
Plains, Georgia (13,28)
Georgia, Washington (21,40)
Westmoreland, Virginia (46,68)
大写字母开头的为人名:
Eg:
Carter(1,7)
Plains (13,19)
Georgia (21,28)
Washington (30,40)
Westmoreland (46,58)
Virginia (60,68)
人名与地名相对应的规则为相隔1-2个tokens,根据此规则拼接人名和地名的列表
Eg:
人名 | 地名 | 人名-地名 |
---|---|---|
Carter | Plains,Georgia | Carter from Plains, Georgia |
Plains | Georgia,Washington | Plains,Georgia,Washington |
Georgia | Westmoreland,Virginia | Georgia无匹配 |
Washington | Washington from Westmoreland,Virginia | |
Westmoreland | Westmoreland无匹配 | |
Virginia | Virginia无匹配 |
最终我们得到三个列表:
人名 | 地名 | 人名-地名 |
---|---|---|
Carter | Plains,Georgia | Carter from Plains, Georgia |
Plains | Georgia,Washington | Plains,Georgia,Washington |
Georgia | Westmoreland,Virginia | Washington from Westmoreland,Virginia |
Washington | ||
Westmoreland | ||
Virginia |
利用AQL
create view:regex语句
- 提取第一个字母大写的token
create view Cap as
extract regex /[A-Z][a-z]*/
on D.text as Cap
from Document D;
output:
- 提取美国州名
create view Stt as
extract regex /Washington|Georgia|Virginia/
on D.text
return group 0 as Stt
from Document D;
output:
create view:pattern语句
create view Loc as
extract pattern (<C.Cap>) /,/ (<S.Stt>)
return group 0 as Loc
and group 1 as Cap
and group 2 as Stt
from Cap C, Stt S;
output:
- 拼接人名和地名,要求只相隔1-2个token的pattern语句:
create view PerLoc as
extract pattern (<P.Per>) <Token>{1,2} (<L.Loc)
return group 0 as PerLoc
and group 1 as Per
and group 2 as Loc
from Per P, Loc L;
output:
create view: select语句
- 从现成的view中选列
create view PerLocOnly as
select PL.PerLoc as PerLoc
from PerLoc PL;
create view: output语句
- 输出语句
output view Cap;
output view Stt;
output view Loc;
output view Per;
output view PerLoc;
output view PerLocOnly;
项目流程
AQL Subset 语法
- 递归下降的语法树结构
关键字
关键字如下,大小写敏感。
create,view,as, output,select, from,extract, regex,
on,return, group, and, Token, pattern
实现细节
- Interpreter
- Lexer(词法解释器)
- 建议AQL的每个token都记录行号、列号,方便进行词法错误的提示
- extract regex /正则表达式/, 使用了
'/'
来包含正则表达式,可以在词法分析时忽略前后的'/'
,将正则表达式整体作为一个token保存并返回给语法分析
- Parser(语法分析器)
- 递归下降的语法分析
- 可自行调整语法的产生式,只要符合规定的语法规则
- Lexer(词法解释器)
- Text Tokenizer (文本提取器)
- 从给定的文本数据中将一个个token分离出来
- 划分规则:以非数字和非单词为间隔,除了空白符的一切字符或字符串都可以作为token,空白符被忽略
- Pattern Matcher(模式匹配器)
- 在语法分析过程中遇到特定pattern时执行的匹配操作
- 可以当做对若干表进行拼接
- Eg:
- 关于
<Token>{min,max}
,这里min和max指中间相隔了min到max个Token符合的模式
- output格式化输出
- 将view的内容进行格式化输出
- 将view的内容进行格式化输出
- 错误处理
- Regular Expression Engine(正则表达式引擎,已提供)
- 提供已实现的regex.cpp及接口函数findall
- regex.cpp实现了NFA方式的正则引擎
- 支持特性:
- 连接、选择、任意
- ab, a|b, .
- 重复(贪婪与非贪婪模式)
- , +, ?, ?, +?, ??
- 转义
- \r, \n, \t
- 字符类
- [a-zA-Z0-9]
- 捕获与非捕获
- a(b)c, a(?:b)c
- 全文多次匹配
- [a-zA-Z0-9_]能够找到文本中所有的标识符,而不仅仅是匹配第一个找到的
- 连接、选择、任意
- 正则文法
regex → alt
alt → concat | alt '|' concat
concat → repeat | concat repeat
repeat → single
| single '*' | single '*' '?'
| single '+' | single '+' '?'
| single '?' | single '?' '?'
single → '(' alt ')'
| '(' '?' ':' alt ')'
| '.' | CHAR | CHARCLASS
- 正则函数接口:
vector<vector<int>> findall(const char *regex, const char *content)
- 函数功能
- 返回正则表达式在文本中的所有匹配
- 函数参数
- regex:正则表达式
- content: 文本
- 返回结果
- 返回正则表达式在文本中的所有匹配(外层vector)
- 每一次匹配(内层vector)包含所有捕获块的文本匹配范围,其中每相邻两个整数表示一个捕获块范围(捕获块为圆括号包含起来的子表达式匹配到的内容)
- 捕获块从0开始计数,第0个捕获块表示整个正则表达式,默认总是存在
- 捕获块范围为左闭右开区间
举例
说明
- 使用C/C++编写,允许使用STL
- 代码干净整洁,变量命名合理
输入文件
- *.aql为AQL程序文件
- *.input为输入文本
- *.output为输出文本
执行过程
References
具体实现
在详细分析了整个项目的要求之后,可以将其分为四个模块:
- 1) Tokenizer:用以在输入文本中提取符合规则的Token,作为Parser的输入之一
- 2) Lexer:用以提取输入的AQL语句中的关键字,作为Parser的输入之一
- 3) Parser:根据Tokenizer和Lexer传进来的参数进行AQL语句的词法分析以及语义操作,并且要遵循递归下降的语法分析
- 4) output:格式化输出表格
那么在划分好模块,清楚要实现的功能之后,要做的第一件事就是为每个模块设计好其数据结构。
Tokenizer(取词器)
首先从Tokenizer开始。根据项目要求,对于文本的一个Token,需要记录的属性有:在文本的开始和结束的位置以及内容。那么很容易可以初步设计出一个结构体:
struct TextToken {
int begin;
int end;
string content;
};
有了TextToken的结构之后,我们需要将文本信息从文件中读取,存储到字符数组中,交由正则表达式引擎来帮助我们提取符合某一正则表达式的所有TextToken,保存在vector<vector<TextToken>>
中。这里需要一个读取文件的函数:
char *readText(const char* filePath) {
char* text;
ifstream file;
file.open(filePath);// open input file
int length;
if (!file) {
cout << "打开文件失败!" << endl;
return NULL;
}
file.seekg(0, std::ios::end); // go to the end
length = file.tellg(); // report location (this is the length)
file.seekg(0, std::ios::beg); // go back to the beginning
text = new char[length]; // allocate memory for a buffer of appropriate dimension
file.read(text, length); // read the whole file into the buffer
file.close();
return text;
}
以及用以返回TextToken集合的函数:
vector<vector<TextToken> > Tokenizer(char* regular, char* text) {
vector< vector<int> > result;
//调用正则表达式引擎
result = findall(regular,text);
vector<vector<TextToken> > return_result;
if (result.size() == 0) {
return return_result;
}
//搜索所有tokens
for (int i = 0 ; i < result[0].size() ; i+=2) {
vector<TextToken> col;
for (int j = 0 ; j < result.size() ; j++) {
TextToken temp;
temp.content = "";
for (int k = result[j][i] ; k < result[j][i+1] ; k++) {
temp.content += text[k];
}
temp.begin = result[j][i];
temp.end = result[j][i+1];
col.push_back(temp);
}
return_result.push_back(col);
}
return return_result;
}
Lexer(词法分析器)
当Lexer接收一条AQL语句时,它的职责就是从这个字符串中提取出AQL语言的非终结符序列、关键字等信息,加以处理之后将结果传给Parser。那么对于一整个Lexer类,我们可以将其细化为:
- LexerToken:与TextToken类似,但它是记录AQL语句中的每一个划分出来的单元,包含Type、value、位置等信息;
struct LexerToken
{
Type type;
string value;
int line;
int sen_num;
int begin;
int end;
LexerToken(string str, int line, int sen_num, int begin, int end, Type type) {
this->value = str;
this->line = line;
this->sen_num = sen_num;
this->begin = begin;
this->end = end;
this->type = type;
}
LexerToken() {
}
};
- Type:枚举类型,用以记录AQL语句中的所有保留字,可根据LexerToken中value的值来判断返回的枚举值
enum Type{
CREATE, VIEW, AS, OUTPUT, SELECT, FROM, EXTRACT, REGEX, ON, RETURN,
GROUP, AND, TOKEN, PATTERN, ID, DOT, REG, NUM, LESSTHAN, GREATERTHAN,
LEFTBRACKET, RIGHTBRACKET, CURLYLEFTBRACKET, CURLYRIGHTBRACKET, SEMICOLON, COMMA, EMPTY
};
Type judgeType(string value) {
if (value.compare("create") == 0) {
return CREATE;
}
else if (value.compare("view") == 0) {
return VIEW;
}
else if (value.compare("as") == 0) {
return AS;
}
else if (value.compare("output") == 0) {
return OUTPUT;
}
else if (value.compare("select") == 0) {
return SELECT;
}
else if (value.compare("from") == 0) {
return FROM;
}
else if (value.compare("extract") == 0) {
return EXTRACT;
}
else if (value.compare("regex") == 0) {
return REGEX;
}
else if (value.compare("on") == 0) {
return ON;
}
else if (value.compare("return") == 0) {
return RETURN;
}
else if (value.compare("group") == 0) {
return GROUP;
}
else if (value.compare("and") == 0) {
return AND;
}
else if (value.compare("Token") == 0) {
return TOKEN;
}
else if (value.compare("pattern") == 0) {
return PATTERN;
}
else if (value.compare(".") == 0) {
return DOT;
}
else if (value.compare("<") == 0) {
return LESSTHAN;
}
else if (value.compare(">") == 0) {
return GREATERTHAN;
}
else if (value.compare("(") == 0) {
return LEFTBRACKET;
}
else if (value.compare(")") == 0) {
return RIGHTBRACKET;
}
else if (value.compare("{") == 0) {
return CURLYLEFTBRACKET;
}
else if (value.compare("}") == 0) {
return CURLYRIGHTBRACKET;
}
else if (value.compare(";") == 0) {
return SEMICOLON;
}
else if (value.compare(",") == 0) {
return COMMA;
}
else
{
if (value[0] == '/' && value[value.length()-1] == '/')
{
return REG;
}
int flag = 1;
for (int i = 0; i < value.length(); i++)
{
if (value[i] > '9' || value[i] < '0') {
flag = 0;
}
}
if (flag == 1) {
return NUM;
}
return ID;
}
return EMPTY;
}
那么,结合LexerToken和Type的内容,我们得到Lexer类的具体实现:对于每一条AQL语句字符串,在字符串中准确地根据分隔符划分出LexerToken,记录其各个属性,完整后保存到vector<LexerToken> tokens
之中,再将tokens传给Parser,由Parser来完成它的工作。
#ifndef LEXER
#define LEXER
#include<iostream>
#include<fstream>
#include<stdio.h>
#include<vector>
#include<string>
#include"LexerToken.h"
#include"Type.cpp"
using namespace std;
bool isSeparator(char c) {
return c == '.' || c == ','
|| c == ';' || c == '<'
|| c == '>' || c == '('
|| c == ')' || c == '{'
|| c == '}';
}
class Lexer {
public:
Lexer(){
};
void startLexing(const char *filePath) {
tokens.clear();
ifstream file;
file.open(filePath);
string str= "";
int sen_num = 0;
int line = 0;
if (!file) {
cout << "文件打开失败!" << endl;
return;
}
while (getline(file, str))
{
string value = "";
int first = 0;
int last = 0;
int length = str.size();
int token_length = 0;
for (int i = 0; i < length; i++) {
while (!((str[i] == ' ')||(str[i] == '\t')||(str[i] == '\r')||(str[i] == '\n')) && i < length) {
if (str[i] == '/') {
value += str[i];
token_length++;
i++;
while (str[i] != '/') {
value += str[i];
token_length++;
i++;
}
}
else if (isSeparator(str[i])) {
break;
}
value += str[i];
token_length++;
i++;
//cout << value << endl;
}
if (token_length) {
last = i;
first = i - token_length;
LexerToken t(value, line, sen_num, first, last, judgeType(value));
tokens.push_back(t);
value = "";
token_length = 0;
}
if (isSeparator(str[i])) {
value += str[i];
token_length++;
last = i;
first = i - token_length;
LexerToken t(value, line, sen_num, first, last, judgeType(value));
tokens.push_back(t);
value = "";
token_length = 0;
if (str[i] == ';')sen_num++;
}
}
line++;
}
file.close();
};
~Lexer() {
if (tokens.size() != 0)
tokens.clear();
};
vector<LexerToken> getTokens() {
return tokens;
};
private:
vector<LexerToken> tokens;
};
#endif
Parser(语法分析器)
整个项目最重要的部分就是Parser了,因为涉及到的逻辑远超于Tokenizer和Lexer的部分。语法分析器要担任的职责就是根据词法分析器传进来的LexerTokens,利用递归下降的语法分析方法,逐渐将整条AQL语句分成原子语句,使得向前看遇到的下一个LexerToken可以下降至抽象语法树(AST)的叶子节点,执行对应的逻辑。
所设计的Parser类如下:
#ifndef PARSER
#define PARSER
#include<iostream>
#include<vector>
#include<map>
#include "Lexer.h"
#include "LexerToken.h"
#include "Tokenizer.cpp"
#include "Type.cpp"
#include "printview.cpp"
using namespace std;
// 存取ducument的数据结构
struct Doc {
char* text;
};
class Parser{
public:
// 构造函数,两个参数:document文件+AQL文件。
Parser(const char* inputFile, const char* aqlFile){
// 初始化position
position = 0;
// 初始化document
document.text = readText(inputFile);
// 词法分析
Lexer l;
l.startLexing(aqlFile);
lexer_tokens = l.getTokens();
// 切割全文所有tokens
all_text_tokens = Tokenizer("([0-9a-zA-Z_])+|[^\\f\\n\\r\\t\\v\' \']", document.text)[0];
};
//读取要解析的LexerToken
LexerToken readLexerToken() {
};
//程序运行入口
void run() {
};
//解析aql语句
void aql_stmt() {
};
//解析create语句
void create_stmt() {
};
//解析select
map<string,vector<TextToken> > select_stmt() {
};
//解析regex语句
map<string, vector<TextToken> > regex_stmt() {
};
// 解析pattern语句
map<string, vector<TextToken> > pattern_stmt() {
};
vector<TextToken> pattern_expr(map<string, map<string, vector<TextToken> > > from_map,
map<string, vector<TextToken> > &child_map) {
}
vector<TextToken> pattern_pkg(map<string, map<string, vector<TextToken> > > from_map) {
}
vector<TextToken> pattern_group(map<string, map<string, vector<TextToken> > > from_map,
map<string, vector<TextToken> > &child_map) {
}
vector<TextToken> pattern_regex(string regex) {
}
vector<TextToken> pattern_token(vector<TextToken> col1,
vector<TextToken> col2,
int min,
int max,
map<string, vector<TextToken> > &child_map,
bool is_col1_group,
bool is_col2_group) {
}
//输入beigin和end,根据all_text_tokens,返回begin和end中间有几个token
int token_num_between(int begin, int end) {
}
//解析return语句
map<string, string> return_stmt() {
};
//解析from语句
map<string, map<string, vector<TextToken> > > from_stmt() {
};
//创建表名和列的键值对
void createView(string view_name, map<string, vector<TextToken> > view_columns) {
};
//返回列里的内容
vector<TextToken> getColumn(string view_name, string column_name) {
}
}
int str2int(string s) {
int i;
stringstream ss;
ss<<s;
ss>>i;
return i;
}
string int2str(int num) {
stringstream ss;
ss << num;
string result = ss.str();
return result;
}
private:
// string view_name;
vector<LexerToken> lexer_tokens;
vector<TextToken> all_text_tokens;
Doc document;
//表——string是表名,嵌套map里是各个列
map<string, map<string, vector<TextToken> > > views;
LexerToken this_token;
int position;
};
#endif
这里遵循的AST结构如下(借用laiyuan同学的图),绿色的线表示函数调用,黑色表示终结符匹配:
整体思路大致相同,只是具体实现细节可能会因人而异,并且划分的细致程度也会有所区别。
比如:
aql_stmt → create_stmt ; | output_stmt ;
表明当输入一条aql时,判断下一个LexerToken的值
- 如果Type == CREATE, 则进入create_stmt函数;
- 如果Type == OUTPUT, 则进入output函数;
- 否则语法错误,因为目前实现只有这两种。
其他语句也大体如此,并且不同的语句会对应不同的处理逻辑,这里主要就是具体实现了,就不一一赘述。
Output(输出表格)
这部分相对于前面而言就比较简单了,主要实现准确提取view中的内容并格式化输出即可。
#include<iostream>
#include<map>
#include<vector>
#include<string>
#include<sstream>
#include "Tokenizer.cpp"
using namespace std;
string int_to_string(int a) {
stringstream ss;
ss << a;
return ss.str();
}
string outputwhiffletree(int length) {
string s = "";
for (int i = 0; i < length; i++) {
s += "-";
}
return s;
}
string outputwhitespace(int length) {
string s = "";
for (int i = 0; i < length; i++) {
s += " ";
}
return s;
}
void printView(map<string, vector<TextToken> > view, string view_name) {
//map<string, map<string, vector<TextToken> > >::iterator it = view.begin();
map<string, vector<TextToken> > col_information = view;
map<string, vector<TextToken> >::iterator iter;
map<string, vector<string> > output_string;
vector<TextToken> col;
vector<string> colname;
int *max_length = new int[col_information.size()];
int num = 0;
for (iter = col_information.begin(); iter != col_information.end(); iter++) {
string col_name = iter->first;
col = iter->second;
int max_string_length = 0;
vector<string> col_string;
for (int i = 0; i < col.size(); i++) {
string content_string = col[i].content;
string temp = " " + content_string + ":(" + int_to_string(col[i].begin) + "," + int_to_string(col[i].end)+") ";
col_string.push_back(temp);
if (max_string_length < temp.size())
max_string_length = temp.size();
temp.clear();
}
max_length[num] = max_string_length;
num++;
output_string.insert(pair<string, vector<string> >(col_name, col_string));
colname.push_back(col_name);
}
cout << "View: "<< view_name << endl;
if (col.size() == 0) {
cout << "empty set" << endl;
cout << endl;
return;
}
string first_line = "+";
for (int i = 0; i < col_information.size(); i++) {
first_line += outputwhiffletree(max_length[i]) + "+";
}
cout<<first_line << endl;
string sencond_line = "|";
for (int i = 0; i < col_information.size(); i++) {
sencond_line += " " + colname[i] + outputwhitespace(max_length[i] - colname[i].size()-1) + "|";
}
cout << sencond_line << endl;
cout << first_line << endl;
for (int i = 0; i < col.size(); i++) {
cout << "|";
for (int j = 0; j < col_information.size(); j++) {
cout << output_string[colname[j]][i] + outputwhitespace(max_length[j] - output_string[colname[j]][i].size() ) << "|";
}
cout << endl;
}
cout << first_line << endl;
cout << col.size() <<" rows in set" << endl;
cout << endl;
}
项目收获
作为编译原理期末project,AQL Subset确实是一个很好的项目。刚刚开始时会觉得无从下手,因为上课学习的知识还没有实践过。这一个project让我们很好地体会了编译器自顶向下、分治的思想,让我们学习到如何将一个大项目逐层分解,直到分成个人可以完成的模块,与队友分工合作。而我自己在队伍中承担的任务是为整个项目设定模块的数据结构、相关类的内容组成以及后期代码整合、debug等任务。在完成任务的过程中,确实认识到一个好的架构对于项目的影响是多么巨大。前期项目进度有些缓慢就是因为架构不够完备,在出现问题时才去对架构进行调整,因此而牵扯到的代码量还不小。好在与队友互相交流之后,有惊无险地完成了本次project。