史上最最最没用程序——自写平衡化学方程式

创作历史

前言

在几天前无意中看到了有这么一个 bce 库,能够平衡化学方程式系数,我就想着这不就是配配元素嘛,自己按看到的例子进行尝试,结果…
程序设计上越来越复杂,也不知道他那 bce-console.exe 仅仅 104KB 大小的程序怎么就能如此迅速配平化学方程(可能比较懒,所以像后面电子守恒和未知数关系的我就没有去研究如何做对应的解决方案)。

例子

>> P+O2=P2O5
4P+5O2=2P2O5
>> H2O(g)+Fe=Fe3O4+H2
4H2O(g)+3Fe=Fe3O4+4H2
>> CO+Fe2O3=Fe+CO2
3CO+Fe2O3=2Fe+3CO2
>> C2H5OH+O2=CO2+H2O
C2H5OH+3O2=2CO2+3H2O
>> Cl2+<e->=Cl<e->
Cl2+2<e->=2Cl<e->
>> Cu+Fe<3e+>=Cu<2e+>+Fe<2e+>
Cu+2Fe<3e+>=Cu<2e+>+2Fe<2e+>
>> LiOH+H2O2+H2O=Li2O2.H2O2.3H2O
2LiOH+2H2O2+H2O=Li2O2.H2O2.3H2O
>> C{n}H{2n+2}+O2=CO2+H2O
{(n+1)^(-1)}C{n}H{2*n+2}+{(1/2)*(3*n+1)/(n+1)}O2={n/(n+1)}CO2+H2O
>> CH3(CHCH){n}CH3+Cl2=CH3(CHClCHCl){n}CH3
CH3(CHCH){n}CH3+{n}Cl2=CH3(CHClCHCl){n}CH3
>> X-<e->=X<{n}e+>
X-{n}<e->=X<{n}e+>
>> CH4;HCN;NH3;O2;H2O
2HCN+6H2O=2CH4+2NH3+3O2
>> CH4(g);HCN(g);NH3(g);O2(g);H2O(g)
2CH4(g)+2NH3(g)+3O2(g)=2HCN(g)+6H2O(g)

运行截图

bce-console.exe运行结果(pip install bce安装)

史上最最最没用程序——自写平衡化学方程式

总程序运行结果

史上最最最没用程序——自写平衡化学方程式

需要的库

import re  # 正则表达式库,用于提取元素和数目
import string  # 获取所有空白字符,替换掉化学方程式中多余空白字符以防出现意外
from sympy import solve, symbols  # 用于解方程的库,solve是求解函数,symbols可以生成符号变量

步骤

这里采用等式左右两边所有化学元素数量种类守恒的方式来解决配平问题。

拆分等式左右侧

由于本文不考虑其他守恒规则,所以先过滤掉其他规则的化学方程式(最后只剩下例子的前4个)。
这里的 leftright 就是化学方程式等号左右两边的内容。

if '=' not in ChemicalEquations and ';' in ChemicalEquations:
    all_items = ChemicalEquations.split(';')
    pass  # 自动化合价计算等式先不做,先做元素配平
elif 'e+>' in ChemicalEquations or 'e->' in ChemicalEquations or '{' in ChemicalEquations or '.' in ChemicalEquations:
    pass  # 暂时不搞电子<e+>/<e->和含./{n}的化学物质等的运算
else:
    left, right = ChemicalEquations.split('=')

拆分每个化学物质和±号

化学物质组成

欲获得化学物质,首先得知道有哪些化学元素符号。
当然这边的化学物质是包括电子和未知数倍数关系提取的,所以很长。
这结果并不是完全只有化学物质,还包括了化学物质后的 ± 号,虽然没有电子参与一般都是 + 号。

1、倍数元素:\d|{[A-Za-z0-9+-]}
2、电荷元素:<(?:\d|{[A-Za-z0-9+-]})?e[+-]>
3、化学元素:[A-Z][a-z]?\.?
4、物质状态:\([gls]\)  # g是气体,l是液体,s是固体
5、化学物质:[+-]?(<(?:\d+|{[A-Za-z0-9+-]+})?e[+-]>|(?:(?:\(?(?:[A-Z][a-z]?\.?)+(?:\d+|{[A-Za-z0-9+-]+})?\)?)+(?:<(?:\d+|{[A-Za-z0-9+-]+})?e[+-]>)?)+)(?:\([gls]\))?([+-]?)
6、[②]物质:[+-]?(<(?:\d+|{[A-Za-z0-9+-]+})?e[+-]>|(?:[A-Za-z0-9.]+(?:\([A-Za-z0-9]+\))?(?:{[A-Za-z0-9+-]+})?(?:<(?:\d+|{[A-Za-z0-9+-]+})?e[+-]>)?)+)(?:\([gls]\))?([+-]?)

化学物质提取

left, right = ChemicalEquations.split('=')
items = re.compile(R'[+-]?(<(?:\d+|{[A-Za-z0-9+-]+})?e[+-]>|(?:(?:\(?(?:[A-Z][a-z]?\.?)+(?:\d+|{[A-Za-z0-9+-]+})?\)?)+(?:<(?:\d+|{[A-Za-z0-9+-]+})?e[+-]>)?)+)(?:\([gls]\))?([+-]?)')
left_items, right_items = items.findall(left), items.findall(right)

到这里所有化学物质和运算符 ± 就全部被提取出来了,如何用一行代码让他们从二层列表中脱出来到一层列表呢?用万能的匿名表达式就能实现

get_all = lambda _, __ = eval("[]"): __.clear() or [[__.append(____) for ____ in ___ if ____] for ___ in _] and __

这里为什么要用 __ = eval("[]") 而不用 __ = []__ = list() 呢?这里先简单的解释一下 python 是怎么帮我们把值赋给变量的:
python会把同一值所在地址赋值给变量,也就是说,两个不同变量名赋值同一值,他们的内存地址相同【id(XXX)相同】。所以这边如果赋值为空列表,那会干扰其他同时运行的空列表,所以使用空列表作为默认参数会有问题,解释器会报凸显的黄色,而使用 eval("[]") 计算出的列表就不会报黄警告,虽然这也是空列表,不过为了避免多次执行出现的变量意外,我在使用前执行了 list..clear() 来清空列表,这样最后得到的结果只会有后面执行的。然后 or 后面的列表并不是需要的结果,只是借助了列表施展列表推导式,在推导式中为列表变量添加值,最后用 and 变量将提取出元素的列表值传回去。

统计每个化学物质内每个化学元素的数量

判断等式双方元素种类是否一致

这里的 ELEMENT 是用来提取出所有化学元素包括电子的。
这里为什么不直接用 assert elements == right_elements 呢?因为列表是有序的,只要顺序不一样列表就不一样,这里只需要种类一致,所以采用集合相等,或集合反交集为空来判断两边元素种类是否一致。

ELEMENT = re.compile(R'([A-Z][a-z]?\.?|<(?:\d|{[A-Za-z0-9+-]})?e[+-]>)')
elements = list(set(get_all([ELEMENT.findall(item) for item in left_item_list if item not in list('+-')])))
right_elements = list(set(get_all([ELEMENT.findall(item) for item in right_item_list if item not in list('+-')])))
# assert not (set(elements) ^ set(right_elements)), '化学方程式两方元素不守恒!'
assert set(elements) == set(right_elements), '化学方程式两方元素不守恒!'

计算左右双方元素数量,获取每个元素在每个物质中的占比

接下来就是统计左右元素数量,scale_group 用于获取每个元素在不同物质中的数量,left_item_dictright_item_dict 用于获取每个物质中每个元素的数量,先以物质键中获取元素数量,再反转以元素为键,获取元素在不同物质中的数量。

scale_group = {element: {} for element in elements}
left_item_dict = {item: {} for item in left_item_list if item not in list('+-')}
right_item_dict = {item: {} for item in right_item_list if item not in list('+-')}
for item_list, item_dict in ((left_item_list, left_item_dict), (right_item_list, right_item_dict)):
    for item in item_list:
        if item not in list('+-'):
            for element in re.compile(R'([A-Z][a-z]?)(\d*)').findall(item):
                if element[0] not in item_dict[item]:
                    item_dict[item][element[0]] = 0
                item_dict[item][element[0]] += element[1].isdigit() and int(element[1]) or 1
for element in elements:
    for item_dict in (left_item_dict, right_item_dict):
        for item in item_dict:
            if element in item:
                scale_group[element][item] = item_dict[item][element]

解化学元素守恒方程

按物质名称创建符号变量

symbols_list = symbols([item for item in (left_item_list + right_item_list) if item not in list('+-')], positive=True, integer=True, real=True)  # 为每个化学物质创建元素符号
for symbol in symbols_list:
    globals()[str(symbol)] = symbol  # 将符号变量释放到全局,为下文eval()字符串转变量算式做基础

根据左右元素总量关系创建关系式

关系为:每个元素中,左边元素数量之和 - 右边元素数量之和 = 0。
eval(temp_str) 是为了将字符串计算成符号变量方程

solve_list = []
for element in elements:
    temp_str = ''
    temp = []
    for item in left_item_dict:
        index = scale_group[element].get(str(item), 
        if index:
            temp.append(f'{index}*{str(item)}')
    temp_str += '+'.join(temp)
    temp = []
    for item in right_item_dict:
        index = scale_group[element].get(str(item), 
        if index:
            temp.append(f'{index}*{str(item)}')
    if temp:
        temp_str += '-' + '-'.join(temp)
    solve_list.append(eval(temp_str))

求解各个化学物质之间的比例关系

res 为各个化学物质系数之间比例关系的解:{其他化学物质:n * 某化学物质}
can_zhao 为作为参照的化学物质,通过每个比例除以它得到纯数字比例,并将它设为1,就得到了他们之间的数字比例关系。
观察到一般 slove 求解的关系比例分子已经是最小公倍数,而且分母只会有一个数,所以此处将分母提取出来让所有比例数字乘以分母,得到最小公倍数系数比例组。

res = solve(solve_list, symbols_list)
can_zhao = [item for item in symbols_list if str(item) == list(set(list(left_item_dict.keys()) + list(right_item_dict.keys())) - set(list(map(str, res.keys()))))[0]][0]
for item in res:
    res[item] /= can_zhao
res[can_zhao] = 1
fen_mu = re.compile(R'/(\d+)?').findall(str(res))
if fen_mu:
    bs = int(fen_mu[0])
    for item in res:
        res[item] *= bs

取比例组最小公倍数为化学物质系数

生成配平的化学方程式,chemical_equations 就是需要的配平后的化学方程式

chemical_equations = []
for item_dict in (left_item_dict, right_item_dict):
    all_item = []
    all_index = []
    for item in item_dict:
        all_item.append(item)
        all_index.append(res[[i for i in symbols_list if str(i) == item][0]])
    chemical_equations.append(' + '.join([f'{"" if all_index[i] == 1 else all_index[i]}{all_item[i]}' for i in range(len(item_dict))]))
chemical_equations = ' = '.join(chemical_equations)

总代码

# _*_ coding:utf-8 _*_
# Project: 最最最没用程序合集
# FileName: AutoBalancingChemicalEquations.py
# UserName: user_from_future博主
# ComputerUser:user_from_future
# Day: 2022/2/21
# Time: 20:21
# IDE: PyCharm
# 2022年,所有bug都将会被丢到海里喂鲨鱼!我说的!不容反驳!

# 自动配平化学方程式
# import bce

import re
import string
from sympy import solve, symbols

"""
逻辑流程:
1、拆分等式左右侧
2、拆分每个化学物质和+-号
3、统计每个化学物质内每个化学元素的数量
4、解化学元素守恒方程
5、取比例组最小公倍数为化学物质系数
元素特征:
1、倍数元素:\d|{[A-Za-z0-9+-]}
2、电荷元素:<(?:\d|{[A-Za-z0-9+-]})?e[+-]>
3、化学元素:[A-Z][a-z]?\.?
4、物质状态:\([gls]\)  # g是气体,l是液体,s是固体
5、化学物质:[+-]?(<(?:\d+|{[A-Za-z0-9+-]+})?e[+-]>|(?:(?:\(?(?:[A-Z][a-z]?\.?)+(?:\d+|{[A-Za-z0-9+-]+})?\)?)+(?:<(?:\d+|{[A-Za-z0-9+-]+})?e[+-]>)?)+)(?:\([gls]\))?([+-]?)
6、[②]物质:[+-]?(<(?:\d+|{[A-Za-z0-9+-]+})?e[+-]>|(?:[A-Za-z0-9.]+(?:\([A-Za-z0-9]+\))?(?:{[A-Za-z0-9+-]+})?(?:<(?:\d+|{[A-Za-z0-9+-]+})?e[+-]>)?)+)(?:\([gls]\))?([+-]?)

试验过程:
>> P+O2=P2O5
4P+5O2=2P2O5
>> H2O(g)+Fe=Fe3O4+H2
4H2O(g)+3Fe=Fe3O4+4H2
>> CO+Fe2O3=Fe+CO2
3CO+Fe2O3=2Fe+3CO2
>> C2H5OH+O2=CO2+H2O
C2H5OH+3O2=2CO2+3H2O
>> Cl2+<e->=Cl<e->
Cl2+2<e->=2Cl<e->
>> Cu+Fe<3e+>=Cu<2e+>+Fe<2e+>
Cu+2Fe<3e+>=Cu<2e+>+2Fe<2e+>
>> LiOH+H2O2+H2O=Li2O2.H2O2.3H2O
2LiOH+2H2O2+H2O=Li2O2.H2O2.3H2O
>> C{n}H{2n+2}+O2=CO2+H2O
{(n+1)^(-1)}C{n}H{2*n+2}+{(1/2)*(3*n+1)/(n+1)}O2={n/(n+1)}CO2+H2O
>> CH3(CHCH){n}CH3+Cl2=CH3(CHClCHCl){n}CH3
CH3(CHCH){n}CH3+{n}Cl2=CH3(CHClCHCl){n}CH3
>> X-<e->=X<{n}e+>
X-{n}<e->=X<{n}e+>
>> CH4;HCN;NH3;O2;H2O
2HCN+6H2O=2CH4+2NH3+3O2
>> CH4(g);HCN(g);NH3(g);O2(g);H2O(g)
2CH4(g)+2NH3(g)+3O2(g)=2HCN(g)+6H2O(g)
"""

ChemicalEquationsList = [
    'P+O2=P2O5',                                # 4P+5O2+2P2O5
    'H2O(g)+Fe=Fe3O4+H2',                       # 4H2O(g)+3Fe=Fe3O4+4H2
    'CO+Fe2O3=Fe+CO2',                          # 3CO+Fe2O3=2Fe+3CO2
    'C2H5OH+O2=CO2+H2O',                        # C2H5OH+3O2=2CO2+3H2O
    'Cl2+<e->=Cl<e->',                          # Cl2+2<e->=2Cl<e->
    'Cu+Fe<3e+>=Cu<2e+>+Fe<2e+>',               # Cu+2Fe<3e+>=Cu<2e+>+2Fe<2e+>
    'LiOH+H2O2+H2O=Li2O2.H2O2.3H2O',            # 2LiOH+2H2O2+H2O=Li2O2.H2O2.3H2O
    'C{n}H{2n+2}+O2=CO2+H2O',                   # {(n+1)^(-1)}C{n}H{2*n+2}+{(1/2)*(3*n+1)/(n+1)}O2={n/(n+1)}CO2+H2O
    'CH3(CHCH){n}CH3+Cl2=CH3(CHClCHCl){n}CH3',  # CH3(CHCH){n}CH3+{n}Cl2=CH3(CHClCHCl){n}CH3
    'X-<e->=X<{n}e+>',                          # X-{n}<e->=X<{n}e+>
    'CH4;HCN;NH3;O2;H2O',                       # 2HCN+6H2O=2CH4+2NH3+3O2
    'CH4(g);HCN(g);NH3(g);O2(g);H2O(g)',        # 2CH4(g)+2NH3(g)+3O2(g)=2HCN(g)+6H2O(g)
]


ELEMENT = re.compile(R'([A-Z][a-z]?|<(?:\d|{[A-Za-z0-9+-]})?e[+-]>)')


def max_gys(num1, num2):  # 两个数最大公约数
    while num2:
        temp = num1 % num2
        num1, num2 = num2, temp
    return num1


def min_gbs(num1, num2):  # 两个数最小公倍数
    return num1 * num2 // max_gys(num1, num2)


# 将二层列表中的内容转成一层列表
get_all = lambda _, __ = eval("[]"): __.clear() or [[__.append(____) for ____ in ___ if ____] for ___ in _] and __


def handle_double_elements_list_scale(element1, element2, chemicals, scale_group):  # 同步两个元素组中相同的元素
    shared = list(set(chemicals[element1]) & set(chemicals[element2]))
    if shared:
        multiply = min_gbs(scale_group[element1][shared[0]], scale_group[element2][shared[0]])
        shared1 = scale_group[element1][shared[0]]
        for item in scale_group[element1]:
            scale_group[element1][item] *= (multiply // shared1)
        shared2 = scale_group[element2][shared[0]]
        for item in scale_group[element2]:
            scale_group[element2][item] *= (multiply // shared2)
    return element2


def calc_equations(left_item_list, right_item_list):
    elements = list(set(get_all([ELEMENT.findall(item) for item in left_item_list if item not in list('+-')])))
    right_elements = list(set(get_all([ELEMENT.findall(item) for item in right_item_list if item not in list('+-')])))
    assert set(elements) == set(right_elements), '化学方程式两方元素不守恒!'
    symbols_list = symbols([item for item in (left_item_list + right_item_list) if item not in list('+-')], positive=True, integer=True, real=True)  # 为每个化学物质创建元素符号
    for symbol in symbols_list:
        globals()[str(symbol)] = symbol  # 将符号变量释放到全局,为下文eval()字符串转变量算式做基础
    scale_group = {element: {} for element in elements}
    left_item_dict = {item: {} for item in left_item_list if item not in list('+-')}
    right_item_dict = {item: {} for item in right_item_list if item not in list('+-')}
    for item_list, item_dict in ((left_item_list, left_item_dict), (right_item_list, right_item_dict)):
        for item in item_list:
            if item not in list('+-'):
                for element in re.compile(R'([A-Z][a-z]?)(\d*)').findall(item):
                    if element[0] not in item_dict[item]:
                        item_dict[item][element[0]] = 0
                    item_dict[item][element[0]] += element[1].isdigit() and int(element[1]) or 1
    for element in elements:
        for item_dict in (left_item_dict, right_item_dict):
            for item in item_dict:
                if element in item:
                    scale_group[element][item] = item_dict[item][element]
    solve_list = []
    for element in elements:
        temp_str = ''
        temp = []
        for item in left_item_dict:
            index = scale_group[element].get(str(item), "")
            if index:
                temp.append(f'{index}*{str(item)}')
        temp_str += '+'.join(temp)
        temp = []
        for item in right_item_dict:
            index = scale_group[element].get(str(item), "")
            if index:
                temp.append(f'{index}*{str(item)}')
        if temp:
            temp_str += '-' + '-'.join(temp)
        solve_list.append(eval(temp_str))
    res = solve(solve_list, symbols_list)
    can_zhao = [item for item in symbols_list if str(item) == list(set(list(left_item_dict.keys()) + list(right_item_dict.keys())) - set(list(map(str, res.keys()))))[0]][0]
    for item in res:
        res[item] /= can_zhao
    res[can_zhao] = 1
    fen_mu = re.compile(R'/(\d+)?').findall(str(res))
    if fen_mu:
        bs = int(fen_mu[0])
        for item in res:
            res[item] *= bs
    chemical_equations = []
    for item_dict in (left_item_dict, right_item_dict):
        all_item = []
        all_index = []
        for item in item_dict:
            all_item.append(item)
            all_index.append(res[[i for i in symbols_list if str(i) == item][0]])
        chemical_equations.append(' + '.join([f'{"" if all_index[i] == 1 else all_index[i]}{all_item[i]}' for i in range(len(item_dict))]))
    chemical_equations = ' = '.join(chemical_equations)
    return chemical_equations


for ChemicalEquations in ChemicalEquationsList:
    for space in string.whitespace:
        ChemicalEquations = ChemicalEquations.replace(space, '')
    if '=' not in ChemicalEquations and ';' in ChemicalEquations:
        all_items = ChemicalEquations.split(';')
        pass  # 自动化合价计算等式先不做,先做元素配平
    elif 'e+>' in ChemicalEquations or 'e->' in ChemicalEquations or '{' in ChemicalEquations or '.' in ChemicalEquations:
        pass  # 暂时不搞电子<e+>/<e->和含./{n}的化学物质等的运算
    else:
        left, right = ChemicalEquations.split('=')
        items = re.compile(R'[+-]?(<(?:\d+|{[A-Za-z0-9+-]+})?e[+-]>|(?:(?:\(?(?:[A-Z][a-z]?\.?)+(?:\d+|{[A-Za-z0-9+-]+})?\)?)+(?:<(?:\d+|{[A-Za-z0-9+-]+})?e[+-]>)?)+)(?:\([gls]\))?([+-]?)')
        left_items, right_items = items.findall(left), items.findall(right)
        print(ChemicalEquations + '\n\t' + calc_equations(list(get_all(left_items)), list(get_all(right_items))))

结束语

当然了,我这个只能解决没有电子转移和没有未知数关系的方程式,剩余的比较懒没想做,有兴趣的可以完善我这个程序哦~
想要标准的化学方程式配平结果,请安装 bce 库,使用 bce-console.exe 程序求解配平。
几天后可能就要手术去了,届时我将不能及时回复各个评论。作为第一个也是最后一个不正紧的打算,就像这样随意尝试一下,最后得出结论:程序员还是搬运代码来的香~ (前人的知识就是现人的工具)

上一篇:python的数据结构和基本语法


下一篇:git reset、git checkout、和 git revert区分要点