化学式解析器c++

Chemical formula parser C++

本文关键字:c++ 化学式      更新时间:2023-10-16

我目前正在研究一个程序,它可以解析化学式并返回分子量和组成百分比。下面的代码可以很好地处理化合物,如H2O、LiOH、CaCO3,甚至C12H22O11。然而,它不能理解括号内多原子离子的化合物,如(NH4)2SO4

我不是在找一个人来为我写程序,只是给我一些提示,告诉我如何完成这样的任务。

当前,程序遍历输入的字符串raw_molecule,首先找到每个元素的原子序数,并将其存储在一个向量中(我使用map<string, int>来存储名称和原子#)。然后查找每个元素的数量。

bool Compound::parseString() {
map<string,int>::const_iterator search;
string s_temp;
int i_temp;
for (int i=0; i<=raw_molecule.length(); i++) {
if ((isupper(raw_molecule[i]))&&(i==0))
s_temp=raw_molecule[i];
else if(isupper(raw_molecule[i])&&(i!=0)) {
// New element- so, convert s_temp to atomic # then store in v_Elements
search=ATOMIC_NUMBER.find (s_temp);
if (search==ATOMIC_NUMBER.end()) 
return false;// There is a problem
else
v_Elements.push_back(search->second); // Add atomic number into vector
s_temp=raw_molecule[i]; // Replace temp with the new element
}
else if(islower(raw_molecule[i]))
s_temp+=raw_molecule[i]; // E.g. N+=a which means temp=="Na"
else
continue; // It is a number/parentheses or something
}
// Whatever's in temp must be converted to atomic number and stored in vector
search=ATOMIC_NUMBER.find (s_temp);
if (search==ATOMIC_NUMBER.end()) 
return false;// There is a problem
else
v_Elements.push_back(search->second); // Add atomic number into vector
// --- Find quantities next --- // 
for (int i=0; i<=raw_molecule.length(); i++) {
if (isdigit(raw_molecule[i])) {
if (toInt(raw_molecule[i])==0)
return false;
else if (isdigit(raw_molecule[i+1])) {
if (isdigit(raw_molecule[i+2])) {
i_temp=(toInt(raw_molecule[i])*100)+(toInt(raw_molecule[i+1])*10)+toInt(raw_molecule[i+2]);
v_Quantities.push_back(i_temp);
}
else {
i_temp=(toInt(raw_molecule[i])*10)+toInt(raw_molecule[i+1]);
v_Quantities.push_back(i_temp);
}
}
else if(!isdigit(raw_molecule[i-1])) { // Look back to make sure the digit is not part of a larger number
v_Quantities.push_back(toInt(raw_molecule[i])); // This will not work for polyatomic ions
}
}
else if(i<(raw_molecule.length()-1)) {
if (isupper(raw_molecule[i+1])) {
v_Quantities.push_back(1);
}
}
// If there is no number, there is only 1 atom. Between O and N for example: O is upper, N is upper, O has 1.
else if(i==(raw_molecule.length()-1)) {
if (isalpha(raw_molecule[i]))
v_Quantities.push_back(1);
}
}
return true;
}

这是我的第一个帖子,所以如果我包含的信息太少(或可能太多),请原谅我。

虽然您可能能够做一个类似于临时扫描程序的事情来处理一级父级,但用于此类事情的规范技术是编写一个真正的解析器。

有两种常见的方法…

递归下降
  1. 机器生成的基于语法规范文件的自底向上解析器。

(从技术上讲,还有第三类,PEG,它是机器自顶向下生成的。)

无论如何,对于情形1,当您看到(时,您需要编写对解析器的递归调用,然后从此递归级别返回)标记。

通常创建一个树状的内部表示;这被称为语法树,但在您的情况下,您可能可以跳过它,只返回递归调用的原子量,并添加到将从第一个实例返回的级别。

对于情形2,您需要使用像yacc这样的工具来将语法转换为解析器。

您的解析器可以理解某些事情。它知道当它看到N时,这意味着"氮原子类型"。当它看到O时,它意味着"氧型原子"。

这与c++中标识符的概念非常相似。当编译器看到int someNumber = 5;时,它说:"存在一个int类型的名为someNumber的变量,其中存储了数字5"。如果您稍后使用名称someNumber,它知道您在谈论someNumber(只要您在正确的范围内)。

回到原子解析器。当解析器看到一个原子后面跟着一个数字时,它就知道将这个数字应用到那个原子上。所以O2的意思是2个氧原子。N2表示"2个氮原子型"。

这对解析器意味着一些东西。这意味着仅仅看到一个原子是不够的。这是一个很好的开始,但是仅仅知道分子中有多少原子是不够的。它需要读取下一个内容。因此,如果它看到O后面跟着N,它就知道O表示"1个氧原子"。如果它看到O后面什么都没有(输入的末尾),那么它再次表示"1个氧原子"。

这是你目前拥有的。但是是错的。因为数字并不总能改变原子;有时,它们修饰原子的基团。如(NH4)2SO4.

所以现在,您需要更改解析器的工作方式。当它看到O时,它需要知道这不是"氧原子类型"。它是一个">含氧"。O2为"2 Groups containing Oxygen"

一个基团可以包含一个或多个原子。所以当你看到(时,你知道你正在创建一个。因此,当您看到(...)3时,您看到的是"3 Groups containing…"。

那么(NH4)2是什么?它是"2个基团含[1个基团含氮,后面跟着4个基团含氢]"。

这样做的关键是理解我刚刚写的内容。组可以包含其他组。它们成群筑巢。如何实现嵌套?

你的解析器现在看起来是这样的:

NumericAtom ParseAtom(input)
{
Atom = ReadAtom(input); //Gets the atom and removes it from the current input.
if(IsNumber(input)) //Returns true if the input is looking at a number.
{
int Count = ReadNumber(input); //Gets the number and removes it from the current input.
return NumericAtom(Atom, Count);
}
return NumericAtom(Atom, 1);
}
vector<NumericAtom> Parse(input)
{
vector<NumericAtom> molecule;
while(IsAtom(input))
molecule.push_back(ParseAtom(input));
return molecule;
}

您的代码调用ParseAtom(),直到输入耗尽,将每个原子+计数存储在数组中。显然,这里有一些错误检查,但我们现在先忽略它。

您需要做的是停止解析原子。您需要解析,它们可以是单个原子,也可以是由()对表示的一组原子。

Group ParseGroup(input)
{
Group myGroup; //Empty group
if(IsLeftParen(input)) //Are we looking at a `(` character?
{
EatLeftParen(input); //Removes the `(` from the input.
myGroup.SetSequence(ParseGroupSequence(input)); //RECURSIVE CALL!!!
if(!IsRightParen(input)) //Groups started by `(` must end with `)`
throw ParseError("Inner groups must end with `)`.");
else
EatRightParen(input); //Remove the `)` from the input.
}
else if(IsAtom(input))
{
myGroup.SetAtom(ReadAtom(input)); //Group contains one atom.
}
else
throw ParseError("Unexpected input."); //error
//Read the number.
if(IsNumber(input))
myGroup.SetCount(ReadNumber(input));
else
myGroup.SetCount(1);
return myGroup;
}
vector<Group> ParseGroupSequence(input)
{
vector<Group> groups;
//Groups continue until the end of input or `)` is reached.
while(!IsRightParen(input) and !IsEndOfInput(input)) 
groups.push_back(ParseGroup(input));
return groups;
}

这里最大的区别是ParseGroup(类似于ParseAtom函数)将调用ParseGroupSequence。它将调用ParseGroup。可以调用ParseGroupSequence。等。Group既可以包含一个原子,也可以包含Groups序列(如NH4),存储为vector<Group>

当函数可以直接或间接调用自己时,称为递归。只要不是无限递归,这是可以的。这是不可能的,因为它只会在每次看到(时递归。

这是如何工作的呢?那么,让我们考虑一些可能的输入:

NH3

调用
  1. ParseGroupSequence。它不在input或)的末尾,所以它调用ParseGroup
    1. ParseGroup看到N,它是一个原子。它把这个原子加到Group上。然后它看到一个H,它不是一个数字。所以它设置Group的计数为1,然后返回Group
  2. ParseGroupSeqeunce中,我们将返回的组存储在序列中,然后在循环中迭代。我们没有看到input或)的结束,所以它调用ParseGroup:
    1. ParseGroup看到一个H,它是一个原子。它把这个原子加到Group上。然后它看到一个3,这是一个数字。因此它读取这个数字,将其设置为Group的计数,并返回Group
  3. 回到ParseGroupSeqeunce,我们将返回的Group存储在序列中,然后在循环中迭代。我们没有看到),但是我们看到了输入的结束。所以我们返回当前的vector<Group>

(NH3) 2

调用
  1. ParseGroupSequence。它不在input或)的末尾,所以它调用ParseGroup
    1. ParseGroup看到一个(,这是一个Group的开始。它吃掉这个字符(从输入中删除它)并在Group上调用ParseGroupSequence
      1. ParseGroupSequence不在input或)的末尾,所以它调用ParseGroup
        1. ParseGroup看到N,这是一个原子。它把这个原子加到Group上。然后它看到一个H,它不是一个数字。所以它设置组的计数为1,然后返回Group
      2. 回到ParseGroupSeqeunce,我们将返回的组存储在序列中,然后在循环中迭代。我们没有看到input或)的结束,所以它调用ParseGroup:
        1. ParseGroup看到H,这是一个原子。它把这个原子加到Group上。然后它看到一个3,这是一个数字。因此它读取这个数字,将其设置为Group的计数,并返回Group
      3. ParseGroupSeqeunce中,我们将返回的组存储在序列中,然后在循环中迭代。我们没有看到输入的结尾,但是我们看到看到)。所以我们返回当前的vector<Group>
  2. 回到ParseGroup的第一个调用中,我们得到vector<Group>。我们把它作为序列插入到当前的Group中。我们检查下一个字符是否为),吃掉它,然后继续。我们看到一个2,它是一个数字。因此它读取这个数字,将其设置为Group的计数,并返回Group
现在,回到最初的ParseGroupSequence调用,我们将返回的Group存储在序列中,然后在循环中迭代。我们没有看到),但是我们看到了输入的结束。所以我们返回当前的vector<Group>

。该解析器使用递归"下降"到每个组。因此,这种解析器被称为"递归下降解析器"(对于这种东西有一个正式的定义,但这是对概念的一个很好的外行理解)。

为您想要读取和识别的字符串写下语法规则通常是有帮助的。语法就是一堆规则,这些规则规定了哪些字符序列是可以接受的,哪些是不可接受的。它有助于在编写程序之前和编写程序时使用语法,并且可以将其输入解析器生成器(如DigitalRoss所描述的)

例如,没有多原子离子的简单化合物的规则如下:

Compound:  Component { Component };
Component: Atom [Quantity] 
Atom: 'H' | 'He' | 'Li' | 'Be' ...
Quantity: Digit { Digit }
Digit: '0' | '1' | ... '9'
  • [...]被读取为可选的,并且将在程序中进行if测试(要么存在,要么缺失)
  • |是备选项,if也是备选项。否则如果…else或switch 'test',表示输入必须匹配以下
  • 中的一个
  • { ... }被读取为0或更多的重复,并且将在程序
  • 中作为while循环。
  • 引号之间的字符是字符串中的文字字符。所有其他单词都是规则的名称,对于递归下降解析器来说,最终都是被调用来分割和处理输入的函数的名称。

例如,实现'Quantity'规则的函数只需要读取一个或多个数字字符,并将其转换为整数。实现Atom规则的函数读取足够的字符以确定它是哪个原子,并将其存储起来。

递归下降解析器的一个优点是错误消息可能非常有用,其形式为"期望一个Atom名称,但得到%c",或"期望一个')',但到达字符串的末尾"。发生错误后恢复有点复杂,因此您可能希望在第一个错误时抛出异常。

那么多原子离子只是一层括号吗?如果是,语法可能是:

Compound: Component { Component }  
Component: Atom [Quantity] | '(' Component { Component } ')' [Quantity];
Atom: 'H' | 'He' | 'Li' ...
Quantity: Digit { Digit }
Digit: '0' | '1' | ... '9'

还是更复杂,符号必须允许嵌套括号。一旦弄清楚了这一点,您就可以找出一种解析方法。

我不知道你的问题的全部范围,但是递归下降解析器编写起来相对简单,并且看起来适合你的问题。

考虑将你的程序重构为一个简单的递归下降解析器。

首先,您需要更改parseString函数,使其接受要解析的string,以及通过引用传递的要开始解析的当前位置。

这样你可以构造你的代码,当你看到一个(你调用相同的函数在下一个位置得到一个Composite返回,并消耗关闭的)。当您看到单独的)时,您返回而不使用它。这使您可以使用()无限嵌套的公式,尽管我不确定是否有必要(距离我上次看到化学式已经有20多年了)。

这样,您只需编写一次解析复合的代码,并根据需要多次重用它。这将很容易补充您的阅读器使用的公式与破折号等,因为您的解析器将只需要处理基本的构建块。

也许您可以在解析之前去掉括号。你需要找出有多少"括号中的括号"(抱歉我的英语不好),然后重写成以"最深的"开头:

  1. (NH 4<子>(Na<子>2 H<子>4)<子>3锌)2<子><子>4(这个公式并不意味着什么事,实际上…)

  2. (NH 4<子>Na<子>6 H<子>12锌)2<子><子>4

  3. NH<子>8子> 1224 H<子>锌<子>2<子>4

  4. 没有括号,让我们运行NH8Na12H24Zn2SO4