【问题标题】:Find non-ASCII characters in varchar columns using SQL Server使用 SQL Server 在 varchar 列中查找非 ASCII 字符
【发布时间】:2011-04-22 22:09:56
【问题描述】:

如何使用 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) 范围之外的任何字符

【问题讨论】:

标签: sql-server tsql sql-server-2005 non-ascii-characters


【解决方案1】:

此脚本在一列中搜索非 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 '|';

【讨论】:

  • 这适用于一个小的变化 Varchar(128) 需要更大,因为要存储 2 个字符。我做了 Varchar(200)。运行我的数据库确实需要一些时间。我也很惊讶不能使用范围来简化这个过程。即像 '%[^| -|~]%'转义'|'我试图让一个范围工作,但它没有返回正确的信息。
  • 我也将 127 更改为 126。我不想要 DEL 字符。
【解决方案2】:

试试这样的:

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)”行以及在这种情况下它将做什么?
  • "OPTION (MAXRECURSION 1000)" 对于 CTE 是必需的,它递归地构建一组从 1 到 1000 的行,默认值为 100(我认为)任何嵌套递归调用超出默认值需要设置此选项。如果您有一个数字表stackoverflow.com/q/1393951/65223,则不需要 CTE 或此“OPTION (MAXRECURSION 1000)”行
【解决方案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 无法序列化 节点“字段”的数据,因为它包含一个字符 (0x001F) 在 XML 中是不允许的。要使用 FOR XML 检索此数据,请将其转换为 转换为二进制、varbinary 或图像数据类型并使用 BINARY BASE64 指令。

当您编写xml文件并在验证时遇到无效字符错误时非常有用。

【讨论】:

    【解决方案4】:

    我已经成功运行了这段代码

    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,但它确实可以用于像这样的探索性查询。

    【讨论】:

    • 简单快捷。谢谢!
    • @vash 很棒的解决方案,喜欢它。
    • 虽然我有时会编辑答案以包含已省略的分号,但在这里这样做是不对的,因为对于您正在使用的代码,答案将不再准确。但重要的是不要离开它们。见:stackoverflow.com/questions/710683/…
    【解决方案5】:

    在一些真实世界的数据上运行各种解决方案 - 12M 行 varchar 长度 ~30,大约 9k 不可靠的行,没有全文索引,patIndex 解决方案是最快的,它也选择了最多的行。

    (预先运行 km。将缓存设置为已知状态,运行 3 个进程,最后再次运行 km - 最后 2 次运行 km 给出了 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
    

    【讨论】:

      【解决方案6】:

      这是我为检测带有扩展 ascii 字符的列而构建的 UDF。它很快,您可以扩展要检查的字符集。第二个参数允许您在检查标准字符集之外的任何内容或允许扩展集之间切换:

      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
      

      【讨论】:

        【解决方案7】:

        这是使用 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
        

        【讨论】:

        • 这真的很有趣。您能解释一下这是如何工作的吗?
        • Gerhard 正在为 PATINDEX 函数提供正则表达式。正则表达式是 [^ !-~]。我不确定他为什么在其中包含感叹号,因为它就在数字上的空格字符之后。关键是正则表达式会找到不在空格波浪号 (32-126) 范围内的字符。
        • 值得注意的是,PATINDEX 函数不接受任何正则表达式模式。它有自己的语法,在某些方面类似于正则表达式。
        【解决方案8】:

        网络上有一个用户定义的函数“Parse Alphanumeric”。 Google UDF 解析字母数字,您应该找到它的代码。此用户定义的函数会删除所有不适合 0-9、a-z 和 A-Z 的字符。

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

        这应该会为您带回所有带有无效字符的 last_name 的记录......虽然您的奖励积分问题更具挑战性,但我认为案例陈述可以处理它。这有点伪代码,我不完全确定它是否可以工作。

        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
        

        这里的好处是对于每一列,您只需要在此处扩展联合语句,而在此脚本的 case 语句版本中,您需要对每一列进行三次比较

        【讨论】:

        • 对我自己的评论...case 语句版本,我提到了单行包含多个列的错误值。如果 first_name 和 last_name 都有一个错误的值......我认为 case 语句会找到 first_name 部分并正确显示它,但会在那里结束并且不能正确显示 last_name 值。可能不是最佳解决方案....我帖子底部的子查询版本将所有表值合并为 id,columnname,value 格式似乎更实用且更易于遵循
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-06-21
        • 2010-10-15
        • 2013-11-17
        • 1970-01-01
        • 2010-12-27
        • 2019-05-13
        相关资源
        最近更新 更多