使用SQL Server查找varchar列中的非ASCII字符

77

如何使用SQL Server返回带有非ASCII字符的行?
如果您能展示如何对一列执行此操作,那就太棒了。

我现在正在做类似于这样的事情,但它不起作用。

select *
from Staging.APARMRE1 as ar
where ar.Line like '%[^!-~ ]%'

如果能够跨越表中所有varchar列,那就更加出色了!在这个解决方案中,最好返回三列:

  • 该记录的标识字段。(这将允许使用另一个查询来查看整个记录。)
  • 列名
  • 包含无效字符的文本
 Id | FieldName | InvalidText       |
----+-----------+-------------------+
 25 | LastName  | Solís             |
 56 | FirstName | François          |
100 | Address1  | 123 Ümlaut street |

无效字符是指位于SPACE(3210)到~(12710)范围之外的任何字符。


1
请查看 https://dev59.com/_HRB5IYBdhLWcg3wN08P#2683496。 - Akira Yamamoto
8个回答

112

这里提供了使用PATINDEX进行单列搜索的解决方案。
它还显示了StartPosition、InvalidCharacter和ASCII代码。

select line,
  patindex('%[^ !-~]%' COLLATE Latin1_General_BIN,Line) as [Position],
  substring(line,patindex('%[^ !-~]%' COLLATE Latin1_General_BIN,Line),1) as [InvalidCharacter],
  ascii(substring(line,patindex('%[^ !-~]%' COLLATE Latin1_General_BIN,Line),1)) as [ASCIICode]
from  staging.APARMRE1
where patindex('%[^ !-~]%' COLLATE Latin1_General_BIN,Line) >0

1
这真的很有意思。你能解释一下它是如何工作的吗? - StevenWhite
6
Gerhard在PATINDEX函数中提供了一个正则表达式,该正则表达式为[^ !-~]。我不确定他为什么要包括感叹号字符,因为它在数值上紧跟着空格字符。关键是该正则表达式可以找到那些不属于Space-Tilde范围(32-126)的字符。 - Anssssss
值得注意的是,PATINDEX函数不接受任何正则表达式模式。它有自己的语法,在某些方面类似于正则表达式。 - Daz
ASCII可打印字符的字符代码范围是32到126。这意味着查找除了空格(Char(32))以外的任何字符,或者从![Char(33)]到~[Char(126)]的字符范围内的任何字符,即整个可打印ASCII范围。方括号表示匹配一个范围内的字符,而开放方括号后面的^表示不在此范围内。 - BateTech

30

我一直成功地运行了这段代码

declare @UnicodeData table (
     data nvarchar(500)
)
insert into 
    @UnicodeData
values 
    (N'Horse�')
    ,(N'Dog')
    ,(N'Cat')

select
    data
from
    @UnicodeData 
where
    data collate LATIN1_GENERAL_BIN != cast(data as varchar(max))

对于已知列,这很有效。

为了额外的积分,我编写了这个快速脚本来搜索给定表中的所有nvarchar列以查找Unicode字符。

declare 
    @sql    varchar(max)    = ''
    ,@table sysname         = 'mytable' -- enter your table here

;with ColumnData as (
    select
        RowId               = row_number() over (order by c.COLUMN_NAME)
        ,c.COLUMN_NAME
        ,ColumnName         = '[' + c.COLUMN_NAME + ']'
        ,TableName          = '[' + c.TABLE_SCHEMA + '].[' + c.TABLE_NAME + ']' 
    from
        INFORMATION_SCHEMA.COLUMNS c
    where
        c.DATA_TYPE         = 'nvarchar'
        and c.TABLE_NAME    = @table
)
select
    @sql = @sql + 'select FieldName = ''' + c.ColumnName + ''',         InvalidCharacter = [' + c.COLUMN_NAME + ']  from ' + c.TableName + ' where ' + c.ColumnName + ' collate LATIN1_GENERAL_BIN != cast(' + c.ColumnName + ' as varchar(max)) '  +  case when c.RowId <> (select max(RowId) from ColumnData) then  ' union all ' else '' end + char(13)
from
    ColumnData c

-- check
-- print @sql
exec (@sql)

我不是动态SQL的粉丝,但它确实在像这样的探索性查询中有其用途。


简单快捷。谢谢! - thrawnis
@vash 很棒的解决方案,喜欢它。 - Chris Diver
虽然我有时会编辑答案以包括被省略的分号,但在这里这样做就不正确了,因为答案将不再与您使用的代码准确匹配。 但是,重要的是不要省略它们。 参见:https://dev59.com/qHRB5IYBdhLWcg3wH0WW - Stewart

27

可以尝试像这样:

DECLARE @YourTable table (PK int, col1 varchar(20), col2 varchar(20), col3 varchar(20));
INSERT @YourTable VALUES (1, 'ok','ok','ok');
INSERT @YourTable VALUES (2, 'BA'+char(182)+'D','ok','ok');
INSERT @YourTable VALUES (3, 'ok',char(182)+'BAD','ok');
INSERT @YourTable VALUES (4, 'ok','ok','B'+char(182)+'AD');
INSERT @YourTable VALUES (5, char(182)+'BAD','ok',char(182)+'BAD');
INSERT @YourTable VALUES (6, 'BAD'+char(182),'B'+char(182)+'AD','BAD'+char(182)+char(182)+char(182));

--if you have a Numbers table use that, other wise make one using a CTE
WITH AllNumbers AS
(   SELECT 1 AS Number
    UNION ALL
    SELECT Number+1
        FROM AllNumbers
        WHERE Number<1000
)
SELECT 
    pk, 'Col1' BadValueColumn, CONVERT(varchar(20),col1) AS BadValue --make the XYZ in convert(varchar(XYZ), ...) the largest value of col1, col2, col3
    FROM @YourTable           y
        INNER JOIN AllNumbers n ON n.Number <= LEN(y.col1)
    WHERE ASCII(SUBSTRING(y.col1, n.Number, 1))<32 OR ASCII(SUBSTRING(y.col1, n.Number, 1))>127
UNION
SELECT 
    pk, 'Col2' BadValueColumn, CONVERT(varchar(20),col2) AS BadValue --make the XYZ in convert(varchar(XYZ), ...) the largest value of col1, col2, col3
    FROM @YourTable           y
        INNER JOIN AllNumbers n ON n.Number <= LEN(y.col2)
    WHERE ASCII(SUBSTRING(y.col2, n.Number, 1))<32 OR ASCII(SUBSTRING(y.col2, n.Number, 1))>127
UNION
SELECT 
    pk, 'Col3' BadValueColumn, CONVERT(varchar(20),col3) AS BadValue --make the XYZ in convert(varchar(XYZ), ...) the largest value of col1, col2, col3
    FROM @YourTable           y
        INNER JOIN AllNumbers n ON n.Number <= LEN(y.col3)
    WHERE ASCII(SUBSTRING(y.col3, n.Number, 1))<32 OR ASCII(SUBSTRING(y.col3, n.Number, 1))>127
order by 1
OPTION (MAXRECURSION 1000);

输出:

pk          BadValueColumn BadValue
----------- -------------- --------------------
2           Col1           BA¶D
3           Col2           ¶BAD
4           Col3           B¶AD
5           Col1           ¶BAD
5           Col3           ¶BAD
6           Col1           BAD¶
6           Col2           B¶AD
6           Col3           BAD¶¶¶

(8 row(s) affected)

有趣的方法KM。出于我的好奇心...我可以问一下为什么在语句末尾加上“OPTION (MAXRECURSION 1000)”这一行是必要的,以及在这种情况下它会做什么吗? - Twelfth
3
CTE需要使用"OPTION (MAXRECURSION 1000)"参数,以递归地从1到1000构建一组行,其默认值为100(我认为)。对于超过默认值的嵌套CTE递归调用,需要设置此选项。如果您有一个数字表(https://dev59.com/R3M_5IYBdhLWcg3wZSTX),则不需要CTE或这个"OPTION (MAXRECURSION 1000)"参数。 - KM.

16

这个脚本搜索一个列中的非ASCII字符。它生成一个包含所有有效字符的字符串,这里使用代码点32到127。然后它搜索不符合列表的行:

declare @str varchar(128);
declare @i int;
set @str = '';
set @i = 32;
while @i <= 127
    begin
    set @str = @str + '|' + char(@i);
    set @i = @i + 1;
    end;

select  col1
from    YourTable
where   col1 like '%[^' + @str + ']%' escape '|';

2
这个可以工作,只需要做一个小改变:Varchar(128) 需要更大一些,因为存储了2个字符。我将其改为 Varchar(200)。运行整个数据库需要一些时间。我也很惊讶为什么不能使用范围来简化这个过程。例如:like '%[^| -|~]%' escape '|'。我尝试使用范围,但它没有返回正确的信息。 - Gerhard Weiss
我还将127改为了126。我不想要DEL字符。 - Gerhard Weiss

10

在一些真实的数据上运行各种解决方案-12M行varchar长度约为30,大约有9k个可疑的行,在执行过程中没有全文索引,patIndex解决方案是最快的,它还选择了最多的行。

(预先运行km.以将缓存设置为已知状态,运行3个进程,最后再次运行km- km的最后2次运行时间相差不到2秒)

Gerhard Weiss的patindex解决方案-运行时间0:38,返回9144行

select dodgyColumn from myTable fcc
WHERE  patindex('%[^ !-~]%' COLLATE Latin1_General_BIN,dodgyColumn ) >0

MT 提供的子字符串数字解决方案 -- 运行时间 1:16,返回了 8996 行结果

select dodgyColumn from myTable fcc
INNER JOIN dbo.Numbers32k dn ON dn.number<(len(fcc.dodgyColumn ))
WHERE ASCII(SUBSTRING(fcc.dodgyColumn , dn.Number, 1))<32 
    OR ASCII(SUBSTRING(fcc.dodgyColumn , dn.Number, 1))>127

Deon Robertson的UDF解决方案--运行时间3:47,返回7316行记录

select dodgyColumn 
from myTable 
where dbo.udf_test_ContainsNonASCIIChars(dodgyColumn , 1) = 1

3

查找哪个字段存在无效字符:

SELECT * FROM Staging.APARMRE1 FOR XML AUTO, TYPE

您可以使用以下查询进行测试:
SELECT top 1 'char 31: '+char(31)+' (hex 0x1F)' field
from sysobjects
FOR XML AUTO, TYPE

结果将是:

消息 6841,级别 16,状态 1,行 3 FOR XML 无法将节点“field”中包含的字符(0x001F)序列化为数据,因为这在 XML 中不允许。若要使用 FOR XML 检索此数据,请将其转换为二进制、varbinary 或图像数据类型,并使用 BINARY BASE64 指令。

当您编写 XML 文件并在验证时出现无效字符错误时,它非常有用。

3

网上有一个名为“解析字母数字”的用户定义函数。Google搜索“UDF解析字母数字”,您应该能够找到其代码。这个用户定义函数会删除所有不符合0-9、a-z和A-Z之间字符的内容。

Select * from Staging.APARMRE1 ar
where udf_parsealpha(ar.last_name) <> ar.last_name

这将为您带回任何具有无效字符的last_name记录...虽然您的奖励积分问题更具挑战性,但我认为一个case语句可以处理它。这是一些伪代码,我不确定它是否有效。

Select id, case when udf_parsealpha(ar.last_name) <> ar.last_name then 'last name'
when udf_parsealpha(ar.first_name) <> ar.first_name then 'first name'
when udf_parsealpha(ar.Address1) <> ar.last_name then 'Address1'
end, 
case when udf_parsealpha(ar.last_name) <> ar.last_name then ar.last_name
when udf_parsealpha(ar.first_name) <> ar.first_name then ar.first_name
when udf_parsealpha(ar.Address1) <> ar.last_name then ar.Address1
end
from Staging.APARMRE1 ar
where udf_parsealpha(ar.last_name) <> ar.last_name or
udf_parsealpha(ar.first_name) <> ar.first_name or
udf_parsealpha(ar.Address1) <> ar.last_name 

我在论坛帖子框中写下了这个内容...所以我不太确定它是否能正常运行,但应该很接近。如果单个记录有两个字段包含无效字符,我不确定它会如何表现。
作为替代方案,您可以将from子句从一个单独的表更改为一个子查询,类似于以下内容:
select id,fieldname,value from (
Select id,'last_name' as 'fieldname', last_name as 'value'
from Staging.APARMRE1 ar
Union
Select id,'first_name' as 'fieldname', first_name as 'value'
from Staging.APARMRE1 ar
---(and repeat unions for each field)
)
where udf_parsealpha(value) <> value

这里的好处是对于每一列,您只需要在此扩展union语句,而在使用case语句版本的脚本中,您需要为每一列重复三次该比较操作。


在我自己的注释中...使用case语句版本,我提到了一个单行具有多个带有错误值的列。如果first_name和last_name都有错误值...我认为case语句将找到first_name部分并正确显示它,但会在那里结束,并不能正确显示last_name的值。这可能不是最优解决方案...在我的帖子底部的子查询版本,将所有表格值联合成id、columnname、value格式似乎更加实用和易于理解。 - Twelfth

2

这是我编写的一个UDF,用于检测包含扩展ASCII字符的列。它非常快速,并且您可以扩展要检查的字符集。第二个参数允许您在检查标准字符集之外的任何内容或允许扩展集之间切换:

create function [dbo].[udf_ContainsNonASCIIChars]
(
@string nvarchar(4000),
@checkExtendedCharset bit
)
returns bit
as
begin

    declare @pos int = 0;
    declare @char varchar(1);
    declare @return bit = 0;

    while @pos < len(@string)
    begin
        select @char = substring(@string, @pos, 1)
        if ascii(@char) < 32 or ascii(@char) > 126 
            begin
                if @checkExtendedCharset = 1
                    begin
                        if ascii(@char) not in (9,124,130,138,142,146,150,154,158,160,170,176,180,181,183,184,185,186,192,193,194,195,196,197,199,200,201,202,203,204,205,206,207,209,210,211,212,213,214,216,217,218,219,220,221,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,248,249,250,251,252,253,254,255)
                            begin
                                select @return = 1;
                                select @pos = (len(@string) + 1)
                            end
                        else
                            begin
                                select @pos = @pos + 1
                            end
                    end
                else
                    begin
                        select @return = 1;
                        select @pos = (len(@string) + 1)    
                    end
            end
        else
            begin
                select @pos = @pos + 1
            end
    end

    return @return;

end

用法:

select Address1 
from PropertyFile_English
where udf_ContainsNonASCIIChars(Address1, 1) = 1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接