SQL:从全名字段中parsing第一个,中间和最后一个名字

如何使用SQLparsing全名字段中的第一个,中间和最后一个名称?

我需要尝试匹配不是全名直接匹配的名称。 我希望能够把全名分解成第一名,中间名和最后一名。

数据不包含任何前缀或后缀。 中间名是可选的。 数据格式为“First First Last”。

我对一些实际的解决scheme感兴趣,让我有90%的方式。 如前所述,这是一个复杂的问题,所以我会分别处理特殊情况。

这是一个独立的例子,可以轻松操纵testing数据。

在这个例子中,如果你的名字有三个以上的部分,那么所有“额外”的东西都会被放在LAST_NAME字段中。 被标识为“标题”的特定string例如“DR”,“MRS”和“MR”是例外。

如果中间名称丢失,那么您只需获取FIRST_NAME和LAST_NAME(MIDDLE_NAME将为NULL)。

你可以将它打散成一个巨大的SUBSTRINGs嵌套,但是可读性已经足够困难,就像在SQL中这样做一样。

编辑 – 处理以下特殊情况:

1 – 名称字段为NULL

2 – 名称字段包含前导/尾随空格

3 – 名称字段在名称内具有> 1个连续空格

4 – 名称字段仅包含名字

5 – 为了便于阅读,在最终输出中包含原始全名作为单独的列

6 – 将特定的前缀列表作为单独的“标题”列

SELECT FIRST_NAME.ORIGINAL_INPUT_DATA ,FIRST_NAME.TITLE ,FIRST_NAME.FIRST_NAME ,CASE WHEN 0 = CHARINDEX(' ',FIRST_NAME.REST_OF_NAME) THEN NULL --no more spaces? assume rest is the last name ELSE SUBSTRING( FIRST_NAME.REST_OF_NAME ,1 ,CHARINDEX(' ',FIRST_NAME.REST_OF_NAME)-1 ) END AS MIDDLE_NAME ,SUBSTRING( FIRST_NAME.REST_OF_NAME ,1 + CHARINDEX(' ',FIRST_NAME.REST_OF_NAME) ,LEN(FIRST_NAME.REST_OF_NAME) ) AS LAST_NAME FROM ( SELECT TITLE.TITLE ,CASE WHEN 0 = CHARINDEX(' ',TITLE.REST_OF_NAME) THEN TITLE.REST_OF_NAME --No space? return the whole thing ELSE SUBSTRING( TITLE.REST_OF_NAME ,1 ,CHARINDEX(' ',TITLE.REST_OF_NAME)-1 ) END AS FIRST_NAME ,CASE WHEN 0 = CHARINDEX(' ',TITLE.REST_OF_NAME) THEN NULL --no spaces @ all? then 1st name is all we have ELSE SUBSTRING( TITLE.REST_OF_NAME ,CHARINDEX(' ',TITLE.REST_OF_NAME)+1 ,LEN(TITLE.REST_OF_NAME) ) END AS REST_OF_NAME ,TITLE.ORIGINAL_INPUT_DATA FROM ( SELECT --if the first three characters are in this list, --then pull it as a "title". otherwise return NULL for title. CASE WHEN SUBSTRING(TEST_DATA.FULL_NAME,1,3) IN ('MR ','MS ','DR ','MRS') THEN LTRIM(RTRIM(SUBSTRING(TEST_DATA.FULL_NAME,1,3))) ELSE NULL END AS TITLE --if you change the list, don't forget to change it here, too. --so much for the DRY prinicple... ,CASE WHEN SUBSTRING(TEST_DATA.FULL_NAME,1,3) IN ('MR ','MS ','DR ','MRS') THEN LTRIM(RTRIM(SUBSTRING(TEST_DATA.FULL_NAME,4,LEN(TEST_DATA.FULL_NAME)))) ELSE LTRIM(RTRIM(TEST_DATA.FULL_NAME)) END AS REST_OF_NAME ,TEST_DATA.ORIGINAL_INPUT_DATA FROM ( SELECT --trim leading & trailing spaces before trying to process --disallow extra spaces *within* the name REPLACE(REPLACE(LTRIM(RTRIM(FULL_NAME)),' ',' '),' ',' ') AS FULL_NAME ,FULL_NAME AS ORIGINAL_INPUT_DATA FROM ( --if you use this, then replace the following --block with your actual table SELECT 'GEORGE W BUSH' AS FULL_NAME UNION SELECT 'SUSAN B ANTHONY' AS FULL_NAME UNION SELECT 'ALEXANDER HAMILTON' AS FULL_NAME UNION SELECT 'OSAMA BIN LADEN JR' AS FULL_NAME UNION SELECT 'MARTIN J VAN BUREN SENIOR III' AS FULL_NAME UNION SELECT 'TOMMY' AS FULL_NAME UNION SELECT 'BILLY' AS FULL_NAME UNION SELECT NULL AS FULL_NAME UNION SELECT ' ' AS FULL_NAME UNION SELECT ' JOHN JACOB SMITH' AS FULL_NAME UNION SELECT ' DR SANJAY GUPTA' AS FULL_NAME UNION SELECT 'DR JOHN S HOPKINS' AS FULL_NAME UNION SELECT ' MRS SUSAN ADAMS' AS FULL_NAME UNION SELECT ' MS AUGUSTA ADA KING ' AS FULL_NAME ) RAW_DATA ) TEST_DATA ) TITLE ) FIRST_NAME 

颠倒这个问题,添加列来保存各个部分,并合并它们以获得全名。

这将是最好的答案的原因是,没有办法确定一个人已经注册为他们的名字,他们的中间名是什么。

比如,你会怎么分割?

 Jan Olav Olsen Heggelien 

这虽然是虚构的,但在挪威是一个合法的名字,可以,但不一定要这样分裂:

 First name: Jan Olav Middle name: Olsen Last name: Heggelien 

或者像这样:

 First name: Jan Olav Last name: Olsen Heggelien 

或者像这样:

 First name: Jan Middle name: Olav Last name: Olsen Heggelien 

我会想像在大多数语言中可以find类似的事件。

因此,不要试图解释没有足够信息的数据来正确解释数据,而是要把正确的解释结合起来,以得到全名。

不知道“全名”是如何格式化的,很难回答。

可以是“姓氏,名字中间名”或“名字中间名姓”等。

基本上你必须使用SUBSTRINGfunction

 SUBSTRING ( expression , start , length ) 

而且可能是CHARINDEX函数

 CHARINDEX (substr, expression) 

找出你想要提取的每个部分的开始和长度。

所以我们说格式是“名字姓氏”你可以(未经testing,但应该是closures):

 SELECT SUBSTR(fullname, 1, CHARINDEX(' ', fullname) - 1) AS FirstName, SUBSTR(fullname, CHARINDEX(' ', fullname) + 1, len(fullname)) AS LastName FROM YourTable 

除非你有非常非常好的数据,否则这是一个不小的挑战。 一个天真的方法是标记空格,并假设三个标记的结果是[first,middle,last]和一个双标记的结果是[first,last],但是你将不得不处理多个标记,单词(例如“Van Buren”)和多个中间名。

对于一个免费的基于SQL CLR的解决scheme,请确保从Ambient Concepts中检查SqlName,这对于在数据库级别parsing名称有很大的帮助。

http://ambientconcepts.com/sqlname

你确定完整的法律名称将永远包括第一,中间和最后? 我知道那些只有一个名称为完整法定名称的人,说实话,我不确定这是他们的姓氏或名字。 :-)我也知道那些拥有多个Fisrt名字的人,但是没有中间名。 还有一些人有多个中间名。

然后还有“完整法定名称”中的名称顺序。 据我所知,在一些亚洲文化中,“姓氏”以“完整法定名称”排在第一位。

更实际一点,您可以将全名分割为空白,并将第一个标记作为名字和最后一个标记(或者只有一个名字的唯一标记)作为姓氏。 虽然这假定订单总是一样的。

就像#1说的那样,这不是小事。 姓名,首字母缩写,双重名字,逆名字序列和各种其他exception都可能毁掉您精心devise的function。

你可以使用第三方库(插件/免责声明 – 我曾在这个产品):

http://www.melissadata.com/nameobject/nameobject.htm

我会做这个迭代过程。

1)将表转储到一个平面文件来处理。

2)编写一个简单的程序来打破你的名字,用空格作为分隔符,其中firsts token是第一个名字,如果有3个token,那么token 2是middle name,token 3是last name。 如果有2个令牌,那么第二个令牌是最后一个名字。 (Perl,Java或C / C ++,语言无关紧要)

3)眼球的结果。 寻找不符合这个规则的名字。

4)使用该示例,创build一个新的规则来处理该exception…

5)冲洗并重复

最终你会得到一个修复所有数据的程序。

如果你试图用PHPparsing一个人名,我推荐Keith Beckman的nameparse.php脚本 。

复制情况下,网站出现故障:

 <? /* Name: nameparse.php Version: 0.2a Date: 030507 First: 030407 License: GNU General Public License v2 Bugs: If one of the words in the middle name is Ben (or St., for that matter), or any other possible last-name prefix, the name MUST be entered in last-name-first format. If the last-name parsing routines get ahold of any prefix, they tie up the rest of the name up to the suffix. ie: William Ben Carey would yield 'Ben Carey' as the last name, while, Carey, William Ben would yield 'Carey' as last and 'Ben' as middle. This is a problem inherent in the prefix-parsing routines algorithm, and probably will not be fixed. It's not my fault that there's some odd overlap between various languages. Just don't name your kids 'Something Ben Something', and you should be alright. */ function norm_str($string) { return trim(strtolower( str_replace('.','',$string))); } function in_array_norm($needle,$haystack) { return in_array(norm_str($needle),$haystack); } function parse_name($fullname) { $titles = array('dr','miss','mr','mrs','ms','judge'); $prefices = array('ben','bin','da','dal','de','del','der','de','e', 'la','le','san','st','ste','van','vel','von'); $suffices = array('esq','esquire','jr','sr','2','ii','iii','iv'); $pieces = explode(',',preg_replace('/\s+/',' ',trim($fullname))); $n_pieces = count($pieces); switch($n_pieces) { case 1: // array(title first middles last suffix) $subp = explode(' ',trim($pieces[0])); $n_subp = count($subp); for($i = 0; $i < $n_subp; $i++) { $curr = trim($subp[$i]); $next = trim($subp[$i+1]); if($i == 0 && in_array_norm($curr,$titles)) { $out['title'] = $curr; continue; } if(!$out['first']) { $out['first'] = $curr; continue; } if($i == $n_subp-2 && $next && in_array_norm($next,$suffices)) { if($out['last']) { $out['last'] .= " $curr"; } else { $out['last'] = $curr; } $out['suffix'] = $next; break; } if($i == $n_subp-1) { if($out['last']) { $out['last'] .= " $curr"; } else { $out['last'] = $curr; } continue; } if(in_array_norm($curr,$prefices)) { if($out['last']) { $out['last'] .= " $curr"; } else { $out['last'] = $curr; } continue; } if($next == 'y' || $next == 'Y') { if($out['last']) { $out['last'] .= " $curr"; } else { $out['last'] = $curr; } continue; } if($out['last']) { $out['last'] .= " $curr"; continue; } if($out['middle']) { $out['middle'] .= " $curr"; } else { $out['middle'] = $curr; } } break; case 2: switch(in_array_norm($pieces[1],$suffices)) { case TRUE: // array(title first middles last,suffix) $subp = explode(' ',trim($pieces[0])); $n_subp = count($subp); for($i = 0; $i < $n_subp; $i++) { $curr = trim($subp[$i]); $next = trim($subp[$i+1]); if($i == 0 && in_array_norm($curr,$titles)) { $out['title'] = $curr; continue; } if(!$out['first']) { $out['first'] = $curr; continue; } if($i == $n_subp-1) { if($out['last']) { $out['last'] .= " $curr"; } else { $out['last'] = $curr; } continue; } if(in_array_norm($curr,$prefices)) { if($out['last']) { $out['last'] .= " $curr"; } else { $out['last'] = $curr; } continue; } if($next == 'y' || $next == 'Y') { if($out['last']) { $out['last'] .= " $curr"; } else { $out['last'] = $curr; } continue; } if($out['last']) { $out['last'] .= " $curr"; continue; } if($out['middle']) { $out['middle'] .= " $curr"; } else { $out['middle'] = $curr; } } $out['suffix'] = trim($pieces[1]); break; case FALSE: // array(last,title first middles suffix) $subp = explode(' ',trim($pieces[1])); $n_subp = count($subp); for($i = 0; $i < $n_subp; $i++) { $curr = trim($subp[$i]); $next = trim($subp[$i+1]); if($i == 0 && in_array_norm($curr,$titles)) { $out['title'] = $curr; continue; } if(!$out['first']) { $out['first'] = $curr; continue; } if($i == $n_subp-2 && $next && in_array_norm($next,$suffices)) { if($out['middle']) { $out['middle'] .= " $curr"; } else { $out['middle'] = $curr; } $out['suffix'] = $next; break; } if($i == $n_subp-1 && in_array_norm($curr,$suffices)) { $out['suffix'] = $curr; continue; } if($out['middle']) { $out['middle'] .= " $curr"; } else { $out['middle'] = $curr; } } $out['last'] = $pieces[0]; break; } unset($pieces); break; case 3: // array(last,title first middles,suffix) $subp = explode(' ',trim($pieces[1])); $n_subp = count($subp); for($i = 0; $i < $n_subp; $i++) { $curr = trim($subp[$i]); $next = trim($subp[$i+1]); if($i == 0 && in_array_norm($curr,$titles)) { $out['title'] = $curr; continue; } if(!$out['first']) { $out['first'] = $curr; continue; } if($out['middle']) { $out['middle'] .= " $curr"; } else { $out['middle'] = $curr; } } $out['last'] = trim($pieces[0]); $out['suffix'] = trim($pieces[2]); break; default: // unparseable unset($pieces); break; } return $out; } ?> 

另一种简单的方法是使用parsename

 select full_name, parsename(replace(full_name, ' ', '.'), 3) as FirstName, parsename(replace(full_name, ' ', '.'), 2) as MiddleName, parsename(replace(full_name, ' ', '.'), 1) as LastName from YourTableName 

资源

  1. 获取一个sql正则expression式函数。 示例: http : //msdn.microsoft.com/en-us/magazine/cc163473.aspx
  2. 使用正则expression式提取名称。

我build议Expresso学习/构build/testing正则expression式。 旧的免费版本 , 新的商业版本

我曾经用一个500个字符的正则expression式来parsing任意string的第一个,最后一个和中间名。 即使使用这种声音的正则expression式,由于input的完全不一致性,它的准确率也只有97%左右。 不过,总比没有好。

根据已经提出的关于名称和其他exception空间的警告,下面的代码至less可以处理98%的名字。 (注:凌乱的SQL,因为我没有在我使用的数据库中的正则expression式选项。)

**警告:凌乱的SQL如下:

 create table parsname (fullname char(50), name1 char(30), name2 char(30), name3 char(30), name4 char(40)); insert into parsname (fullname) select fullname from ImportTable; update parsname set name1 = substring(fullname, 1, locate(' ', fullname)), fullname = ltrim(substring(fullname, locate(' ', fullname), length(fullname))) where locate(' ', rtrim(fullname)) > 0; update parsname set name2 = substring(fullname, 1, locate(' ', fullname)), fullname = ltrim(substring(fullname, locate(' ', fullname), length(fullname))) where locate(' ', rtrim(fullname)) > 0; update parsname set name3 = substring(fullname, 1, locate(' ', fullname)), fullname = ltrim(substring(fullname, locate(' ', fullname), length(fullname))) where locate(' ', rtrim(fullname)) > 0; update parsname set name4 = substring(fullname, 1, locate(' ', fullname)), fullname = ltrim(substring(fullname, locate(' ', fullname), length(fullname))) where locate(' ', rtrim(fullname)) > 0; // fullname now contains the last word in the string. select fullname as FirstName, '' as MiddleName, '' as LastName from parsname where fullname is not null and name1 is null and name2 is null union all select name1 as FirstName, name2 as MiddleName, fullname as LastName from parsname where name1 is not null and name3 is null 

代码的工作原理是创build一个临时表(parsname)并用空格标记全名。 任何以name3或name4中的值结尾的名称都是不合格的,需要以不同的方式处理。

这里有一个存储过程,将第一个字find名字,最后一个字到Last Name和所有在中间名。

 create procedure [dbo].[import_ParseName] ( @FullName nvarchar(max), @FirstName nvarchar(255) output, @MiddleName nvarchar(255) output, @LastName nvarchar(255) output ) as begin set @FirstName = '' set @MiddleName = '' set @LastName = '' set @FullName = ltrim(rtrim(@FullName)) declare @ReverseFullName nvarchar(max) set @ReverseFullName = reverse(@FullName) declare @lengthOfFullName int declare @endOfFirstName int declare @beginningOfLastName int set @lengthOfFullName = len(@FullName) set @endOfFirstName = charindex(' ', @FullName) set @beginningOfLastName = @lengthOfFullName - charindex(' ', @ReverseFullName) + 1 set @FirstName = case when @endOfFirstName <> 0 then substring(@FullName, 1, @endOfFirstName - 1) else '' end set @MiddleName = case when (@endOfFirstName <> 0 and @beginningOfLastName <> 0 and @beginningOfLastName > @endOfFirstName) then ltrim(rtrim(substring(@FullName, @endOfFirstName , @beginningOfLastName - @endOfFirstName))) else '' end set @LastName = case when @beginningOfLastName <> 0 then substring(@FullName, @beginningOfLastName + 1 , @lengthOfFullName - @beginningOfLastName) else '' end return end 

这就是我的名字

 DECLARE @FirstName nvarchar(255), @MiddleName nvarchar(255), @LastName nvarchar(255) EXEC [dbo].[import_ParseName] @FullName = N'Scott The Other Scott Kowalczyk', @FirstName = @FirstName OUTPUT, @MiddleName = @MiddleName OUTPUT, @LastName = @LastName OUTPUT print @FirstName print @MiddleName print @LastName output: Scott The Other Scott Kowalczyk 

正如其他人所说,你不能从简单的编程方式。

考虑这些例子:

  • 总统“乔治·赫伯特·沃克·布什”(第一中间最后)

  • 总统刺客“约翰·威尔克斯展位”(First Last Last)

  • 吉他手“Eddie Van Halen”(倒数第一)

  • 而他的妈妈可能称他为Edward Lodewijk Van Halen(第一个中间的最后一个)

  • 着名的被抛弃的“玛丽安·萨默斯”(First First Last)

  • 新墨西哥共和党GOP主席 “Fernando C de Baca”(首尾持续最后)

我不确定SQL服务器,但在Postgres你可以做这样的事情:

 SELECT SUBSTRING(fullname, '(\\w+)') as firstname, SUBSTRING(fullname, '\\w+\\s(\\w+)\\s\\w+') as middle, COALESCE(SUBSTRING(fullname, '\\w+\\s\\w+\\s(\\w+)'), SUBSTRING(fullname, '\\w+\\s(\\w+)')) as lastname FROM public.person 

正则expression式可能会更简洁一些; 但你明白了。 对于有两个双重名字的人来说,这样做是不行的(在荷兰,我们有这个“Jan van der Ploeg”),所以我会非常小心的结果。

我们当然都明白,没有完美的方法可以解决这个问题,但是有些解决scheme可以让你走得比别人更远。

特别是,如果你只有一些常用的前缀(先生,夫人,等等),中缀(von,de,del等),后缀(Jr,III) ,Sr等)等等。 如果你有一些共同的名字(在不同的语言/文化中,如果你的名字是多样的),那么你也可以猜测中间的单词是否可能是姓氏的一部分。

BibTeX还实施了一些启发式的方法,让你在那里的一部分, 它们封装在Text::BibTeX::Name perl模块中。 这是一个简单的代码示例,可以完成合理的工作。

 use Text::BibTeX; use Text::BibTeX::Name; $name = "Dr. Mario Luis de Luigi Jr."; $name =~ s/^\s*([dm]rs?.?|miss)\s+//i; $dr=$1; $n=Text::BibTeX::Name->new($name); print join("\t", $dr, map "@{[ $n->part($_) ]}", qw(first von last jr)), "\n"; 

我遇到的最大的问题是像“鲍勃·R·史密斯,小”的情况。 我使用的algorithm发布在http://www.blackbeltcoder.com/Articles/strings/splitting-a-name-into-first-and-last-names 。 我的代码是用C#编写的,但是如果你必须用SQL,你可以把它移植到C#中。

这将工作在大小写string是名字/中间名/姓氏

 Select DISTINCT NAMES , SUBSTRING(NAMES , 1, CHARINDEX(' ', NAMES) - 1) as FirstName, RTRIM(LTRIM(REPLACE(REPLACE(NAMES,SUBSTRING(NAMES , 1, CHARINDEX(' ', NAMES) - 1),''),REVERSE( LEFT( REVERSE(NAMES), CHARINDEX(' ', REVERSE(NAMES))-1 ) ),'')))as MiddleName, REVERSE( LEFT( REVERSE(NAMES), CHARINDEX(' ', REVERSE(NAMES))-1 ) ) as LastName From TABLENAME 

@JosephStyons和@Digs的工作非常棒! 我使用了部分工作为SQL Server 2016和更新的版本创build了一个新的函数。 这一个也处理后缀,以及前缀。

 CREATE FUNCTION [dbo].[NameParser] ( @name nvarchar(100) ) RETURNS TABLE AS RETURN ( WITH prep AS ( SELECT original = @name, cleanName = REPLACE(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(@name)),' ',' '),' ',' '), '.', ''), ',', '') ) SELECT prep.original, aux.prefix, firstName.firstName, middleName.middleName, lastName.lastName, aux.suffix FROM prep CROSS APPLY ( SELECT prefix = CASE WHEN LEFT(prep.cleanName, 3) IN ('MR ', 'MS ', 'DR ', 'FR ') THEN LEFT(prep.cleanName, 2) WHEN LEFT(prep.cleanName, 4) IN ('MRS ', 'LRD ', 'SIR ') THEN LEFT(prep.cleanName, 3) WHEN LEFT(prep.cleanName, 5) IN ('LORD ', 'LADY ', 'MISS ', 'PROF ') THEN LEFT(prep.cleanName, 4) ELSE '' END, suffix = CASE WHEN RIGHT(prep.cleanName, 3) IN (' JR', ' SR', ' II', ' IV') THEN RIGHT(prep.cleanName, 2) WHEN RIGHT(prep.cleanName, 4) IN (' III', ' ESQ') THEN RIGHT(prep.cleanName, 3) ELSE '' END ) aux CROSS APPLY ( SELECT baseName = LTRIM(RTRIM(SUBSTRING(prep.cleanName, LEN(aux.prefix) + 1, LEN(prep.cleanName) - LEN(aux.prefix) - LEN(aux.suffix)))), numParts = (SELECT COUNT(1) FROM STRING_SPLIT(LTRIM(RTRIM(SUBSTRING(prep.cleanName, LEN(aux.prefix) + 1, LEN(prep.cleanName) - LEN(aux.prefix) - LEN(aux.suffix)))), ' ')) ) core CROSS APPLY ( SELECT firstName = CASE WHEN core.numParts <= 1 THEN core.baseName ELSE LEFT(core.baseName, CHARINDEX(' ', core.baseName, 1) - 1) END ) firstName CROSS APPLY ( SELECT remainder = CASE WHEN core.numParts <= 1 THEN '' ELSE LTRIM(SUBSTRING(core.baseName, LEN(firstName.firstName) + 1, 999999)) END ) work1 CROSS APPLY ( SELECT middleName = CASE WHEN core.numParts <= 2 THEN '' ELSE LEFT(work1.remainder, CHARINDEX(' ', work1.remainder, 1) - 1) END ) middleName CROSS APPLY ( SELECT lastName = CASE WHEN core.numParts <= 1 THEN '' ELSE LTRIM(SUBSTRING(work1.remainder, LEN(middleName.middleName) + 1, 999999)) END ) lastName ) GO SELECT * FROM dbo.NameParser('Madonna') SELECT * FROM dbo.NameParser('Will Smith') SELECT * FROM dbo.NameParser('Neil Degrasse Tyson') SELECT * FROM dbo.NameParser('Dr. Neil Degrasse Tyson') SELECT * FROM dbo.NameParser('Mr. Hyde') SELECT * FROM dbo.NameParser('Mrs. Thurston Howell, III')