如何在不使用空格作为单词分隔符的语言(如中文)上执行Python的split()函数?

22
我想把一个句子分成单词列表。
对于英语和欧洲语言来说,这很容易,只需要使用split()函数。
>>> "This is a sentence.".split()
['This', 'is', 'a', 'sentence.']

但我还需要处理一些语言(比如中文)中没有使用空格作为单词分隔符的句子。

>>> u"这是一个句子".split()
[u'\u8fd9\u662f\u4e00\u4e2a\u53e5\u5b50']

显然这不起作用。

如何将这样的句子分成单词列表?

更新:

到目前为止,答案似乎表明这需要自然语言处理技术,而中文中的词边界是模糊的。我不确定我理解为什么会这样。对我来说,中文单词/字符的边界非常明确。每个中文单词/字符都有相应的Unicode,并作为单独的单词/字符显示在屏幕上。

那么模糊性从哪里来?正如您在Python控制台输出中所看到的,Python没有问题告诉我示例句由5个字符组成:

 - u8fd9
 - u662f
 - u4e00
 - u4e2a
 - u53e5
 - u5b50

显然Python没有问题告诉我们单词/字符的边界,我只需要把这些单词/字符放在列表中。


3
简短回答:你不需要这样做。 - JUST MY correct OPINION
2
那么,中文使用什么作为单词分隔符? - Jim Brissom
5
“句子”是一个词吗?由于每个Unicode字符都是一个汉字,所以“字符边界”不是问题。难点在于知道哪些汉字属于同一个词。 - sth
1
似乎混淆来自于“单词”和“字符”的含义。我从未想过“句子”是一个单独的单词。每个中文字符都是一个“字”,被翻译为“word”。 “句子”显然有两个“字”,因此我从未将其视为一个单词。 - Continuation
7
“字”翻译为“word”是常见但具有误导性的。 “字”意指一个字符,而不是一个词。 英语中的“word”应翻译为“詞”。 - Porculus
@Continuation “中文的词边界对我来说似乎非常明确。” 请参阅我在Mark Byers答案下的评论。 - NullUserException
9个回答

21
你可以做到这一点,但不能使用标准库函数。正则表达式也无法帮助你。
你所描述的任务是自然语言处理(NLP)领域的一部分。已经有很多关于在中文词边界处拆分中文单词的研究工作。我建议您使用其中一个现有的解决方案,而不是尝试自己编写。

歧义来自哪里?

你列出的是中文字符。它们与英语中的字母或音节类似(但与 NullUserException 在评论中指出的不完全相同)。字符边界没有歧义-这非常明确。但你询问的不是字符边界,而是单词边界。中文单词可能由多个字符组成。
如果你只想找到字符,那么这很简单,不需要 NLP 库。只需将消息解码为 Unicode 字符串(如果尚未完成),然后使用对内置函数list的调用将 Unicode 字符串转换为列表。这将给你一个字符串中字符的列表。对于你的具体示例:
>>> list(u"这是一个句子")

确实。我可以补充 http://alias-i.com/lingpipe/demos/tutorial/chineseTokens/read-me.html - Jim Brissom
绝大多数汉字本身都有独立的意义。例如:"这是一个句子"中的每个汉字都有自己的意思 - "这=this," "是=is", "一=one," "个=a", "句=sentence." 麻烦的地方在于,一些组合汉字代表一个单独的“事物”(例如:“句子”代表“sentence”),而有时组合汉字的意义与其中的单个汉字完全不同。 - NullUserException
3
因此,这些Unicode字符中的每一个对于中文来说都是一个“字”,它不同于一个“词”,但也不等同于西方的字母或音节。 - NullUserException

15

注意:在Python 3中使用list('...')(对于Python 2是u'...')通常不会以一般意义上的字符形式给出Unicode字符串;相反,它很可能会导致一系列16位代码点。这对于所有“窄”CPython版本都是正确的,这占了今天Python安装的绝大部分。

当Unicode在1990年代首次提出时,建议使用16位足以涵盖通用文本编码的所有需求,因为它将128个代码点(7位)和256个代码点(8位)转换为高达65,536个代码点。然而,很快就明显,那只是一厢情愿的想法;今天,在Unicode版本5.2中定义了约100,000个代码点,并有数千个待包含的代码点。为了实现这一点,Unicode必须从16位移动到(概念上的)32位(尽管它没有充分利用32位地址空间)。

为了与基于Unicode仍为16位的假设构建的软件保持兼容性,设计了所谓的代理对,其中从特定指定的块中使用两个16位代码点表示超过65,536的代码点,即超出Unicode所称的“基本多语言平面”或BMP,并且被开玩笑地称为该编码的“星体”平面,因为它们相对难以捉摸并且给从事文本处理和编码领域的人带来了不断的头疼。

窄版CPython在某些情况下相当透明地处理代理对,但在其他情况下仍将无法执行正确操作。字符串拆分是其中较为麻烦的一种情况。在窄版Python构建中,list('abc大def')(或通过转义写作list('abc\u5927\U00027C3Cdef'))将导致['a'、'b'、'c'、'大'、'\ud85f'、'\udc3c'、'd'、'e'、'f'],其中'\ud85f'、'\udc3c'是代理对。顺便说一下,'\ud85f\udc3c'是JSON标准期望您编写以表示U-27C3C的内容。这两个代码点中的任何一个单独使用都是无用的;格式良好的Unicode字符串始终只能具有代理对。

因此,要将字符串拆分为字符,你真正需要做的是:

from re import compile as _Re

_unicode_chr_splitter = _Re( '(?s)((?:[\ud800-\udbff][\udc00-\udfff])|.)' ).split

def split_unicode_chrs( text ):
  return [ chr for chr in _unicode_chr_splitter( text ) if chr ]

以下正则表达式可以正确返回['a', 'b', 'c', '大', '', 'd', 'e', 'f'](注意:您可能可以重写正则表达式,使过滤空字符串变得不必要)。

如果你只想将文本分割成汉字,则在此时你几乎已经完成了所有工作。我不确定OP对“单词”的概念是什么,但对我来说,“这是一个句子”可以被等分为“这 | 是 | 一 | 个 | 句子”以及“这是 | 一个 | 句子”,这取决于你的观点。然而,任何超出字符和字符类(符号 vs 空格 vs 字母等)的概念的内容都远远超出了Unicode和Python内置的范畴;您需要进行一些自然语言处理才能做到这一点。让我备注一下,虽然您的示例'yes the United Nations can!'.split()成功演示了split方法对大量数据的实用性,但它未能正确地将英文文本解析成单词:它未能识别United Nations为一个单词,同时却错误地认为can!是一个单词,而这显然不是。该方法会产生误报和漏报。这取决于您的数据和您想要实现的目标,这可能是您想要的,也可能不是。


1
联合国是两个词,即使它是一个专有名词。 - Stefan Sullivan
赞赏你认识到OP的问题涉及两个非常不同的问题(Unicode代码点和中文单词概念),并分别解决它们,这是这里几乎没有其他人做到的。 - hugo

7

好的,我明白了。

我需要做的可以通过简单地使用list()来完成:

>>> list(u"这是一个句子")
[u'\u8fd9', u'\u662f', u'\u4e00', u'\u4e2a', u'\u53e5', u'\u5b50']

感谢您提供的所有意见。

3
你认为你需要的东西并不是很有用。这就像试图从“break”和“fast”这两个独立的概念中提取“早餐”的含义一样。 - John Machin
3
我会建议让 OP 自行决定哪些是有用的。早餐确实是为了“打破”长时间停止进食后再次进食。这个回答没有帮助吗? - flow
@flow: "早餐" (不是 "是") 为了 "打破" "禁食" ... 连接之薄弱就像 "九龙" -> "九龙城" 一样。 - John Machin
九龍(Kowloon)的字面意思是“九條龍”,至少對於數以百萬計不了解更多的人來說是這樣。正如維基百科所述,“相傳九龍地名由來最常見的解釋是九龍北部的八條山脈亦即為龍脈,加上皇帝自己,便是九條龍脈;另一說法是九只是代表多的意思”。至於早餐,http://www.thefreedictionary.com/breakfast 提供了以下解釋:“中古英語brekfast:breken,打破;參見break + faste,快速(來自古諾爾斯語fasta,禁食;參見印歐語根中的past-)。而且,無論是“was”、“is”還是“will”,都沒有問題。 - flow
这里有许多漂亮的中文符号,让我羡慕那些真正能读懂它们的人。 ;) - mlvljr
这是针对仅中文句子的答案。那么在同一句中使用混合英语/中文呢?有关混合拆分的答案在混合拆分中得到解答。 - Jesse

5

最好的中文分词工具是pynlpir。

import pynlpir
pynlpir.open()
mystring = "你汉语说的很好!"
tokenized_string = pynlpir.segment(mystring, pos_tagging=False)

>>> tokenized_string
['你', '汉语', '说', '的', '很', '好', '!']

请注意,pynlpir存在一个臭名昭著但易于解决的许可问题,您可以在互联网上找到大量解决方案。 您只需下载此存储库中的有效许可证并替换NLPIR文件夹中的NLPIR.user文件,然后重新启动您的环境即可。


4
像中文这样的语言对单词的定义非常灵活。例如,“马”是“ma”的一个意思。“上”或“在……上面”是“shang”的一个意思。一个组合词是“马上”,字面意思是“骑在马上”,但通常用于表示“立刻”。你需要一本包含复合词的良好词典,并使用最长匹配方法查找字典。德语(著名的例子是像“多瑙河蒸汽航行公司董事的妻子”这样的东西被表达为一个单词)、突厥语、芬兰语和匈牙利语中都有很多复合词,其中许多词汇在词典中找不到,需要进行分解才能理解它们。
你遇到的问题是语言学问题,与Python无关。

我猜我们在将“词”这个术语应用于中文时有不同的用法。对我来说,“马上”只是两个字/字符。 马是第一个,上是第二个。 - Continuation
3
对我而言,一个词是由一个或多个字符组成的字符串,具有特定的含义。需要注意的是,一些字符本身没有意义,只有与其他字符结合才有意义。仅有字符列表并不是很有用。 - John Machin

1

list() 是针对仅有中文的句子的解决方案。对于大多数情况下的英汉混合语句,可以在 hybrid-split 找到答案,只需从 Winter 处复制以下内容即可。

def spliteKeyWord(str):
    regex = r"[\u4e00-\ufaff]|[0-9]+|[a-zA-Z]+\'*[a-z]*"
    matches = re.findall(regex, str, re.UNICODE)
    return matches

1


0
如果字符串长度超过30,则取前27个字符并在末尾添加“...”,否则返回原字符串。
str='中文2018-2020年一区6、8、10、12号楼_「工程建设文档102332号」'
result = len(list(str)) >= 30 and ''.join(list(str)[:27]) + '...' or str

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接