categories | tags | ||||
---|---|---|---|---|---|
|
|
Excel中某个单元格的计算结果可以作为其他单元格计算公式的一部分,即二者存在依赖/引用的关系。尤其是对于复杂的表格,这种依赖关系可以方便我们追踪数据的相互作用。Excel已经在Formula
->Formula Auditing
组提供了可视化公式引用关系的功能:Trace Precedents
和Trace Dependents
,同时也可以通过相应的API来编程获取引用关系。本文即记录使用Python的win32com
模块来实现之。
假设几个单元格的公式如下:
## sheet1:
A1=1
B1=A1+1
B2=A1+2
## sheet2
B1=Sheet1!A1+1
B1=Sheet1!A1+2
显然B1
的值依赖于A1
,Excel中将B1
称为A1
的dependents,A1
称为B1
的Precedents。同理B2
,以及sheet2中的B1
、B2
都是A1
的Dependents;并且,B1
和B2
也可以有自己的Dependents。这样的话,sheet1!B1
、sheet1!B2
、sheet2!B1
和sheet2!B2
是A1
的直接Dependents,而sheet1!B1
、sheet1!B2
、sheet2!B1
和sheet2!B2
的Dependents都是A1
的间接Dependents。
在Excel中将光标定位到A1
,然后点击Formula
->Formula Auditing
->Trace Dependents
,会出现三个表示引用关系的箭头。其中两个实线箭头指向当前工作表内的两个引用sheet1!B1
和sheet1!B2
,虚线箭头指向其他工作表的引用即sheet2!B1
和sheet2!B2
。双击虚线箭头会出现Go To
对话框,其中列出了sheet2!B1
和sheet2!B2
,选择相应记录即可跳转到目标位置。
Trace Dependents
操作及其意义同理。
接下来,我们的问题是如何使用Python的win32com
模块找出A1
的所有Dependents。
win32com
模块是pywin32
的一部分,因此在命令行运行下面指令进行安装
pip install pywin32
关于win32com
操作Excel的基本代码可以参考
ExcelRange
对象的两个属性precedents
和dependents
记录了当前工作表范围内的依赖/引用关系,本例中
Range('A1').dependents = Range('B1'), Range('B2')
考虑到该属性不能在某个单元格不存在公式上的引用关系时使用,则可以组织Python代码如下:
def get_direct_precedents(rng):
'''get all precedents of current range from current sheet'''
try:
res = [x.address for x in rng.precedents]
except Exception as e:
res = []
return res
def get_direct_dependents(rng):
'''get all dependents of current range from current sheet'''
try:
res = [x.address for x in rng.dependents]
except Exception as e:
res = []
return res
本例中,如何才能获取到sheet2工作表中B1
对sheet1中A1
的引用关系呢?我们需要使用更一般的获取引用关系的函数NavigateArrow
1:
Range.NavigateArrow (TowardPrecedent, ArrowNumber, LinkNumber)
这个函数名称很形象,对应Excel中显示依赖/引用箭头的操作,其中,
TowardPrecedent
表示查找箭头的方向,True
表示查找precedents,False
表示查找dependentsArrowNumber
表示箭头的序号,例如本例中有三个箭头,则依次取为1,2,3LinkNumber
仅对工作表外引用有效,表示工作表外引用的序号。例如本例中的sheet2!B1
和sheet2!B2
,对应了序号1,2
这样的话,Range('A1').NavigateArrow(False, 3, 2)
即表示Sheet2!B2
。
NavigateArrow
仅仅获取了直接引用,继续递归直接引用即可获取所有的间接引用了。结合此思路,参考代码如下:
def get_all_dependents(xlApp, rng):
'''get all dependents of current range from all worksheets'''
# show dependents arrows first
rng.ShowDependents()
i, j = 0, 0
res = set()
while True:
i += 1
# navigate to next dependent:
# - True -> precedents direction
# - False -> dependent direction
returnRange = rng.NavigateArrow(False, i)
# if it's an external reference -> parent sheets are different
if rng.Parent.Name != returnRange.Parent.Name:
while returnRange:
j += 1
try:
returnRange = rng.NavigateArrow(False, i, j)
res.add((returnRange.Parent.Name, returnRange.address))
res |= get_all_dependents(xlApp, returnRange)
except Exception as e:
# out of range: NavigateArrow method of Range class failed
returnRange = None
# if it's an internal reference
else:
# the last reference would be itself -> then stop the loop
# for merged ranges, the last reference is the first cell
if xlApp.Intersect(returnRange, rng):
break
res.add((returnRange.Parent.Name, returnRange.address))
res |= get_all_dependents(xlApp, returnRange)
return res
至此,主要内容已结束。对于引用关系复杂的工作簿,上述返回集合可能非常大,因此考虑生成器的版本:
def get_all_dependents(xlApp, rng):
'''get all dependents of current range from all worksheets'''
# show dependents arrows first
rng.ShowDependents()
i, j = 0, 0
while True:
i += 1
# navigate to next dependent:
# - True -> precedents direction
# - False -> dependent direction
returnRange = rng.NavigateArrow(False, i)
# if it's an external reference -> parent sheets are different
if rng.Parent.Name != returnRange.Parent.Name:
while returnRange:
j += 1
try:
returnRange = rng.NavigateArrow(False, i, j)
yield (returnRange.Parent.Name, returnRange.address)
# it does not work if just call this generator,
# instead it should be iterated explicitly
# get_all_dependents(xlApp, returnRange)
for x in get_all_dependents(xlApp, returnRange):
yield x
except Exception as e:
# out of range: NavigateArrow method of Range class failed
returnRange = None
# if it's an internal reference
else:
# the last reference would be itself -> then stop the loop
# for merged ranges, the last reference is the first cell
if xlApp.Intersect(returnRange, rng):
break
yield (returnRange.Parent.Name, returnRange.address)
for x in get_all_dependents(xlApp, returnRange):
yield x
yield
关键字可以方便地将普通函数转为生成器,需要注意的是转换递归函数时,需要显式地迭代生成器,否则它不会自动计算下一个值。