Thanks to visit codestin.com
Credit goes to Github.com

Skip to content

jiangyimm/AddressParsing

 
 

Repository files navigation

AddressParsing

简单地址归一化C#算法

.NET Core (2.0)

Nuget 安装

PM> Install-Package AddressParsing

性能

在本人开发笔记本(i7-8750H,单核最高睿频 4.1,内存随意):
1:一级区划(省、自治区、直辖市)开头的地址,单次匹配≈0.01毫秒,每秒可处理地址数量:100000

地址:河北省石家庄市裕华区槐安东路121号万达写字楼
00:00:00.0000097
Weight:1
MatchedRegions:MatchType:PathName, MatchIndex:0, MatchName:河北省石家庄市裕华区, MatchRegion:河北省 - 石家庄市 - 裕华区
处理:河北省 - 石家庄市 - 裕华区 - 槐安东路121号万达写字楼

2:二、三级区划(市、市辖区、县)开头的地址,单次匹配最快≤0.01毫秒,最慢也就是未匹配到,平均时间<0.5毫秒,每秒处理地址数量:2000-100000

地址:石家庄市裕华区槐安东路121号万达写字楼
00:00:00.0000110
Weight:1
MatchedRegions:MatchType:PathName, MatchIndex:0, MatchName:石家庄市裕华区, MatchRegion:河北省 - 石家庄市 - 裕华区
处理:河北省 - 石家庄市 - 裕华区 - 槐安东路121号万达写字楼

地址:馒檐犬乔搯钙^旊戠掾齊鉟馒檐犬乔搯钙^旊戠掾齊鉟
00:00:00.0003754

用法

var address = "上海市闵行区浦江镇陈行路2388号浦江科技广场9号楼";
var matchitems = AddressParser.ParsingAddress(address);

//AddressParser.ParsingAddress(address); 方法有可能会匹配到多条记录
foreach (var matchitem in matchitems)
{
    //matchitem 包含一些匹配信息:权重、多个匹配项、路径终点等,已重写ToString()
	Console.WriteLine(matchitem);
	
	//AddressParser.FinalCut(matchitem, address)方法根据匹配结果裁剪得到地址:上海市 - 上海市 - 闵行区 - 浦江镇陈行路2388号浦江科技广场9号楼
	Console.WriteLine(AddressParser.FinalCut(matchitem, address));
}

输出示例:

原始:闸北区大统路938弄6号1301室
上海市 - 上海市 - 闸北区 - 大统路938弄6号1301室
原始:崇明县 陈家镇 陈家镇裕民路 79号
上海市 - 上海市 - 崇明县 - 陈家镇 陈家镇裕民路 79号
原始:虹口区 江湾镇 场中路 803弄4号402室
上海市 - 上海市 - 虹口区 - 江湾镇 场中路 803弄4号402室
原始:浦东新区 高桥镇  潼港一村15号401室
上海市 - 上海市 - 浦东新区 - 高桥镇  潼港一村15号401室
原始:静安区 江宁路街道 陕西北路 525弄4号31室
上海市 - 上海市 - 静安区 - 江宁路街道 陕西北路 525弄4号31室
原始:闸北区 共和新路街道 洛川东路 352弄7号403
上海市 - 上海市 - 闸北区 - 共和新路街道 洛川东路 352弄7号403
原始:闸北区彭浦新村210号甲306室
上海市 - 上海市 - 闸北区 - 彭浦新村210号甲306室
原始:江苏省海门市常乐镇颐生村一组14号
江苏省 - 南通市 - 海门市 - 常乐镇颐生村一组14号
原始:虹口区 四川北街道 多伦路 201弄99号
上海市 - 上海市 - 虹口区 - 四川北街道 多伦路 201弄99号
原始:宝山区 通河街道  呼玛二村193号101室
黑龙江省 - 双鸭山市 - 宝山区 - 通河街道  呼玛二村193号101室
上海市 - 上海市 - 宝山区 - 通河街道  呼玛二村193号101室

算法解析

首先准备一下结构性数据

其中每一个区划都是一个Region

安徽省

宿州市

砀山县
萧县

准备好以上数据结构后,有几个概念需要理解下:

  • Name
    安徽省 -> 安徽省
    宿州市 -> 宿州市
    砀山县 -> 砀山县
  • ShortName:可有多个
    安徽省 -> 安徽
    宿州市 -> 宿州、宿县
    砀山县 -> 砀山
  • PathName:顶级地址不用设置PathName,二三级区划(路径名称按照字符串长度降序排列,其实就是直属的和间接直属的NameShortName的组合):
    宿州市的路径名称为:安徽省宿州市、安徽省宿州、安徽宿州市、安徽宿州
    砀山县的路径名称为:安徽省宿州市砀山县、安徽省宿州市砀山、安徽宿州市砀山县、安徽宿州砀山,宿州市砀山县、宿州砀山、……
  • Path
    安徽省 -> 安徽省
    宿州市 -> 安徽省 - 宿州市
    砀山县 -> 安徽省 - 宿州市 - 砀山县
  • PathContains
    Path中,下级的Path始终是包含自己和上级的Path
    如砀山县的Path:安徽省 - 宿州市 - 砀山县
    包含宿州市的Path:安徽省 - 宿州市

算法解析

  • 预处理
    在匹配地址之前首先移除干扰性的分隔符或其他基本不会出现在地址中的字符,如以下地址:
    安徽省宿州市砀山县芒砀路999号A幢8楼
    安徽省;宿州市;砀山县;芒砀路999号A幢8楼
    安徽省 - 宿州市 - 砀山县芒砀路999号A幢8楼
    【安徽省】【宿州市】【砀山县】芒砀路999号A幢8楼
    [安徽省][宿州市][砀山县]芒砀路999号A幢8楼
    安徽省,宿州市,砀山县,芒砀路999号A幢8楼
    ……
    这些地址经过移除常用分割字符后,变为:安徽省宿州市砀山县芒砀路999号A幢8楼,算法内部其实是在处理这个地址。进行这一步的目的是为了后期的PathName匹配
/// <summary>
/// 地址常用分割符,用来首次处理地址时移除
/// </summary>
internal static char[] SplitterChars { get; } = new char[]
{
	'~','!','@','#','$','%','^','&','*','(',')','-','+','_','=',':',';','\'','"','?','|','\\','{','}','[',']','<','>',',','.',' ',
	'!','¥','…','(',')','—','【','】','、',':',';','“','’','《','》','?',',',' '
};
  • 递归匹配全称和简称
    全称和简称匹配使用的是 AddressParser类的MatchRoughly方法,该方法按全称、简称递归匹配(使用string.IndexOf()方法)内部区划字典,如果未匹配到全称,则匹配简称。每得到一个匹配结果生成一个MatchRegionItem实例,该实例记录了一些匹配信息:匹配到的区划Region、匹配模式MatchType、匹配的索引、匹配的Region的名称或简称。
    在一次递归匹配中,若匹配到Region,则记录下匹配到的Index,由于地址的结构是具有包含关系的,在Children匹配的时候就从当前地址的Index开始进行下级匹配,直到三级区划匹配完成或者字符串匹配结束。
    如地址:安徽宿州市砀山县芒砀路999号A幢8楼
    1:一级地名匹配(地址:安徽宿州市砀山县芒砀路999号A幢8楼):
    简称匹配到安徽,记录下匹配信息后,进行Children的匹配,Children的匹配是从索引2开始匹配的,也就是字符串变为:宿州市砀山县芒砀路999号A幢8楼
    2:二级地名匹配(地址:宿州市砀山县芒砀路999号A幢8楼):
    全称匹配到宿州市,记录信息,同理在进行三级匹配
    3:三级地名匹配(地址:砀山县芒砀路999号A幢8楼):
    全称匹配到砀山县,记录信息
    其中步骤123都可以失败,无论失败与否,都会进行下级匹配,因为若是匹配到下级,通过地址的上下级包含关系也是可以确定地址的。
    这样一次完整的递归会产生0-3个MatchRegionItem对象,以便后续处理。
    在匹配简称的时候有一点细节,在使用简称匹配地址时,为了避免“匹配到但不是”带来的问题,我们在匹配到的索引向后和向前的2个字符中寻找一下字符,若包含这些字符,则本次简称匹配视为失败,全称几乎不存在此问题:
/// <summary>
/// 非三级地区常用后缀和前缀
/// </summary>
internal static string[] RegionInvalidSuffix { get; } = new string[]
{
    "街", "路", "村", "弄", "幢", "号", "道",
    "大厦", "工业", "产业", "广场", "科技", "公寓", "中心", "小区", "花园", "大道", "农场",
    "0","1","2","3","4","5","6","7","8","9",
    "0","1","2","3","4","5","6","7","8","9",
    "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",
    "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",
    "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",

    //"沟", "屯", "坡", "组", "庄", "苑", "墅", "寓",
};

如一些以区划命名的道路、小区、广场、大厦、xx号等这类地址:
上海市闸北区西藏南路99号

  • 从全称和简称匹配结果中匹配路径名
    在以上步骤匹配完成后,将首次匹配的结果再次进行路径名匹配,由于记录下了每个匹配的Region,故根据基础结构可以得到该Region的所有PathName,然后使用方法AddressParser.MatchPathName()进行路径名匹配(路径名匹配放到最后是因为,地址只有包含了全称或简称才有可能包含路径名,这样做可以缩小路径匹配的集合),匹配完成后将结果与全称和简称的匹配结果进行合并,如地址:安徽宿州市砀山县芒砀路999号A幢8楼
    路径名匹配会再次命中关键字安徽宿州市宿州市砀山县安徽砀山县,由此会再次产生三个MatchRegionItem对象,名且MatchType = PathName
  • 合并
    1:首先筛选出MatchType = MatchType.PathName的匹配结果, 如以上地址:安徽宿州市砀山县芒砀路999号A幢8楼,命中的MatchType = PathName的结果有3个(权重Weight均为1):
    MatchRegionItem:砀山县 -> 安徽省 - 宿州市 - 砀山县
    MatchRegionItem:宿州市 -> 安徽省 - 宿州市
    MatchRegionItem:安徽省 -> 安徽省
    通过PathContains进行合并后变为一个结果(权重Weight是3):
    MatchRegionItem:砀山县 -> 安徽省 - 宿州市 - 砀山县
    然后在所有的合并结果中,取权重最大的一组,在本例中我们合并后得到:
    MatchRegionItem:砀山县 -> 安徽省 - 宿州市 - 砀山县
    2:筛选出MatchType = MatchType.Name的匹配结果,并重复以上步骤
    3:筛选出MatchType = MatchType.ShortName的匹配结果,并重复以上步骤
    其中步骤2、3均在上一步未得到结果的情况下执行,因为上一步的结果总是比下一步的结果更能起到决定性的作用。
  • 规则处理 在合并之后需要进行最后的一些修修剪剪,这些规则有:
    1:在结果中取命中索引最小的一组结果
    2:在结果中取命中字数最多的一组结果
    3:在结果中取命中等级最小的一组结果
  • 后续 算法会不定期微调和性能提升,所以源码和解析可能有点出入。
    (待补充)

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C# 100.0%