如何替换字符串中的奇数模式?

Posted

技术标签:

【中文标题】如何替换字符串中的奇数模式?【英文标题】:How I can replace odd patterns inside a string? 【发布时间】:2014-09-22 19:09:37 【问题描述】:

我正在用 SQL 创建一个临时过程,因为我有一个用 markdown 编写的表的值,因此它在 Web 浏览器中显示为呈现的 html (markdown 到 HTML 转换)。

当前列的字符串如下所示:

Questions about **general computing hardware and software** are off-topic for Stack Overflow unless they directly involve tools used primarily for programming. You may be able to get help on [Super User](http://superuser.com/about)

我目前正在处理粗体和斜体文本。这意味着(在粗体文本的情况下)我需要将奇数 N 次替换为模式**with<b>,偶数次替换为</b>。 我看到了replace(),但它对字符串的所有模式进行了替换。

那么我如何才能只替换奇数或偶数的子字符串?

更新:有些人想知道我使用的是什么架构,所以看看here。

如果您愿意,还可以多加一点:到 html 超链接的 markdown 样式超链接看起来并不那么简单。

【问题讨论】:

什么是“损坏子串”? @RobertHarvey 例如,如果在字符串中,我有** ** ** ** ** **,那么这个子字符串/模式在当前字符串中出现了六次。现在,假设我根据它们的位置为每个模式分配一个数字,我将使用1; 2; 3; 4; 5 访问它们中的每一个,因此损害子字符串对应于位置1; 3; 5; 并将子字符串配对到位置2; 4; 6 奇数位和偶数位,那么呢? @RobertHarvey :对不起,我以为它和法语中的词是同一个词。所以是的,你是对的。 我想如果你能确定它是字符串的哪个实例(第一个、第二个等),你可以使用MODULO函数:<position> % 2。如果返回 0,则为偶数,非零为奇数。 【参考方案1】:

使用STUFF函数和一个简单的WHILEloop:

CREATE FUNCTION dbo.fn_OddEvenReplace(@text nvarchar(500), 
                                      @textToReplace nvarchar(10), 
                                      @oddText nvarchar(10), 
                                      @evenText nvarchar(500))
RETURNS varchar(max)
AS
BEGIN
    DECLARE @counter tinyint
    SET @counter = 1

    DECLARE @switchText nvarchar(10)
    WHILE CHARINDEX(@textToReplace, @text, 1) > 0
    BEGIN
        SELECT @text = STUFF(@text, 
                    CHARINDEX(@textToReplace, @text, 1), 
                    LEN(@textToReplace), 
                    IIF(@counter%2=0,@evenText,@oddText)),
                @counter = @counter + 1
    END
    RETURN @text
END

你可以这样使用它:

SELECT dbo.fn_OddEvenReplace(column, '**', '<b>', '</b>')
FROM table

更新:

这被重写为一个SP:

CREATE PROC dbo.##sp_OddEvenReplace @text nvarchar(500), 
                                  @textToReplace nvarchar(10), 
                                  @oddText nvarchar(10), 
                                  @evenText nvarchar(10),
                                  @returnText nvarchar(500) output
AS
BEGIN
    DECLARE @counter tinyint
    SET @counter = 1

    DECLARE @switchText nvarchar(10)
    WHILE CHARINDEX(@textToReplace, @text, 1) > 0
    BEGIN
        SELECT @text = STUFF(@text, 
                    CHARINDEX(@textToReplace, @text, 1), 
                    LEN(@textToReplace), 
                    IIF(@counter%2=0,@evenText,@oddText)),
                @counter = @counter + 1
    END
    SET @returnText = @text
END
GO

并执行:

DECLARE @returnText nvarchar(500)
EXEC dbo.##sp_OddEvenReplace '**a** **b** **c**', '**', '<b>', '</b>', @returnText output

SELECT @returnText

【讨论】:

好的。现在我懂了。如果我将这段代码重写到存储过程中会有帮助吗? @user2284570 更新:)【参考方案2】:

根据 OP 的要求,我已修改我之前的答案以作为临时存储过程执行。我已经留下了我之前的答案,因为我相信对字符串表的使用也很有用。

如果已知一个 Tally(或 Numbers)表已经存在至少 8000 个值,则可以省略 CTE 的标记部分,并将 CTE 引用 tally 替换为现有的 Tally 表。

create procedure #HtmlTagExpander(
     @InString   varchar(8000) 
    ,@OutString  varchar(8000)  output
)as 
begin
    declare @Delimiter  char(2) = '**';

    create table #t( 
         StartLocation  int             not null
        ,EndLocation    int             not null

        ,constraint PK unique clustered (StartLocation desc)
    );

    with 
          -- vvv Only needed in absence of Tally table vvv
    E1(N) as ( 
        select 1 from (values
            (1),(1),(1),(1),(1),
            (1),(1),(1),(1),(1)
        ) E1(N)
    ),                                              --10E+1 or 10 rows
    E2(N) as (select 1 from E1 a cross join E1 b),  --10E+2 or 100 rows
    E4(N) As (select 1 from E2 a cross join E2 b),  --10E+4 or 10,000 rows max
    tally(N) as (select row_number() over (order by (select null)) from E4),
          -- ^^^ Only needed in absence of Tally table ^^^

    Delimiter as (
        select len(@Delimiter)     as Length,
               len(@Delimiter)-1   as Offset
    ),
    cteTally(N) AS (
        select top (isnull(datalength(@InString),0)) 
            row_number() over (order by (select null)) 
        from tally
    ),
    cteStart(N1) AS 
        select 
            t.N 
        from cteTally t cross join Delimiter 
        where substring(@InString, t.N, Delimiter.Length) = @Delimiter
    ),
    cteValues as (
        select
             TagNumber = row_number() over(order by N1)
            ,Location   = N1
        from cteStart
    ),
    HtmlTagSpotter as (
        select
             TagNumber
            ,Location
        from cteValues
    ),
    tags as (
        select 
             Location       = f.Location
            ,IsOpen         = cast((TagNumber % 2) as bit)
            ,Occurrence     = TagNumber
        from HtmlTagSpotter f
    )
    insert #t(StartLocation,EndLocation)
    select 
         prev.Location
        ,data.Location
    from tags data
    join tags prev
       on prev.Occurrence = data.Occurrence - 1
      and prev.IsOpen     = 1;

    set @outString = @Instring;

    update this
    set @outString = stuff(stuff(@outString,this.EndLocation,  2,'</b>')
                                           ,this.StartLocation,2,'<b>')
    from #t this with (tablockx)
    option (maxdop 1);
end
go

这样调用:

declare @InString   varchar(8000) 
       ,@OutString  varchar(8000);

set @inString = 'Questions about **general computing hardware and software** are off-topic **for Stack Overflow.';
exec #HtmlTagExpander @InString,@OutString out; select @OutString;

set @inString = 'Questions **about** general computing hardware and software **are off-topic** for Stack Overflow.';
exec #HtmlTagExpander @InString,@OutString out; select @OutString;
go

drop procedure #HtmlTagExpander;
go

它作为输出产生:

Questions about <b>general computing hardware and software</b> are off-topic **for Stack Overflow.

Questions <b>about</b> general computing hardware and software <b>are off-topic</b> for Stack Overflow.

【讨论】:

@user2284570:最好的(也是唯一正确的)谢谢是赞成和接受。 ;-) 如果你回答我的额外部分,我可能会接受...我还有一个问题,但这是other question @user2284570:我会咬人的——但我在上面的链接中的任何地方都看不到这样的功能。请展开。 问题里这么写的:“如果你想多加一点:markdown风格的超链接到html超链接的转换看起来没那么简单。” @user2284570:我看到了——如果它有一点点意义,我就不会问这个问题了。【参考方案3】:

一种选择是使用正则表达式,因为它使替换此类模式变得非常简单。 RegEx 函数未内置在 SQL Server 中,因此您需要使用 SQL CLR,无论是由您编译还是从现有库中编译。

对于这个例子,我将使用SQL# (SQLsharp) 库(我是该库的作者),但免费版本中提供了 RegEx 函数。

SELECT SQL#.RegEx_Replace
(
   N'Questions about **general computing hardware and software** are off-topic\
for Stack Overflow unless **they** directly involve tools used primarily for\
**programming. You may be able to get help on [Super User]\
(https://superuser.com/about)', -- @ExpressionToValidate
   N'\*\*([^\*]*)\*\*', -- @RegularExpression
   N'<b>$1</b>', -- @Replacement
   -1, -- @Count (-1 = all)
   1, - @StartAt
   'IgnoreCase' -- @RegEx options
);

上面的模式\*\*([^\*]*)\*\* 只是寻找任何被双星号包围的东西。在这种情况下,您无需担心奇数/偶数。这也意味着如果由于某种原因在字符串中有一个额外的**,您将不会得到一个格式不正确的&lt;b&gt;-only 标记。我在原始字符串中添加了两个额外的测试用例:在单词 they 周围的一组完整的 ** 和在单词 programming 之前的一组不匹配的 **。输出是:

Questions about <b>general computing hardware and software</b> are off-topicfor Stack Overflow unless <b>they</b> directly involve tools used primarily for **programming. You may be able to get help on [Super User](https://superuser.com/about)

呈现为:

关于通用计算硬件和软件的问题对于 Stack Overflow 来说是题外话,除非它们直接涉及主要用于**编程的工具。您或许可以通过Super User获得帮助

【讨论】:

正则表达式会非常好,但我在我的问题中添加了一个标签。 @user2284570:标签?你的意思是 sql 标签吗? CLR不是一种选择吗?如果是这样,也许为 T-SQL 添加一个标签将有助于澄清以及在问题的文本中提及这一点(即 CLR 不是一个选项)。虽然如果不是一个选项,为什么不呢?只是好奇。 我添加了dataexplorer 标签。如需更多信息,请查看我的query。 @user2284570:好的,我现在明白了。我会说,从您的问题文本中仍然有点不清楚您的限制是什么。我认为,如果您清楚地说明您正在使用 SEDE 的 GUI 版本 并且 您遇到问题的字段是 CloseAsOffTopicReasonTypes.MarkdownMini,它已经在 Markdown 中格式化,这将有所帮助。我想我总是可以向 SE 建议他们加载 SQL# 并使 RegEx 和 String 函数可供在 SEDE 中编写查询的人使用:-)。 @user2284570:另外,你是如何创建一个函数的?我不明白 SEDE 是如何做到这一点的。【参考方案4】:

此解决方案利用了 Jeff Moden 在this article on the Running Sum problem in SQL 中描述的技术。这个解决方案很长,但是通过在聚集索引上使用 SQL Server 中的Quirky Update,有望比基于游标的解决方案更高效地处理大型数据集。

更新 - 修改如下以对字符串表进行操作

假设存在这样创建的计数表(至少有 8000 行):

create table dbo.tally (
     N int not null
    ,unique clustered (N desc)
);
go

with 
E1(N) as ( 
    select 1 from (values
        (1),(1),(1),(1),(1),
        (1),(1),(1),(1),(1)
    ) E1(N)
),                                              --10E+1 or 10 rows
E2(N) as (select 1 from E1 a cross join E1 b),  --10E+2 or 100 rows
E4(N) As (select 1 from E2 a cross join E2 b)   --10E+4 or 10,000 rows max
insert dbo.tally(N)
select row_number() over (order by (select null)) from E4;
go

还有一个 HtmlTagSpotter 函数定义如下:

create function dbo.HtmlTagSPotter(
     @pString       varchar(8000)
    ,@pDelimiter    char(2))
returns table with schemabinding as
return
   WITH 
        Delimiter as (
        select len(@pDelimiter)     as Length,
               len(@pDelimiter)-1   as Offset
    ),
    cteTally(N) AS (
        select top (isnull(datalength(@pstring),0)) 
            row_number() over (order by (select null)) 
        from dbo.tally
    ),
    cteStart(N1) AS (--==== Returns starting position of each "delimiter" )
        select 
            t.N 
        from cteTally t cross join Delimiter 
        where substring(@pString, t.N, Delimiter.Length) = @pDelimiter
    ),
    cteValues as (
        select
             ItemNumber = row_number() over(order by N1)
            ,Location   = N1
        from cteStart
    )
    select
         ItemNumber
        ,Location
    from cteValues
go

然后运行以下 SQL 将执行所需的替换。请注意,末尾的内部连接可防止转换任何尾随的“奇数”标签:

create table #t( 
     ItemNo         int             not null
    ,Item           varchar(8000)       null
    ,StartLocation  int             not null
    ,EndLocation    int             not null

    ,constraint PK unique clustered (ItemNo,StartLocation desc)
);

with data(i,s) as ( select i,s from (values
        (1,'Questions about **general computing hardware and software** are off-topic **for Stack Overflow.')
       ,(2,'Questions **about **general computing hardware and software** are off-topic **for Stack Overflow.')
          --....,....1....,....2....,....3....,....4....,....5....,....6....,....7....,....8....,....9....,....0
    )data(i,s)
),
tags as (
    select 
         ItemNo         = data.i
        ,Item           = data.s
        ,Location       = f.Location
        ,IsOpen         = cast((TagNumber % 2) as bit)
        ,Occurrence     = TagNumber
    from data
    cross apply dbo.HtmlTagSPotter(data.s,'**') f
)
insert #t(ItemNo,Item,StartLocation,EndLocation)
select 
     data.ItemNo
    ,data.Item
    ,prev.Location
    ,data.Location
from tags data
join tags prev
   on prev.ItemNo       = data.ItemNo
  and prev.Occurrence = data.Occurrence - 1
  and prev.IsOpen     = 1

union all

select 
    i,s,8001,8002
from data
;

declare @ItemNo     int
       ,@ThisStting varchar(8000);

declare @s varchar(8000);
update this
    set @s = this.Item = case when this.StartLocation > 8000
                              then this.Item
                              else stuff(stuff(@s,this.EndLocation,  2,'</b>')
                                                 ,this.StartLocation,2,'<b>')
                         end
from #t this with (tablockx)
option (maxdop 1);

select
    Item
from (
    select 
         Item
        ,ROW_NUMBER() over (partition by ItemNo order by StartLocation) as rn
    from #t
) t
where rn = 1
go

屈服:

Item
------------------------------------------------------------------------------------------------------------
Questions about <b>general computing hardware and software</b> are off-topic **for Stack Overflow.
Questions <b>about </b>general computing hardware and software<b> are off-topic </b>for Stack Overflow.

【讨论】:

@user2284570:对不起?我不明白。 看看我的问题。这很简单,我问过如何通过创建函数来做到这一点,但我不能创建函数……只允许临时过程。那么你可以更新你的答案吗? @user2284570 你传递给过程的参数是什么?只是要转换的字符串?您想要输出字符串中的结果还是结果集?也许你想传入一个表名和列名?请更具体。

以上是关于如何替换字符串中的奇数模式?的主要内容,如果未能解决你的问题,请参考以下文章

使用拆分字符串自定义共享按钮中的意图或如何拆分字符串并替换奇数逗号

如何使用与号 (&) 替换匹配模式中的字符

根据匹配用替换字符串替换字符串中的正则表达式模式

替换字符串中第一次出现的模式[重复]

替换字符串中的多个模式(Markdown 文本)

替换 PHP 中的模式 [关闭]