我编写了两个函数,一个是实际函数,另一个是一个助手,用于将camelCase字符串转换为snake_case字符串(我称其为小写联合)。欢迎任何改进的想法。

def _cc2jl(string):
    """Camel case to joint-lower helper."""

    for index, current in enumerate(string):

        if is_upper(current):
            if index > 0:
                previous = string[index-1]

                if is_upper(previous):
                    try:
                        next = string[index+1]
                    except IndexError:
                        yield current.lower()
                        continue
                    else:
                        if is_upper(next):
                            yield current.lower()
                            continue

                yield '_' + current.lower()
                continue

            yield current.lower()
            continue

        yield current


def cc2jl(string):
    """Converts a camel case string to joint-lower."""

    return ''.join(_cc2jl(string))


预期的行为:

>>> cc2jl('m')
'm'
>>> cc2jl('AA')
'aa'
>>> cc2jl('MySQLDatabase')
'my_sql_database'
>>> cc2jl('GarbageCollection')
'garbage_collection'
>>> cc2jl('AAAAAAAAAAAAAAAAA')
'aaaaaaaaaaaaaaaaa'


#1 楼

我认为正则表达式将是最容易做到的。您只需要找到字符串中的每个单词。


名称以大写字母开头。 [A-Z]。但是由于正则表达式其余部分的工作方式,我们可以将其更改为.,以便我们匹配所有单词,即使是以_开头的单词。

该单词将包含大写或小写字母,都带有其他字符-不包括_



大写字母:


该单词不会小写或带有_[^a-z_]+

您不需要最后一个大写字母或_(?=[A-Z_])

如果它是字符串中的最后一个,则需要上面的内容。 (?=[A-Z_]|$)




小写:


该单词不会大写或带有_[^A-Z_]+






因此您可以使用:

(.(?:[^a-z_]+(?=[A-Z_]|$)|[^A-Z_]+))


然后您要对它们应用以下内容:



在名称前加上一个_,除非:


这是名称中的第一个单词
该单词已经以_开头



将单词转换为小写

使:

def _jl_match(match):
    group = match.group()
    prefix = bool(match.start() and not group.startswith('_'))
    return '_' * prefix + group.lower()


所以我要使用:

def _jl_match(match):
    group = match.group()
    prefix = bool(match.start() and not group.startswith('_'))
    return '_' * prefix + group.lower()

REGEX = r'(.(?:[^a-z_]+(?=[A-Z_]|$)|[^A-Z_]+))'

def _cc2jl(string):
    return re.subn(REGEX, _jl_match, string)[0]


评论


\ $ \ begingroup \ $
很好,我知道应该有一种方法可以处理单个正则表达式,但是我的正则表达式太弱了,无法提出一个...我在时序图中添加了您的功能,这就是最快(到目前为止)。我确实对其进行了修改,以便首先编译正则表达式。
\ $ \ endgroup \ $
–地狱
18年1月25日在19:50



\ $ \ begingroup \ $
这是可以使用的各种正则表达式的比较。
\ $ \ endgroup \ $
– 200_success
18年1月25日在19:52

\ $ \ begingroup \ $
因为它的简单性和性能,我选择了您的解决方案:github.com/HOMEINFO/strflib/commit/…
\ $ \ endgroup \ $
–理查德·诺伊曼(Richard Neumann)
18年1月26日在9:02

\ $ \ begingroup \ $
@RichardNeumann谢谢你。但这不是您违反的BY部分-我很高兴您没有:)这是SA部分。如果SE使用CC-BY-SA 4.0,并且您已许可使用GPLv3,则不会有任何违规行为。但他们使用CC-BY-SA 3.0,):
\ $ \ endgroup \ $
– Peilonrayz
18年1月26日,11:33



\ $ \ begingroup \ $
@RichardNeumann很好。我想知道您是否也可以这样做... IANAL,所以我想是时候去Law.SE,; P
\ $ \ endgroup \ $
– Peilonrayz
18年1月26日在12:07

#2 楼

有两件事:


为什么有2个功能?
现在我们建立了(1),您可以摆脱yield
yield在您要分批处理物料并且不想等待首先创建整个总体时很有用。在这里不是这种情况。

有些continue确实有意义,而有些冗余。如果没有降低可读性,可以将带有相应if子句的嵌套else语句与and合并。
>
def cc2jl(my_str):
  """Camel case to joint-lower"""

  r = my_str[0].lower()
  for i, letter in enumerate(my_str[1:], 1):
    if letter.isupper():
      try:
        if my_str[i-1].islower() or my_str[i+1].islower():
          r += '_'
      except IndexError:
        pass
    r += letter.lower()
  return r

print(cc2jl('m'))                  #-> m
print(cc2jl('AA'))                 #-> aa
print(cc2jl('MySQLDatabase'))      #-> my_sql_database
print(cc2jl('GarbageCollection'))  #-> garbage_collection
print(cc2jl('AAAAAAAAAAAAAAAAA'))  #-> aaaaaaaaaaaaaaaaa



最后,一些避免使用try块的想法引起了我的困扰。

我们可以利用Python的逻辑表达式,并(重新)编写以下内容:

def cc2jl(my_str):
  """Camel case to joint-lower"""

  r = my_str[0].lower()
  for i, letter in enumerate(my_str[1:], 1):
    if letter.isupper():
      if my_str[i-1].islower() or (i != len(my_str)-1 and my_str[i+1].islower()):
        r += '_'
    r += letter.lower()
  return r


请注意i != len(my_str)-1左侧的and。如果返回False,则根本不会评估my_str[i+1],因此无法对raise进行评估。

评论


\ $ \ begingroup \ $
清理OP代码很不错!我在答案中将您的函数添加到了时序图中,它比我的regex函数和OP的函数要快(可能是由于摆脱了生成器的开销)。
\ $ \ endgroup \ $
–地狱
18年1月25日在14:07

\ $ \ begingroup \ $
@Graipher我最不喜欢我的方法的是try-except块,但没有想到更优雅的东西。
\ $ \ endgroup \ $
– Ma0
18年1月25日在14:11

\ $ \ begingroup \ $
如果不是0 \ $ \ endgroup \ $
–地狱
18年1月25日在14:15

\ $ \ begingroup \ $
@Graipher 0我不需要,因为我正在遍历my_str [1:]。我尝试遍历my_str [1:-1]并处理返回行中的最后一个字母,但这没什么用更好。总之感谢!
\ $ \ endgroup \ $
– Ma0
18年1月25日在14:17

#3 楼

首先要注意命名:cc2jl是一个非常隐秘的名称。给公用函数一个更清晰的名称,例如to_snake_case或类似的名称(请注意,该函数不会对snake_case中已经存在的字符串执行任何操作)。


我很喜欢这个事实您使用了生成器方法,该方法可以确保仅在传递字符串时进行,这似乎是正则表达式的理想之地。一些快速的谷歌搜索发现此功能非常简单:但是遵循代码虽然编写起来非常简单,但也并不容易,尤其是在深层嵌套中。

它的工作原理是先分割大写字母,再分割一个或多个小写字母,以便最后一个大写字母转到小写字母运行。因此,"ABCdef"变为"AB_Cdef"

然后将非大写字母的行分隔开,后面是带有"_"的单个大写字母,因此"abcD"变为"abc_D"。要从中获得更多性能,您应该预编译正则表达式:

import re

def convert(name):
    s1 = re.sub('(.)([A-Z][a-z]+)', r'_', name)
    return re.sub('([a-z0-9])([A-Z])', r'_', s1).lower()




性能方面,所有算法都是相似的: >


不足为奇,它们都具有线性行为(作为字符串长度的函数)。

我的regex版本需要进行两次传递字符串,因此始终较慢。 @ Ev.Kounis在他的答案中编写的函数优于我们两个函数,但是@Peilonrayz的正则表达式方法甚至更快,因为它只能执行一次通过,但是带有正则表达式。

注意所有功能都非常快,因此只要您每秒需要几千次以下就可以了。


测试字符串是使用以下代码生成的:

first_cap_re = re.compile('(.)([A-Z][a-z]+)')
all_cap_re = re.compile('([a-z0-9])([A-Z])')
def convert(name):
    s1 = first_cap_re.sub(r'_', name)
    return all_cap_re.sub(r'_', s1).lower()


在Python 3中可能是:

import random
import string

strings = [''.join(random.choice(string.ascii_letters) for _ in range(n))
           for n in range(10, 2000, 20)]


评论


\ $ \ begingroup \ $
如果我了解正则表达式,我会很喜欢这个答案。我的脑子太小了。 +1
\ $ \ endgroup \ $
– Ma0
18年1月25日在14:08

\ $ \ begingroup \ $
@ Ev.Kounis我完全同意(因为我自己没有提出它,请参阅源链接)。这里至少有一些解释:第一个替换处理连续的大写字母(因此它将大写字母从最后一个大写字母中拆分出来),而最后一个替换将小写字母和大写字母后跟拆分。
\ $ \ endgroup \ $
–地狱
18年1月25日在14:13



\ $ \ begingroup \ $
感谢您的深入分析和性能评估。由于后者,我接受了@peilonrayz解决方案。
\ $ \ endgroup \ $
–理查德·诺伊曼(Richard Neumann)
18年1月26日在9:02



#4 楼

它可以更短一些:

def cc2jl(s):
  return "".join(["_"+l if i and l.isupper() and not s[i-1:i+2].isupper() else l for i, l in enumerate(s)]).lower()


正则表达式替代项:

rx = re.compile(r"(?<=.)(((?<![A-Z])[A-Z])|([A-Z](?=[a-z])))")
def cc2jl(s):
    return rx.sub("_\1", s).lower()


评论


\ $ \ begingroup \ $
我的观察结果是代码长于所需的事实,我的答案是替代解决方案。与正则表达式相比,可读性是主观的,我认为它更易于阅读。
\ $ \ endgroup \ $
– Utku M. ALTINKAYA
18年1月26日在6:14

\ $ \ begingroup \ $
@Graipher已修复
\ $ \ endgroup \ $
– Utku M. ALTINKAYA
18年1月26日在8:06

\ $ \ begingroup \ $
在计时中添加了功能
\ $ \ endgroup \ $
–地狱
18年1月26日在9:12

\ $ \ begingroup \ $
“(...)与regex相比,我认为它更易于阅读”。不
\ $ \ endgroup \ $
– Grajdeanu Alex
18年1月26日在11:24

\ $ \ begingroup \ $
感谢@Graipher,我已经修改了该代码并添加了正则表达式替代项
\ $ \ endgroup \ $
– Utku M. ALTINKAYA
18年1月26日在12:11