确定源文件中使用的制表符宽度的好的启发式方法是啥?

Posted

技术标签:

【中文标题】确定源文件中使用的制表符宽度的好的启发式方法是啥?【英文标题】:What is a good heuristic for determining the tab width used in a source file?确定源文件中使用的制表符宽度的好的启发式方法是什么? 【发布时间】:2011-10-19 21:00:10 【问题描述】:

我想确定用空格缩进的源文件中使用的制表符宽度。 这对于具有特别规则缩进的文件来说并不难,其中前导空格仅用于缩进,始终是制表符宽度的倍数,并且缩进每次增加一级。 但是许多文件会与这种常规缩进有所不同,通常是为了某种形式的垂直对齐。因此,我正在寻找一种很好的启发式方法来估计使用的标签宽度,从而允许不规则缩进的一些可能性。

这样做的动机是为 SubEthaEdit 编辑器编写扩展。不幸的是,SubEthaEdit 没有使选项卡宽度可用于脚本,所以我将根据文本猜测它。

一个合适的启发式应该:

性能足够好,可以交互使用。我不认为这会是一个问题,如果需要,可以只使用部分文本。 独立于语言。 返回合适的最长标签宽度。例如,如果每个缩进实际上是两倍的级别,则任何具有四个空格的制表符宽度的文件也可能是具有两个空格制表符的文件。显然,四个空格将是正确的选择。 如果压痕完全规则,请务必正确。

一些简化因素:

可以假定至少有一行是缩进的。 可以假定制表符宽度至少为两个空格。 可以肯定地假设缩进仅使用空格。并不是我对tab有什么反对——恰恰相反,我会先检查是否有用于缩进的tab,并单独处理。这确实意味着可能无法正确处理缩进混合制表符和空格,但我认为这并不重要。 可以假设没有行只包含空格。 并非所有语言都需要正确处理。例如,像 lisp 和 go 这样的语言的成功或失败将完全无关紧要,因为它们通常不是手动缩进的。 不需要完美。如果偶尔需要手动调整几行,世界不会结束。

你会采取什么方法,你认为它的优点和缺点是什么?

如果您想在答案中提供工作代码,最好的方法可能是使用 shell 脚本,该脚本从 stdin 读取源文件并将制表符宽度写入 stdout。伪代码或清晰的文字描述也可以。

一些结果

为了测试不同的策略,我们可以将不同的策略应用于语言分布的标准库中的文件,因为它们可能遵循语言的标准缩进。我将考虑 Python 2.7 和 Ruby 1.8 库(系统框架安装在 Mac OS X 10.7 上),它们的预期选项卡宽度分别为 4 和 2。不包括以制表符开头的行或没有以至少两个空格开头的行的文件。

Python:

                     Right  None  Wrong
Mode:                 2523     1    102
First:                2169     1    456
No-long (12):         2529     9     88
No-long (8):          2535    16     75
LR (changes):         2509     1    116
LR (indent):          1533     1   1092
Doublecheck (10):     2480    15    130
Doublecheck (20):     2509    15    101

鲁比:

                     Right  None  Wrong
Mode:                  594    29     51
First:                 578     0     54
No-long (12):          595    29     50
No-long (8):           597    29     48
LR (changes):          585     0     47
LR (indent):           496     0    136
Doublecheck (10):      610     0     22
Doublecheck (20):      609     0     23

在这些表格中,“正确”应视为确定语言标准制表符宽度,“错误”应视为不等于语言标准宽度的非零制表符宽度,而“无”应视为零制表符-宽度或没有答案。 “模式”是选择最频繁发生的缩进变化的策略; “First”是对第一行缩进进行缩进; “不长”是FastAl排除缩进大的行并采取模式的策略,数字表示允许的最大缩进变化; “LR”是Patrick87基于线性回归的策略,有基于行间缩进变化和行绝对缩进的变体; “Doublecheck”(忍不住双关语!)是 Mark 对 FastAl 策略的修改,限制可能的 tab 宽度并检查一半模态值是否也经常出现,有两个不同的阈值用于选择较小的宽度。

【问题讨论】:

IMO,合理的做法是:如果ts=8不起作用,则拒绝文件并向作者投诉。 @William Pursell 这比我想要的要严格一些。 ;) 悬赏以尝试获得更多想法。我还将添加一个基线答案,至少应该做到这一点。 【参考方案1】:

对于您想要支持的每种语言,您都需要进行一些解析: 1) 排除 cmets(逐行或逐块,也可以嵌套?) 2)找到子块的开口(@98​​7654321@在类C语言中,begin在pascal中,do在shell等中)

那就看看子块打开后空格数增加了多少。做一些简单的统计——找出最频繁的值、最大值和最小值、平均值。这样您还可以查看缩进是否正常以及有多少。

【讨论】:

【参考方案2】:

也许做一些类似...

    获取文件中所有标签宽度的列表 删除 50% 最不频繁的条目 按升序对剩余条目进行排序 计算 (a, b) 对的列表,其中 b 位于制表符宽度列表中,a 给出该制表符宽度的排名。 绘制最佳拟合线 最佳拟合线的斜率是标签宽度的猜测值。四舍五入到最接近的整数。

例子:

    list = [4, 4, 6, 8, 8, 4, 4, 4, 8, 8, 12, 5, 11, 13, 12, 12] 列表 = [4, 4, 4, 4, 4, 8, 8, 8] 已排序 [(1, 4), (1, 4), (1, 4), (1, 4), (1, 4), (2, 8), (2, 8), (2, 8 )] 最佳拟合线是 b = 4a + 0 (R^2 = 0) 斜率为 4,所以这可能是标签宽度。

【讨论】:

当你提到制表符宽度时,你是指行的前导缩进还是连续行之间缩进的变化? 我的方法会近似两者:前导缩进是 y 截距,缩进的变化是斜率。或者,这一行将给出缩进空间与制表符深度的函数。 好的,那么我将跟进两个版本的问题和 cmets。对于缩进宽度,即使所有缩进变化的幅度相同,该方法似乎也会出错;这在实践中可能不是问题,并且很可能需要改进整体结果——由经验确定。我注意到您的示例数字没有零——这是故意的吗? 在缩进变化的情况下,似乎假设大多数变化是标签宽度的偶数倍,我不确定---再说一次,我会凭经验解决。您的示例数字既没有零也没有负数。是否有意省略缩进?使用非零变化的幅度? 不,您可以添加零。我不确定我是否了解这可能会出错。这是一个经验问题,将曲线拟合到数据——对于压痕,人们会假设线性曲线是最合适的——是标准做法。我唯一一次看到这种方法失败是当所有缩进级别都相同时......在这种情况下,你告诉我这个人使用的是什么缩进方案!【参考方案3】:

作为基线,可以简单地计算所有缩进增加,并将最频繁的增加作为制表符宽度。作为一个 shell 脚本,每个流水线阶段都有小动作,它可能看起来像这样:

#!/bin/sh

grep -v -E '^[[:space:]]*$' | 
  sed 's/^\([[:space:]]*\).*/\1/' | 
    awk ' print length($0) ' | 
      awk '$1 > prev  print $1 - prev   prev = $1 ' | 
        sort | 
          uniq -c | 
            sort -k1nr | 
              awk ' print $2 ' | 
                head -n 1

这个实现是O(n log(n)),其中n 是文件中的行数,但它可以很容易地在O(n) 中完成。

【讨论】:

我喜欢这个纯粹是因为它的反常。在产生 9 个进程后,我认为 sort 的非线性 O() 行为对于典型的源文件来说不是问题。 @Jürgen 这是一个循序渐进的说明,管道的每个阶段都有一个动作,作为其他人可以轻松修改的基线——我想要的不仅仅是想法一个有效的实施。这并不一定会导致快速实现(两个连续的awk 阶段看起来特别令人震惊,获得最大值的排序策略也是如此)。也就是说,它运行在一个有 10k 行的文件上,没有明显的时间延迟;足够快的交互使用并不是什么限制! 我完全理解这一点。我的 python 脚本使用几乎相同的策略。【参考方案4】: 对于文件中的每一行 如果缩进多于前一个,则将差异添加到列表中 如果 > 12 则丢弃,可能是续行 生成列表中#s的频率表 #1 可能是您的答案。

编辑

我打开了 VB.Net(不是吗?:-) 我的意思是:

    Sub Main()
        Dim lines = IO.File.ReadAllLines("ProveGodExists.c")
        Dim previndent As Integer = 0
        Dim indent As Integer
        Dim diff As Integer
        Dim Diffs As New Dictionary(Of Integer, Integer)
        For Each line In lines
            previndent = indent
            indent = Len(line) - Len(LTrim(line))
            diff = indent - previndent
            If diff > 0 And diff < 13 Then
                If Diffs.ContainsKey(diff) Then
                    Diffs(diff) += 1
                Else
                    Diffs.Add(diff, 1)
                End If
            End If
        Next
        Dim freqtbl = From p In Diffs Order By p.Value Descending
        Console.WriteLine("Dump of frequency table:")
        For Each item In freqtbl
            Console.WriteLine(item.Key.ToString & " " & item.Value.ToString)
        Next
        Console.WriteLine("My wild guess at tab setting: " & freqtbl(0).Key.ToString)
        Console.ReadLine()
    End Sub

结果:

频率表转储: 4 748 8 22 12 12 2 2 9 2 3 1 6 1 我对标签设置的疯狂猜测:4

希望对您有所帮助。

【讨论】:

还不错,但不能,例如如果 45% 的选项卡宽度为 7 且 55% 为 9,则确定选项卡宽度为 8。不过,这很有趣。 @Patrick87 - 如果您对频率表进行排序,那么这些 #s 将在后续插槽中。但是,我不认为 OP 想要那样。我重新阅读了这个问题,但我仍然认为他只想要最有可能的候选人。 @Patrick87 我不希望缩进永远不会更改 8 的文件的制表符宽度为 8。您给出的数字似乎是一个例外情况,人们不必担心太多. 更具体地说,对于这个答案,它非常符合我正在寻找的内容。最后,如果你能想出一个很好的规则来消除虚假缩进,那么像缩进的模式变化这样简单的选择策略应该可以做得很好。我稍后会实现这个,看看“大缩进”是否是一个很好的测试虚假缩进。 您的选择是(实际上)2、3、4、5、6、7、8。我会用这种方法扫描前 50-100 条非空行并选择最高的。如果命中是 8、6 或 4,我会进行第二次检查,看看 4、3 或 2 是否是第二高的,然后选择那个。对于您的 7 种可能性中的每一种,我都会选择一个“合理化”方案来解决问题。【参考方案5】:

好的,由于您需要与语言无关的解决方案,因此我们将无法使用任何语法提示。尽管您说您不想要完美的解决方案,但这里有一个适用于大多数语言的非常好的解决方案。

我实际上必须解决密码学中的一个类似问题才能在polyalphabetic cipher 中获得正确的代码字长。这种加密是基本的 Caesar-chiffre(字母表的每个字母移动 n 个字母),其中密码用于不同地移动字母(nth明文的字母被密码的 mod(nth, length(cryptword)) 字母移动)。选择的武器是autocorrelation。

算法是这样的:

    在行首的空格结束后去除所有字符 - 保持行尾标记不变。 删除零空格的行(因为它们只是空白行) 计算每一行的空白宽度并将其保存在数组中lengths 自相关:循环直到最大估计数 - 可能相当高,如 32 或其他 - 当前迭代应为 i。对于每次迭代,计算每个条目与 ith 条目之间的距离。计算距离数 = 0(nth(n+i)th 条目的值相同),保存在数组中作为键 i em>。 您现在有一个相同对出现的数组。计算该数组的平均值,并删除该平均值附近的所有值(留下自相关的尖峰)。尖峰将是最小值的倍数,最小值将是搜索到的用于缩进的空格数。

自相关是一个非常好的函数,可用于所有需要检测数据流中重复值的情况。它大量用于信号处理并且速度非常快(取决于估计的信号重复的最大距离)。

是的,当时我用自相关破解了多字母密文。 ;)

【讨论】:

非常有趣的方法。我已经有一段时间没有进行任何信号处理了,但我想我可以看到它是如何工作的。基于低频占主导地位的假设(即,步骤 4 中的限制是低通滤波器的一种形式),您实质上是在建议一种进行廉价傅立叶变换的方法。第五步剔除在频域功率谱中贡献很小的值。听起来对吗? 实现这一点,我发现你的第 4 步不是很清楚: i 代表什么?这似乎是被比较的行索引之间的差异,但它是如何在最后转换为制表符宽度的呢?也许它应该是二维自相关? @michael-j-barber 听起来不错,但老实说,信号处理并不是我最擅长的领域。我还阅读了与 FFT 的相似之处。最后,您尝试通过将信号与其自身与偏移量进行比较来放大尖峰。想象一个正弦波,您复制并迭代地增加偏移量,直到两个波再次匹配。这会显着放大信号,因此您可以通过查看偏移量来确定波长。如果您将字母表中的字母数字作为波的值,那么对于密文也是如此,但现在这真的是话题了。 ;) @Michael-j-barber: i 是正在测试的迭代或当前偏移量。查看Index of Coincidence 以获取有关解决多字母雪佛兰的详细说明。也许这会比我用 500 个字符提供的更清楚。 我会看一下这篇文章,希望它能把事情弄清楚。现在,第 4 点和第 5 点似乎在说计算有多少对具有偏移 i 的行具有相同的缩进,并选择具有高计数的偏移量。但这会忽略实际的缩进,无法恢复它。不要忘记您可以编辑您的答案:500 个字符的限制不是问题!【参考方案6】:

启发式:

    获取从一行到下一行 > 0 的所有缩进更改的列表。 制作此列表中所有值的频率表。 取频率最高的值。

Python 脚本,采用文件名或标准输入并打印最佳缩进数:

#!/usr/bin/env python

import fileinput, collections

def leadingSpaceLen(line):
    return len(line) - len(line.lstrip())

def indentChange(line1, line2):
    return leadingSpaceLen(line2) - leadingSpaceLen(line1)

def indentChanges(lines):
    return [indentChange(line1, line2)
        for line1, line2 in zip(lines[:-1], lines[1:])]

def bestIndent(lines):
    f = collections.defaultdict(lambda: 0)
    for change in indentChanges(lines):
        if change > 0:
            f[change] += 1
    return max(f.items(), key=lambda x: x[1])[0]

if __name__ == '__main__':
    print bestIndent(tuple(fileinput.input()))

【讨论】:

【参考方案7】:

您的选择是(实际上)2、3、4、5、6、7、8。

我会使用@FastAl 建议的方法扫描前 50-100 行左右。我可能倾向于只是盲目地从带有文本的任何行的前面拉空格数并计算空白字符串的长度。如果您有可用的正则表达式,则左修剪线和运行长度两次似乎是一种浪费。另外,我会做System.Math.abs(indent - previndent),这样你就可以得到去缩进的数据。正则表达式是这样的:

row.matches('^( +)[^ ]') # grab all the spaces from line start to non-space.

一旦您获得了 7 个选项中哪一个的计数最高的统计数据,就可以将其作为第一个猜测。对于 8、6 和 4,您应该检查是否还有 4 和 2、3 或 2 的重要计数(第二名或超过 10% 或其他一些便宜的启发式)。如果有很多 12(或 9s)可能暗示 4(或 3)也是比 8(或 6)更好的选择。一次删除或添加超过 2 个级别(通常是折叠的结束括号)非常罕见。

无关紧要的喃喃自语

我看到的一个问题是,旧的 .c 代码尤其具有这种令人讨厌的模式:

code level 0
/* Fancy comments get weird spacing because there 
 * is an extra space beyond the *
 * looks like one space!
 */
  code indent (2 spaces)
  /* Fancy comments get weird spacing because there 
   * is an extra space beyond the *
   * looks like three spaces!
   */

code level 0
  code indent (2 spaces)
  /* comment at indent level 1
     With no stars you wind up with 2 spaces + 3 spaces.
  */

哎呀。我不知道您如何处理这样的评论标准。对于像“c”这样的代码,您可能必须处理 2.0 版中的特殊 cmets……但我现在暂时忽略它。

您的最后一个问题是处理与您的假设不符的行。我的建议是将它们“标记”到深度,然后将多余的空格留在原处。如果你必须纠正我会这样做:rowtabdepth = ceiling((rowspacecount - (tabwidth/2)) / tabwidth)

【讨论】:

这为 ruby​​ 标准库提供了一个很好的改进,但实际上对 python 的损失很小——从绝对值来看,它看起来更多,但从百分比来看,ruby 的收益超过了Python。查看 Python 出错的地方,没有比“no-long8”更多的文件要正确。使用 20% 的阈值似乎比您猜测的 10% 好一点。我发现您的描述有点不清楚,读起来就像您正在使用绝对缩进,但指的是关于差异的 FastAl;也许需要进行一些编辑。 像你提到的 C 那样精细的布局正是我强调“不是所有语言,不需要完美”的原因。即使使用精确的制表符宽度,也很难插入与格式匹配的文本:最好调用indent 或类似名称。 你是对的,我混合了两个答案,很糟糕。 :-/ 我将调整答案以将其推向像@FastAl's 这样的相对标签。

以上是关于确定源文件中使用的制表符宽度的好的启发式方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

在这个例子中避免使用 iterrows 的好方法是啥?

用于折叠一组可能重叠的范围的好的通用算法是啥?

在 C++ 中声明实例变量而不构造它们的好方法是啥? [关闭]

在 Ruby 中解析制表符分隔文件的最佳方法是啥?

Rails:验证链接(URL)的好方法是啥?

使用 C# 或经典 ASP (VBScript) 从 PDF 中提取文本的好方法是啥? [关闭]