Python:在单词边界上拆分Unicode字符串

10

我需要将一个字符串缩短为140个字符。

目前我正在执行以下操作:

if len(tweet) > 140:
    tweet = re.sub(r"\s+", " ", tweet) #normalize space
    footer = "… " + utils.shorten_urls(post['url'])
    avail = 140 - len(footer)
    words = tweet.split()
    result = ""
    for word in words:
        word += " "
        if len(word) > avail:
            break
        result += word
        avail -= len(word)
    tweet = (result + footer).strip()
    assert len(tweet) <= 140

对于英语和类似英语的字符串,这很好用,但是对于中文字符串来说会失败,因为tweet.split()只返回一个数组:

>>> s = u"简讯:新華社報道,美國總統奧巴馬乘坐的「空軍一號」專機晚上10時42分進入上海空域,預計約30分鐘後抵達浦東國際機場,開展他上任後首次訪華之旅。"
>>> s
u'\u7b80\u8baf\uff1a\u65b0\u83ef\u793e\u5831\u9053\uff0c\u7f8e\u570b\u7e3d\u7d71\u5967\u5df4\u99ac\u4e58\u5750\u7684\u300c\u7a7a\u8ecd\u4e00\u865f\u300d\u5c08\u6a5f\u665a\u4e0a10\u664242\u5206\u9032\u5165\u4e0a\u6d77\u7a7a\u57df\uff0c\u9810\u8a08\u7d0430\u5206\u9418\u5f8c\u62b5\u9054\u6d66\u6771\u570b\u969b\u6a5f\u5834\uff0c\u958b\u5c55\u4ed6\u4e0a\u4efb\u5f8c\u9996\u6b21\u8a2a\u83ef\u4e4b\u65c5\u3002'
>>> s.split()
[u'\u7b80\u8baf\uff1a\u65b0\u83ef\u793e\u5831\u9053\uff0c\u7f8e\u570b\u7e3d\u7d71\u5967\u5df4\u99ac\u4e58\u5750\u7684\u300c\u7a7a\u8ecd\u4e00\u865f\u300d\u5c08\u6a5f\u665a\u4e0a10\u664242\u5206\u9032\u5165\u4e0a\u6d77\u7a7a\u57df\uff0c\u9810\u8a08\u7d0430\u5206\u9418\u5f8c\u62b5\u9054\u6d66\u6771\u570b\u969b\u6a5f\u5834\uff0c\u958b\u5c55\u4ed6\u4e0a\u4efb\u5f8c\u9996\u6b21\u8a2a\u83ef\u4e4b\u65c5\u3002']

如何做到处理I18N(国际化)?这在所有语言中都有意义吗?

我使用的是Python 2.5.4,如果有关系的话。

9个回答

8

中文通常没有单词间的空格,而符号在不同的上下文中可能有不同的意义。您需要理解文本以便在单词边界处拆分它。换句话说,总的来说,您所尝试做的并不容易。


将一个中文字符串进行子串操作是否有意义?比如我执行s[:120],那么结果还能被读取吗? - Paul Tarjan
5
你可能最终只剩下单词的一半,这可能会完全改变它的意思。想象一下把"assist"在前三个字母处分开会发生什么。 - Mark Byers
好的,谢谢。在其他语言中,“...”是否意味着相同的事情,或者有另一个“省略号”字符? - Paul Tarjan
我不确定要使用哪个字符,但维基百科在这个问题上有一些信息:http://en.wikipedia.org/wiki/Ellipsis#In_Chinese - Mark Byers
1
据我所知,没有特殊的CJK省略号字符。CJK字符的宽度是拉丁字符的两倍(“全角”),因此最好像维基百科文章所说的那样使用两个省略号字符:“在中文和有时在日语中,省略号字符是通过输入两个连续的水平省略号(U+2026)来完成的。”所有这些都假定您已经确定了所涉及的语言实际上是中文,而不是也使用CJK字符并可能具有不同省略约定和问题的日语或韩语。 - John Machin
有一个省略号字符,至少在日语中是这样,但它的意思与三个ASCII句点相同。 "..." - Noah

5

对于中文分词以及其他自然语言处理的高级任务,如果不是完整解决方案,可以考虑将NLTK作为一个很好的起点--它是一个丰富的基于Python的工具包,特别适合学习自然语言处理技术(有时还足够好,能够为您提供可行的解决方案)。


3
"not rarely" 的意思是 "不少见",相当于 "通常不会" 或者 "有时候也会发生"。 - Laurence Gonsalves
@Laurence,这取决于您的典型自然语言处理任务有多前沿,以及您需要将代码生产化和性能调优到什么程度。如果您正在处理数千兆字节的文本或需要低延迟响应,则必须部署在大型、高度可扩展的并行集群上,NLTK 最多只能让您草拟一个原型,而不能为您的要求提供可行的解决方案;对于较低容量和更具时间容忍度的任务,特别是像分词这样的众所周知的任务,“通常”适用——但是存在各种中间需求和特殊问题怪癖!-) - Alex Martelli
2
我真的不想为分词发现训练NLP解决方案。我相信已经有人做过这个了,只是想要一个预装的分词器。 - Paul Tarjan

3

re.U 标志 将根据 Unicode 字符属性数据库处理 \s

然而,根据 Python 的 Unicode 数据库,给定的字符串显然不包含任何空格字符:

>>> x = u'\u7b80\u8baf\uff1a\u65b0\u83ef\u793e\u5831\u9053\uff0c\u7f8e\u570b\u7e3d\u7d71\u5967\u5df4\u99ac\u4e58\u5750\u7684\u300c\u7a7a\u8ecd\u4e00\u865f\u300d\u5c08\u6a5f\u665a\u4e0a10\u664242\u5206\u9032\u5165\u4e0a\u6d77\u7a7a\u57df\uff0c\u9810\u8a08\u7d0430\u5206\u9418\u5f8c\u62b5\u9054\u6d66\u6771\u570b\u969b\u6a5f\u5834\uff0c\u958b\u5c55\u4ed6\u4e0a\u4efb\u5f8c\u9996\u6b21\u8a2a\u83ef\u4e4b\u65c5\u3002'
>>> re.compile(r'\s+', re.U).split(x)
[u'\u7b80\u8baf\uff1a\u65b0\u83ef\u793e\u5831\u9053\uff0c\u7f8e\u570b\u7e3d\u7d71\u5967\u5df4\u99ac\u4e58\u5750\u7684\u300c\u7a7a\u8ecd\u4e00\u865f\u300d\u5c08\u6a5f\u665a\u4e0a10\u664242\u5206\u9032\u5165\u4e0a\u6d77\u7a7a\u57df\uff0c\u9810\u8a08\u7d0430\u5206\u9418\u5f8c\u62b5\u9054\u6d66\u6771\u570b\u969b\u6a5f\u5834\uff0c\u958b\u5c55\u4ed6\u4e0a\u4efb\u5f8c\u9996\u6b21\u8a2a\u83ef\u4e4b\u65c5\u3002']

没错,但是英语中的“whitespace”指的是单词分隔符,而中文中没有单词分隔符,只有作为句子分隔符的空格。 - Paul Tarjan

2

我尝试使用PyAPNS进行推送通知的解决方案,并想分享一下对我有用的内容。我遇到的问题是,在UTF-8中截断256个字节会导致通知被丢弃。我必须确保通知被编码为“unicode_escape”才能使其正常工作。我认为这是因为结果是以JSON而不是原始UTF-8发送的。无论如何,以下是对我有用的函数:

def unicode_truncate(s, length, encoding='unicode_escape'):
    encoded = s.encode(encoding)[:length]
    return encoded.decode(encoding, 'ignore')

1

经过与一些本地粤语、普通话和日语说话者的交流,似乎正确的做法很难,但在互联网帖子的上下文中,我的当前算法对他们仍然有意义。

也就是说,他们已经习惯了“按空格分段并在末尾添加……”的处理方式。

所以我会继续懒惰并坚持这种处理方式,直到有人抱怨无法理解为止。

对于我原始实现的唯一更改将是不强制在最后一个词上加空格,因为在任何语言中都是不必要的(并使用Unicode字符…&#x2026而不是... 三个点以节省2个字符)。


这是HTML中的命名实体:&hellip;,表示水平省略号。 - ephemient

1
基本上,在CJK(除了带空格的韩语)中,您需要使用字典查找来正确分割单词。根据您对“单词”的确切定义,日语可能比这更困难,因为并非所有的单词屈折变化形式(例如“行こう”与“行った”)都会出现在字典中。是否值得付出努力取决于您的应用。

0

这会将单词拆分的决定传递给re模块,但对于您来说可能已足够好了。

import re

def shorten(tweet, footer="", limit=140):
    """Break tweet into two pieces at roughly the last word break
    before limit.
    """
    lower_break_limit = limit / 2
    # limit under which to assume breaking didn't work as expected

    limit -= len(footer)

    tweet = re.sub(r"\s+", " ", tweet.strip())
    m = re.match(r"^(.{,%d})\b(?:\W|$)" % limit, tweet, re.UNICODE)
    if not m or m.end(1) < lower_break_limit:
        # no suitable word break found
        # cutting at an arbitrary location,
        # or if len(tweet) < lower_break_limit, this will be true and
        # returning this still gives the desired result
        return tweet[:limit] + footer
    return m.group(1) + footer

谢谢。我添加了一个检查,以确保没有单词边界。对于英文字符串,这个方法非常有效,但是对于我的中文示例(将其加倍以使其变长),我最终得到的字符串长度为137个字符,而不是140个字符。len(shorten(s*2, "... end")) - Paul Tarjan
这意味着它按预期工作,因为它在最后一个\b\W处中断。然而,我不知道中文是否在该文本中实际上是单词分隔符。尝试使用shorten("abcde " * 3, "", 13)来获得另一个示例,说明它如何在限制长度之前中断。 - Roger Pate

0
你需要的是中文分词工具。分词并不是一项容易解决的任务,目前还没有完美的解决方案。有几个工具可以选择:
  1. CkipTagger

    由台湾中央研究院(Academia Sinica)开发。

  2. jieba

    由百度工程师孙浚亿开发。

  3. pkuseg

    由北京大学语言计算与机器学习组(Language Computing and Machine Learning Group)开发。

如果你想要进行字符切分,虽然没有多大用处,但是也是可以做到的。

>>> s = u"简讯:新華社報道,美國總統奧巴馬乘坐的「空軍一號」專機晚上10時42分進入上海空域,預計約30分鐘後抵達浦東國際機場,開展他上任後首次訪華之旅。"
>>> chars = list(s)
>>> chars
[u'\u7b80', u'\u8baf', u'\uff1a', u'\u65b0', u'\u83ef', u'\u793e', u'\u5831', u'\u9053', u'\uff0c', u'\u7f8e', u'\u570b', u'\u7e3d', u'\u7d71', u'\u5967', u'\u5df4', u'\u99ac', u'\u4e58', u'\u5750', u'\u7684', u'\u300c', u'\u7a7a', u'\u8ecd', u'\u4e00', u'\u865f', u'\u300d', u'\u5c08', u'\u6a5f', u'\u665a', u'\u4e0a', u'1', u'0', u'\u6642', u'4', u'2', u'\u5206', u'\u9032', u'\u5165', u'\u4e0a', u'\u6d77', u'\u7a7a', u'\u57df', u'\uff0c', u'\u9810', u'\u8a08', u'\u7d04', u'3', u'0', u'\u5206', u'\u9418', u'\u5f8c', u'\u62b5', u'\u9054', u'\u6d66', u'\u6771', u'\u570b', u'\u969b', u'\u6a5f', u'\u5834', u'\uff0c', u'\u958b', u'\u5c55', u'\u4ed6', u'\u4e0a', u'\u4efb', u'\u5f8c', u'\u9996', u'\u6b21', u'\u8a2a', u'\u83ef', u'\u4e4b', u'\u65c5', u'\u3002']
>>> print('/'.join(chars))
简/讯/:/新/華/社/報/道/,/美/國/總/統/奧/巴/馬/乘/坐/的/「/空/軍/一/號/」/專/機/晚/上/1/0/時/4/2/分/進/入/上/海/空/域/,/預/計/約/3/0/分/鐘/後/抵/達/浦/東/國/際/機/場/,/開/展/他/上/任/後/首/次/訪/華/之/旅/。

-1

节约两个字符,使用省略号(0x2026)代替三个点!


1
在UTF-8中,省略号占用3个字节,因此在这方面没有太多可以节省的空间 :) - Adam Byrtek
2
我故意使用了“字符”而不是“字节”这个词。 :) - a paid nerd
1
Adam的意思是:你可以节省两个Unicode字符,但在UTF-8中,U+2026需要3个字节,而三个点每个需要1个字节,因此在存储时并没有节省。我的注释:从概念上讲,最好使用省略号字符。 - John Machin

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