语音合成(TTS)从零搭建一个完整的TTS系统-第二节-文本归一化
一、概述
本节我们做语音合成的第一步处理,先要把文本进行规整,转换为标准的中文汉字,包括:阿拉伯数字转中文数字(18斤),特殊符号转中文(10$),长句子断句(通过标点等分成小段)等。这个过程叫做文本归一化。我们先通过python脚本实现,然后使用c++语言实现,方便系统的集成和移植。
二、脚本实现
import logging
import logging.config
import logging.handlersimport re
import numpy as np
import cn_tnclass NormalizeHandler():def __init__(self):self.old_chars = cn_tn.CHINESE_PUNC_LIST + cn_tn.string.punctuationself.new_chars = ' ' * len(self.old_chars)self.del_chars = ''self.norm_text = ''def normalize(self, text):try:self.text = textself.norm_text = ''logging.info("=======get======:%s"%(self.text))self.text = self.text.upper()result_list = re.split(r'[,。、!:?(),.?!:()]', self.text)alphabet_list = ['A', 'B', 'C', 'D', 'E','F', 'G', 'H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q', 'R', 'S', 'T','U', 'V', 'W', 'X', 'Y','Z']logging.info("split:%s"%str(result_list))for i in range(len(result_list)):if result_list[i] == '':continueresult_list[i] = cn_tn.NSWNormalizer(result_list[i]).normalize()result_list[i] = result_list[i].translate(str.maketrans(self.old_chars, self.new_chars, self.del_chars))logging.info("=======text norm======:%s"%(result_list[i]))result_list[i] = re.findall(u'[\u4e00-\u9fa5ABCDEFGHIJKLMNOPQRSTUVWXYZ]', result_list[i])logging.info("=======find all text======:%s"%(result_list[i]))result_list[i] = "".join(str(kk) for kk in result_list[i])for kk in range(len(alphabet_list)):before_str = alphabet_list[kk]after_str = ',%s,'%(alphabet_list[kk])result_list[i] = result_list[i].replace(before_str, after_str)logging.info("=======replace alphabet======:%s"%(result_list[i]))sub_result_list = re.split(r'[,]', result_list[i])logging.info("sub split:%s"%str(sub_result_list))for ii in range(len(sub_result_list)):logging.info("=======sub result======:%s"%(sub_result_list[ii]))if sub_result_list[ii] == '':logging.info("continue")continueelif sub_result_list[ii] in alphabet_list:logging.info("alphabet")self.norm_text += ' ' + sub_result_list[ii].lower() + ' 'else:self.norm_text += sub_result_list[ii]self.norm_text += ' 'except Exception as e:logging.info('error, failed to get group score.',str(e))def main():handle = NormalizeHandler()text = '我的快递单号是 YT9876543210,显示已到小区驿站。这辆车的车牌号是京 A・8B7C6,在停车场 B 区 3 排 5 号车位。初始密码为 Abc123456。实验室里 A-03 号试剂瓶存放着浓度为 50% 的硫酸溶液。手机 WiFi 要连接名称为 Home_Network_2024 的无线网络,密码是 Hn@202408。产品型号为 XJ - 2025A,生产日期标注在包装盒右下角的 20250315。参加马拉松比赛,我的参赛号码布是 D1212,起点在 A 区拱门处。'handle.normalize(text)print(handle.norm_text)if __name__ == '__main__':main()
#!/usr/bin/env python3
# coding=utf-8import sys, os, argparse, codecs, string, re# ================================================================================ #
# basic constant
# ================================================================================ #
CHINESE_DIGIS = u'零一二三四五六七八九'
BIG_CHINESE_DIGIS_SIMPLIFIED = u'零壹贰叁肆伍陆柒捌玖'
BIG_CHINESE_DIGIS_TRADITIONAL = u'零壹貳參肆伍陸柒捌玖'
SMALLER_BIG_CHINESE_UNITS_SIMPLIFIED = u'十百千万'
SMALLER_BIG_CHINESE_UNITS_TRADITIONAL = u'拾佰仟萬'
LARGER_CHINESE_NUMERING_UNITS_SIMPLIFIED = u'亿兆京垓秭穰沟涧正载'
LARGER_CHINESE_NUMERING_UNITS_TRADITIONAL = u'億兆京垓秭穰溝澗正載'
SMALLER_CHINESE_NUMERING_UNITS_SIMPLIFIED = u'十百千万'
SMALLER_CHINESE_NUMERING_UNITS_TRADITIONAL = u'拾佰仟萬'ZERO_ALT = u'〇'
ONE_ALT = u'幺'
TWO_ALTS = [u'两', u'兩']POSITIVE = [u'正', u'正']
NEGATIVE = [u'负', u'負']
POINT = [u'点', u'點']
# PLUS = [u'加', u'加']
# SIL = [u'杠', u'槓']# 中文数字系统类型
NUMBERING_TYPES = ['low', 'mid', 'high']CURRENCY_NAMES = '(人民币|美元|日元|英镑|欧元|马克|法郎|加拿大元|澳元|港币|先令|芬兰马克|爱尔兰镑|' \'里拉|荷兰盾|埃斯库多|比塞塔|印尼盾|林吉特|新西兰元|比索|卢布|新加坡元|韩元|泰铢)'
CURRENCY_UNITS = '((亿|千万|百万|万|千|百)|(亿|千万|百万|万|千|百|)元|(亿|千万|百万|万|千|百|)块|角|毛|分)'
COM_QUANTIFIERS = '(匹|张|座|回|场|尾|条|个|首|阙|阵|网|炮|顶|丘|棵|只|支|袭|辆|挑|担|颗|壳|窠|曲|墙|群|腔|' \'砣|座|客|贯|扎|捆|刀|令|打|手|罗|坡|山|岭|江|溪|钟|队|单|双|对|出|口|头|脚|板|跳|枝|件|贴|' \'针|线|管|名|位|身|堂|课|本|页|家|户|层|丝|毫|厘|分|钱|两|斤|担|铢|石|钧|锱|忽|(千|毫|微)克|' \'毫|厘|分|寸|尺|丈|里|寻|常|铺|程|(千|分|厘|毫|微)米|撮|勺|合|升|斗|石|盘|碗|碟|叠|桶|笼|盆|' \'盒|杯|钟|斛|锅|簋|篮|盘|桶|罐|瓶|壶|卮|盏|箩|箱|煲|啖|袋|钵|年|月|日|季|刻|时|周|天|秒|分|旬|' \'纪|岁|世|更|夜|春|夏|秋|冬|代|伏|辈|丸|泡|粒|颗|幢|堆|条|根|支|道|面|片|张|颗|块)'# punctuation information are based on Zhon project (https://github.com/tsroten/zhon.git)
CHINESE_PUNC_STOP = '!?。。'
CHINESE_PUNC_NON_STOP = '"#$%&'()*+,-/:;<=>@[\]^_`{|}~⦅⦆「」、、〃《》「」『』【】〔〕〖〗〘〙〚〛〜〝〞〟〰〾〿–—‘’‛“”„‟…‧﹏'
CHINESE_PUNC_OTHER = '·〈〉-'
CHINESE_PUNC_LIST = CHINESE_PUNC_STOP + CHINESE_PUNC_NON_STOP + CHINESE_PUNC_OTHER# ================================================================================ #
# basic class
# ================================================================================ #
class ChineseChar(object):"""中文字符每个字符对应简体和繁体,e.g. 简体 = '负', 繁体 = '負'转换时可转换为简体或繁体"""def __init__(self, simplified, traditional):self.simplified = simplifiedself.traditional = traditional#self.__repr__ = self.__str__def __str__(self):return self.simplified or self.traditional or Nonedef __repr__(self):return self.__str__()class ChineseNumberUnit(ChineseChar):"""中文数字/数位字符每个字符除繁简体外还有一个额外的大写字符e.g. '陆' 和 '陸'"""def __init__(self, power, simplified, traditional, big_s, big_t):super(ChineseNumberUnit, self).__init__(simplified, traditional)self.power = powerself.big_s = big_sself.big_t = big_tdef __str__(self):return '10^{}'.format(self.power)@classmethoddef create(cls, index, value, numbering_type=NUMBERING_TYPES[1], small_unit=False):if small_unit:return ChineseNumberUnit(power=index + 1,simplified=value[0], traditional=value[1], big_s=value[1], big_t=value[1])elif numbering_type == NUMBERING_TYPES[0]:return ChineseNumberUnit(power=index + 8,simplified=value[0], traditional=value[1], big_s=value[0], big_t=value[1])elif numbering_type == NUMBERING_TYPES[1]:return ChineseNumberUnit(power=(index + 2) * 4,simplified=value[0], traditional=value[1], big_s=value[0], big_t=value[1])elif numbering_type == NUMBERING_TYPES[2]:return ChineseNumberUnit(power=pow(2, index + 3),simplified=value[0], traditional=value[1], big_s=value[0], big_t=value[1])else:raise ValueError('Counting type should be in {0} ({1} provided).'.format(NUMBERING_TYPES, numbering_type))class ChineseNumberDigit(ChineseChar):"""中文数字字符"""def __init__(self, value, simplified, traditional, big_s, big_t, alt_s=None, alt_t=None):super(ChineseNumberDigit, self).__init__(simplified, traditional)self.value = valueself.big_s = big_sself.big_t = big_tself.alt_s = alt_sself.alt_t = alt_tdef __str__(self):return str(self.value)@classmethoddef create(cls, i, v):return ChineseNumberDigit(i, v[0], v[1], v[2], v[3])class ChineseMath(ChineseChar):"""中文数位字符"""def __init__(self, simplified, traditional, symbol, expression=None):super(ChineseMath, self).__init__(simplified, traditional)self.symbol = symbolself.expression = expressionself.big_s = simplifiedself.big_t = traditionalCC, CNU, CND, CM = ChineseChar, ChineseNumberUnit, ChineseNumberDigit, ChineseMathclass NumberSystem(object):"""中文数字系统"""passclass MathSymbol(object):"""用于中文数字系统的数学符号 (繁/简体), e.g.positive = ['正', '正']negative = ['负', '負']point = ['点', '點']"""def __init__(self, positive, negative, point):self.positive = positiveself.negative = negativeself.point = pointdef __iter__(self):for v in self.__dict__.values():yield v# class OtherSymbol(object):
# """
# 其他符号
# """
#
# def __init__(self, sil):
# self.sil = sil
#
# def __iter__(self):
# for v in self.__dict__.values():
# yield v# ================================================================================ #
# basic utils
# ================================================================================ #
def create_system(numbering_type=NUMBERING_TYPES[1]):"""根据数字系统类型返回创建相应的数字系统,默认为 midNUMBERING_TYPES = ['low', 'mid', 'high']: 中文数字系统类型low: '兆' = '亿' * '十' = $10^{9}$, '京' = '兆' * '十', etc.mid: '兆' = '亿' * '万' = $10^{12}$, '京' = '兆' * '万', etc.high: '兆' = '亿' * '亿' = $10^{16}$, '京' = '兆' * '兆', etc.返回对应的数字系统"""# chinese number units of '亿' and largerall_larger_units = zip(LARGER_CHINESE_NUMERING_UNITS_SIMPLIFIED, LARGER_CHINESE_NUMERING_UNITS_TRADITIONAL)larger_units = [CNU.create(i, v, numbering_type, False)for i, v in enumerate(all_larger_units)]# chinese number units of '十, 百, 千, 万'all_smaller_units = zip(SMALLER_CHINESE_NUMERING_UNITS_SIMPLIFIED, SMALLER_CHINESE_NUMERING_UNITS_TRADITIONAL)smaller_units = [CNU.create(i, v, small_unit=True)for i, v in enumerate(all_smaller_units)]# digischinese_digis = zip(CHINESE_DIGIS, CHINESE_DIGIS,BIG_CHINESE_DIGIS_SIMPLIFIED, BIG_CHINESE_DIGIS_TRADITIONAL)digits = [CND.create(i, v) for i, v in enumerate(chinese_digis)]digits[0].alt_s, digits[0].alt_t = ZERO_ALT, ZERO_ALTdigits[1].alt_s, digits[1].alt_t = ONE_ALT, ONE_ALTdigits[2].alt_s, digits[2].alt_t = TWO_ALTS[0], TWO_ALTS[1]# symbolspositive_cn = CM(POSITIVE[0], POSITIVE[1], '+', lambda x: x)negative_cn = CM(NEGATIVE[0], NEGATIVE[1], '-', lambda x: -x)point_cn = CM(POINT[0], POINT[1], '.', lambda x,y: float(str(x) + '.' + str(y)))# sil_cn = CM(SIL[0], SIL[1], '-', lambda x, y: float(str(x) + '-' + str(y)))system = NumberSystem()system.units = smaller_units + larger_unitssystem.digits = digitssystem.math = MathSymbol(positive_cn, negative_cn, point_cn)# system.symbols = OtherSymbol(sil_cn)return systemdef chn2num(chinese_string, numbering_type=NUMBERING_TYPES[1]):def get_symbol(char, system):for u in system.units:if char in [u.traditional, u.simplified, u.big_s, u.big_t]:return ufor d in system.digits:if char in [d.traditional, d.simplified, d.big_s, d.big_t, d.alt_s, d.alt_t]:return dfor m in system.math:if char in [m.traditional, m.simplified]:return mdef string2symbols(chinese_string, system):int_string, dec_string = chinese_string, ''for p in [system.math.point.simplified, system.math.point.traditional]:if p in chinese_string:int_string, dec_string = chinese_string.split(p)breakreturn [get_symbol(c, system) for c in int_string], \[get_symbol(c, system) for c in dec_string]def correct_symbols(integer_symbols, system):"""一百八 to 一百八十一亿一千三百万 to 一亿 一千万 三百万"""if integer_symbols and isinstance(integer_symbols[0], CNU):if integer_symbols[0].power == 1:integer_symbols = [system.digits[1]] + integer_symbolsif len(integer_symbols) > 1:if isinstance(integer_symbols[-1], CND) and isinstance(integer_symbols[-2], CNU):integer_symbols.append(CNU(integer_symbols[-2].power - 1, None, None, None, None))result = []unit_count = 0for s in integer_symbols:if isinstance(s, CND):result.append(s)unit_count = 0elif isinstance(s, CNU):current_unit = CNU(s.power, None, None, None, None)unit_count += 1if unit_count == 1:result.append(current_unit)elif unit_count > 1:for i in range(len(result)):if isinstance(result[-i - 1], CNU) and result[-i - 1].power < current_unit.power:result[-i - 1] = CNU(result[-i - 1].power +current_unit.power, None, None, None, None)return resultdef compute_value(integer_symbols):"""Compute the value.When current unit is larger than previous unit, current unit * all previous units will be used as all previous units.e.g. '两千万' = 2000 * 10000 not 2000 + 10000"""value = [0]last_power = 0for s in integer_symbols:if isinstance(s, CND):value[-1] = s.valueelif isinstance(s, CNU):value[-1] *= pow(10, s.power)if s.power > last_power:value[:-1] = list(map(lambda v: v *pow(10, s.power), value[:-1]))last_power = s.powervalue.append(0)return sum(value)system = create_system(numbering_type)int_part, dec_part = string2symbols(chinese_string, system)int_part = correct_symbols(int_part, system)int_str = str(compute_value(int_part))dec_str = ''.join([str(d.value) for d in dec_part])if dec_part:return '{0}.{1}'.format(int_str, dec_str)else:return int_strdef num2chn(number_string, numbering_type=NUMBERING_TYPES[1], big=False,traditional=False, alt_zero=False, alt_one=False, alt_two=True,use_zeros=True, use_units=True):def get_value(value_string, use_zeros=True):striped_string = value_string.lstrip('0')# record nothing if all zerosif not striped_string:return []# record one digitselif len(striped_string) == 1:if use_zeros and len(value_string) != len(striped_string):return [system.digits[0], system.digits[int(striped_string)]]else:return [system.digits[int(striped_string)]]# recursively record multiple digitselse:result_unit = next(u for u in reversed(system.units) if u.power < len(striped_string))result_string = value_string[:-result_unit.power]return get_value(result_string) + [result_unit] + get_value(striped_string[-result_unit.power:])system = create_system(numbering_type)int_dec = number_string.split('.')if len(int_dec) == 1:int_string = int_dec[0]dec_string = ""elif len(int_dec) == 2:int_string = int_dec[0]dec_string = int_dec[1]else:raise ValueError("invalid input num string with more than one dot: {}".format(number_string))if use_units and len(int_string) > 1:result_symbols = get_value(int_string)else:result_symbols = [system.digits[int(c)] for c in int_string]dec_symbols = [system.digits[int(c)] for c in dec_string]if dec_string:result_symbols += [system.math.point] + dec_symbolsif alt_two:liang = CND(2, system.digits[2].alt_s, system.digits[2].alt_t,system.digits[2].big_s, system.digits[2].big_t)for i, v in enumerate(result_symbols):if isinstance(v, CND) and v.value == 2:next_symbol = result_symbols[i +1] if i < len(result_symbols) - 1 else Noneprevious_symbol = result_symbols[i - 1] if i > 0 else Noneif isinstance(next_symbol, CNU) and isinstance(previous_symbol, (CNU, type(None))):if next_symbol.power != 1 and ((previous_symbol is None) or (previous_symbol.power != 1)):result_symbols[i] = liang# if big is True, '两' will not be used and `alt_two` has no impact on outputif big:attr_name = 'big_'if traditional:attr_name += 't'else:attr_name += 's'else:if traditional:attr_name = 'traditional'else:attr_name = 'simplified'result = ''.join([getattr(s, attr_name) for s in result_symbols])# if not use_zeros:# result = result.strip(getattr(system.digits[0], attr_name))if alt_zero:result = result.replace(getattr(system.digits[0], attr_name), system.digits[0].alt_s)if alt_one:result = result.replace(getattr(system.digits[1], attr_name), system.digits[1].alt_s)for i, p in enumerate(POINT):if result.startswith(p):return CHINESE_DIGIS[0] + result# ^10, 11, .., 19if len(result) >= 2 and result[1] in [SMALLER_CHINESE_NUMERING_UNITS_SIMPLIFIED[0],SMALLER_CHINESE_NUMERING_UNITS_TRADITIONAL[0]] and \result[0] in [CHINESE_DIGIS[1], BIG_CHINESE_DIGIS_SIMPLIFIED[1], BIG_CHINESE_DIGIS_TRADITIONAL[1]]:result = result[1:]return result# ================================================================================ #
# different types of rewriters
# ================================================================================ #
class Cardinal:"""CARDINAL类"""def __init__(self, cardinal=None, chntext=None):self.cardinal = cardinalself.chntext = chntextdef chntext2cardinal(self):return chn2num(self.chntext)def cardinal2chntext(self):return num2chn(self.cardinal)class Digit:"""DIGIT类"""def __init__(self, digit=None, chntext=None):self.digit = digitself.chntext = chntext# def chntext2digit(self):# return chn2num(self.chntext)def digit2chntext(self):return num2chn(self.digit, alt_two=False, use_units=False)class TelePhone:"""TELEPHONE类"""def __init__(self, telephone=None, raw_chntext=None, chntext=None):self.telephone = telephoneself.raw_chntext = raw_chntextself.chntext = chntext# def chntext2telephone(self):# sil_parts = self.raw_chntext.split('<SIL>')# self.telephone = '-'.join([# str(chn2num(p)) for p in sil_parts# ])# return self.telephonedef telephone2chntext(self, fixed=False):if fixed:sil_parts = self.telephone.split('-')self.raw_chntext = '<SIL>'.join([num2chn(part, alt_two=False, use_units=False) for part in sil_parts])self.chntext = self.raw_chntext.replace('<SIL>', '')else:sp_parts = self.telephone.strip('+').split()self.raw_chntext = '<SP>'.join([num2chn(part, alt_two=False, use_units=False) for part in sp_parts])self.chntext = self.raw_chntext.replace('<SP>', '')return self.chntextclass Fraction:"""FRACTION类"""def __init__(self, fraction=None, chntext=None):self.fraction = fractionself.chntext = chntextdef chntext2fraction(self):denominator, numerator = self.chntext.split('分之')return chn2num(numerator) + '/' + chn2num(denominator)def fraction2chntext(self):numerator, denominator = self.fraction.split('/')return num2chn(denominator) + '分之' + num2chn(numerator)class Date:"""DATE类"""def __init__(self, date=None, chntext=None):self.date = dateself.chntext = chntext# def chntext2date(self):# chntext = self.chntext# try:# year, other = chntext.strip().split('年', maxsplit=1)# year = Digit(chntext=year).digit2chntext() + '年'# except ValueError:# other = chntext# year = ''# if other:# try:# month, day = other.strip().split('月', maxsplit=1)# month = Cardinal(chntext=month).chntext2cardinal() + '月'# except ValueError:# day = chntext# month = ''# if day:# day = Cardinal(chntext=day[:-1]).chntext2cardinal() + day[-1]# else:# month = ''# day = ''# date = year + month + day# self.date = date# return self.datedef date2chntext(self):date = self.datetry:year, other = date.strip().split('年', 1)year = Digit(digit=year).digit2chntext() + '年'except ValueError:other = dateyear = ''if other:try:month, day = other.strip().split('月', 1)month = Cardinal(cardinal=month).cardinal2chntext() + '月'except ValueError:day = datemonth = ''if day:day = Cardinal(cardinal=day[:-1]).cardinal2chntext() + day[-1]else:month = ''day = ''chntext = year + month + dayself.chntext = chntextreturn self.chntextclass Money:"""MONEY类"""def __init__(self, money=None, chntext=None):self.money = moneyself.chntext = chntext# def chntext2money(self):# return self.moneydef money2chntext(self):money = self.moneypattern = re.compile(r'(\d+(\.\d+)?)')matchers = pattern.findall(money)if matchers:for matcher in matchers:money = money.replace(matcher[0], Cardinal(cardinal=matcher[0]).cardinal2chntext())self.chntext = moneyreturn self.chntextclass Percentage:"""PERCENTAGE类"""def __init__(self, percentage=None, chntext=None):self.percentage = percentageself.chntext = chntextdef chntext2percentage(self):return chn2num(self.chntext.strip().strip('百分之')) + '%'def percentage2chntext(self):return '百分之' + num2chn(self.percentage.strip().strip('%'))# ================================================================================ #
# NSW Normalizer
# ================================================================================ #
class NSWNormalizer:def __init__(self, raw_text):self.raw_text = '^' + raw_text + '$'self.norm_text = ''def _particular(self):text = self.norm_textpattern = re.compile(r"(([a-zA-Z]+)二([a-zA-Z]+))")matchers = pattern.findall(text)if matchers:# print('particular')for matcher in matchers:text = text.replace(matcher[0], matcher[1]+'2'+matcher[2], 1)self.norm_text = textreturn self.norm_textdef normalize(self):text = self.raw_text# 规范化日期pattern = re.compile(r"\D+((([089]\d|(19|20)\d{2})年)?(\d{1,2}月(\d{1,2}[日号])?)?)")matchers = pattern.findall(text)if matchers:#print('date')for matcher in matchers:text = text.replace(matcher[0], Date(date=matcher[0]).date2chntext(), 1)# 规范化金钱pattern = re.compile(r"\D+((\d+(\.\d+)?)[多余几]?" + CURRENCY_UNITS + r"(\d" + CURRENCY_UNITS + r"?)?)")matchers = pattern.findall(text)if matchers:#print('money')for matcher in matchers:text = text.replace(matcher[0], Money(money=matcher[0]).money2chntext(), 1)# 规范化固话/手机号码# 手机# http://www.jihaoba.com/news/show/13680# 移动:139、138、137、136、135、134、159、158、157、150、151、152、188、187、182、183、184、178、198# 联通:130、131、132、156、155、186、185、176# 电信:133、153、189、180、181、177pattern = re.compile(r"\D((\+?86 ?)?1([38]\d|5[0-35-9]|7[678]|9[89])\d{8})\D")matchers = pattern.findall(text)if matchers:#print('telephone')for matcher in matchers:text = text.replace(matcher[0], TelePhone(telephone=matcher[0]).telephone2chntext(), 1)# 固话pattern = re.compile(r"\D((0(10|2[1-3]|[3-9]\d{2})-?)?[1-9]\d{6,7})\D")matchers = pattern.findall(text)if matchers:# print('fixed telephone')for matcher in matchers:text = text.replace(matcher[0], TelePhone(telephone=matcher[0]).telephone2chntext(fixed=True), 1)# 规范化分数pattern = re.compile(r"(\d+/\d+)")matchers = pattern.findall(text)if matchers:#print('fraction')for matcher in matchers:text = text.replace(matcher, Fraction(fraction=matcher).fraction2chntext(), 1)# 规范化百分数text = text.replace('%', '%')pattern = re.compile(r"(\d+(\.\d+)?%)")matchers = pattern.findall(text)if matchers:#print('percentage')for matcher in matchers:text = text.replace(matcher[0], Percentage(percentage=matcher[0]).percentage2chntext(), 1)# 规范化纯数+量词pattern = re.compile(r"(\d+(\.\d+)?)[多余几]?" + COM_QUANTIFIERS)matchers = pattern.findall(text)if matchers:#print('cardinal+quantifier')for matcher in matchers:text = text.replace(matcher[0], Cardinal(cardinal=matcher[0]).cardinal2chntext(), 1)# 规范化数字编号pattern = re.compile(r"(\d{4,32})")matchers = pattern.findall(text)if matchers:#print('digit')for matcher in matchers:text = text.replace(matcher, Digit(digit=matcher).digit2chntext(), 1)# 规范化纯数pattern = re.compile(r"(\d+(\.\d+)?)")matchers = pattern.findall(text)if matchers:#print('cardinal')for matcher in matchers:text = text.replace(matcher[0], Cardinal(cardinal=matcher[0]).cardinal2chntext(), 1)self.norm_text = textself._particular()return self.norm_text.lstrip('^').rstrip('$')def nsw_test_case(raw_text):print('I:' + raw_text)print('O:' + NSWNormalizer(raw_text).normalize())print('')def nsw_test():nsw_test_case('固话:0595-23865596或23880880。')nsw_test_case('固话:0595-23865596或23880880。')nsw_test_case('手机:+86 19859213959或15659451527。')nsw_test_case('分数:32477/76391。')nsw_test_case('百分数:80.03%。')nsw_test_case('编号:31520181154418。')nsw_test_case('纯数:2983.07克或12345.60米。')nsw_test_case('日期:1999年2月20日或09年3月15号。')nsw_test_case('金钱:12块5,34.5元,20.1万')nsw_test_case('特殊:O2O或B2C。')nsw_test_case('3456万吨')nsw_test_case('2938个')nsw_test_case('938')nsw_test_case('今天吃了115个小笼包231个馒头')nsw_test_case('有62%的概率')if __name__ == '__main__':#nsw_test()p = argparse.ArgumentParser()p.add_argument('ifile', help='input filename, assume utf-8 encoding')p.add_argument('ofile', help='output filename')p.add_argument('--to_upper', action='store_true', help='convert to upper case')p.add_argument('--to_lower', action='store_true', help='convert to lower case')p.add_argument('--has_key', action='store_true', help="input text has Kaldi's key as first field.")p.add_argument('--log_interval', type=int, default=100000, help='log interval in number of processed lines')args = p.parse_args()ifile = codecs.open(args.ifile, 'r', 'utf8')ofile = codecs.open(args.ofile, 'w+', 'utf8')n = 0for l in ifile:key = ''text = ''if args.has_key:cols = l.split(maxsplit=1)key = cols[0]if len(cols) == 2:text = cols[1].strip()else:text = ''else:text = l.strip()# casesif args.to_upper and args.to_lower:sys.stderr.write('cn_tn.py: to_upper OR to_lower?')exit(1)if args.to_upper:text = text.upper()if args.to_lower:text = text.lower()# NSW(Non-Standard-Word) normalizationtext = NSWNormalizer(text).normalize()# Punctuations removalold_chars = CHINESE_PUNC_LIST + string.punctuation # includes all CN and EN punctuationsnew_chars = ' ' * len(old_chars)del_chars = ''text = text.translate(str.maketrans(old_chars, new_chars, del_chars))#if args.has_key:ofile.write(key + '\t' + text + '\n')else:if text.strip() != '': # skip empty line in pure text format(without Kaldi's utt key)ofile.write(text + '\n')n += 1if n % args.log_interval == 0:sys.stderr.write("cn_tn.py: {} lines done.\n".format(n))sys.stderr.flush()sys.stderr.write("cn_tn.py: {} lines done in total.\n".format(n))sys.stderr.flush()ifile.close()ofile.close()
三、工程化
3.1 代码结构
├── CMakeLists.txt
├── bin
│ ├── CMakeLists.txt
│ └── test-digital-convert.cc
├── build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ │ ├── 3.22.1
│ │ │ ├── CMakeCCompiler.cmake
│ │ │ ├── CMakeCXXCompiler.cmake
│ │ │ ├── CMakeDetermineCompilerABI_C.bin
│ │ │ ├── CMakeDetermineCompilerABI_CXX.bin
│ │ │ ├── CMakeSystem.cmake
│ │ │ ├── CompilerIdC
│ │ │ │ ├── CMakeCCompilerId.c
│ │ │ │ ├── a.out
│ │ │ │ └── tmp
│ │ │ └── CompilerIdCXX
│ │ │ ├── CMakeCXXCompilerId.cpp
│ │ │ ├── a.out
│ │ │ └── tmp
│ │ ├── CMakeDirectoryInformation.cmake
│ │ ├── CMakeOutput.log
│ │ ├── CMakeRuleHashes.txt
│ │ ├── CMakeTmp
│ │ ├── Makefile.cmake
│ │ ├── Makefile2
│ │ ├── TargetDirectories.txt
│ │ ├── cmake.check_cache
│ │ ├── libmouse-digital.a.dir
│ │ │ ├── DependInfo.cmake
│ │ │ ├── build.make
│ │ │ ├── cmake_clean.cmake
│ │ │ ├── compiler_depend.make
│ │ │ ├── compiler_depend.ts
│ │ │ └── progress.make
│ │ └── progress.marks
│ ├── Makefile
│ ├── bin
│ │ ├── CMakeFiles
│ │ │ ├── CMakeDirectoryInformation.cmake
│ │ │ ├── progress.marks
│ │ │ └── test-digital-convert.dir
│ │ │ ├── DependInfo.cmake
│ │ │ ├── build.make
│ │ │ ├── cmake_clean.cmake
│ │ │ ├── compiler_depend.make
│ │ │ ├── compiler_depend.ts
│ │ │ ├── depend.make
│ │ │ ├── flags.make
│ │ │ ├── link.txt
│ │ │ ├── progress.make
│ │ │ ├── test-digital-convert.cc.o
│ │ │ └── test-digital-convert.cc.o.d
│ │ ├── Makefile
│ │ ├── cmake_install.cmake
│ │ └── test-digital-convert
│ ├── cmake_install.cmake
│ ├── libmouse-digital.a
│ └── src
│ ├── CMakeFiles
│ │ ├── CMakeDirectoryInformation.cmake
│ │ ├── mouse-digital.dir
│ │ │ ├── DependInfo.cmake
│ │ │ ├── build.make
│ │ │ ├── cmake_clean.cmake
│ │ │ ├── cmake_clean_target.cmake
│ │ │ ├── compiler_depend.make
│ │ │ ├── compiler_depend.ts
│ │ │ ├── depend.make
│ │ │ ├── flags.make
│ │ │ ├── link.txt
│ │ │ ├── mouse-digital-api.cc.o
│ │ │ ├── mouse-digital-api.cc.o.d
│ │ │ ├── mouse-digital-convert.cc.o
│ │ │ ├── mouse-digital-convert.cc.o.d
│ │ │ ├── mouse-digital-datenorm.cc.o
│ │ │ ├── mouse-digital-datenorm.cc.o.d
│ │ │ ├── mouse-digital-fractionnorm.cc.o
│ │ │ ├── mouse-digital-fractionnorm.cc.o.d
│ │ │ ├── mouse-digital-moneynorm.cc.o
│ │ │ ├── mouse-digital-moneynorm.cc.o.d
│ │ │ ├── mouse-digital-negativenorm.cc.o
│ │ │ ├── mouse-digital-negativenorm.cc.o.d
│ │ │ ├── mouse-digital-normalize.cc.o
│ │ │ ├── mouse-digital-normalize.cc.o.d
│ │ │ ├── mouse-digital-percentnorm.cc.o
│ │ │ ├── mouse-digital-percentnorm.cc.o.d
│ │ │ ├── mouse-digital-phonenorm.cc.o
│ │ │ ├── mouse-digital-phonenorm.cc.o.d
│ │ │ ├── mouse-digital-quantnorm.cc.o
│ │ │ ├── mouse-digital-quantnorm.cc.o.d
│ │ │ ├── mouse-digital-remainnorm.cc.o
│ │ │ ├── mouse-digital-remainnorm.cc.o.d
│ │ │ ├── mouse-digital-replacenorm.cc.o
│ │ │ ├── mouse-digital-replacenorm.cc.o.d
│ │ │ ├── mouse-digital-specialnorm.cc.o
│ │ │ ├── mouse-digital-specialnorm.cc.o.d
│ │ │ └── progress.make
│ │ └── progress.marks
│ ├── Makefile
│ ├── cmake_install.cmake
│ └── libmouse-digital.a
├── include
├── lib
├── src
│ ├── CMakeLists.txt
│ ├── mouse-digital-api.cc
│ ├── mouse-digital-api.h
│ ├── mouse-digital-convert.cc
│ ├── mouse-digital-convert.h
│ ├── mouse-digital-datenorm.cc
│ ├── mouse-digital-datenorm.h
│ ├── mouse-digital-fractionnorm.cc
│ ├── mouse-digital-fractionnorm.h
│ ├── mouse-digital-moneynorm.cc
│ ├── mouse-digital-moneynorm.h
│ ├── mouse-digital-negativenorm.cc
│ ├── mouse-digital-negativenorm.h
│ ├── mouse-digital-normalize.cc
│ ├── mouse-digital-normalize.h
│ ├── mouse-digital-percentnorm.cc
│ ├── mouse-digital-percentnorm.h
│ ├── mouse-digital-phonenorm.cc
│ ├── mouse-digital-phonenorm.h
│ ├── mouse-digital-quantnorm.cc
│ ├── mouse-digital-quantnorm.h
│ ├── mouse-digital-remainnorm.cc
│ ├── mouse-digital-remainnorm.h
│ ├── mouse-digital-replacenorm.cc
│ ├── mouse-digital-replacenorm.h
│ ├── mouse-digital-session.h
│ ├── mouse-digital-specialnorm.cc
│ └── mouse-digital-specialnorm.h
└── test├── test-digital-convert -> ../build/bin/test-digital-convert├── test.total├── test.txt├── test1.txt├── test2.txt├── test3.txt├── test4.txt├── test5.txt└── test6.txt
3.2 核心代码
(1) mouse-digital-api.cc
/**@author : aflyingwolf*@date : 2025.4.20*@file : mouse-digital-api.cc* */
#include "mouse-digital-convert.h"
#include "mouse-digital-api.h"
using namespace mouse_digital;void *api_create_digital_handle() {DigitalConvert *handle = new DigitalConvert();handle->Load();return (void*) handle;
}
void api_destroy_digital_handle(void *handle) {DigitalConvert *handle_ = (DigitalConvert *)handle;delete handle_;
}int api_start_digital_convert(void *handle) {return 0;
}int api_process_digital_convert(void *handle, const char *text, digital_result *res) {DigitalConvert *handle_ = (DigitalConvert *)handle;std::string ret = handle_->Process(text);memset(res->result, 0, 2048);if (ret.size() < 2048) {memcpy(res->result, ret.c_str(), ret.size());res->len = ret.size();} else {memcpy(res->result, ret.c_str(), 2047);res->len = 2047;}return 0;
}int api_end_digital_convert(void *handle) {return 0;
}
(2) mouse-digital-api.h
/**@author : aflyingwolf*@date : 2025.4.20*@file : mouse-digital-api.h* */
#ifndef __MOUSE_DIGITAL_API_H__
#define __MOUSE_DIGITAL_API_H__
#ifdef __cplusplus
extern "C"
{
#endiftypedef struct _digital_result {char result[2048];int len;
} digital_result;void *api_create_digital_handle();
void api_destroy_digital_handle(void *handle);int api_start_digital_convert(void *handle);
int api_process_digital_convert(void *handle, const char *text, digital_result *res);
int api_end_digital_convert(void *handle);#ifdef __cplusplus
}
#endif#endif
(3) mouse-digital-convert.cc
/**@author : aflyingwolf*@date : 2025.4.20*@file : mouse-digital-convert.cc* */#include "mouse-digital-convert.h"
#include "mouse-digital-moneynorm.h"
#include "mouse-digital-datenorm.h"
#include "mouse-digital-phonenorm.h"
#include "mouse-digital-fractionnorm.h"
#include "mouse-digital-percentnorm.h"
#include "mouse-digital-replacenorm.h"
#include "mouse-digital-quantnorm.h"
#include "mouse-digital-remainnorm.h"
#include "mouse-digital-negativenorm.h"
#include "mouse-digital-specialnorm.h"namespace mouse_digital {
DigitalConvert::DigitalConvert() {session_ = NULL;
}
DigitalConvert::~DigitalConvert() {if (session_) {delete session_;session_ = NULL;}for (int i = 0; i < normer_.size(); i++) {delete normer_[i];}
}
void DigitalConvert::Load() {session_ = new Session();normer_.clear();ReplaceNorm *replace_normer = new ReplaceNorm();normer_.push_back(replace_normer);PhoneNorm *phone_normer = new PhoneNorm();normer_.push_back(phone_normer);SpecialNorm *special_normer = new SpecialNorm();normer_.push_back(special_normer);DateNorm *date_normer = new DateNorm();normer_.push_back(date_normer);MoneyNorm *money_normer = new MoneyNorm();normer_.push_back(money_normer);FractionNorm *fraction_normer = new FractionNorm();normer_.push_back(fraction_normer);PercentNorm *percent_normer = new PercentNorm();normer_.push_back(percent_normer);QuantNorm *quant_normer = new QuantNorm();normer_.push_back(quant_normer);NegativeNorm *negative_normer = new NegativeNorm();normer_.push_back(negative_normer);RemainNorm *remain_normer = new RemainNorm();normer_.push_back(remain_normer);for (int i = 0; i < normer_.size(); i++) {DigitalNorm *norm = normer_[i];norm->Load();}
}
std::string DigitalConvert::Process(std::string text) {session_->text_ = text;session_->text_ = std::string("B") + session_->text_ + std::string("E");for (int i = 0; i < normer_.size(); i++) {DigitalNorm *norm = normer_[i];norm->Process(session_);}session_->text_.erase(session_->text_.begin());session_->text_.erase(session_->text_.end()-1);return session_->text_;
}}
(4) mouse-digital-convert.h
/**@author : aflyingwolf*@date : 2025.4.20*@file : mouse-digital-convert.h* */#ifndef __MOUSE_DIGITAL_CONVERT_H__
#define __MOUSE_DIGITAL_CONVERT_H__
#include "mouse-digital-session.h"
#include <vector>
#include "mouse-digital-normalize.h"
namespace mouse_digital {class DigitalConvert {public:DigitalConvert();~DigitalConvert();void Load();std::string Process(std::string text);private:Session *session_;std::vector<DigitalNorm*> normer_;
};}
#endif
(5) mouse-digital-datenorm.cc
/**@author : aflyingwolf*@date : 2025.4.20*@file : mouse-digital-datenorm.cc* */#include "mouse-digital-datenorm.h"
namespace mouse_digital {
DateNorm::DateNorm() {
}
DateNorm::~DateNorm() {
}
void DateNorm::Load() {std::string pattern("((\\D)((\\d+)(年))?((\\d{1,2})(月))((\\d{1,2})(日|号)?)?(\\D))");compile_pattern_ = std::regex(pattern);std::string pattern_year("((\\D)((\\d{2,4})(年))((\\d{1,2})(月))?((\\d{1,2})(日|号)?)?(\\D))");compile_pattern_year_ = std::regex(pattern_year);std::string pattern_ri("((\\D)((\\d+)(年))?((\\d{1,2})(月))?((\\d{1,2})(日|号))(\\D))");compile_pattern_ri_ = std::regex(pattern_ri);
}
void DateNorm::Process(Session *session) {std::string text = session->text_;std::smatch result;while (std::regex_search(text, result, compile_pattern_ri_)) {std::string str_prefix = result.prefix().str();std::string str_suffix = result.suffix().str();std::string cur_str("");for (int i = 0; i < 13; i++) {//std::cout << i << " " << result.str(i) << " " << result[i].matched << std::endl;if (i == 2 && result[i].matched) {cur_str += result.str(i);} else if (i == 4 && result[i].matched) {cur_str += ConvertDigital(result.str(i));} else if (i == 5 && result[i].matched) {cur_str += result.str(i);} else if (i == 7 && result[i].matched) {cur_str += ConvertNum(result.str(i));} else if (i == 8 && result[i].matched) {cur_str += result.str(i);} else if (i == 10 && result[i].matched) {cur_str += ConvertNum(result.str(i));} else if (i == 11 && result[i].matched) {cur_str += result.str(i);} else if (i == 12 && result[i].matched) {cur_str += result.str(i);}}text = str_prefix + cur_str + str_suffix;}while (std::regex_search(text, result, compile_pattern_year_)) {std::string str_prefix = result.prefix().str();std::string str_suffix = result.suffix().str();std::string cur_str("");for (int i = 0; i < 13; i++) {//std::cout << i << " " << result.str(i) << " " << result[i].matched << std::endl;if (i == 2 && result[i].matched) {cur_str += result.str(i);} else if (i == 4 && result[i].matched) {cur_str += ConvertDigital(result.str(i));} else if (i == 5 && result[i].matched) {cur_str += result.str(i);} else if (i == 7 && result[i].matched) {cur_str += ConvertNum(result.str(i));} else if (i == 8 && result[i].matched) {cur_str += result.str(i);} else if (i == 10 && result[i].matched) {cur_str += ConvertNum(result.str(i));} else if (i == 11 && result[i].matched) {cur_str += result.str(i);} else if (i == 12 && result[i].matched) {cur_str += result.str(i);}}text = str_prefix + cur_str + str_suffix;}while (std::regex_search(text, result, compile_pattern_)) {std::string str_prefix = result.prefix().str();std::string str_suffix = result.suffix().str();std::string cur_str("");for (int i = 0; i < 13; i++) {//std::cout << i << " " << result.str(i) << " " << result[i].matched << std::endl;if (i == 2 && result[i].matched) {cur_str += result.str(i);} else if (i == 4 && result[i].matched) {cur_str += ConvertDigital(result.str(i));} else if (i == 5 && result[i].matched) {cur_str += result.str(i);} else if (i == 7 && result[i].matched) {cur_str += ConvertNum(result.str(i));} else if (i == 8 && result[i].matched) {cur_str += result.str(i);} else if (i == 10 && result[i].matched) {cur_str += ConvertNum(result.str(i));} else if (i == 11 && result[i].matched) {cur_str += result.str(i);} else if (i == 12 && result[i].matched) {cur_str += result.str(i);}}text = str_prefix + cur_str + str_suffix;}session->text_ = text;
}}
(6) mouse-digital-datenorm.h
/**@author : aflyingwolf*@date : 2025.4.20*@file : mouse-digital-datenorm.h* */#ifndef __MOUSE_DIGITAL_DATENORM_H__
#define __MOUSE_DIGITAL_DATENORM_H__
#include "mouse-digital-normalize.h"
#include <regex>
#include <string>
#include <iostream>
namespace mouse_digital {class DateNorm : public DigitalNorm {public:DateNorm();~DateNorm();virtual void Load();virtual void Process(Session *session);private:std::regex compile_pattern_;std::regex compile_pattern_year_;std::regex compile_pattern_ri_;
};}
#endif
3.3 demo
#include <stdio.h>
#include <string.h>
#include "mouse-digital-api.h"
int main(int argc, char *argv[]) {if (argc < 2) {printf("%s input test-file\n", argv[0]);return -1;}FILE *fp = fopen(argv[1], "r");void *handle = api_create_digital_handle();char line[1024];memset(line, 0, sizeof(line));while(fgets(line, 1024, fp) != NULL){if (line[0] == '#') {continue;} else {digital_result result;int len = strlen(line);if (line[len-1] == '\n') {line[len-1] = 0;}api_start_digital_convert(handle);api_process_digital_convert(handle, line, &result);api_end_digital_convert(handle);printf("origin:%s\n", line);printf("convet:%s\n\n\n", result.result);}}fclose(fp);api_destroy_digital_handle(handle);return 0;
}
四、举例
4.1 c++测试demo1
./test-digital-convert test.total
origin:这块黄金重达324.75克
convet:这块黄金重达三百二十四点七五克origin:她出生于86年8月18日,她弟弟出生于1995年3月1日
convet:她出生于八六年八月十八日,她弟弟出生于一九九五年三月一日origin:她出生于86年,她弟弟出生于11日
convet:她出生于八六年,她弟弟出生于十一日origin:电影中梁朝伟扮演的陈永仁的编号27149
convet:电影中梁朝伟扮演的陈永仁的编号二七一四九origin:现场有7/12的观众投出了赞成票
convet:现场有十二分之七的观众投出了赞成票origin:现场有-7/12的观众投出了赞成票
convet:现场有负十二分之七的观众投出了赞成票origin:随便来几个价格12块5,34.5元,20.1万
convet:随便来几个价格十二块五,三十四点五元,二十点一万origin:明天有62%的概率降雨
convet:明天有百分之六十二的概率降雨origin:明天-62%的概率降雨
convet:明天负百分之六十二的概率降雨origin:这是固话0421-33441122或这是手机+86 18544139121
convet:这是固话零四二幺三三四四幺幺二二或这是手机加上八六幺八五四四幺三九幺二幺origin:-234
convet:负二百三十四origin:-234.5
convet:负二百三十四点五origin:生猪产能逐渐恢复,2021年生猪供应将进一步好转。而随着供需形势持续改善,猪肉价格已经连续22周下降,部分地区继续实现“猪肉自由”。
convet:生猪产能逐渐恢复,二零二幺年生猪供应将进一步好转。而随着供需形势持续改善,猪肉价格已经连续二十二周下降,部分地区继续实现“猪肉自由”。origin:农业农村部对全国500个集贸市场监测数据显示,6月份第4周,全国生猪均价为13.76元/公斤。从1月份第3周的36.01元/公斤起,已连续22周下跌,累计跌幅达62%。6月份第4周,全国猪肉价格为24.60元/公斤,较1月份第3周下跌29.62元/公斤,累计跌幅达54.6%。
convet:农业农村部对全国五百个集贸市场监测数据显示,六月份第四周,全国生猪均价为十三点七六元/公斤。从一月份第三周的三十六点零幺元/公斤起,已连续二十二周下跌,累计跌幅达百分之六十二。六月份第四周,全国猪肉价格为二十四点六零元/公斤,较一月份第三周下跌二十九点六二元/公斤,累计跌幅达百分之五十四点六。origin:从供给看,当前全国猪肉供应非常充裕。数据显示,1月至5月,全国定点屠宰企业生猪屠宰量同比增长40.4%。1月至5月,猪肉进口196万吨,同比增长13.7%。牛肉、羊肉和禽肉进口量也分别增加了18.6%、22.4%和13%。
convet:从供给看,当前全国猪肉供应非常充裕。数据显示,一月至五月,全国定点屠宰企业生猪屠宰量同比增长百分之四十点四。一月至五月,猪肉进口一百九十六万吨,同比增长百分之十三点七。牛肉、羊肉和禽肉进口量也分别增加了百分之十八点六、百分之二十二点四和百分之十三。origin:猪肉市场价格持续回落,受多因素叠加影响。在国内生猪产能已成功恢复的情况下,猪价下跌在客观上也倒逼产业加快转型升级步伐。目前,不少大中型养殖企业加速淘汰落后产能,代之以产能更高的二元母猪。
convet:猪肉市场价格持续回落,受多因素叠加影响。在国内生猪产能已成功恢复的情况下,猪价下跌在客观上也倒逼产业加快转型升级步伐。目前,不少大中型养殖企业加速淘汰落后产能,代之以产能更高的二元母猪。origin:今日下午,格力电器(000651.SZ)披露了6月26日投资者关系活动的主要内容。据了解,包括格力电器董事长兼总裁董明珠和格力电器副总裁、董事会秘书邓晓博参与接待,在场机构及个人投资者共计226人。
convet:今日下午,格力电器(零零零六五幺.SZ)披露了六月二十六日投资者关系活动的主要内容。据了解,包括格力电器董事长兼总裁董明珠和格力电器副总裁、董事会秘书邓晓博参与接待,在场机构及个人投资者共计二百二十六人。origin:格力电器表示,本次参与持股员工总数可能超过上万人,“尽管方案设定的锁定期是两年,但实际原则上是持有至退休。”对于设定10%的业绩考核指标,格力电器称,公司有自己的发展目标,会保持稳定增长,对冰洗、生活电器产品充满信心,不切实际的业绩考核目标是没有意义的。
convet:格力电器表示,本次参与持股员工总数可能超过上万人,“尽管方案设定的锁定期是两年,但实际原则上是持有至退休。”对于设定百分之十的业绩考核指标,格力电器称,公司有自己的发展目标,会保持稳定增长,对冰洗、生活电器产品充满信心,不切实际的业绩考核目标是没有意义的。origin:仅仅5天。
convet:仅仅五天。origin:6月25日,TCL科技耗资约1.5亿元回购了1928.12万股,占公司总股本的0.14%,回购价格区间为每股7.69元至7.8元。
convet:六月二十五日,TCL科技耗资约一点五亿元回购了一千九百二十八点幺二万股,占公司总股本的百分之零点幺四,回购价格区间为每股七点六九元至七点八元。origin:而TCL科技6月20日才确定了回购方案,拟出资6亿元至7亿元回购部分股份,回购价不超过每股12元。据此测算,TCL科技拟回购股份比例占总股本约0.36%至0.42%。
convet:而TCL科技六月二十日才确定了回购方案,拟出资六亿元至七亿元回购部分股份,回购价不超过每股十二元。据此测算,TCL科技拟回购股份比例占总股本约百分之零点三六至百分之零点四二。origin:TCL科技快速实施回购并非个案。上海证券报资讯统计显示,6月1日至26日,有8家公司在发布回购方案后不久便实施了首次回购。
convet:TCL科技快速实施回购并非个案。上海证券报资讯统计显示,六月一日至二十六日,有八家公司在发布回购方案后不久便实施了首次回购。origin:TCL科技2020年实现营业收入766.77亿元,同比增长33.9%;归属于上市公司股东净利润(下称“净利润”)43.88亿元,同比增长67.63%;截至2020年底,公司资产负债率65.1%,经营性净现金流入167亿元。TCL科技董事长李东生此前表示:“公司(2020年)经营效率持续改善,全面超额完成年度预算目标。”
convet:TCL科技二零二零年实现营业收入七百六十六点七七亿元,同比增长百分之三十三点九;归属于上市公司股东净利润(下称“净利润”)四十三点八八亿元,同比增长百分之六十七点六三;截至二零二零年底,公司资产负债率百分之六十五点幺,经营性净现金流入一百六十七亿元。TCL科技董事长李东生此前表示:“公司(二零二零年)经营效率持续改善,全面超额完成年度预算目标。”origin:作为TCL科技的重要子公司,TCL华星围绕印刷OLED、QLED以及Mirco-LED等新型显示技术、关键材料和设备领域持续加大研发投入,取得重大进展。公司2020年新增PCT专利申请1536件,累计PCT专利申请数量达12797件。同时,公司战略投资日本JOLED公司,加快布局下一代新型显示技术。
convet:作为TCL科技的重要子公司,TCL华星围绕印刷OLED、QLED以及Mirco-LED等新型显示技术、关键材料和设备领域持续加大研发投入,取得重大进展。公司二零二零年新增PCT专利申请一千五百三十六件,累计PCT专利申请数量达一万二千七百九十七件。同时,公司战略投资日本JOLED公司,加快布局下一代新型显示技术。origin:对外并购方面,TCL科技竞价收购了天津中环电子信息集团有限公司,进而间接持有中环股份和天津普林的控股权,以及中环计算机等业务资产,借此踏入半导体光伏和半导体材料产业赛道。
convet:对外并购方面,TCL科技竞价收购了天津中环电子信息集团有限公司,进而间接持有中环股份和天津普林的控股权,以及中环计算机等业务资产,借此踏入半导体光伏和半导体材料产业赛道。origin:目前,TCL科技的核心业务由半导体显示产业、半导体光伏及半导体材料产业以及产业金融和投资平台三个业务板块组成。李东生曾表示:“本人有信心,2021年本集团整体营收和利润将获得显著增长。”
convet:目前,TCL科技的核心业务由半导体显示产业、半导体光伏及半导体材料产业以及产业金融和投资平台三个业务板块组成。李东生曾表示:“本人有信心,二零二幺年本集团整体营收和利润将获得显著增长。”origin:2021年第一季度,TCL科技实现营业收入321.44亿元,同比增长133.91%;净利润24.04亿元,同比增长488.97%。
convet:二零二幺年第一季度,TCL科技实现营业收入三百二十一点四四亿元,同比增长百分之一百三十三点九幺;净利润二十四点零四亿元,同比增长百分之四百八十八点九七。origin:然而,当前TCL科技的股价表现并不理想。
convet:然而,当前TCL科技的股价表现并不理想。origin:6月21日,TCL科技发布《2021-2023年员工持股计划(第一期)(草案)》,面向中高层管理人员和优秀核心骨干员工制订了本期持股计划,资金规模上限不超过7.4亿元,来源为公司在2019年和2021年分别通过回购方案所回购的股份。
convet:六月二十一日,TCL科技发布《二零二幺到二零二三年员工持股计划(第一期)(草案)》,面向中高层管理人员和优秀核心骨干员工制订了本期持股计划,资金规模上限不超过七点四亿元,来源为公司在二零幺九年和二零二幺年分别通过回购方案所回购的股份。origin:行业景气度提升区别于此前回购
convet:行业景气度提升区别于此前回购origin:区别于2019年确定的回购方案,TCL科技发布2021年回购方案的背景是,当前面板行业的景气度高涨。
convet:区别于二零幺九年确定的回购方案,TCL科技发布二零二幺年回购方案的背景是,当前面板行业的景气度高涨。origin:李东生此前认为:“2021年受芯片和玻璃供应短缺及新产能开出延误影响,LCD面板市场供需持续偏紧,预期2021年上半年产品价格依然坚挺,下半年供需大致平衡。”
convet:李东生此前认为:“二零二幺年受芯片和玻璃供应短缺及新产能开出延误影响,LCD面板市场供需持续偏紧,预期二零二幺年上半年产品价格依然坚挺,下半年供需大致平衡。”origin:TCL科技近期在机构调研报告中介绍,从2020年6月至今,各个尺寸的TV面板上涨幅度不同,以32英寸为代表的小尺寸产品涨幅达180%,而65英寸、75英寸等大尺寸产品的涨幅在60%至70%。
convet:TCL科技近期在机构调研报告中介绍,从二零二零年六月至今,各个尺寸的TV面板上涨幅度不同,以三十二英寸为代表的小尺寸产品涨幅达百分之一百八十,而六十五英寸、七十五英寸等大尺寸产品的涨幅在百分之六十至百分之七十。origin:行业景气度高涨,迎来机构投资者密切关注。从2021年3月至今,TCL科技共接待了5场机构调研,每场均有上百家机构参与。
convet:行业景气度高涨,迎来机构投资者密切关注。从二零二幺年三月至今,TCL科技共接待了五场机构调研,每场均有上百家机构参与。origin:2021-2015
convet:二零二幺-二零幺五origin:-2135
convet:负二千一百三十五origin:笨男人+笨女人=结婚;笨男人+聪明女人=离婚;聪明男人+笨女人=-婚外情;聪明男人+聪明女人=浪漫爱情。
convet:笨男人加上笨女人等于结婚;笨男人加上聪明女人等于离婚;聪明男人加上笨女人等于-婚外情;聪明男人加上聪明女人等于浪漫爱情。
4.2 python测试demo2
转换前:我的快递单号是 YT9876543210,显示已到小区驿站。这辆车的车牌号是京 A・8B7C6,在停车场 B 区 3 排 5 号车位。初始密码为 Abc123456。实验室里 A-03 号试剂瓶存放着浓度为 50% 的硫酸溶液。手机 WiFi 要连接名称为 Home_Network_2024 的无线网络,密码是 Hn@202408。产品型号为 XJ - 2025A,生产日期标注在包装盒右下角的 20250315。参加马拉松比赛,我的参赛号码布是 D1212,起点在 A 区拱门处。
转换后:我的快递单号是 y t 九八七六五四三二一零 显示已到小区驿站 这辆车的车牌号是京 a 八 b 七 c 六 在停车场 b 区三排五号车位 初始密码为 a b c 一二三四五六 实验室里 a 零三号试剂瓶存放着浓度为百分之五十的硫酸溶液 手机 w i f i 要连接名称为 h o m e n e t w o r k 二零二四的无线网络 密码是 h n 二零二四零八 产品型号为 x j 二零二五 a 生产日期标注在包装盒右下角的二零二五零三一五 参加马拉松比赛 我的参赛号码布是 d 一二一二 起点在 a 区拱门处
五、总结
本节我们对语音合成前端中的文本归一化模块通过正则匹配的方式进行转换,并进行了c++工程化,以便进行下一步处理。需要注意的是要注意正则匹配的前后关系。