本章主要讲述词典数据的导入与词典数据的初步分析。作者从网络上找到了很多公开的词典库,尽力搜罗并整合入库。
目前总计获得词条17,656,557条记录,其中不重复的词条有12,926,204条,去重大约4,730,353条(有的词条可能重复2次以上)。经过统计发现:重复2次以上的词条总计2,062,167条。可以说重复次数越多的词条是人们越关心的部分。其他低频词条的实际意义并不是很大。
有关词典数据的初步分析还可以参考“选择工作平台”以及《我的NLP(自然语言处理)历程(2)——词频统计》中的数据表。
随机从词典数据中抽取了部分数据样例,如图1所示。source表明的是数据来源。length是内容长度。content是具体的数据内容。enable是一个使能控制参数,0表示待定。remark则是数据内容的附加说明。可能是拼音注音,也可能是词典释义,或者词性标注,五花八门什么都有。
下面来讲下这些数据处理过程所需要消耗的时间。
(1)拷贝数据
INSERT INTO [dbo].DictionaryContent
([source], [length], [content], [remark])
SELECT [classification], LEN([content]), [content], [remark] FROM [nldb2].[dbo].Dictionary;
总计1700多万条记录拷贝耗时5分零7秒。每秒大致能处理5万7千多条记录。
(2)加载数据
将数据库中的1700多万条数据加载到内存的Dictionary对象中,以完成一个统计项目。
[Microsoft.SqlServer.Server.SqlProcedure]
public static void SqlReloadEntries()
{
// 记录日志
LogTool.LogMessage("DictionaryTool", "SqlReloadEntries", "清理数据记录!");
// 清理数据
entries.Clear();
// 记录日志
LogTool.LogMessage("DictionaryTool", "SqlReloadEntries", "加载数据记录!");
// 指令字符串
string cmdString =
"SELECT [content], [count] FROM [dbo].[DictionaryContent];";
// 创建数据库连接
SqlConnection sqlConnection = new SqlConnection("context connection = true");
try
{
// 开启数据库连接
sqlConnection.Open();
// 创建指令
SqlCommand sqlCommand =
new SqlCommand(cmdString, sqlConnection);
// 创建数据阅读器
SqlDataReader reader = sqlCommand.ExecuteReader();
// 循环处理
while (reader.Read())
{
// 获得内容
string strValue = reader.GetString(0);
// 检查结果
if (strValue == null || strValue.Length <= 0) continue;
// 获得计数
int count = reader.GetInt32(1);
// 检查数据
if (!entries.ContainsKey(strValue))
{
// 增加记录
entries.Add(strValue, count < 0 ? 0 : count);
}
else
{
// 更新记录
if (count > entries[strValue]) entries[strValue] = count;
}
}
// 关闭数据阅读器
reader.Close();
}
catch (System.Exception ex) { throw ex; }
finally
{
// 检查状态并关闭连接
if (sqlConnection.State == ConnectionState.Open) sqlConnection.Close();
}
// 记录日志
LogTool.LogMessage("\tentries.count = " + entries.Count);
// 记录日志
LogTool.LogMessage("DictionaryTool", "SqlReloadEntries", "数据记录已加载!");
}
该过程出奇地快,仅用时28秒左右。
(3)统计原始语料中,词典数据出现的频次
原始语料数据总计10,101,283条,总计977,485,287个字符。词典不重复记录12,926,204条。词典中数据最大长度为52。完整的全部扫描一遍下来,预计匹配计算量将达到\(6×10^{17}\) 以上。
这么大的匹配计算量,用传统的存储过程将会十分缓慢:需要频繁地调用UPDATE语句。由于原始语料数据已经达到千万级,UPDATE在有索引支持的情况下已经达到效率极限,整体运行速度肯定快不起来。
实际项目采用Dictionary内存对象处理,最后再执行UPDATE,则可以显著提高统计效率。最后用时2小时9分零9秒。大致每秒能处理1千1百多条记录。
(4)更新数据
用内存中的统计结果更新词典数据表。
[Microsoft.SqlServer.Server.SqlProcedure]
public static void SqlUpdateEntries()
{
// 记录日志
LogTool.LogMessage("DictionaryTool", "SqlUpdateEntries", "更新数据!");
LogTool.LogMessage(string.Format("\tentries.count = {0}", entries.Count));
// 生成批量处理语句
string cmdString =
"UPDATE [dbo].[DictionaryContent] " +
"SET [count] = @SqlCount WHERE [content] = @SqlEntry; " +
"IF @@ROWCOUNT <= 0 " +
"INSERT INTO [dbo].[DictionaryContent] " +
"([content], [count]) VALUES (@SqlEntry, @SqlCount); ";
// 创建数据库连接
SqlConnection sqlConnection = new SqlConnection("context connection = true");
try
{
// 开启数据库连接
sqlConnection.Open();
// 记录日志
LogTool.LogMessage("DictionaryTool", "SqlUpdateEntries", "数据连接已开启!");
// 开启事务处理模式
SqlTransaction sqlTransaction =
sqlConnection.BeginTransaction();
// 记录日志
LogTool.LogMessage("DictionaryTool", "SqlUpdateEntries", "事务处理已开启!");
// 创建指令
SqlCommand sqlCommand =
new SqlCommand(cmdString, sqlConnection);
// 设置事物处理模式
sqlCommand.Transaction = sqlTransaction;
// 记录日志
LogTool.LogMessage("DictionaryTool", "SqlUpdateEntries", "T-SQL指令已创建!");
// 遍历参数
foreach (KeyValuePair<string, int> kvp in entries)
{
// 获得描述
int count = kvp.Value;
// 记录日志
//LogTool.LogMessage(string.Format("content = {0}", kvp.Key));
//LogTool.LogMessage(string.Format("\tcount = {0}", count));
// 清理参数
sqlCommand.Parameters.Clear();
// 设置参数
sqlCommand.Parameters.AddWithValue("SqlCount", count);
sqlCommand.Parameters.AddWithValue("SqlEntry", kvp.Key);
// 执行指令(尚未执行)
sqlCommand.ExecuteNonQuery();
}
// 记录日志
LogTool.LogMessage("DictionaryTool", "SqlUpdateEntries", "批量指令已添加!");
// 提交事务处理
sqlTransaction.Commit();
// 记录日志
LogTool.LogMessage("DictionaryTool", "SqlUpdateEntries", "批量指令已提交!");
}
catch (System.Exception ex) { throw ex; }
finally
{
// 检查状态并关闭连接
if (sqlConnection.State == ConnectionState.Open) sqlConnection.Close();
}
// 记录日志
LogTool.LogMessage("DictionaryTool", "SqlUpdateEntries", "数据记录已更新!");
}
该过程实际总耗时7分52秒。每秒大致能更新2万7千多条记录。
最后介绍下两个技术点:
(1)CLR C# 线程同步
在日志模块之中,需要将日志内容先存储至内存的一个对象中(例如:List<string[]>)。然后再做一个表值返回函数去读取这个内存对象,并返回日志内容。
为了能够实时查看日志内容,就需要对日志记录和日志读取做一个线程同步。否则将会有异常抛出(例如:Enumerator已经被改变)。
using System;
using System.Collections;
using System.Data.SqlTypes;
using System.Collections.Generic;
using Microsoft.SqlServer.Server;
using System.Runtime.CompilerServices;
public partial class LogTool
{
// 是否记录
private static bool DEBUG = true;
// 最大日志量
private readonly static int MAX_LOGS = 1024;
// 日志记录
private static List<string[]> logs = new List<string[]>();
[Microsoft.SqlServer.Server.SqlFunction]
public static SqlBoolean SqlSetLog(SqlBoolean sqlLog)
{
// 设置数值
DEBUG = sqlLog.Value;
// 返回结果
return DEBUG;
}
[MethodImpl(MethodImplOptions.Synchronized)]
[Microsoft.SqlServer.Server.SqlFunction
(DataAccess = DataAccessKind.Read,
FillRowMethodName = "GetLogs_FillRow",
TableDefinition = "LogValue nvarchar(4000)")]
public static IEnumerable SqlGetLogs()
{
// 计数器
int count = 0;
// 数组
List<string> items = new List<string>();
// 检查数量
foreach (string[] item in logs)
{
// 修改计数器
count++;
// 检查参数
if (item.Length == 1)
{
// 发送消息
items.Add(item[0]);
}
else if (item.Length == 2)
{
// 发送消息
items.Add(item[0] + " > " + item[1]);
}
else if (item.Length == 3)
{
// 发送消息
items.Add(item[0] + " " + item[1] + " > " + item[2]);
}
else if (item.Length == 4)
{
// 发送消息
items.Add(item[0] + " " + item[1] + "." + item[2] + " > " + item[3]);
}
}
// 删除内容
if (count > 0) logs.RemoveRange(0, count);
// 返回结果
return items;
}
public static void GetLogs_FillRow(object logResultObj, out SqlString Value)
{
Value = (string)logResultObj;
}
[MethodImpl(MethodImplOptions.Synchronized)]
public static void LogMessage(string strMessage)
{
// 检查参数
if (DEBUG)
{
// 检查日志记录
if (logs.Count >= MAX_LOGS) logs.RemoveAt(0);
// 加入日志记录
logs.Add(new string[] { DateTime.Now.ToString("HH:mm:ss"), strMessage });
}
}
[MethodImpl(MethodImplOptions.Synchronized)]
public static void LogMessage(string strModule, string strFunction, string strMessage)
{
// 检查参数
if (DEBUG)
{
// 检查日志记录
if (logs.Count >= MAX_LOGS) logs.RemoveAt(0);
// 加入日志记录
logs.Add(new string[] { DateTime.Now.ToString("HH:mm:ss"), strModule, strFunction, strMessage });
}
}
}
使用库System.Runtime.CompilerServices。
然后在函数中声明[MethodImpl(MethodImplOptions.Synchronized)]即可保证线程同步。
(2)SQLServer 自动日志压缩
SQLServer日志增长得比数据库本身要快很多。数据文件才4G,日志文件就已经16G。如果长期不清理,那么日志文件很快就会撑爆磁盘。因此需要部署一个计划任务,定时清理SQLServer的日志。
USE [master]
GO
ALTER DATABASE [nldb3] SET RECOVERY SIMPLE WITH NO_WAIT
GO
ALTER DATABASE [nldb3] SET RECOVERY SIMPLE
GO
USE [nldb3]
GO
DBCC SHRINKFILE(N'nldb3_log', 2, TRUNCATEONLY)
GO
USE [master]
GO
ALTER DATABASE [nldb3] SET RECOVERY FULL WITH NO_WAIT
GO
ALTER DATABASE [nldb3] SET RECOVERY FULL
GO
读者可以修改数据库名和日志名,并将此T-SQL语句部署至任务之中即可。