我目前正在开发一个程序,可以解析化学式并返回分子量和百分比组成。以下代码适用于H 2 O,LiOH,CaCO 3 等化合物,甚至C 12 H 22 < /子> 0 <子> 11 子>。但是,它无法理解括号内的多原子离子,如(NH 4 ) 2 SO 4 。
我不是在寻找一个必须为我编写程序的人,而是给我一些关于如何完成这项任务的提示。
目前,程序迭代输入的字符串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;
}
这是我的第一篇文章,所以如果我收集的信息太少(或者说太多),请原谅我。
答案 0 :(得分:6)
虽然你可以做一个类似ad-hoc扫描器的东西来处理一个级别的parens,但是用于这类事情的规范技术是编写一个真正的解析器。
有两种常见的方法可以做到这一点......
(从技术上讲,还有第三类,PEG,机器生成自上而下。)
无论如何,对于案例1,当您看到(
时,需要对您的解析器进行递归调用,然后在)
上从此递归级别返回令牌。
通常会创建一个树状内部表示;这被称为一个语法树,但在你的情况下,你可以跳过它,只返回递归调用的原子权重,增加你将从第一个实例返回的级别。
对于案例2,您需要使用像 yacc 这样的工具将语法转换为解析器。
答案 1 :(得分:4)
您的解析器了解某些事情。它知道当它看到N
时,这意味着&#34;氮原子的类型&#34;。当它看到O
时,它意味着&#34;氧气类型原子&#34;。
这与C ++中的标识符概念非常相似。当编译器看到int someNumber = 5;
时,它会说,&#34;存在名为someNumber
int
类型的变量,其中存储了数字5
&#34;。如果您以后使用名称someNumber
,则表示您已经在谈论 someNumber
(只要您处于正确的范围内)。
返回原子解析器。当你的解析器看到一个后跟一个数字的原子时,它知道将该数字应用于那个原子。所以O2
表示&#34; 2氧原子类型&#34;。 N2
表示&#34; 2原子氮类型。&#34;
这对您的解析器来说意味着什么。这意味着看到一个原子并不足够。这是一个良好的开端,但仅知道分子中存在多少原子是不够的。它需要阅读下一件事。因此,如果它看到O
后跟N
,则它知道O
表示&#34; 1氧气类型原子&#34;。如果它看到O
后面没有任何东西(输入结束),那么它再次表示&#34; 1氧气类型原子&#34;。
你现在拥有的是什么。但它错误。因为数字不总是修改原子;有时,他们会修改原子的组。与(NH4)2SO4
中一样。
现在,您需要更改解析器的工作方式。当它看到O
时,它需要知道这不是&#34;氧气类型的原子&#34;。它是含有氧气的&#34; 组&#34;。 O2
是&#34; 2个包含氧气的组&#34;。
一个组可以包含一个或多个原子。因此,当您看到(
时,您就知道自己正在创建组。因此,当您看到(...)3
时,您会看到&#34; 3个包含...&#34;的组。
那么,(NH4)2
是什么?它是含有[1个含有氮的基团,然后含有4个含氢基团的基团]&#34;
这样做的关键是理解我刚写的内容。群组可以包含其他群组。有小组嵌套。你如何实现嵌套?
好吧,你的解析器目前看起来像这样:
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()
,直到输入干涸,将每个原子+计数存储在一个数组中。显然你在那里有一些错误检查,但是现在让我们忽略它。
您需要做的是停止解析原子。您需要解析 groups ,它们是单个原子,或由()
对表示的一组原子。
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
可以包含一个原子或一系列Group
s(例如NH4
),存储为vector<Group>
当函数可以自己调用(直接或间接)时,它被称为递归。这是好的,只要它不会无限地递归。并且没有机会,因为它只会在每次看到(
时递归。
那么这是如何工作的?好吧,让我们考虑一些可能的输入:
ParseGroupSequence
被调用。它不在输入的末尾或)
,因此它会调用ParseGroup
。
ParseGroup
看到N
,这是一个原子。它将此原子添加到Group
。然后它会看到H
,这不是数字。因此,它将Group
的计数设置为1,然后返回Group
。ParseGroupSeqeunce
,我们将返回的组存储在序列中,然后在循环中迭代。我们看不到输入的结尾或)
,因此它会调用ParseGroup
:
ParseGroup
看到H
,这是一个原子。它将此原子添加到Group
。然后它会看到3
,这是一个数字。因此,它会读取此数字,将其设置为Group
的计数,然后返回Group
。ParseGroupSeqeunce
,我们将返回的Group
存储在序列中,然后在循环中迭代。我们没有看到)
,但我们做看到输入结束。所以我们返回当前的vector<Group>
。ParseGroupSequence
被调用。它不在输入的末尾或)
,因此它会调用ParseGroup
。
ParseGroup
会看到(
,这是Group
的开头。它会吃掉这个字符(从输入中删除它)并在ParseGroupSequence
上调用Group
。
ParseGroupSequence
不在输入末尾或)
,因此会调用ParseGroup
。
ParseGroup
看到N
,这是一个原子。它将此原子添加到Group
。然后它会看到H
,这不是数字。因此,它将组的计数设置为1,然后返回Group
。ParseGroupSeqeunce
,我们将返回的组存储在序列中,然后在循环中迭代。我们看不到输入的结尾或)
,因此它会调用ParseGroup
:
ParseGroup
看到H
,这是一个原子。它将此原子添加到Group
。然后它会看到3
,这是一个数字。因此,它会读取此数字,将其设置为Group
的计数,然后返回Group
。ParseGroupSeqeunce
,我们将返回的组存储在序列中,然后在循环中迭代。我们没有看到输入结束,但我们执行请参阅)
。所以我们返回当前的vector<Group>
。ParseGroup
,我们返回vector<Group>
。我们将它作为序列粘贴到我们当前的Group
中。我们检查下一个字符是否为)
,吃掉它,然后继续。我们看到2
,这是一个数字。因此,它会读取此数字,将其设置为Group
的计数,然后返回Group
。ParseGroupSequence
调用,我们将返回的Group
存储在序列中,然后在循环中迭代。我们没有看到)
,但我们做看到输入结束。所以我们返回当前的vector<Group>
。此解析器使用递归到#34;下降&#34;进入每个小组。因此,这种解析器被称为&#34;递归下降解析器&#34; (这是对这类事情的正式定义,但这是对这一概念的良好理解)。
答案 2 :(得分:3)
写下您想要阅读和识别的字符串的语法规则通常很有帮助。语法只是一堆规则,它们说明什么样的字符序列是可以接受的,并且暗示是不可接受的。它有助于在编写程序之前和编写程序时使用语法,并且可能会被提供给解析器生成器(如DigitalRoss所述)
例如,没有多原子离子的简单化合物的规则如下:
Compound: Component { Component };
Component: Atom [Quantity]
Atom: 'H' | 'He' | 'Li' | 'Be' ...
Quantity: Digit { Digit }
Digit: '0' | '1' | ... '9'
[...]
被视为可选项,并且将在程序中进行if
测试(无论是存在还是缺失)|
是替代方案,if .. else if .. else或switch'test'也是如此,它说输入必须匹配其中一个{ ... }
被读作重复0或更多,并且在程序中将是一个while循环例如,实现“数量”规则的函数只需读取一个或多个数字字符,并将它们转换为整数。实现Atom
规则的函数会读取足够多的字符来确定它是哪个原子,并将其存储起来。
关于递归下降解析器的一个好处是错误消息可能非常有用,并且形式为“期望一个Atom名称,但得到%c”或“期待一个”)但是到达了字符串的结尾”。在发生错误后恢复有点复杂,因此您可能希望在第一个错误时抛出异常。
多原子离子只是一个括号水平?如果是这样,语法可能是:
Compound: Component { Component }
Component: Atom [Quantity] | '(' Component { Component } ')' [Quantity];
Atom: 'H' | 'He' | 'Li' ...
Quantity: Digit { Digit }
Digit: '0' | '1' | ... '9'
或者它是否更复杂,并且符号必须允许嵌套括号。一旦清楚,您就可以找到解析的方法。
我不知道问题的整个范围,但递归下降解析器编写相对简单,并且看起来足以解决您的问题。
答案 3 :(得分:1)
考虑将您的计划重新构建为简单的Recursive Descent Parser。
首先,您需要更改parseString
函数以使string
被解析,以及从中开始解析的当前位置,通过引用传递。
通过这种方式,您可以构建代码,以便在看到(
时,在下一个位置调用相同的函数返回Composite
,并使用关闭)
。当您单独看到)
时,您将返回而不会消耗它。这使您可以使用(
和)
无限嵌套的公式,虽然我不确定是否有必要(自上次看到化学式以来已超过20年)。
通过这种方式,您只需编写一次解析复合的代码,并根据需要多次重复使用它。很容易补充你的读者使用破折号等公式,因为你的解析器只需要处理基本的构建块。
答案 4 :(得分:0)
也许你可以在解析之前摆脱括号。你需要找到有多少“括号中的括号”(对不起我的英语)并将其重写为“最深”的开头:
(NH <子> 4 子>(钠<子> 2 子>ħ<子> 4 子>)<子> 3 子> Zn)的<子> 2 SO 4 (这个公式并不意味着任何,实际上......)
(NH <子> 4 子>娜<子> 6 子>ħ<子> 12 子> Zn)的<子> 2 子> SO <子> 4 < /子>
NH <子> 8 子>娜<子> 12 子>ħ<子> 24 子>锌<子> 2 子> SO <子> 4 子>
没有括号,让我们运行你的代码NH 8 Na 12 H 24 Zn 2 SO <子> 4 子>