扫描仪和扫描仪生成器






4.95/5 (9投票s)
2001年4月10日
2分钟阅读

101214

2710
支持在同一个对象中实现两种常见的扫描器方法。
引言
扫描器将字符流分解为一系列标记。这与人类读者将字符分组为单词、数字和标点符号,从而达到更高的抽象层次类似。文本
0-201-05866-9, cancelled, "Parallel Program Design"
例如,可以被翻译成以下标记
T_ISBN T_COMMA T_CANCELLED T_COMMA T_TITLE
其中 T_ISBN
、T_COMMA
等是整数常量。实现通用扫描器有两种方法。
- 扫描器是类的一个对象。
搜索的标记通过调用成员函数来指定。
- 扫描器是根据正则表达式自动生成的。
这是一个两阶段方法。首先,你指定扫描器,然后运行生成器,它会输出“C”源代码。
第一种方法更适合于频繁更改的项目。第二种方法可以提供卓越的性能,但缺点是生成的“C”代码对于人类来说几乎无法阅读,因此不应进行编辑。
本文中介绍的扫描器和扫描器生成器结合了两种方法,并为两种实现策略提供了一个接口。
扫描器类的接口
扫描器REXI_Scan
基于已经发表在文章“快速正则表达式”中的正则表达式设施。要使用它,您需要- 为每个要识别的标记指定一个正则表达式。
- 设置源字符串。
- 重复调用
Scan
,直到它返回REXI_Scan::eEos
。
class REXI_Scan : public REXI_Base { public: REXI_Scan(char cLineBreak= '\n'); //related function 'GetNofLines' /*initialize scanner with symbol definitions 1.STEP */ REXI_DefErr AddSymbolDef (string strRegExp,int nIdAnswer); REXI_DefErr AddHelperRegDef (string strName,string strRegExp); REXI_DefErr SetToSkipRegExp (string strRegExp= "[ \r\n\v\t]*"); /* set source 2.STEP */ inline void SetSource (const char* pszSource); /* Read next token, then return symbolId ('nIdAnswer' from 'AddSymbolDef') 3.STEP */ int Scan (); /* retrieve,set information after a call to 'Scan' */ inline string GetTokenString ()const; inline void SkipChars (int nOfChars=1); inline int GetLastSymbol ()const; inline int GetNofLines ()const; };
示例用法
enum ESymbol{T_ERR,T_AVAILABLE,T_CANCELLED, T_COMMA,T_LINEBREAK,T_ISBN,T_TITLE}; struct Info{ Info():m_eKey(T_ERR){} string m_sISBN; ESymbol m_eKey; string m_sTitle; }; int main(int argc,char* argv[]) { const int ncOk= REXI_DefErr::eNoErr; const char szTestSrc[]= "3-8272-5737-9,AVAILABLE, \"XML praxis und referenz\"\r\n" "0-201-05866-9,cancelled, \"Parallel Program Design\"\r\n"; REXI_Scan scanner; REXI_DefErr err; /* STEP 1: initialize scanner with symbol definitions */ err= scanner.AddSymbolDef ("(AVAILABLE)\\i",T_AVAILABLE); assert(err.eErrCode==ncOk); err= scanner.AddSymbolDef ("(CANCELLED)\\i",T_CANCELLED); assert(err.eErrCode==ncOk); err= scanner.AddSymbolDef (",",T_COMMA); assert(err.eErrCode==ncOk); err= scanner.AddSymbolDef ("\\n",T_LINEBREAK); assert(err.eErrCode==ncOk); err= scanner.AddHelperRegDef("$Int_","[0-9]+\\-"); assert(err.eErrCode==ncOk); err= scanner.AddSymbolDef ("$Int_ $Int_ $Int_ [0-9]+", T_ISBN); assert(err.eErrCode==ncOk); err= scanner.AddSymbolDef (" \"( [^\"\\n] | \\\"] )* \"", T_TITLE); assert(err.eErrCode==ncOk); err= scanner.SetToSkipRegExp("[ \\t\\v\\r]*"); assert(err.eErrCode==ncOk); /* STEP 2 : set source */ scanner.SetSource(szTestSrc); int nNofLines=0; int nRes; Info info; vector<Info> vecInfos; /* STEP 3: read until eos */ while( (nRes=scanner.Scan())!=REXI_Scan::eEos ){ switch(nRes){ case T_AVAILABLE: case T_CANCELLED: info.m_eKey= (enum ESymbol)nRes; break; case T_TITLE: info.m_sTitle= scanner.GetTokenString(); break; case T_ISBN: info.m_sISBN= scanner.GetTokenString(); break; case T_LINEBREAK: vecInfos.push_back(info); info= Info(); break; case REXI_Scan::eIllegal: cout << "Illegal:" << scanner.GetTokenString() << endl; while( (nRes=scanner.Scan())!=REXI_Scan::eEos && nRes!= T_LINEBREAK); info= Info(); break; } } cout << "Number of correct read records: " << vecInfos.size() << endl; char c; cin >> c; return 0; }
扫描器生成器的接口
扫描器生成器是一个非常简单的 GUI 程序。它允许您指定和运行测试扫描器,并最终生成指定扫描器的源代码。生成的代码使用一个 REXI_Scan
派生的扫描器,并提供两个不同的代码部分。通过条件指令 #ifdef REXI_STATIC_SCANNER
控制,激活一个高效的硬编码扫描器或像上面描述的那样工作的扫描器。
要生成的扫描器的规范使用正则表达式,并支持 4 种不同的方式来指定一个标记,如下所示。
if #T_Quote= '[^']' $Int= [0-9]+ ##T_FLOAT= $Int (\. $Int)?
重要的是,您需要用制表符分隔标记定义。现在,让我们看看上述 4 种定义意味着什么。
1. Token if The scanner searches for exactly the word 'if'
and automagically creates a constant T_if for the token
2. Token #T_Quote= '[^']' The leading # means:
The next identifier up to the = is the name of the token constant,
then the token definition follows.
3. Helper $Int= [0-9]+ Defines a helper definition,
which can be used later.
4. Token ##T_FLOAT= $Int (\. $Int)?
The leading ## means: Same as # but do postprocessing
after recognizing this token.
生成的扫描器片段
int Simple::Scan() { #ifdef REXI_STATIC_SCANNER int nRes= FastScan(); #else int nRes= REXI_Scan::Scan(); #endif switch(nRes){ case eIllegal:{ m_sIllegal= GetTokenString(); return nRes; } case T_PRICE:{ // add your postprocessing code here return nRes; } default: return nRes; } }
预期用途
扫描逗号分隔的文件、为 C++ 源代码实现美化打印器或为解释器构建扫描器都是潜在的应用领域。 还有很多免费可用的扫描器生成器(lex、bison),但据我所知,没有一个生成具有如此简洁接口的扫描器。