【问题标题】:Script to remove Python comments/docstrings删除 Python 注释/文档字符串的脚本
【发布时间】:2010-12-18 16:00:01
【问题描述】:

是否有可以从 Python 源代码中删除 cmets 和 docstrings 的 Python 脚本或工具?

它应该处理以下情况:

"""
aas
"""
def f():
    m = {
        u'x':
            u'y'
        } # faake docstring ;)
    if 1:
        'string' >> m
    if 2:
        'string' , m
    if 3:
        'string' > m

所以最后我想出了一个简单的脚本,它使用标记化模块并删除评论标记。它似乎工作得很好,除了我无法在所有情况下删除文档字符串。看看你是否可以改进它以删除文档字符串。

import cStringIO
import tokenize

def remove_comments(src):
    """
    This reads tokens using tokenize.generate_tokens and recombines them
    using tokenize.untokenize, and skipping comment/docstring tokens in between
    """
    f = cStringIO.StringIO(src)
    class SkipException(Exception): pass
    processed_tokens = []
    last_token = None
    # go thru all the tokens and try to skip comments and docstrings
    for tok in tokenize.generate_tokens(f.readline):
        t_type, t_string, t_srow_scol, t_erow_ecol, t_line = tok

        try:
            if t_type == tokenize.COMMENT:
                raise SkipException()

            elif t_type == tokenize.STRING:

                if last_token is None or last_token[0] in [tokenize.INDENT]:
                    # FIXEME: this may remove valid strings too?
                    #raise SkipException()
                    pass

        except SkipException:
            pass
        else:
            processed_tokens.append(tok)

        last_token = tok

    return tokenize.untokenize(processed_tokens)

我还想在具有良好单元测试覆盖率的大量脚本上对其进行测试。你能推荐这样一个开源项目吗?

【问题讨论】:

  • @mavnn :),不要问,我们需要保护我们的代码不被窥探 LOL
  • -1:-OO 选项从字节码中删除 cmets。为什么要乱搞其他东西?通过删除文档字符串(可能包含有用的单元测试)来混淆代码是没有意义的。
  • @S.Lott -OO 将编译后的代码冻结为不同的 python 版本。我同意你的观点,这不是通常有用的任务,但在极少数情况下需要它。这对我来说也是一个很好的玩具任务,所以+1。
  • 一个用例可能是计算代码行数。 cloc 包含文档字符串 - 如果没有,我会更喜欢。

标签: python comments


【解决方案1】:

这是对Dan's solution 的修改,使其可在 Python3 上运行 + 还删除空行 + 使其可以使用:

import io, tokenize, re
def remove_comments_and_docstrings(source):
    io_obj = io.StringIO(source)
    out = ""
    prev_toktype = tokenize.INDENT
    last_lineno = -1
    last_col = 0
    for tok in tokenize.generate_tokens(io_obj.readline):
        token_type = tok[0]
        token_string = tok[1]
        start_line, start_col = tok[2]
        end_line, end_col = tok[3]
        ltext = tok[4]
        if start_line > last_lineno:
            last_col = 0
        if start_col > last_col:
            out += (" " * (start_col - last_col))
        if token_type == tokenize.COMMENT:
            pass
        elif token_type == tokenize.STRING:
            if prev_toktype != tokenize.INDENT:
                if prev_toktype != tokenize.NEWLINE:
                    if start_col > 0:
                        out += token_string
        else:
            out += token_string
        prev_toktype = token_type
        last_col = end_col
        last_lineno = end_line
    out = '\n'.join(l for l in out.splitlines() if l.strip())
    return out
with open('test.py', 'r') as f:
    print(remove_comments_and_docstrings(f.read()))

【讨论】:

  • 谢谢!只是备注ltext 变量从未使用过。
【解决方案2】:

我找到了一种更简单的方法来使用 ast 和 astunparse 模块(可从 pip 获得)。它将代码文本转换为语法树,然后 astunparse 模块在没有 cmets 的情况下再次打印出代码。我不得不用一个简单的匹配去掉文档字符串,但它似乎有效。我一直在查看输出,到目前为止,此方法的唯一缺点是它会从您的代码中删除所有换行符。

import ast, astunparse

with open('my_module.py') as f:
    lines = astunparse.unparse(ast.parse(f.read())).split('\n')
    for line in lines:
        if line.lstrip()[:1] not in ("'", '"'):
            print(line)

【讨论】:

  • 这是唯一正确的方法,imo。 lstrip() ... in 命令应该通过查看 ast 并排除文档字符串节点来替换。它依赖于 unparse 以某种方式表现,并且从不使用多行语句等。
【解决方案3】:

我刚刚使用了 Dan McDougall 给出的代码,发现了两个问题。

  1. 空的新行太多,所以我决定每次有两个连续的新行时删除行
  2. 处理 Python 代码时,所有空格都丢失了(缩进除外),因此“importanything”之类的内容更改为“importAnything”,这会导致问题。我在需要完成的保留 Python 单词之前和之前添加了空格。我希望我没有犯任何错误。

我想我已经通过添加(在返回之前)几行来解决这两个问题:

# Removing unneeded newlines from string
buffered_content = cStringIO.StringIO(content) # Takes the string generated by Dan McDougall's code as input
content_without_newlines = ""
previous_token_type = tokenize.NEWLINE
for tokens in tokenize.generate_tokens(buffered_content.readline):
    token_type = tokens[0]
    token_string = tokens[1]
    if previous_token_type == tokenize.NL and token_type == tokenize.NL:
        pass
    else:
        # add necessary spaces
        prev_space = ''
        next_space = ''
        if token_string in ['and', 'as', 'or', 'in', 'is']:
            prev_space = ' '
        if token_string in ['and', 'del', 'from', 'not', 'while', 'as', 'elif', 'global', 'or', 'with', 'assert', 'if', 'yield', 'except', 'import', 'print', 'class', 'exec', 'in', 'raise', 'is', 'return', 'def', 'for', 'lambda']:
            next_space = ' '
        content_without_newlines += prev_space + token_string + next_space # This will be our new output!
    previous_token_type = token_type

【讨论】:

    【解决方案4】:

    我是“mygod,他使用正则表达式编写了一个 python 解释器...”(即 pyminifier)的作者,提到了at that link below =)。
    我只是想插话说我已经使用 tokenizer 模块对代码进行了相当大的改进(我发现了这个问题 =))。

    您会很高兴地注意到,代码不再过多地依赖正则表达式,而是使用了分词器,效果非常好。无论如何,这是来自 pyminifier
    remove_comments_and_docstrings() 函数 (注意:它适用于先前发布的代码中断的边缘情况):

    import cStringIO, tokenize
    def remove_comments_and_docstrings(source):
        """
        Returns 'source' minus comments and docstrings.
        """
        io_obj = cStringIO.StringIO(source)
        out = ""
        prev_toktype = tokenize.INDENT
        last_lineno = -1
        last_col = 0
        for tok in tokenize.generate_tokens(io_obj.readline):
            token_type = tok[0]
            token_string = tok[1]
            start_line, start_col = tok[2]
            end_line, end_col = tok[3]
            ltext = tok[4]
            # The following two conditionals preserve indentation.
            # This is necessary because we're not using tokenize.untokenize()
            # (because it spits out code with copious amounts of oddly-placed
            # whitespace).
            if start_line > last_lineno:
                last_col = 0
            if start_col > last_col:
                out += (" " * (start_col - last_col))
            # Remove comments:
            if token_type == tokenize.COMMENT:
                pass
            # This series of conditionals removes docstrings:
            elif token_type == tokenize.STRING:
                if prev_toktype != tokenize.INDENT:
            # This is likely a docstring; double-check we're not inside an operator:
                    if prev_toktype != tokenize.NEWLINE:
                        # Note regarding NEWLINE vs NL: The tokenize module
                        # differentiates between newlines that start a new statement
                        # and newlines inside of operators such as parens, brackes,
                        # and curly braces.  Newlines inside of operators are
                        # NEWLINE and newlines that start new code are NL.
                        # Catch whole-module docstrings:
                        if start_col > 0:
                            # Unlabelled indentation means we're inside an operator
                            out += token_string
                        # Note regarding the INDENT token: The tokenize module does
                        # not label indentation inside of an operator (parens,
                        # brackets, and curly braces) as actual indentation.
                        # For example:
                        # def foo():
                        #     "The spaces before this docstring are tokenize.INDENT"
                        #     test = [
                        #         "The spaces before this string do not get a token"
                        #     ]
            else:
                out += token_string
            prev_toktype = token_type
            last_col = end_col
            last_lineno = end_line
        return out
    

    【讨论】:

      【解决方案5】:

      尝试测试以 NEWLINE 结尾的每个令牌块。然后正确的文档字符串模式(包括它用作注释但未分配给__doc__的情况)我相信是(假设匹配是从NEWLINE之后的文件开头执行的):

      ( DEDENT+ | INDENT? ) STRING+ COMMENT? NEWLINE
      

      这应该处理所有棘手的情况:字符串连接、行继续、模块/类/函数文档字符串、字符串后同一行中的注释。注意,NL 和 NEWLINE 标记是有区别的,所以我们不需要担心表达式中的单个字符串。

      【讨论】:

      【解决方案6】:

      这样就可以了:

      """ Strip comments and docstrings from a file.
      """
      
      import sys, token, tokenize
      
      def do_file(fname):
          """ Run on just one file.
      
          """
          source = open(fname)
          mod = open(fname + ",strip", "w")
      
          prev_toktype = token.INDENT
          first_line = None
          last_lineno = -1
          last_col = 0
      
          tokgen = tokenize.generate_tokens(source.readline)
          for toktype, ttext, (slineno, scol), (elineno, ecol), ltext in tokgen:
              if 0:   # Change to if 1 to see the tokens fly by.
                  print("%10s %-14s %-20r %r" % (
                      tokenize.tok_name.get(toktype, toktype),
                      "%d.%d-%d.%d" % (slineno, scol, elineno, ecol),
                      ttext, ltext
                      ))
              if slineno > last_lineno:
                  last_col = 0
              if scol > last_col:
                  mod.write(" " * (scol - last_col))
              if toktype == token.STRING and prev_toktype == token.INDENT:
                  # Docstring
                  mod.write("#--")
              elif toktype == tokenize.COMMENT:
                  # Comment
                  mod.write("##\n")
              else:
                  mod.write(ttext)
              prev_toktype = toktype
              last_col = ecol
              last_lineno = elineno
      
      if __name__ == '__main__':
          do_file(sys.argv[1])
      

      我将保留存根 cmets 代替 docstrings 和 cmets,因为它简化了代码。如果你完全删除它们,你还必须去掉它们之前的缩进。

      【讨论】:

      • 虽然在实践中大多数情况下看起来都不错,但通常并不正确。想象一下像'string' >> obj 这样的语句,例如将字符串存储在obj 中(具有相应的__rrshift__() 方法)。
      • 这也有其他问题。例如,如果一个函数 only 有一个文档字符串,则结果在语法上是无效的。此外,选项卡处理似乎存在一些令人困惑的问题(不是任何人都应该使用选项卡)。看到基于 AST 而不是令牌流的这个想法的正确版本可能会很有趣。
      猜你喜欢
      • 1970-01-01
      • 2013-08-05
      • 2017-12-03
      • 2010-12-09
      • 1970-01-01
      • 2013-04-12
      • 2017-01-19
      • 2012-01-27
      • 2014-04-22
      相关资源
      最近更新 更多