词典导入

本章主要讲述词典数据的导入与词典数据的初步分析。作者从网络上找到了很多公开的词典库,尽力搜罗并整合入库。

目前总计获得词条17,656,557条记录,其中不重复的词条有12,926,204条,去重大约4,730,353条(有的词条可能重复2次以上)。经过统计发现:重复2次以上的词条总计2,062,167条。可以说重复次数越多的词条是人们越关心的部分其他低频词条的实际意义并不是很大

有关词典数据的初步分析还可以参考“选择工作平台”以及《我的NLP(自然语言处理)历程(2)——词频统计》中的数据表。

图1 词典库中的数据样例

随机从词典数据中抽取了部分数据样例,如图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百多条记录。

图2 统计后的结果样例

(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语句部署至任务之中即可。

图3 部署任务

知乎:我的NLP(自然语言处理)历程(9)——词典导入