9

我一直在尝试创建一个匹配任何 Excel 公式中的任何引用的正则表达式模式,包括绝对引用、相对引用和外部引用。我需要返回整个参考,包括工作表和工作簿名称。

我无法找到有关 Excel A1 符号的详尽文档,但通过大量测试,我确定了以下内容:

  • 公式前面有一个等号“=”
  • 公式中的字符串用双引号括起来,在查找真正的引用之前需要删除,否则=A1&"A1"会破坏正则表达式
  • 工作表名称最多可包含 31 个字符,不包括 \ / ? * [ ] :
  • 外部引用中的工作表名称必须以 bang 成功=Sheet1!A1
  • 外部引用中的工作簿名称必须用方括号括起来=[Book1.xlsx]Sheet1!A1
  • 工作簿路径(如果引用是对已关闭工作簿中的范围的引用,则 Excel 添加)始终用单引号括起来,并位于工作簿名称的括号左侧'C:\[Book1.xlsx]Sheet1'!A1
  • 某些字符(例如,不间断空格)导致 Excel 将工作簿和工作表名称括在单引号的外部引用中,但我不知道具体是哪些字符 ='[Book 1.xlsx]Sheet 1'!A1
  • 即使启用了 R1C1 表示法,Range.Formula仍以 A1 表示法返回引用。Range.FormulaR1C1以 R1C1 表示法返回引用。
  • 3D 参考样式允许在一个工作簿上使用一系列工作表名称=SUM([Book5]Sheet1:Sheet3!A1)
  • 可以在公式中指定命名范围:
    • 名称的第一个字符必须是字母、下划线字符 (_) 或反斜杠 (\)。名称中的剩余字符可以是字母、数字、句点和下划线字符。
    • 您不能使用大写和小写字符“C”、“c”、“R”或“r”作为定义的名称,因为它们都用作为当前选定的单元格选择行或列的简写,当您在名称或转到文本框中输入它们。
    • 名称不能与单元格引用相同,例如 Z$100 或 R1C1。
    • 空格不允许作为名称的一部分。
    • 名称最长可达 255 个字符。
    • 名称可以包含大写和小写字母。Excel 不区分名称中的大小写字符。

这是我想出的用 VBA 程序进行测试的方法。我也更新了处理名称的代码:

Sub ReturnFormulaReferences()

    Dim objRegExp As New VBScript_RegExp_55.RegExp
    Dim objCell As Range
    Dim objStringMatches As Object
    Dim objReferenceMatches As Object
    Dim objMatch As Object
    Dim intReferenceCount As Integer
    Dim intIndex As Integer
    Dim booIsReference As Boolean
    Dim objName As Name
    Dim booNameFound As Boolean

    With objRegExp
        .MultiLine = True
        .Global = True
        .IgnoreCase = True
    End With

    For Each objCell In Selection.Cells
        If Left(objCell.Formula, 1) = "=" Then

            objRegExp.Pattern = "\"".*\"""
            Set objStringMatches = objRegExp.Execute(objCell.Formula)

            objRegExp.Pattern = "(\'.*(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\'\!" _
            & "|(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\!)?" _
            & "(\$?[a-z]{1,3}\$?[0-9]{1,7}(\:\$?[a-z]{1,3}\$?[0-9]{1,7})?" _
            & "|\$[a-z]{1,3}\:\$[a-z]{1,3}" _
            & "|[a-z]{1,3}\:[a-z]{1,3}" _
            & "|\$[0-9]{1,7}\:\$[0-9]{1,7}" _
            & "|[0-9]{1,7}\:[0-9]{1,7}" _
            & "|[a-z_\\][a-z0-9_\.]{0,254})"
            Set objReferenceMatches = objRegExp.Execute(objCell.Formula)

            intReferenceCount = 0
            For Each objMatch In objReferenceMatches
                intReferenceCount = intReferenceCount + 1
            Next

            Debug.Print objCell.Formula
            For intIndex = intReferenceCount - 1 To 0 Step -1
                booIsReference = True
                For Each objMatch In objStringMatches
                    If objReferenceMatches(intIndex).FirstIndex > objMatch.FirstIndex _
                    And objReferenceMatches(intIndex).FirstIndex < objMatch.FirstIndex + objMatch.Length Then
                        booIsReference = False
                        Exit For
                    End If
                Next

                If booIsReference Then
                    objRegExp.Pattern = "(\'.*(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\'\!" _
                    & "|(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\!)?" _
                    & "(\$?[a-z]{1,3}\$?[0-9]{1,7}(\:\$?[a-z]{1,3}\$?[0-9]{1,7})?" _
                    & "|\$[a-z]{1,3}\:\$[a-z]{1,3}" _
                    & "|[a-z]{1,3}\:[a-z]{1,3}" _
                    & "|\$[0-9]{1,7}\:\$[0-9]{1,7}" _
                    & "|[0-9]{1,7}\:[0-9]{1,7})"
                    If Not objRegExp.Test(objReferenceMatches(intIndex).Value) Then 'reference is not A1
                        objRegExp.Pattern = "^(\'.*(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\'\!" _
                        & "|(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\!)" _
                        & "[a-z_\\][a-z0-9_\.]{0,254}$"
                        If Not objRegExp.Test(objReferenceMatches(intIndex).Value) Then 'name is not external
                            booNameFound = False
                            For Each objName In objCell.Worksheet.Parent.Names
                                If objReferenceMatches(intIndex).Value = objName.Name Then
                                    booNameFound = True
                                    Exit For
                                End If
                            Next
                            If Not booNameFound Then
                                objRegExp.Pattern = "^(\'.*(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\'\!" _
                                & "|(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\!)"
                                For Each objName In objCell.Worksheet.Names
                                    If objReferenceMatches(intIndex).Value = objRegExp.Replace(objName.Name, "") Then
                                        booNameFound = True
                                        Exit For
                                    End If
                                Next
                            End If
                            booIsReference = booNameFound
                        End If
                    End If
                End If

                If booIsReference Then
                    Debug.Print "  " & objReferenceMatches(intIndex).Value _
                    & " (" & objReferenceMatches(intIndex).FirstIndex & ", " _
                    & objReferenceMatches(intIndex).Length & ")"
                End If
            Next intIndex
            Debug.Print

        End If
    Next

    Set objRegExp = Nothing
    Set objStringMatches = Nothing
    Set objReferenceMatches = Nothing
    Set objMatch = Nothing
    Set objCell = Nothing
    Set objName = Nothing

End Sub

任何人都可以打破或改善这一点吗?如果没有关于 Excel 公式语法的详尽文档,很难知道这是否正确。

谢谢!

4

4 回答 4

3

jtolle 引导我朝着正确的方向前进。据我所知,这就是我想要做的。我一直在测试,它似乎工作。

stringOriginFormula = rangeOrigin.Formula
rangeOrigin.Cut rangeDestination
rangeOrigin.Formula = stringOriginFormula

谢谢jtolle!

于 2009-12-18T00:28:16.930 回答
3

我在这里晚了几年,但我一直在寻找类似的东西,所以深入研究了这个。您使用的主要模式是这样的:

objRegExp.Pattern = "(\'.*(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\'\!" _
& "|(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\!)?" _
& "(\$?[a-z]{1,3}\$?[0-9]{1,7}(\:\$?[a-z]{1,3}\$?[0-9]{1,7})?" _
& "|\$[a-z]{1,3}\:\$[a-z]{1,3}" _
& "|[a-z]{1,3}\:[a-z]{1,3}" _
& "|\$[0-9]{1,7}\:\$[0-9]{1,7}" _
& "|[0-9]{1,7}\:[0-9]{1,7}" _
& "|[a-z_\\][a-z0-9_\.]{0,254})"

基本上,您有六个替代范围参考(第 3-8 行),其中任何一个都会自行生成匹配项,还有两个替代可选文件名/工作表名称前缀(第 1-2 行)。

对于两个前缀替代方案,唯一的区别是第一个用单引号括起来,在初始引号之后有一个额外的点星。这些单引号主要出现在工作表名称中有空格时。点星的目的,在初始单引号后匹配不受约束的文本,尚不清楚,它似乎会产生问题。我将在下面讨论这些问题。除此之外,这两个替代前缀是相同的,我将它们统称为可选外部前缀 (OEP)。

OEP 有自己的两个可选前缀(在任一替代方案中都相同)。第一个是工作簿名称,括号中的开放式点星。

(\[.*\])? 

第二个是“3D”单元格引用,两个工作表名称用冒号分隔;它是包含冒号的初始工作表名称。这里的模式是一个否定字符类,允许最多 31 个字符,除了正斜杠、反斜杠、问号、星号、括号或冒号,后跟冒号:

([^\:\\\/\?\*\[\]]{1,31}\:)?

最后,对于 OEP,它唯一需要的部分是:工作表名称,与可选工作表名称相同,但没有冒号。效果是(如果这些都正常工作)所需的工作表名称将匹配(如果可以),然后只有当存在 3d 参考或附加的前括号文本时,其可选前缀才会匹配。

工作簿/工作表名称前缀的问题:首先,第一行开头的点星号包含过多。类似地,工作表名称的否定字符类似乎需要其他字符,包括括号、逗号、加号、减号、等号和 bang。否则,额外的材料将被解释为工作表名称的一部分。在我的测试中,这种过度包含发生在以下任何一种情况下:

=SUM(Sheet1!A1,Sheet2!A2)
=Sheet1!A1+Sheet2!A2
=Sheet1!A1-Sheet2!A2

工作表名称可以包含其中一些字符,因此需要一些额外的措施来解决这个问题。例如,可以将工作表命名为“(Sheet1)”,给出一个奇怪的公式,如:

=SUM('(Sheet1)'!A1:A2)

您想获得带有工作表名称的内部括号,而不是外部括号。Excel 将单引号放在那个单引号上,就像在工作表名称中使用空格一样。然后,您可以在非单引号版本中排除括号,因为在单引号中可以。但请注意 Excel 似乎甚至允许在工作表名称中使用单引号。将这些命名怪癖发挥到极致,我刚刚成功命名了一个工作表“Hi'Sheet1'SUM('Sheet2'!A1,A2)!”。这很荒谬,但它指出了可能发生的事情。我在这样做时了解到,如果我在工作表名称中包含单引号,则公式会使用第二个单引号转义单引号。因此,引用我刚刚创建的工作表的 SUM(A1:A2) 最终看起来像这样:

=SUM('Hi''Sheet1''SUM(''Sheet2''!A1,A2)!'!A1:A2)

这实际上确实让我们对 Excel 解析器本身有了一些了解。我怀疑要充分处理这个问题,您可能希望单独(在正则表达式之外)将潜在的工作表名称或工作簿名称与实际工作表名称进行比较,就像您对命名范围所做的那样。

这导致正则表达式中允许使用六种形式的单元格引用(其中任何一种,如果满足,将产生匹配):

1.) 具有行和列的单单元格或多单元格范围

"(\$?[a-z]{1,3}\$?[0-9]{1,7}(\:\$?[a-z]{1,3}\$?[0-9]{1,7})?"

这里的 open paren 在 6 个选项的末尾关闭。否则,此行允许类型为“$A$1”、“A1”、“$A1”、“A$1”的基本单元格引用,或在多单元格范围内的任意组合(“$A1:A$2 “, ETC。)。

2.) 仅具有绝对引用的全列或多列范围

"|\$[a-z]{1,3}\:\$[a-z]{1,3}"

这个允许类型为“$A:$B”的单元格引用,两者都带有美元符号。请注意,仅一侧的美元符号将不匹配。

3.) 仅具有相对引用的全列或多列范围

"|[a-z]{1,3}\:[a-z]{1,3}"

此行与最后一行类似,但仅匹配没有美元符号。请注意,仅一侧的美元符号在这里也不匹配。

4.) 仅具有绝对引用的全行或多行范围

"|\$[0-9]{1,7}\:\$[0-9]{1,7}"

此行允许类型为“$1:$2”的单元格引用,两者都带有美元符号。

5.) 仅具有相对引用的全行或多行范围

"|[0-9]{1,7}\:[0-9]{1,7}" 

这个版本和上一个版本一样,但只匹配没有美元符号。

6.) 其他可能是命名范围的文本

 "|[a-z_\\][a-z0-9_\.]{0,254})"

最后,第六个选项允许文本。稍后在 sub 中将此文本与实际命名范围进行比较。

我在这里看到的主要遗漏是具有绝对引用和相对引用的范围,类型为“A:$A”或“1:$1”。虽然 $A:A 因包含“A:A”而被捕获,但“A:$A”未被捕获。您可以通过组合 2 和 3 以及将 4 和 5 与可选的美元符号组合来解决这个问题并简化正则表达式:

objRegExp.Pattern = "(\'.*(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\'\!" _
& "|(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\!)?" _
& "(\$?[a-z]{1,3}\$?[0-9]{1,7}(\:\$?[a-z]{1,3}\$?[0-9]{1,7})?" _
& "|\$?[a-z]{1,3}\:\$?[a-z]{1,3}" _
& "|\$?[0-9]{1,7}\:\$?[0-9]{1,7}" _
& "|[a-z_\\][a-z0-9_\.]{0,254})"

将这些进一步结合起来似乎会遇到一切都是可选的问题。

另一个问题是用于匹配字符串的初始正则表达式模式,您可以使用它来删除包含在带引号的字符串中的潜在范围: objRegExp.Pattern = "\"".*\"""字符串在公式的开头和结尾,点星的贪婪捕获了从初始引号到最终引号的所有内容(换句话说,它将整个公式解释为一个大引号字符串,即使其中有非字符串材料中间)。看来您可以通过使点星变得懒惰(在其后添加问号)来解决此问题。这引发了关于引号内引号的问题,但它们可能不是问题。例如,我测试了这个公式:

="John loves his A1 steak sauce, but said the ""good A1 steak sauce price"" is $" & A2+A3 & " less than the ""bad price"" of $" & A4 & "."

插入单元格值后,此公式计算为:

约翰喜欢他的 A1 牛排酱,但他说“好的 A1 牛排酱价格”比“坏价格”的 8 美元低 5 美元。

将惰性修饰符添加到字符串模式后,上述两个版本的“A1”都被识别为出现在字符串中,因此被删除,而 A2、A3 和 A4 被识别为单元格引用。

我确信我上面的一些语言存在一些技术问题,但希望分析仍然有用。

于 2019-06-16T19:05:01.313 回答
0

谢谢 Ben(我是新来这里发帖的,尽管 Stackoverflow 多年来因为高质量的技术内容引起了我的注意,所以我不确定我是否为作者 J 正确阅读了此页面)

我尝试了发布的解决方案(测试,测试更新,以及使用 range.precendents 的解决方案(正确指出,不包括对其他工作表或其他工作簿的引用)并发现了一个小缺陷:外部工作表名称包含在'单引号' 仅当它是一个数字时;如果它包含空格(以及原始帖子中列出的可能其他字符,如 Ben (?)。通过对正则表达式的简单添加(打开 [)可以更正(添加“ [”,请参见下面的代码)。此外,出于我自己的目的,我将 sub 转换为一个函数,该函数将返回一个逗号分隔的列表,其中删除了重复项(注意,这只会删除相同的引用符号,而不是包含在多个范围):

Public Function CellReflist(Optional r As Range)  ' single cell
Dim result As Object: Dim testExpression As String: Dim objRegEx As Object
If r Is Nothing Then Set r = ActiveCell ' Cells(1, 2)  ' INPUT THE CELL HERE , e.g.    RANGE("A1")
Set objRegEx = CreateObject("VBScript.RegExp")
objRegEx.IgnoreCase = True: objRegEx.Global = True: objRegEx.Pattern = """.*?"""    ' remove expressions
testExpression = CStr(r.Formula)
testExpression = objRegEx.Replace(testExpression, "")
'objRegEx.Pattern = "(([A-Z])+(\d)+)"  'grab the address

objRegEx.Pattern = "(['\[].*?['!])?([[A-Z0-9_]+[!])?(\$?[A-Z]+\$?(\d)+(:\$?[A-Z]+\$?(\d)+)?|\$?[A-Z]+:\$?[A-Z]+|(\$?[A-Z]+\$?(\d)+))"
If objRegEx.Test(testExpression) Then
    Set result = objRegEx.Execute(testExpression)
    If result.Count > 0 Then CellReflist = result(0).Value
    If result.Count > 1 Then
        For i = 1 To result.Count - 1 'Each Match In result
            dbl = False ' poistetaan tuplaesiintymiset
            For j = 0 To i - 1
                If result(i).Value = result(j).Value Then dbl = True
            Next j
            If Not dbl Then CellReflist = CellReflist & "," & result(i).Value 'Match.Value
        Next i 'Match
    End If
End If

结束功能

于 2018-01-01T16:25:49.323 回答
0

我在 Google 表格中解决了类似的问题。

以下从公式中添加/减去行引用。因为我只需要更新行引用,而不是提取我刚刚提取并使用此更新行引用的公式/((?<=[A-Za-z\$:\!])\d+(?![A-Za-z\(!]))|(\d+(?=[:]))/

String.prototype.replaceAt = function(index, replacement, diff = 0) {
    let end = this.substr(index + replacement.length + diff)
    if((this.length - 1) === index) end = ""
    return this.substr(0, index) + replacement + end;
}
// Ref: https://stackoverflow.com/a/1431113/2319414

/**
 * @param row - positive integer to add, negative to subtract rows.
 */
function updateRowReference(formula, row){

  let masked = formula
  const mask = "#"

  // masking double quotes in string literals
  let exp = /""/g
  let result;
  while((result = exp.exec(masked)) !== null){
    masked = masked.replaceAt(result.index, new Array(result[0].length).fill(mask).join(""))
  }
  // masking string literals
  exp = /\"([^\\\"]|\\.)*\"/g
  // Ref: https://stackoverflow.com/a/9260547
  while((result = exp.exec(masked)) !== null){
    masked = masked.replaceAt(result.index, new Array(result[0].length).fill(mask).join(""))
  }

  // updating row references
  const sRow = row.toString()
  // The magic is happening here
  // Just matching a number which is part of range address
  exp = /((?<=[A-Za-z\$:\!])\d+(?![A-Za-z\(!]))|(\d+(?=[:]))/g
  while((result = exp.exec(masked)) !== null){
    const oldRow = Number(result[0])
    // adding/subtracting rows
    const newRow = (row + oldRow).toString()
    
    // preserving formula string length integrity if number of digits of new row is different than old row
    const diff = result[0].length - newRow.length
    masked = masked.replaceAt(result.index, newRow, diff)
    formula = formula.replaceAt(result.index, newRow, diff)
    exp.lastIndex -= diff
  }

  let updated = masked;

  // revert mask
  const array = formula.split("")
  while((result = updated.search(mask)) !== -1){
    updated = updated.replaceAt(result, array[result])
  }

  return updated
}

function test(){
  const cases = [
    "=$A$1", 
    "=A1", 
    "=$A1", 
    "=A$1", 
    "=$A1:B$1", 
    "=1:1", 
    "=Sheet1!1:1", 
    "=Sheet1!$A1:B$1", 
    "=Sheet1!A$1",
    '=IF(AND($C6 <> ""; NOT(ISBLANK(B$6))); IF(SUM(FILTER($F$6:$F$7;$C$6:$C$7 = $C6)) < $G6; 1; IF($E6 = 0; 1; 0)); 0)',
    "=$A$111", "=A111", "=$A111", "=A$111", "=$A111:B$111", 
    "=111:111", 
    "=Sheet1!111:111", 
    "=Sheet1!$A111:B$111", 
    "=Sheet1!A$111",
    '=IF(AND($C111 <> ""; NOT(ISBLANK(B$111))); IF(SUM(FILTER($F$111:$F$112;$C$111:$C$112 = $C111)) < $G111; 1; IF($E111 = 0; 1; 0)); 0)',

    // if string literals have addresses they shouldn't be affected
    '=IF(AND($C111 <> "A1 $A1 $A1:B$1";$C111 <> "Sheet1!1:1";$C111 <> "Sheet1!$A1:B$1"); 1 , 0)'
  ]

  const expectedAdd = [
    '=$A$16',
    '=A16',
    '=$A16',
    '=A$16',
    '=$A16:B$16',
    '=16:16',
    '=Sheet1!16:16',
    '=Sheet1!$A16:B$16',
    '=Sheet1!A$16',
    '=IF(AND($C21 <> ""; NOT(ISBLANK(B$21))); IF(SUM(FILTER($F$21:$F$22;$C$21:$C$22 = $C21)) < $G21; 1; IF($E21 = 0; 1; 0)); 0)',
    '=$A$126',
    '=A126',
    '=$A126',
    '=A$126',
    '=$A126:B$126',
    '=126:126',
    '=Sheet1!126:126',
    '=Sheet1!$A126:B$126',
    '=Sheet1!A$126',
    '=IF(AND($C126 <> ""; NOT(ISBLANK(B$126))); IF(SUM(FILTER($F$126:$F$127;$C$126:$C$127 = $C126)) < $G126; 1; IF($E126 = 0; 1; 0)); 0)',
    '=IF(AND($C126 <> "A1 $A1 $A1:B$1";$C126 <> "Sheet1!1:1";$C126 <> "Sheet1!$A1:B$1"); 1 , 0)' 
    ]

  let results = cases.map(_case => updateRowReference(_case, 15))

  console.log('Test Add')
  console.log(results.every((result, i) => result === expectedAdd[i]))

  console.log('Test Subtract')
  results = results.map(_case => updateRowReference(_case, -15))
  console.log(results.every((result, i) => result === cases[i]))
}

test()

将地址作为字符串的“INDIRECT”函数不会被更新

于 2021-11-17T08:16:14.583 回答