使用Lua实现字典树,进行敏感词匹配

我们在玩游戏的时候,聊天窗口经常会看到有“我**谁”等文字,其中的“**”可能是一个骂人的词语或者是政治敏感词汇,游戏审核部门会要求游戏必须屏蔽敏感词汇,否则不允许上线,所以骂人的词语或句子就被替换成*号了。

游戏项目中是如何处理这样的敏感字匹配的呢?KMP?还是Boyer-Moore算法?其实都不是,基本上这类匹配用的都是字典树的方式, 字典树又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种,它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

它有3个基本性质:
1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
3. 每个节点的所有子节点包含的字符都不相同。

因为现在大多数项目都是使用Lua版本进行热更新,逻辑都写在lua上,所以使用Lua实现字典树是很有必要的。

核心代码如下:

local WordTree ={}

--树节点创建
function WordTree.CreateNode(self, char, flag, childs)
	local node = {}
	node.char = char or nil		--字符
	node.flag = flag or 0		--是否结束标志,0:继续,1:结尾
	node.childs = childs or {}	--保存子节点
	node.isleaf = true --childs数量为0则是叶子节点
	return node
end

//创建根结点
WordTree.m_RootNode = WordTree.CreateNode('root') 

//更新字典树
function WordTree.UpdateNodes(self, words)
	for i, v in pairs(words) do
		local chars = self:GetCharList(string.lower(v))
		if #chars > 0 then
			self:InsertNode(self.m_RootNode, chars, 1)
		end
	end
end

--插入节点
function WordTree.InsertNode(self, parent, chars, index)
	local node = self:FindNode(parent, chars[index])
	if node == nil then
		node = self:CreateNode(chars[index])
		parent.isleaf = false
		table.insert(parent.childs, node)
	end
	local len = #chars
	if index == len then
		node.flag = 1
	else
		index = index + 1
		if index <= len then
			self:InsertNode(node, chars, index)
		end
	end
end

--节点中查找子节点
function WordTree.FindNode(self, node, char)
	local childs = node.childs
	for i, child in ipairs(childs) do
		if child.char == string.lower(char) then 
			return child
		end
	end

end

function WordTree.GetCharList(self, str)
	local list = {}
	while str do
		local utf8 = string.byte(str,1)
		if utf8 == nil then
			break
		end
		--utf8字符1byte,中文3byte
		if utf8 > 127 then
			local tmp = string.sub(str,1,3)
			table.insert(list,tmp)
			str = string.sub(str,4)
		else
			local tmp = string.sub(str,1,1)
			table.insert(list,tmp)
			str = string.sub(str,2)
		end
	end
	return list
end

--将字符串中敏感字用*替换返回
-- flag == true,替换的 * 的数量 = 敏感词长度;flag == false,默认使用 *** 替换
function WordTree.ReplaceMaskWord(self, str, flag)
	local chars = self:GetCharList(str)
	local index = 1
	local node = self.m_RootNode
	local prenode = nil
	local matchs = {}
	local isReplace = false
	local lastMatchLen = nil
	local totalLen = #chars
	local function replace(chars, list, last, flag)
        local stars = ""
		for i=1, last do
			local v = list[i]
			if flag then
				chars[v] = "*"
				isReplace = true
			else
				if isReplace then
					chars[v] = ""
				else
					chars[v] = "***"
					isReplace = true
				end
			end
		end
	end
	while totalLen >= index do
		prenode = node
		node = self:FindNode(node, chars[index])
		if chars[index] == " " then
			if #matchs then
				table.insert(matchs, index)
				node = prenode
			else
				node = self.m_RootNode
			end
		elseif node == nil then
			index = index - #matchs
			if lastMatchLen then
				replace(chars, matchs, lastMatchLen, flag)
				index = index + (lastMatchLen - 1)
				lastMatchLen = nil
			else
				isReplace = false
			end
			node = self.m_RootNode
			matchs = {}
		elseif node.flag == 1 then
			table.insert(matchs, index)
			if node.isleaf or totalLen == index then
				replace(chars, matchs, #matchs, flag)
				lastMatchLen = nil
				matchs = {}
				node = self.m_RootNode
			else
				lastMatchLen = #matchs
			end
		else
			table.insert(matchs, index)
		end
		index = index + 1
	end
	local str = ''
	for i, v in ipairs(chars) do
		str = str..v
	end
	return str
end

--字符串中是否含有敏感字
function WordTree.IsContainMaskWord(self, str)
	local sCheck = string.gsub(str, " ", "")
	local chars = self:GetCharList(sCheck)
	local index = 1
	local node = self.m_RootNode
	local masks = {}
	while #chars >= index do
		node = self:FindNode(node, chars[index])
		if node == nil then
			index = index - #masks 
			node = self.m_RootNode
			masks = {}
		elseif node.flag == 1 then
			return true
		else
			table.insert(masks,index)
		end
		index = index + 1
	end
	return false
end


return WordTree

假如有一组屏蔽字为:

local data={“二货”,  “傻B”,   “二B”}

首先根据屏蔽字表创建字典树:WordTree.UpdateNodes(data);

然后就可以判断是否存在敏感字,如WordTree .IsContainMaskWord(“你是 二货 “)返回true。

快速排序C#实现

快速排序是7大排序算法中最高效的算法,它在C++中的STL、java sdk 、.Net中都有实现。

它的算法时间复杂度为O(nlogn),它的原理是通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

具体C#代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Sort
{
    class Program
    {
       //快速排序
        static void quickSort(int[] arr, int low, int high)
        {
	        if (low < high) {
		        // 找寻基准数据的正确索引
		        int index = getIndex(arr, low, high);

		        // 进行迭代对index之前和之后的数组进行相同的操作使整个数组变成有序
		        quickSort(arr, 0, index - 1);
		        quickSort(arr, index + 1, high);
	        }
        }
       //选取枢轴,也是先选取当中的一个关键字,比如3,然后想办法把它放到一个位置,使它左边的值都比它小,右边的值都比它大,将这样的关键字称为枢轴。
       static int getIndex(int[] arr, int low, int high)
       {
	        // 基准数据
	        int tmp = arr[low];
	        while (low < high) {
		        // 当队尾的元素大于等于基准数据时,向前挪动high指针
		        while (low < high && arr[high] >= tmp) {
			        high--;
		        }
		        // 如果队尾元素小于tmp了,需要将其赋值给low
		        arr[low] = arr[high];
		        // 当队首元素小于等于tmp时,向前挪动low指针
		        while (low < high && arr[low] <= tmp) {
			        low++;
		        }
		        // 当队首元素大于tmp时,需要将其赋值给high
		        arr[high] = arr[low];

	        }
	        // 跳出循环时low和high相等,此时的low或high就是tmp的正确索引位置
	        // 由原理部分可以很清楚的知道low位置的值并不是tmp,所以需要将tmp赋值给arr[low]
	        arr[low] = tmp;
	        return low; // 返回tmp的正确位置
        }
        static void Main(string[] args)
        {
            int[] arr = { 1, 3, 2, 4, 0, 5, 6 };
            quickSort(arr,0,arr.Length-1);
            for (int i = 0; i < arr.Length; ++i) {
                Console.WriteLine(arr[i]);
            }
            Console.ReadKey();
        }
    }
}

以上代码在VS2017中运行通过。

XLua中数字签名的使用

数字签名是一种非对称加密算法,首先生成一对密钥,一个公钥,一个私钥。假如A要给B发送数据,那么A首先用自己生成的密钥对里的私钥(这个 私钥只有A自己知道)对数据进行加密(相当于对数据进行签名),然后发送给B,B收到数据后使用A的公钥验证签名的真实性。

使用数字签名主要有以下的作用:

(1)防冒充(伪造),因为A的私钥只有A自己知道,没有A私钥加密的数据用A的公钥验证必然不通过。

(2)可鉴别身份,如果是经过A签名的数据,那么一定可以用A的公钥验证。

(3)防篡改(防破坏信息的完整性),如果数据修改了,用A的公钥验证不通过。

(4)防重放,如果数据有系列号那么可以防止重新发送数据。

(5)防抵赖。可以防止A不承认自己的数据,因为A的公钥验证了A的数据。

(6)机密性(保密性)。 没有A的公钥无法查看数据。

在XLua中用Tools/KeyPairsGen.exe生成公私钥对,key_ras文件保存的是私钥,key_ras.pub保存的是公钥。

用Tools/FilesSignature.exe对源代码进行签名。

签名后使用AddLoader方式加载签名后的lua文件:

  luaenv.AddLoader(new SignatureLoader("公钥", (ref string filepath) =>
            {
                filepath = Application.dataPath + "test.lua";
               
                if (File.Exists(filepath))
                {
                    return File.ReadAllBytes(filepath);
                }
                else
                {
                    return null;
                }
            }));

在SignatureLoader函数中传入生成的公钥和自己定义CustomLoader即可对签名后lua文件进行验证,SignatureLoader 函数中是具体的验证方式。

lua源码文件功能分析

从官网下载到lua5.2的源代码后 ,展开压缩包,源代码文件全部放在 src子目录下。这些文件根据功能的不同,可以分为大模块。

第一部分: 虚拟机运转的核心功能
lapi.c C语言接口
lctype.c C标准库中 ctype 相关实现
ldebug.c Debug 接口
ldo.c 函数调用以及栈管理
lfunc.c 函数原型及闭包管理
lgc.c 垃圾回收
lmem.c 内存管理接口
lobject.c 对象操作的一些函数
lopcodes.c 虚拟机的字节码定义
lstate.c 全局状态机 lstring.c 字符串池
ltable.c 表类型的相关操作
ltm.c 元方法
lvm.c 虚拟机
lzio.c 输入流接口

第二部分:源代码解析以及预编译字节码 lcode.c 代码生成器
ldump.c 序列化预编译的 Lua字节码
llex.c 词法分析器
lparser.c 解析器
lundump.c 还原预编译的字节码

第三部分:内嵌库 lauxlib.c 库编写用到的辅助函数库
lbaselib.c 基础库
lbitlib.c 位操作库
lcorolib.c 协程库
ldblib.c Debug 库
linit.c 内嵌库的初始化
liolib.c IO 库
lmathlib.c 数学库
loadlib.c 动态扩展库管理
loslib.c OS库
lstrlib.c 字符串库
ltablib.c 表处理库

第四部分:可执行的解析器,字节码编译器 lua.c 解释器
luac.c 字节码编译器

关于XLua与C#之间的通信分析

分析了一下XLua与C#之间的通信方式,发现和SLua,Ulua的区别不是很大。

Lua调用C#:

都是需要先生成一个个wrap文件,C#才能被lua调用。

wrap文件相当于一个接口,Lua先调用 wrap文件 然后 wrap 再调用C#,在 wrap 文件里面实际上是把C#的类函数,字段压入到lua虚拟机的虚拟栈上,再由lua虚拟机出栈后给lua调用的。

当索引系统API、dll库或者第三方库时,无法将代码的具体实现进行代码生成,采用C#的反射方式实现交互,缺点是执行效率低。

也就是说Lua调用C#其实就是:lua->wrap->C#

那么在XLua中C#又是如何调用Lua的呢?

看源码很容易知道,其实是使用如下函数:

 LuaEnv luaenv = new LuaEnv();//创建Lua虚拟机
 luaenv.DoString("CS.UnityEngine.Debug.Log('hello world')");执行lua代码

根据XLua的文档,DoString可以直接执行字符串代码,也可以加载lua文件执行,

分析源码发现DoString其实是调用的外部DLL中的xluaL_loadbuffer函数,如下 DoString 定义:

public object[] DoString(byte[] chunk, string chunkName = "chunk", LuaTable env = null)
        {
#if THREAD_SAFE || HOTFIX_ENABLE
            lock (luaEnvLock)
            {
#endif
                var _L = L;
                int oldTop = LuaAPI.lua_gettop(_L);
                int errFunc = LuaAPI.load_error_func(_L, errorFuncRef);
                if (LuaAPI.xluaL_loadbuffer(_L, chunk, chunk.Length, chunkName) == 0)
                {
                    if (env != null)
                    {
                        env.push(_L);
                        LuaAPI.lua_setfenv(_L, -2);
                    }

                    if (LuaAPI.lua_pcall(_L, 0, -1, errFunc) == 0)
                    {
                        LuaAPI.lua_remove(_L, errFunc);
                        return translator.popValues(_L, oldTop);
                    }
                    else
                        ThrowExceptionFromError(oldTop);
                }
                else
                    ThrowExceptionFromError(oldTop);

                return null;
#if THREAD_SAFE || HOTFIX_ENABLE
            }

而 xluaL_loadbuffer 外部引入声明如下:

 [DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
 public static extern int xluaL_loadbuffer(IntPtr L, byte[] buff, int size, string name);

也就说xluaL_loadbuffer的函数实现并不在源文件中,而是在外部DLL文件中实现的。

继续查找发现它其实是xlua.dll里面的函数,在 xlua.dll 源码xlua.c文件中发现如下定义:

LUALIB_API int xluaL_loadbuffer (lua_State *L, const char *buff, int size,
                                const char *name) {
	return luaL_loadbuffer(L, buff, size, name);
}

根据文件后缀,其实它就是C代码,而且调用的luaL_loadbuffer其实就是lua源码里面的函数。Xlua只是把它封装了一下而已。

继续查找Lua源码,分析Lua.5.3.3的源码发现xluaL_loadbuffer 其实是调用了 lua_load 函数,而 lua_load 实现如下:

LUA_API int lua_load (lua_State *L, lua_Reader reader, void *data,
                      const char *chunkname, const char *mode) {
  ZIO z;
  int status;
  lua_lock(L);
  if (!chunkname) chunkname = "?";
  luaZ_init(L, &z, reader, data);
  status = luaD_protectedparser(L, &z, chunkname, mode);
  if (status == LUA_OK) {  /* no errors? */
    LClosure *f = clLvalue(L->top - 1);  /* get newly created function */
    if (f->nupvalues >= 1) {  /* does it have an upvalue? */
      /* get global table from registry */
      Table *reg = hvalue(&G(L)->l_registry);
      const TValue *gt = luaH_getint(reg, LUA_RIDX_GLOBALS);
      /* set global table as 1st upvalue of 'f' (may be LUA_ENV) */
      setobj(L, f->upvals[0]->v, gt);
      luaC_upvalbarrier(L, f->upvals[0]);
    }
  }
  lua_unlock(L);
  return status;
}

上述代码中调用了luaD_protectedparser来进行parse过程, 在luaD_protectedparser中又调用了f_parser ,在f_parser中根据一些选择来分别处理不同的情况,这就是lua的词法语法语义分析过程。

从上述分析发现,其实C#调用lua,就是C#先调用C代码,然后C调用lua的过程,因为Lua的源码是C写的,lua的代码需要Lua虚拟机解释执行,也就是需要C代码来解析执行。



递归实现斐波那契数列

递归的定义:把一个直接调用自己或通过一系列的调用语句间接的调用自己的函数,叫做递归函数。

斐波那契数列定义如下:
{ 0                        n=0
f(n){ 1                        n=1
{ f(n-1)+f(n-2)    n>1

   public int fbnq(int n)
   {
          if(n < 0)
	     return 0;
	  if (n < 2){
             return n;
	  }
	  else{
	     return fbnq(n - 1) + fbnq(n - 2);
	  }
    }

c#实现KMP算法的优化

KMP算法是有三位前辈提出来的一个模式匹配算法,也就是从一个字符串中查找子串的算法,类似C#里面 indexOf ()函数的功能。

如果利用普通的匹配算法去查找子串,最坏的情况下算法复杂度是O((n-m+1)*m) n代表的主串的长度,m代表的是子串的长度,很显然这个算法比较低效。

所以就有大神们发明了KMP算法,它的原理是比普通的匹配算法节省了比对步骤,如主串m=”abcababca”,子串s=”abcabx”,前5个字符是相同的第6个字符不等,那么可以判断子串s中的第一个字符和主串中的第二个,第三个字符也不相等。KMP算法正是省略这些可以判断步骤达到优化匹配算法的目的。

代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace KMP
{
    class Program
    {
        //计算出子串返回的next数组,当子串不匹配时根据next数组值进行回溯
        static void GetNext(string p, int []next){
            int len = p.Length;
            int i = 0;
            int j = -1;
            next[0] = -1;
            while (i < len)
            {
                if (j == -1 || p[i] == p[j]){
                    ++j;
                    ++i;
                    next[i] = next[j];
                }
                else{
                    j = next[j];//若字符不相同,则j值回溯
                }
            }
        }

        static int Main(string[] args)
        {
            string str = "abccc aabb";
            string sub = "abb";
            int []next = new int[100];
            //计算出子串返回的next数组
            GetNext(sub, next);
            int i = 0;
            int j = 0;
            while (i < str.Length && j < sub.Length) {
                if (j == -1 || str[i] == sub[j] )
                {
                    ++i;
                    ++j;
                }
                else
                    j = next[j];//j回溯到合适的位置
            }
            if (j == sub.Length)
            {
                Console.WriteLine(i - j);
                Console.ReadKey();
                return i - j;
            }
            return 0;
        }
    }
}
  

KMP算法的优化:

后来有人发现上面的KMP算法还是有缺陷的,比如主串m=”aaaabcde” ,子串s=”aaaaax”,由于子串的前面4个字符和主串都是相等的,那么可以用首位next[1]的值去取代与它相等的字符后续的next[j]的值,因此可以对获取next数组的函数进行改良。

static void GetNext(string p, int []next){
            int len = p.Length;
            int i = 0;
            int j = -1;
            next[0] = -1;
            while (i < len)
            {
                if (j == -1 || p[i] == p[j]){
                    j++;
                    i++;
                    if (i < len && p[i] != p[j])
                        next[i] = j;
                    else
                        next[i] = next[j];//改良部分
                }
                else{
                    j = next[j];
                }
            }
        }

设计模式-观察者模式

观察者模式属于23种GOF设计模式里的行为模式,行为模式特别关注对象之间的通信。

目的:定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。

结构图

组成:

(1)、抽象主题角色(Subject):抽象主题把所有观察者对象的引用保存在一个列表中,并提供增加和删除观察者对象的操作,抽象主题角色又叫做抽象被观察者角色,一般由抽象类或接口实现。

(2)、抽象观察者角色(Observer):为所有具体观察者定义一个接口,在得到主题通知时更新自己,一般由抽象类或接口实现。

(3)、具体主题角色(ConcreteSubject):实现抽象主题接口,具体主题角色又叫做具体被观察者角色。

(4)、具体观察者角色(ConcreteObserver):实现抽象观察者角色所要求的接口,以便使自身状态与主题的状态相协调。

设计模式-组合模式

组合模式属于23种GOF设计模式种的结构型模式。

目的: 将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

结构图

组成

(1)、抽象构件角色(Component):这是一个抽象角色,它给参加组合的对象定义出了公共的接口及默认行为,可以用来管理所有的子对象(在透明式的组合模式是这样的)。在安全式的组合模式里,构件角色并不定义出管理子对象的方法,这一定义由树枝结构对象给出。

(2)、树叶构件角色(Leaf):树叶对象是没有下级子对象的对象,定义出参加组合的原始对象的行为。(原始对象的行为可以理解为没有容器对象管理子对象的方法,或者 【原始对象行为】+【管理子对象的行为(Add,Remove等)】=面对客户代码的接口行为集合)

(3)、树枝构件角色(Composite):代表参加组合的有下级子对象的对象,树枝对象给出所有管理子对象的方法实现,如Add、Remove等。

设计模式-适配器模式

在23种GOF设计模式中适配器模式属于结构型模式,结构型模式的意思是这些设计模式关注的是类和对象的组合关系。

目的: 将一个类的接口转换成客户希望的另一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

结构图

组成

(1)、目标角色(Target):定义Client使用的与特定领域相关的接口。
   
(2)、客户角色(Client):与符合Target接口的对象协同。
 
(3)、被适配角色(Adaptee):定义一个已经存在并已经使用的接口,这个接口需要适配。
(4)、适配器角色(Adapte) :适配器模式的核心。它将对被适配Adaptee角色已有的接口转换为目标角色Target匹配的接口。对Adaptee的接口与Target接口进行适配.