不同的频道,面向不同的读者!
最近在进行语音项目,涉及到了时间提取,城市提取等,之前也学过python,对于实体识别这方面也有涉及,于是把python代码方面转化为c#代码,方便自己理解。
时间提取
时间提取是比较复杂的一项提取,因为涉及到了中文日期转数字,年份转数字,时间格式化为字符串,检查时间合法性等等。
(1)中文日期转数字
起初我的想法是,不是只有把传进来的中文直接转为数字即可吗,例如十–>10,但仔细想了一下,发现不对,因为当转化到十位数,百位数,千位数的时候,直接转化是错误的,例如一千二百–>1/1000/2/100
,完全脱离了我们提取的数字。因此不能直接转化。
首先了解一下,我们传入的字符串的正则表达式:
([0-9零一二两三四五六七八九十]+年)?([0-9一二两三四五六七八九十]+月)?([0-9一二两三四五六七八九十]+[号日])?([上中下午晚早]+)?([0-9零一二两三四五六七八九十百]+[点:\.时])?([0-9零一二两三四五六七八九十百]+分?)?([0-9零一二三四五六七八九十百]+秒)?
可以看到,匹配格式为xxxx年xx月xx日xx日这种,因此处理时要注意这些。
接下来开始处理:
定义普通中文数字和十百千万的字典。
private readonly static Dictionary<char, int> UTIL_CN_UNIT = new Dictionary<char, int>
{
{ '十', 10 }, {'百', 100 }, {'千', 1000 }, {'万', 10000 }
};
private readonly static Dictionary<char, int> UTIL_CN_NUM = new Dictionary<char, int>
{
{ '零', 0 },{ '一', 1 }, {'二', 2 },{ '两', 2 }, {'三', 3 },
{ '四', 4 },{ '五', 5 }, { '六', 6 },{ '七', 7 }, {'八', 8 },
{ '九', 9 },{ '0', 0 }, {'1', 1 }, {'2', 2 }, {'3', 3 }, {'4', 4 },
{ '5', 5 }, {'6', 6 },{ '7', 7 }, {'8', 8 }, {'9', 9 }
};
c#的Regex类是处理正则表达式的。
public static int Cn2Dig(string src)
{
if (src == "") return -1;
Match m = Regex.Match(src, @"\d+");
if (m != Match.Empty)
{
return Convert.ToInt32(m.Groups[0].Value);
}
int rsl = 0;
int unit = 1;
for (int i = src.Length - 2; i >= 0; i--)//减二是因为最后一个为号,日,时分这种
{
if (UTIL_CN_UNIT.ContainsKey(src[i]))//十百千这种数字
{
unit = UTIL_CN_UNIT[src[i]];
}
else if (UTIL_CN_NUM.ContainsKey(src[i]))//0~9这种数字
{
int num = UTIL_CN_NUM[src[i]];
rsl += num * unit;
}
else
{
return -1;
}
}
if (rsl < unit) rsl += unit;//防止单个unit位数时没有结果
return rsl;
————————————————
为什么从字符串的后面往前匹配?因为需要从个位数开始累加,否则如果从前开始无法确定是十位,百位还是千位。
(2)中文年份转数字
或许看了上面转中文日期后,你有个疑问,就是为什么还需要单独转年份?那是因为我们没有像月份和日一样的叫法。例如对于2012年,我们会说二零一二年及一二年这种,而不是两千零一十二年。所以对年份要做特殊处理。
public static int Year2Dig(string year)
{
string res = "";
foreach (var item in year)//对于年份而言,直接转化字符串即可
{
if (UTIL_CN_NUM.ContainsKey(item))
res = res + UTIL_CN_NUM[item].ToString();
else
res = res + item;
}
Match m = Regex.Match(res, @"\d+");
if (m != Match.Empty)
{
if (m.Groups[0].Value.Length == 2)//如果是01年,12年这种
return (DateTime.Now.Year / 100) * 100 + Convert.ToInt32(m.Groups[0].Value);
else
return Convert.ToInt32(m.Groups[0].Value);
}
return -1;
}
————————————————
(3)格式化时间
对于传入的中文日期,我们最终是要转化为我们需要的格式,例如yyyy-MM-dd hh:mm:ss等。
用正则表达式提取出中文时间字符串,接下来对字符串进行年份转数字,日期转数字的操作,然后提取出是否包含晚上,中午,下午关键词,如果有则修改时间,没有则返回最后的格式化后的字符串。
public static string ParseDatetime(string msg)
{
if (string.IsNullOrEmpty(msg)) return null;
try
{
Match m = Regex.Match(msg, @"([0-9零一二两三四五六七八九十]+年)?([0-9一二两三四五六七八九十]+月)?([0-9一二两三四五六七八九十]+[号日])?([上中下午晚早]+)?([0-9零一二两三四五六七八九十百]+[点:\.时])?([0-9零一二两三四五六七八九十百]+分?)?([0-9零一二三四五六七八九十百]+秒)?");
if (!string.IsNullOrEmpty(m.Groups[0].Value))
{
Dictionary<string, string> res = new Dictionary<string, string>
{
{"year",m.Groups[1].Value},
{"month",m.Groups[2].Value},
{"day",m.Groups[3].Value},
{ "hour",m.Groups[5].Value},
{ "minute",m.Groups[6].Value},
{ "second",m.Groups[7].Value},
};
Dictionary<string, int> Params = new Dictionary<string, int>();
foreach (var name in res.Keys)
{
if (res[name] != null)
{
int temp;
if (name == "year") temp = Year2Dig(res[name]);
else temp = Cn2Dig(res[name]);
if (temp != -1) Params[name] = temp;
else Params[name] = -1;
}
}
Dictionary<string, int> dateDic = ReplaceToday(Params);
string is_pm = m.Groups[4].Value;
if (!string.IsNullOrEmpty(is_pm))
{
if (is_pm == "下午" || is_pm == "晚上" || is_pm == "中午")
{
int hour = dateDic["hour"];
if (hour < 12)
dateDic["hour"] = dateDic["hour"] + 12;
}
}
return Dic2Str(dateDic);
}
else return null;
}
catch (Exception e)
{
Console.WriteLine(e);
}
return null;
}
————————————————
函数中包含ReplaceToday和Dic2Str两个函数,这时候就体现python对数据处理的强大之处,python的自带处理字典转字符串和代替日期字典的日期的函数的。对此,我们只能自己写。
由于我们日常口头语中可能会说明天早上,后天早上,某某天,某月某日,我们一般不会具体到分秒,年等等,所以对于我们没有具体的方面,我们要做处理,用现在的时间代替我们没有具体的时期。例如“1月7号上午“,其中虽然年份没有出现,但我们能看出说的是今年的1月。
private static Dictionary<string, int> ReplaceToday(Dictionary<string, int> Params)
{
//有直接说八号早上这种说法,但没有八号早上20分这种说法
Dictionary<string, int> dateDic = new Dictionary<string, int>();
if (Params["year"] == -1) dateDic["year"] = DateTime.Now.Year;
else dateDic["year"] = Params["year"];
if (Params["month"] == -1) dateDic["month"] = DateTime.Now.Month;
else dateDic["month"] = Params["month"];
if (Params["day"] == -1) dateDic["day"] = DateTime.Now.Day;
else dateDic["day"] = Params["day"];
if (Params["hour"] == -1) dateDic["hour"] = 0;
else dateDic["hour"] = Params["hour"];
if (Params["minute"] == -1) dateDic["minute"] = 0;
else dateDic["minute"] = Params["minute"];
if (Params["second"] == -1) dateDic["second"] = 0;
else dateDic["second"] = Params["second"];
return dateDic;
}
private static string Dic2Str(Dictionary<string, int> Params)
{
return string.Format("{0}-{1}-{2} {3}:{4}:{5}", Params["year"].ToString(), Params["month"].ToString(),
Params["day"].ToString(), Params["hour"].ToString(), Params["minute"].ToString(), Params["second"].ToString());
}
————————————————
(4)提取时间
时间的提取我们要考虑的因素有很多,例如说话的人说的是明天,后天,或者星期一,周日,下周等等,我们需要识别这些。
private readonly static Dictionary<string, int> keyDate = new Dictionary<string, int>
{
{ "今天",0 }, {"明天", 1 }, {"后天", 2 },{"今天上午" ,3 },{"今天下午",4 }
};
private readonly static Dictionary<string, int> weekdayDic = new Dictionary<string, int>
{
{ "星期一", 0 },{"星期二", 1 },{"星期三", 2 },{"星期四", 3 },{"星期五", 4 },{"星期六", 5 },{"星期天", 6 },
{ "星期日", 6 },{"周一", 0 },{"周二", 1 },{"周三", 2 },{"周四", 3 },{"周五", 4 },{"周六", 5 },{"周天", 6 },{"周日",6 }
};
public static List<string> TimeExtract(string text)
{
string text1;
MatchCollection a = Regex.Matches(text, @"[看等]一下(.*)");//一下会被识别为数词
if (a.Count == 0) text1 = text;
else text1 = a[0].Groups[1].Value;
List<string> timeRes = new List<string>();
//得到的时间字符串
string word = "";
DateTime today = DateTime.Now;
int t = (int)today.DayOfWeek;//C#的周一为1
t = t - 1;
if (t == -1) t = 6;
//暂时无法判断下周三和周六,周三和下周六这两种说法,这里统一为两个下周
int sub = 0;
Match m = Regex.Match(text1, @"(?:下星期|下周|下个星期).*$");
if (m != Match.Empty) sub += 7;
int subCopy = sub;
//分词,得出词语和词性
var posSeg = new PosSegmenter();
var tokens = posSeg.Cut(text1);
string[] wordFlag = { "m", "t" };
int[] dayNum = { 0, 1, 2 };
foreach (var item in tokens)
{
if (keyDate.ContainsKey(item.Word))
{
if (word != "") timeRes.Add(word);
int day = DicGet(keyDate, item.Word, 0);
if (dayNum.Contains(day))
{
word = DateTime.Today.AddDays(day).ToString("D");
}
else if (day == 3)
{
word = DateTime.Today.ToString("D") + "上午";
}
else if (day == 4)
{
word = DateTime.Today.ToString("D") + "下午";
}
}
else if (weekdayDic.ContainsKey(item.Word))
{
if (word != "") timeRes.Add(word);
int day = weekdayDic[item.Word];
sub += day - t;
if (sub > 0)
{
word = DateTime.Today.AddDays(sub).ToString("D");
sub = subCopy;
}
}
else if (word != "")
{
if (wordFlag.Contains(item.Flag))
{
word = word + item.Word;
}
else
{
timeRes.Add(word);
word = "";
}
}
else if (wordFlag.Contains(item.Flag))
{
word = item.Word;
}
}
if (word != "")
timeRes.Add(word);
List<string> result = new List<string>();
foreach (var item in timeRes)
{
string temp = CheckTimeValid(item);
if (temp != null) result.Add(temp);
}
List<string> finalRes = new List<string>();
foreach (var item in result)
{
string temp = ParseDatetime(item);
if (temp != null) finalRes.Add(temp);
}
return finalRes;
}
————————————————
此函数用于提取多个时间,我的项目中只涉及到提取一个,因此没有细分。日期检查部分可以省略。
全部评论