获取包含特定字母的所有单词的正则表达式(unicode 符号)

7 投票
4 回答
3402 浏览
提问于 2025-04-17 13:48

我正在为一个开源语言学习项目写一个Python脚本。假设我有一个XML文件(为了简单起见,也可以是一个Python列表),里面有一组特定语言的单词(在我的例子中,这些单词是泰米尔语,使用的是一种基于婆罗米的印度文字)。

我需要从这些单词中找出那些只用这些字母拼写出来的子集。

举个英语的例子:

words = ["cat", "dog", "tack", "coat"] 

get_words(['o', 'c', 'a', 't']) should return ["cat", "coat"]
get_words(['k', 'c', 't', 'a']) should return ["cat", "tack"]

再举个泰米尔语的例子:

words = [u"மரம்", u"மடம்", u"படம்", u"பாடம்"]

get_words([u'ம', u'ப', u'ட', u'ம்')  should return [u"மடம்", u"படம்")
get_words([u'ப', u'ம்', u'ட') should return [u"படம்"] 

返回单词的顺序,或者输入字母的顺序都不应该影响结果。

虽然我知道unicode代码点和字形之间的区别,但我不太确定它们在正则表达式中是怎么处理的。

在这种情况下,我想要匹配的单词只能由输入列表中的特定字形组成,不能有其他字形(也就是说,跟在字母后面的标记只能跟在那个字母后面,但字形本身可以以任何顺序出现)。

4 个回答

3

要创建一个只匹配特定字符组合的正则表达式其实很简单。你需要用到一个叫“字符类”的东西,里面放入你想要匹配的字符。

我用英文来举个例子。

[ocat] 这是一个字符类,它会匹配集合 [o, c, a, t] 中的任意一个字符。字符的顺序并不重要。

[ocat]+ 在后面加一个 + 符号,就表示可以匹配一个或多个来自这个集合的字符。不过,这样还不够;如果你有一个单词 "coach",这个表达式会匹配并返回 "coac"

\b[ocat]+\b 现在它只会在单词的边界处匹配。 (非常感谢 @Mark Tolonen 教我关于 \b 的知识。)

所以,只需像上面那样构建一个模式,在运行时使用你想要的字符集合,就可以了。你可以用这个模式配合 re.findall()re.finditer() 来使用。

import re

words = ["cat", "dog", "tack", "coat"]

def get_words(chars_seq, words_seq=words):
    s_chars = ''.join(chars_seq)
    s_pat = r'\b[' + s_chars + r']+\b'
    pat = re.compile(s_pat)
    return [word for word in words_seq if pat.match(word)]

assert get_words(['o', 'c', 'a', 't']) == ["cat", "coat"]
assert get_words(['k', 'c', 't', 'a']) == ["cat", "tack"]
3

编辑:好吧,不要使用这里的任何答案。我写这些答案的时候以为Python的正则表达式没有单词边界标记,所以我试图绕过这个缺陷。然后@Mark Tolonen评论说Python有\b作为单词边界标记!所以我又发了一个答案,简单明了,使用了\b。我把这个留在这里,以防有人对解决没有\b的情况感兴趣,但我并不指望会有人。


要创建一个只匹配特定字符集合的字符串的正则表达式其实很简单。你需要使用一个“字符类”,里面放入你想匹配的字符。

我用英语来做这个例子。

[ocat] 这是一个字符类,它会匹配集合[o, c, a, t]中的一个字符。字符的顺序并不重要。

[ocat]+ 在末尾加上+会使它匹配一个或多个来自这个集合的字符。但这还不够;如果你有单词“coach”,它会匹配并返回“coac”。

可惜的是,正则表达式没有“单词边界”的功能。[编辑:这实际上是不正确的,正如我在第一段中所说的。] 我们需要自己创建一个。单词的开始有两种可能:行的开始,或者是空格将我们的单词与前一个单词分开。同样,单词的结束也有两种可能:行的结束,或者是空格将我们的单词与下一个单词分开。

由于我们会匹配一些不想要的额外内容,我们可以用括号把我们想要的模式部分包起来。

要匹配两个选择,我们可以在括号中创建一个组,并用竖线分隔这些选择。Python的正则表达式有一种特殊的表示法,用于创建一个我们不想保留内容的组:(?:)

所以,这里是匹配单词开头的模式。行的开始或空格:(?:^|\s)

这是单词结尾的模式。空格或行的结束:(?:\s|$)

把这些组合在一起,这就是我们的最终模式:

(?:^|\s)([ocat]+)(?:\s|$)

你可以动态构建这个。你不需要把整个东西硬编码。

import re

s_pat_start = r'(?:^|\s)(['
s_pat_end = r']+)(?:\s|$)'

set_of_chars = get_the_chars_from_somewhere_I_do_not_care_where()
# set_of_chars is now set to the string: "ocat"

s_pat = s_pat_start + set_of_chars + s_pat_end
pat = re.compile(s_pat)

现在,这并不检查有效的单词。如果你有以下文本:

This is sensible.  This not: occo cttc

我给你展示的模式会匹配occocttc,而这些实际上并不是单词。它们只是由[ocat]中的字母组成的字符串。

所以只需对Unicode字符串做同样的事情。(如果你使用的是Python 3.x,那么所有字符串都是Unicode字符串,所以没问题。)把泰米尔字符放入字符类中,你就可以了。

这有一个令人困惑的问题:re.findall()并不会返回所有可能的匹配。

编辑:好吧,我搞清楚了让我困惑的地方。

我们希望我们的模式能与re.findall()一起工作,这样你就可以收集所有的单词。但re.findall()只找到不重叠的模式。在我的例子中,re.findall()只返回了['occo'],而不是我预期的['occo', 'cttc']……但这是因为我的模式匹配了occo后面的空格。匹配组没有收集空格,但它仍然被匹配了,由于re.findall()不希望匹配之间有重叠,所以空格被“用掉”了,导致cttc无法匹配。

解决方案是使用Python正则表达式的一个我从未使用过的特性:特殊语法表示“前面不能是”或“后面不能是”。序列\S匹配任何非空白字符,所以我们可以使用它。但标点符号是非空白字符,我认为我们确实希望标点符号来分隔单词。还有特殊语法表示“前面必须是”或“后面必须是”。所以,我认为这是我们能做到的最好方式:

构建一个字符串,表示“当字符类字符串在行首并后面跟着空格时匹配,或者当字符类字符串前面有空格并后面也有空格时匹配,或者当字符类字符串前面有空格并后面跟着行尾时匹配,或者当字符类字符串前面是行首并后面是行尾时匹配”。

这里是使用ocat的模式:

r'(?:^([ocat]+)(?=\s)|(?<=\s)([ocat]+)(?=\s)|(?<=\s)([ocat]+)$|^([ocat]+)$)'

我很抱歉,但我真的认为这是我们能做到的最好方式,同时还能与re.findall()一起工作!

在Python代码中其实更不混乱:

import re

NMGROUP_BEGIN = r'(?:'  # begin non-matching group
NMGROUP_END = r')'  # end non-matching group

WS_BEFORE = r'(?<=\s)'  # require white space before
WS_AFTER = r'(?=\s)'  # require white space after

BOL = r'^' # beginning of line
EOL = r'$' # end of line

CCS_BEGIN = r'(['  #begin a character class string
CCS_END = r']+)'  # end a character class string

PAT_OR = r'|'

set_of_chars = get_the_chars_from_somewhere_I_do_not_care_where()
# set_of_chars now set to "ocat"

CCS = CCS_BEGIN + set_of_chars + CCS_END  # build up character class string pattern

s_pat = (NMGROUP_BEGIN +
    BOL + CCS + WS_AFTER + PAT_OR +
    WS_BEFORE + CCS + WS_AFTER + PAT_OR +
    WS_BEFORE + CCS + EOL + PAT_OR +
    BOL + CCS + EOL +
    NMGROUP_END)

pat = re.compile(s_pat)

text = "This is sensible.  This not: occo cttc"

pat.findall(text)
# returns: [('', 'occo', '', ''), ('', '', 'cttc', '')]

所以,奇怪的是,当我们有可能匹配的替代模式时,re.findall()似乎会为没有匹配的替代返回一个空字符串。所以我们只需要从结果中过滤掉长度为零的字符串:

import itertools as it

raw_results = pat.findall(text)
results = [s for s in it.chain(*raw_results) if s]
# results set to: ['occo', 'cttc']

我想如果直接构建四个不同的模式,分别运行re.findall(),然后把结果连接在一起,可能会更清晰。

编辑:好吧,这里是构建四个模式并尝试每个的代码。我认为这是一个改进。

import re

WS_BEFORE = r'(?<=\s)'  # require white space before
WS_AFTER = r'(?=\s)'  # require white space after

BOL = r'^' # beginning of line
EOL = r'$' # end of line

CCS_BEGIN = r'(['  #begin a character class string
CCS_END = r']+)'  # end a character class string

set_of_chars = get_the_chars_from_somewhere_I_do_not_care_where()
# set_of_chars now set to "ocat"

CCS = CCS_BEGIN + set_of_chars + CCS_END  # build up character class string pattern

lst_s_pat = [
    BOL + CCS + WS_AFTER,
    WS_BEFORE + CCS + WS_AFTER,
    WS_BEFORE + CCS + EOL,
    BOL + CCS
]

lst_pat = [re.compile(s) for s in lst_s_pat]

text = "This is sensible.  This not: occo cttc"

result = []
for pat in lst_pat:
    result.extend(pat.findall(text))

# result set to: ['occo', 'cttc']

编辑:好吧,这里有一个非常不同的方法。我最喜欢这个。

首先,我们将匹配文本中的所有单词。单词被定义为一个或多个不是标点符号且不是空格的字符。

然后,我们使用过滤器从上述单词中移除,只保留由我们想要的字符组成的单词。

import re
import string

# Create a pattern that matches all characters not part of a word.
#
# Note that '-' has a special meaning inside a character class, but it
# is valid punctuation that we want to match, so put in a backslash in
# front of it to disable the special meaning and just match it.
#
# Use '^' which negates all the chars following.  So, a word is a series
# of characters that are all not whitespace and not punctuation.

WORD_BOUNDARY = string.whitespace + string.punctuation.replace('-', r'\-')

WORD = r'[^' + WORD_BOUNDARY + r']+'


# Create a pattern that matches only the words we want.

set_of_chars = get_the_chars_from_somewhere_I_do_not_care_where()
# set_of_chars now set to "ocat"

# build up character class string pattern
CCS = r'[' + set_of_chars + r']+'


pat_word = re.compile(WORD)
pat = re.compile(CCS)

text = "This is sensible.  This not: occo cttc"


# This makes it clear how we are doing this.
all_words = pat_word.findall(text)
result = [s for s in all_words if pat.match(s)]

# "lazy" generator expression that yields up good results when iterated
# May be better for very large texts.
result_genexp = (s for s in (m.group(0) for m in pat_word.finditer(text)) if pat.match(s))

# force the expression to expand out to a list
result = list(result_genexp)

# result set to: ['occo', 'cttc']

编辑:现在我不喜欢以上任何解决方案;请查看其他答案,使用\b的那个,才是Python中最好的解决方案。

5

为了支持可以跨越多个Unicode编码点的字符:

# -*- coding: utf-8 -*-
import re
import unicodedata
from functools import partial

NFKD = partial(unicodedata.normalize, 'NFKD')

def match(word, letters):
    word, letters = NFKD(word), map(NFKD, letters) # normalize
    return re.match(r"(?:%s)+$" % "|".join(map(re.escape, letters)), word)

words = [u"மரம்", u"மடம்", u"படம்", u"பாடம்"]
get_words = lambda letters: [w for w in words if match(w, letters)]

print(" ".join(get_words([u'ம', u'ப', u'ட', u'ம்'])))
# -> மடம் படம்
print(" ".join(get_words([u'ப', u'ம்', u'ட'])))
# -> படம்

这里假设同一个字符可以在一个单词中出现零次或多次。

如果你只想要包含特定字符的单词:

import regex # $ pip install regex

chars = regex.compile(r"\X").findall # get all characters

def match(word, letters):
    return sorted(chars(word)) == sorted(letters)

words = ["cat", "dog", "tack", "coat"]

print(" ".join(get_words(['o', 'c', 'a', 't'])))
# -> coat
print(" ".join(get_words(['k', 'c', 't', 'a'])))
# -> tack

注意:在这种情况下,输出中没有cat,因为cat没有使用所有给定的字符。


什么是规范化?能不能解释一下re.match()正则表达式的语法?

>>> import re
>>> re.escape('.')
'\\.'
>>> c = u'\u00c7'
>>> cc = u'\u0043\u0327'
>>> cc == c
False
>>> re.match(r'%s$' % (c,), cc) # do not match
>>> import unicodedata
>>> norm = lambda s: unicodedata.normalize('NFKD', s)
>>> re.match(r'%s$' % (norm(c),), norm(cc)) # do match
<_sre.SRE_Match object at 0x1364648>
>>> print c, cc
Ç Ç

如果不进行规范化,ccc是无法匹配的。这些字符来自unicodedata.normalize()文档

撰写回答