如何替换字符串中的奇数模式?
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
函数和一个简单的WHILE
loop:
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
);
上面的模式\*\*([^\*]*)\*\*
只是寻找任何被双星号包围的东西。在这种情况下,您无需担心奇数/偶数。这也意味着如果由于某种原因在字符串中有一个额外的**
,您将不会得到一个格式不正确的<b>
-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 你传递给过程的参数是什么?只是要转换的字符串?您想要输出字符串中的结果还是结果集?也许你想传入一个表名和列名?请更具体。以上是关于如何替换字符串中的奇数模式?的主要内容,如果未能解决你的问题,请参考以下文章