正则表达式不匹配部分序列,但匹配完整序列



>我有一些像这样的转义 HTML:

<img border='0' />

我正在尝试匹配和替换完整的转义序列,例如'而不是部分转义序列,例如39,因为39实际上并不在未转义字符串中。实质上,每个转义序列都应被视为单个令牌。

这是一个JS正则表达式。有没有办法排除&;之间的匹配项,同时仍然接受包含这两个字符的序列?

期望的结果:

  • <img border='0' />搜索lt:无匹配项。
  • <img border='0' />搜索39:无匹配项。
  • <img border='0' />搜索':匹配。
  • <img border='0' />搜索border=':匹配。

当前代码:

> var str = '<img border='0' />'
> str.replace(/(border)/gi, '|$1|')
'<img |border|='0' />'  // ok
> str.replace(/(39)/gi, '|$1|')
'<img border=&#0|39|;0&#0|39|; />'  // not ok

注意:我无法取消逃脱,然后重新逃脱以匹配。它必须被逃脱。

OP 希望 JavaScript 正则表达式在处理转义序列(例如<')作为单个字符,并且在替换过程中不会取消转义HTML字符串。

这意味着更换

  1. "< lt"中带有"[lt]""lt"将导致"< [lt]"(避免在实体内匹配)
  2. "<""< lt"中带有"[<]"将导致"[<] lt"(匹配实体)
  3. "&l""[&l]""< &lt"将导致"< [&l]t"(不匹配部分实体)
  4. "< lt;""[t;]""t;"将导致"< l[t;]"(不匹配部分实体)
  5. "< l""[< l]""< lt"将导致"[< l]t"(匹配包括实体)
  6. "lt; &l""[lt; &l]""< &lt"将导致"< &lt"(不匹配部分实体)
  7. "lt; <""[t; <]""t; <"将导致"l[t; <]"(匹配包括实体)
  8. "lt; <""[t; &lt]""t; &lt"将导致"lt; <"(不匹配部分实体)

使用以下正则表达式捕获转义序列(例如<')、

/&[a-z]+;|&#x[a-fd]+;|&#d+;/gi

我们可以使用以下函数作为起点来处理上述大多数情况(#1、#2、#4、#5 和 #7):

function searchAndReplace(searchFor, replacement, str) {
return str.replace(
new RegExp(
prepare(searchFor) + 
"|(&[a-z]+;|&#x[a-f\d]+;|&#\d+;)", // consume entities
"gi"
),
function(m, entity) {
return entity || replacement;
}
);
}
function prepare(str) {
return str.replace(/[^ws]/g, "\$&"); //escape regex metachars [1]
}
// [1] from http://eloquentjavascript.net/09_regexp.html#h_Rhu25fogrG

其余情况(#3、#6、#8)涉及搜索字符串末尾的潜在部分转义序列。

对此的解决方案是在末尾检查searchFor字符串中是否存在潜在的部分转义序列,并附加相应的否定前瞻(?!),以防止匹配有效的转义序列。完整的解决方案(通过一组大约 40 个测试用例)如下所示,应该比.exec()方法更快、更简单:

function searchAndReplace(searchFor, replacement, str) {
return str.replace(
new RegExp(
prepare(searchFor) + 
"|(&[a-z]+;|&#x[a-f0-9]+;|&#\d+;)", 
"gi"
),
function(m, entity) {
return entity || replacement;
}
);
}
function prepare(str) {
var add = "";
if (/&$/.test(str)) {
add = "(?!#x[a-z\d]+;|#\d+;|[a-z]+;)";
} else if (/&[a-z]+$/i.test(str)) {
add = "(?![a-z]*;)";
} else if (/&#$/.test(str)) {
add = "(?!x[a-f\d]+;|\d+;)";
} else if (/&#x$/.test(str)) {
add = "(?![a-f\d]+;)";
} else if (/&#x[a-fd]+$/i.test(str)) {
add = "(?![a-f\d]*;)";
}
return str.replace(/[^ws]/g, "\$&") + add;
}
// test function
function test(searchFor, replacement, str, expected) {
var result = searchAndReplace(searchFor, replacement, str);
console.log(
searchFor +
": " +
(result === expected ? "Passed" : "Failed: " + [expected, result])
);
}
// test cases
test("lt", "[lt]", "<img border='0' />", "<img border='0' />");
test("39", "[39]", "<img border='0' />", "<img border='0' />");
test("'", "[']", "<img border='0' />", "<img border=[']0['] />");
test("border='", "[border=']", "<img border='0' />", "<img [border=']0' />");
test("39&", "[39&]", "39<img border=39'&gt&gt&&#039 t; 0'&39; />", "39<img border=39'&gt&gt&&#039 t; 0'&39; />")
test("0&#", "[0&#]", "39<img border=39'&gt&gt&&#039 t; 0'&39; />", "39<img border=39'&gt&gt&&#039 t; 0'&39; />")
test("lt", "[]", "&lt<t;t&l", "&[]<t;t&l");
test("<", "[]", "&lt<t;t&l", "&lt[]t;t&l");
test("&l", "[]", "&lt<t;t&l", "[]t<t;t[]");
test("t;", "[]", "&lt<t;t&l", "&lt<[]t&l");
test("t&", "[]", "&lt<t;t&l", "&lt<t;[]l");
test("<t", "[]", "&lt<t;t&l", "&lt[];t&l");
test("t<", "[]", "&lt<t;t&l", "&l[]t;t&l");
test("t;t", "[]", "&lt<t;t&l", "&lt<[]&l");
test("t&l", "[]", "&lt<t;t&l", "&lt<t;[]");
test("39", "[]", "&#039'9;9&#", "&#0[]'9;9&#");
test("'", "[]", "&#039'9;9&#", "&#039[]9;9&#");
test("&", "[]", "&#039'9;9&#", "[]#039'9;9[]#");
test("&#", "[]", "&#039'9;9&#", "[]039'9;9[]");
test("9;", "[]", "&#039'9;9&#", "&#039'[]9&#");
test("9&", "[]", "&#039'9;9&#", "&#039'9;[]#");
test("'9", "[]", "&#039'9;9&#", "&#039[];9&#");
test("9'", "[]", "&#039'9;9&#", "&#03[]9;9&#");
test("9;9", "[]", "&#039'9;9&#", "&#039'[]&#");
test("9&#", "[]", "&#039'9;9&#", "&#039'9;[]");
test("x7", "[]", "߿f&#x", "&#[]ff;f&#x");
test("", "[]", "߿f&#x", "&#x7f[]f;f&#x");
test("&", "[]", "߿f&#x", "[]#x7ff;f[]#x");
test("&#", "[]", "߿f&#x", "[]x7ff;f[]x");
test("&#x", "[]", "߿f&#x", "[]7ff;f[]");
test("&#x7", "[]", "߿f&#x", "[]ff;f&#x");
test("f;", "[]", "߿f&#x", "&#x7f[]f&#x");
test("f&", "[]", "߿f&#x", "߿[]#x");
test("f", "[]", "߿f&#x", "&#x7f[];f&#x");
test("f", "[]", "߿f&#x", "&#x7[]f;f&#x");
test("f;f", "[]", "߿f&#x", "&#x7f[]&#x");
test("f&#", "[]", "߿f&#x", "߿[]x");
test("f&#x", "[]", "߿f&#x", "߿[]");
test("t; < lt &l", "[]", "< < lt <lt; < lt &lt", "< < lt <l[]t");

这里的一个选项是在执行实际替换之前,将正在搜索的字符串临时替换为"虚拟"字符串,无论它出现在转义字符序列中的何处。"虚拟"字符串需要是不太可能出现在 HTML 中任何地方的东西。执行实际替换后,可以执行进一步的替换,将"虚拟"字符串更改回要搜索的字符串。

下面是此方法的实际演示,它产生了请求的结果。当需要不使用正则表达式的全局替换时,它使用这种有用的技术,并且这种有用的技术用于将任何字符串转换为字符串文本以在正则表达式中使用(适当转义任何特殊字符)。

var html = "<img border='0' />"
replaceInHtml(html, 'lt', 'replacement');
replaceInHtml(html, '39', 'replacement');
replaceInHtml(html, ''', 'replacement');
replaceInHtml(html, 'border='', 'replacement');
function replaceInHtml(html, str, replacement) {
// A unique string that is unlikely to appear in the HTML
var dummyStr = '!*&$^£"^';
var strInRegex = escapeRegExp(str);
var dummyRegex = new RegExp('(&[#a-zA-Z0-9]*)'
+ strInRegex + '([#a-zA-Z0-9]*;)', 'g');
var replaced = html.replace(dummyRegex, '$1' + dummyStr + '$2');
replaced = replaced.split(str).join(replacement);
replaced = replaced.split(dummyStr).join(str);
console.log('Source:  ' + html
+ 'nReplace: ' + str
+ 'nWith:    ' + replacement
+ 'nGives:   ' + replaced);
}
function escapeRegExp(str) {
return str.replace(/[-[]/{}()*+?.\^$|]/g, "\$&");
}

应该使用正则表达式来检查这一点,但它不能涵盖所有可能的实体,也不是这项工作的最佳工具。虽然下面的方法适用于所有 HTML 实体。

我正在尝试匹配和替换完整的转义序列,例如',但不是部分转义序列,例如39,因为39实际上并不在未转义的字符串中。

基本上,您希望将HTML实体替换为其未显示的形式。这就是下面的函数正在这样做的,你不需要正则表达式。

我将使用这个答案中的unescapeHTML函数Web_Designer

var escape = document.createElement('textarea');
function unescapeHTML(html) {
escape.innerHTML = html;
return escape.textContent;
}

这首先创建一个新的<textarea>元素。在函数内部,作为参数传递的字符串随后被赋值为该文本区域的 innerHTML,然后返回它的 textContent。这是用于取消转义 HTML 实体的技巧。

我们可以重用它来确定字符串是否是有效的 HTML 实体。如果函数能够取消转义它,那么它是有效的 HTML 实体,否则它不是。这是您要确定的。

var escape = document.createElement('textarea');
function unescapeHTML(html) {
escape.innerHTML = html;
return escape.textContent;
}
var str = '&lt;img border=&#039;0&#039; /&gt;';
console.log(unescapeHTML('lt') !== 'lt');
console.log(unescapeHTML('39') !== '39');
console.log(unescapeHTML('&#039;') !== '&#039;');
console.log(unescapeHTML('border=&#039;') !== 'border=&#039;');

有没有办法排除&;之间的匹配,同时仍然接受包含这两个字符的序列?实质上,每个转义序列都应被视为单个令牌。

为了将实体视为单独的令牌,我们可以构建一个正则表达式,在任何目标子字符串之前捕获实体,然后使用回调函数将捕获的实体未修改返回字符串。

例如,当"39"不在实体内时替换它:

str.replace(
/(&[a-z]+;|&#[0-9a-f]+;)|39/gi,
function(m, entity){
return entity || replacement;
}
);

我正在尝试匹配和替换完整的转义序列,例如&#039;,但不是部分转义序列,例如39

替换实体(如&#039;)时,需要不同的方法。以下工作演示处理此问题,并从提供的搜索字符串动态构建正则表达式,处理所有 OP 测试用例:

function searchAndReplace(str, searchFor, replacement){
return /^&([a-z]+|#[da-f]+);/i.test(searchFor) ?
// if searchFor equals or starts with an entity
str.split(searchFor).join(replacement) :
// else
str.replace(
new RegExp(
'(&[a-z]+;|&#[0-9a-f]+;)|' + 
searchFor.replace(/[^ws]/g, "\$&"), //escape metachars
'gi'
),
function(m, entity){
return entity || replacement;
}
);   
}
// test cases
console.log('Search for "border": n' + searchAndReplace(
'&lt;img border=&#039;0&#039; /&gt;', 
'border', '{{border}}'
) + 'nmatch'); //matches
console.log('Search for "0": n' + searchAndReplace(
'&lt;img border=&#039;0&#039; /&gt;', 
'0', '{{0}}'
) + 'nmatch'); //matches outside entities
console.log('Search for "&#039;": n' + searchAndReplace(
'&lt;img border=&#039;0&#039; /&gt;', 
'&#039;', '{{&#039;}}'
) + 'nmatch'); //matches
console.log('Search for "39": n' + searchAndReplace(
'&lt;img border=&#039;0&#039; /&gt;', 
'39', '{{39}}'
) + 'nno match'); //does not match
console.log('Search for "lt": n' + searchAndReplace(
'&lt;img border=&#039;0&#039; /&gt;', 
'lt', '{{lt}}'
) + 'nno match'); //does not match
console.log('Search for "&lt;": n' + searchAndReplace(
'&lt;img border=&#039;0&#039; /&gt;', 
'&lt;', '{{&lt;}}'
) + 'nmatch'); //matches
console.log('Search for "border=&#039;": n' + searchAndReplace(
'&lt;img border=&#039;0&#039; /&gt;', 
'border=&#039;', '{{border=&#039;}}'
) + 'nmatch'); //matches
console.log('Search for "&lt;img": n' + searchAndReplace(
'&lt;img border=&#039;0&#039; /&gt;', 
'&lt;img', '{{&lt;img}}'
) + 'nmatch'); //matches

我首先匹配了&;之间的所有内容:

let str = "39&lt;img border=39&#039;0&#039;&39; /&gt;39";
let search = '39';
let regexp = new RegExp('&[^&;]*?(' + search + ')[^&;]*?;', 'g'); // /&[^&;]*?(SEARCH)[^&;]*?;/g
let match = str.match(regexp);
console.log(match);

然后通过暗示,我想匹配这两个角色之间没有的所有内容:

const prepareRegexp = searchStr => new RegExp('(?:^&[^&;]*?)?('+searchStr+')(?!(?:[^&;]*?;))|(?:(?:^|;)(?:[^&;]*?)('+searchStr+'))', 'gm'); ///(?:^&[^&;]*?)?(SEARCH)(?!(?:[^&;]*?;))|(?:(?:^|;)(?:[^&;]*?)(SEARCH))/g
let find = (str, searchStr) => {
let regexp = prepareRegexp(searchStr);
let foundItemsArray;
let allFoundItems = [];
while ((foundItemsArray = regexp.exec(str)) !== null) {
//foundItemsArray returns as follows:
// [0] - full match
// [1] - first capturing group
// [2] - second capturing group
// To get indexes of found strings you have to use: regexp.lastIndex
// and take into account that second case, matches everything between the last ; or start of a line
// and the searched string
if (foundItemsArray[0] === searchStr) { //case for the first capturing group
allFoundItems.push(foundItemsArray[0]); //0 or 1 it doesn't matter here as the matching group is the same as the capturing group
} else { //case for the second capturing group
allFoundItems.push(foundItemsArray[2]);
}
}

return allFoundItems.length ? allFoundItems : null;
}
//Function 'find' refactored to avoid loop:
find = (str, searchStr) => {
let regexp = prepareRegexp(searchStr);
let allFoundItems = [];

str.replace(prepareRegexp(searchStr), (match, p1, p2) => {
if (p1) {
allFoundItems.push(p1);
} else {
allFoundItems.push(p2);
}
});

return allFoundItems;
}
//And function to replace the searched string:
const replace = (str, searchStr, replaceWith) =>
str.replace(prepareRegexp(searchStr), (match, p1, p2) => {
if (p1) {
return replaceWith;
} else {
return match.replace(searchStr, replaceWith);
}
});
let str = "39&lt;img border=39&#039;0&#039;&39; width: 50%; /&gt;39";
//console.log('Searching "39":', find(str, '39'));
console.log('Searching "&#039;":', find(str, '&#039;'));
//Search &lt;img border=&#039;0&#039; width: 50%; /&gt; for 50:
console.log('Searching "50":', find(str, '50'));
console.log('Replacing "39" with "|39|":', replace(str, '39', '|39|'));
console.log('Replacing "50" with "|50|":', replace(str, '50', '|50|'));
//Now test the string against given examples:
str = '&lt;img border=&#039;0&#039;';
//Search &lt;img border=&#039;0&#039; /&gt; for lt: No match.
console.log('Searching "lt":', find(str, 'lt'));
//Search &lt;img border=&#039;0&#039; /&gt; for 39: No match.
console.log('Searching "39":', find(str, '39'));
//Search &lt;img border=&#039;0&#039; /&gt; for &#039;: Match.
console.log('Searching "&#039;":', find(str, '&#039;'));
console.log('Replacing "&#039;" with "|&#039;|":', replace(str, '&#039;', '|&#039;|'));
//Search &lt;img border=&#039;0&#039; /&gt; for border=&#039;: Match.
console.log('Searching "border=&#039;":', find(str, 'border=&#039;'));
console.log('Replacing "border=&#039;" with "|border=&#039;|":', replace(str, 'border=&#039;', '|border=&#039;|'));
.as-console-wrapper {
max-height: 100% !important;
}

以及正则表达式的细分: https://regex101.com/r/UCNnu1/2

//编辑:

但是,如果搜索字符串后跟;,则与搜索字符串不匹配,因此为了捕获此类字符串,我们需要扩展正则表达式以匹配另一组字符并使用regexp.exec仅捕获有趣的位。 扩展的正则表达式是:

https://regex101.com/r/UCNnu1/3

我更新了代码以使用正则表达式进行替换。

我认为你指的是非捕获组:http://www.regular-expressions.info/brackets.html,这在一些堆栈溢出帖子中得到了轻微的解决(为什么正则表达式的"非捕获"组不起作用,正则表达式,"只是组,不捕获",似乎不起作用)。

也就是说,未捕获的组不会获得自己的组选择器(例如/a(?:[X-Z])([a-c])/g 将匹配 "aZb",但 \1 将等于 "b",而不是 "Z"。

这是你想做的吗?

var str = "&lt;img border-color=&#039;0&#039;"
console.log(str)
console.log(str.match(/((?:[a-z-]+=)?&#.+?;)/gi))
console.log(str.replace(/((?:[a-z-]+=)?&#.+?;)/gi, "|$1|"))

我认为如果我们能够使用回溯是可能的。鉴于正则表达式的味道是JavaScript,在这里,我认为我们不能。这非常接近:[^&;]*(string)[^&;]*(?!9;|t;|;)

更新 2017-04-28
添加39&0&#测试用例 - 无需更改代码 - 哇,这很幸运:)

需要注意的是,Im故意不允许在要搜索的文本中存在与号,除非作为转义序列的开头,无论它是否是有效的转义序列,即即使技术上无效,我也允许&39;是转义序列。这使得很容易说,如果我们被要求找到一个带有 & 符号的字符串,它不是完整转义序列的一部分(0&#') then this should not be matched, and is an invalid search string. The use of即下面的 AmpExcape' 来执行此操作,而不是终止并返回未更改的字符串,这是一种方便,因为此正则表达式的其他用例(即在 JavaScript 之外)不允许我条件分支语句(或匹配的函数回调)。就我的目的而言,这最接近我反对转义 HTML 的定义。

更新 2017-04-26
添加&g测试用例并通过转义查找字符串编辑该案例的答案/代码

更新 2017-04-25

两遍正则表达式解决方案:

function do_replace(test_str, find_str, replace_str) {
let escaped_find_str = AmpExcape(find_str);
escaped_find_str = RegExcape(escaped_find_str);
let escaped_replace_str = RegExcape(replace_str);
let first_regex = new RegExp('(' + escaped_find_str + ')|(&\w*;|&#[0-9a-fA-F]*;)','gi');
let first_pass = test_str.replace(first_regex,'&&$1'+replace_str+'&&$2');
let second_regex = new RegExp('&&(?:'+escaped_replace_str+'&&|(' + escaped_find_str + ')?('+escaped_replace_str + ')?&&)','gi');
let second_pass = first_pass.replace(second_regex,'$2');
return second_pass;
}

我一直在寻找一种仅使用正则表达式的方法,虽然 @sln 和 @tomas-langkaas 的解决方案很有用(嗯,令人兴奋),但我仍然想要一种可以在 JS 及更高版本中使用的方法。

我有一些东西适用于我的测试用例,但它被迫对文本进行多次传递。最初我有三遍,但我想我让它在 2 次通过时工作。显然不如其他答案有效。

我试图匹配:<target string> | <entity>就像那些答案一样,并像@sln在最新更新中所做的那样将顺序翻转为首先成为实体。但我使用&&作为"新"转义序列。策略是:

  • 最初,我们转义查找和替换字符串以在正则表达式中使用它们。 对于<target string>,如果有一个不是 complte 实体一部分的&,那么我们用双&&替换它,目的是防止它被匹配,即我考虑在转义的 HTML 中永远不可能搜索部分实体

  • 第一次传递将任何匹配项(实体或目标字符串)替换为

    &&<target group value><replacement string>&&<entity group value>
    
  • 在目标字符串的匹配项上,<entity group value>将为空,我们将返回&&转义、目标字符串、替换字符串和最终&&转义

  • 在实体的匹配项中,<target group value>现在将为空,因此我们最终返回&&<replacement str>&&后跟实体值。
  • 第二遍可以查找所有出现的&&<target string><replacement string>&&并替换为<replacement string>
  • 同样在第二遍中,我们可以查找&&<replacement string>&&并知道它应该替换为空白。
  • 这次我们不必对实体匹配执行任何操作,因为我们在传递 1 中没有触及它们

以下是带有测试用例的完整代码(从@tomas-langkaas维护的RexExcape的归属):

// helper for building regexes from strings
// http://stackoverflow.com/a/3561711/6738706
function AmpExcape(str) {
return str.replace(/&(w*;|#[0-9a-fA-F]*;)|(&)/g, '&$2$1');
}
function RegExcape(str) {
return str.replace(/[-/\^$*+?.()|[]{}]/g, '\$&');
};
function do_replace(test_str, find_str, replace_str) {
let escaped_find_str = AmpExcape(find_str);
escaped_find_str = RegExcape(escaped_find_str);
let escaped_replace_str = RegExcape(replace_str);
let first_regex = new RegExp('(' + escaped_find_str + ')|(&\w*;|&#[0-9a-fA-F]*;)','gi');
let first_pass = test_str.replace(first_regex,'&&$1'+replace_str+'&&$2');
let second_regex = new RegExp('&&(?:'+escaped_replace_str+'&&|(' + escaped_find_str + ')?('+escaped_replace_str + ')?&&)','gi');
let second_pass = first_pass.replace(second_regex,'$2');
return second_pass;
}
let str = '39&lt;img style=&#039;width: 39;&#039; bor9;wder=39&#039;0&#039;&39; /&gt;39;';
let test_list = ['39','39;','9;','9;w','39&','0&#'];
run_test(str,test_list);
str = '&lt;img border=&#039;0&#039; /$gt;';
test_list = ['lt','39','&#039;','border=&#039;'];
run_test(str,test_list);
str = 'test string ring ring';
test_list = ['ring'];
run_test(str,test_list);
str = '39&lt;img style=&#039;width: 39;&#039; border=&#039;0&#039;&39; /&gt;39;';
test_list = ['border','0','&#039;','39','lt','&lt;','border=&#039;','&lt;img','&g','t;'];
run_test(str,test_list);
function run_test(base_str, find_list) {
let orig_str = 'original';
let max_len = find_list.concat(orig_str).reduce(function(a,b) {
return a > b.length ? a : b.length;
},0);
console.log();
console.log(pad(orig_str,max_len) + ': ' + str);
find_list.map(function(gstr) {
console.log( pad( gstr, max_len) + ': ' + do_replace(str, gstr, '|' + gstr + '|'));
});
}
function pad(str,len) {
while ( str.length < len) { str = str + ' ' };
return str;
}

和输出

original: 39&lt;img style=&#039;width: 39;&#039; bor9;wder=39&#039;0&#039;&39; /&gt;39;
39      : |39|&lt;img style=&#039;width: |39|;&#039; bor9;wder=|39|&#039;0&#039;&39; /&gt;|39|;
39;     : 39&lt;img style=&#039;width: |39;|&#039; bor9;wder=39&#039;0&#039;&39; /&gt;|39;|
9;      : 39&lt;img style=&#039;width: 3|9;|&#039; bor|9;|wder=39&#039;0&#039;&39; /&gt;3|9;|
9;w     : 39&lt;img style=&#039;width: 39;&#039; bor|9;w|der=39&#039;0&#039;&39; /&gt;39;
39&     : 39&lt;img style=&#039;width: 39;&#039; bor9;wder=39&#039;0&#039;&39; /&gt;39;
0&#     : 39&lt;img style=&#039;width: 39;&#039; bor9;wder=39&#039;0&#039;&39; /&gt;39;
original     : &lt;img border=&#039;0&#039; /$gt;
lt           : &lt;img border=&#039;0&#039; /$gt;
39           : &lt;img border=&#039;0&#039; /$gt;
&#039;       : &lt;img border=|&#039;|0|&#039;| /$gt;
border=&#039;: &lt;img |border=&#039;|0&#039; /$gt;
original: test string ring ring
ring    : test st|ring| |ring| |ring|
original     : 39&lt;img style=&#039;width: 39;&#039; border=&#039;0&#039;&39; /&gt;39;
border       : 39&lt;img style=&#039;width: 39;&#039; |border|=&#039;0&#039;&39; /&gt;39;
0            : 39&lt;img style=&#039;width: 39;&#039; border=&#039;|0|&#039;&39; /&gt;39;
&#039;       : 39&lt;img style=|&#039;|width: 39;|&#039;| border=|&#039;|0|&#039;|&39; /&gt;39;
39           : |39|&lt;img style=&#039;width: |39|;&#039; border=&#039;0&#039;&39; /&gt;|39|;
lt           : 39&lt;img style=&#039;width: 39;&#039; border=&#039;0&#039;&39; /&gt;39;
&lt;         : 39|&lt;|img style=&#039;width: 39;&#039; border=&#039;0&#039;&39; /&gt;39;
border=&#039;: 39&lt;img style=&#039;width: 39;&#039; |border=&#039;|0&#039;&39; /&gt;39;
&lt;img      : 39|&lt;img| style=&#039;width: 39;&#039; border=&#039;0&#039;&39; /&gt;39;
&g           : 39&lt;img style=&#039;width: 39;&#039; border=&#039;0&#039;&39; /&gt;39;
t;           : 39&lt;img style=&#039;width: 39;&#039; border=&#039;0&#039;&39; /&gt;39;

更新 2017-04-24

出于几个原因,我放弃了下面的方法,但最重要的是,它只会在没有与号或分号的文本中找到查找字符串的第一次出现。例如,如果匹配是针对ring的,则字符串test string ring ring只会test st|ring| ring ring匹配 - 这对于查找和替换似乎毫无用处 - 我更新答案,以便它至少第一次用于匹配ring因为我之前错过了行首作为允许的终端字符,但我不认为这是所有可能文本的有效解决方案。

原始答案(有修改)

如果您关心分号出现在文本中,而不是作为相应&的终端字符,例如对于内联样式,它可能说style="width: 39;",那么您需要一些复杂的东西:

'((?:^|(?!(?:[^&;]+' + str + ')))(?=(?:(?:&|;|^)[^&;]*))(?:(?!(?:&))(?:(?:^|[;]?)[^&;]*?))?)(' + str + ')'

下面显示的代码将输出原始测试用例:

"original":      &lt;img border=&#039;0&#039; /$gt;
"lt":            &lt;img border=&#039;0&#039; /$gt;
"39":            &lt;img border=&#039;0&#039; /$gt;
"&#039;":        &lt;img border=|&#039;|0|&#039;| /$gt;
"border=&#039;": &lt;img |border=&#039;|0&#039; /$gt;

它还针对文本和搜索词中出现的分号显示特定输出。

"original":      39&lt;img style=&#039;width: 39;&#039; bor;der=39&#039;0&#039;&39; /&gt;39;
test string that may be followed by semi-colon :
|39|&lt;img style=&#039;width: |39|;&#039; bor;der=|39|&#039;0&#039;&39; /&gt;|39|;
test match with semi-colon:
39&lt;img style=&#039;width: |39;|&#039; bor;der=39&#039;0&#039;&39; /&gt;|39;|
test match with semi-colon mid string
39&lt;img style=&#039;width: 39;&#039; |bor;der|=39&#039;0&#039;&39; /&gt;39;

注意:这是该方法分崩离析的示例:

"original":      test string ring ring
test st|ring| ring ring

没有回溯允许"记忆"效果区分上一次匹配后的零字符和零字符,但在转义序列中。因此,如果不匹配字符串中的39,就不可能匹配第二次出现的ring&#039;

下面是示例代码:

function make_regex(str) {
let regexp = new RegExp('((?:^|(?!(?:[^&;]+' + str + ')))(?=(?:(?:&|;|^)[^&;]*))(?:(?!(?:&))(?:(?:^|[;]?)[^&;]*?))?)(' + str + ')','gi');
return regexp;
}
function do_replace(test_str, find_str, replace_str) {
let new_reg = make_regex(find_str);
return  test_str.replace(new_reg,'$1' + replace_str);
}
let str = '39&lt;img style=&#039;width: 39;&#039; bor;der=39&#039;0&#039;&39; /&gt;39;';
console.log();
console.log('"original":     ', str);
console.log('test string that may be followed by semi-colon :');
console.log(do_replace(str, '39', '|$2|' ));
console.log('test match with semi-colon:');
console.log(do_replace(str, '39;', '|39;|' ));
console.log('test match with semi-colon mid string');
console.log(do_replace(str, 'bor;der', '|bor;der|' ))
str = '&lt;img border=&#039;0&#039; /$gt;';
console.log();
console.log('"original":     ', str);
console.log('"lt":           ', do_replace(str, 'lt', '|lt|' ));
console.log('"39":           ', do_replace(str, '39', '|39|' ));
console.log('"&#039;":       ', do_replace(str, '&#039;', '|&#039;|' ));
console.log('"border=&#039;":', do_replace(str, 'border=&#039;', '|border=&#039;|' ));
str = 'test string ring ring';
console.log();
console.log('"original":     ', str);
console.log(do_replace(str, 'ring', '|$2|')); 

重要的是要注意,正则表达式不仅捕获您想要的文本,还捕获在此之前的文本块。这会影响您将$1用作替换值,因为您所需的文本现在已$2

术语的解释并不简单,但可以分解为:

一个负面的前瞻,包括要查找的字符串,以防止开始对非终端字符进行匹配

(?:^|(?!(?:[^&;]+' + str + ')))

积极的前瞻,强制匹配从终端字符或行的开头开始

(?=(?:(?:&|;|^)[^&;]*))

负前瞻,阻止匹配从&开始,但允许行开始或以前的分号

(?:(?!(?:&))(?:(?:^|[;]?)[^&;]*?))?

最后是匹配的字符串

效果是我们匹配包含所需文本的整个文本部分,从前面的终端值到要匹配的字符串的末尾。正因为如此,javascript 最终会得到匹配的完整块的匹配项。例如,当我们要求border=&#039;时,我们最终会得到 块;img border=&#039;.因此,我们定义了两个捕获组,一个用于我们不感兴趣的块部分,另一个用于匹配。这允许我们使用$1$2来重新创建字符串或$1whatever以仅替换我们想要的部分。然后,我们可以在此策略中使用str.replace()

最终版本候选

4/29

此版本应在搜索字符串
末尾处理部分实体,其中部分具有前实体字符,如xxx&yyya&#00等。

这是@TomasLangkaas发现的最后一例病例。
鉴于涵盖了所有其他情况,这是@athancahill或其他任何感兴趣的人的最终候选版本

(请参阅评论和以前的版本)

模型从String.Replace() 更改为while( match = Rx.exec() )

此处进行了说明,但请参阅JS代码以实现。
它仍然使用搜索字符串作为第一个交替,实体作为第二个交替

(?=
# This is the optional entity captured at
# the same position where the search string starts.
# If this entity matches, it means the search string
# matches. Either one may be a partial of the other.
# (1) The container for pre-entity / entity
(                             
# (2) Pre-entity characters 
( sLongest )                
# Entity   
(?:&(?:[a-z_:][a-zd_:.-]*|(?:#(?:[0-9]+|x[0-9a-f]+)))|%[a-z_:][a-zd_:.-]*);
)?                            
)
# (3) The search string ( consumes )
( sToFind )                        
| 
# (4) Or, the entity last  ( consumes ) 
( (?:&(?:[a-z_:][a-zd_:.-]*|(?:#(?:[0-9]+|x[0-9a-f]+)))|%[a-z_:][a-zd_:.-]*); )

请注意,您不能只是将实体语法分解为正则表达式的一部分。
它必须完全匹配,作为一个独特的项目
(在这条路上走一百次,它做不到..)。

请注意,这是一个单次传递,纯正则表达式解决方案,并且非常快。
如果你去掉所有的注释,它实际上只是几行代码。
您可以修改实体子表达式并使用所需的任何内容。
代码结构不需要更改。

//=========================================================
// http://jsfiddle.net/b4b28a38/95/
//=========================================================
// ------------------------
// These are only used when pre-entity partials are detected
var RxEntPartial = new RegExp( '(?:&(?:[a-z_:][a-zd_:.-]*|(?:#(?:[0-9]+|x[0-9a-f]*)?))?|%(?:[a-z_:][a-zd_:.-]*)?)$', 'ig' );
var RxEntFull = new RegExp( '(?:&(?:[a-z_:][a-zd_:.-]*|(?:#(?:[0-9]+|x[0-9a-f]+)))|%[a-z_:][a-zd_:.-]*);', 'ig' );
// ------------------------
function MakeRegex( FindAry ) {
// Escape metachars
var longest = 0;
for (var i = 0; i < FindAry.length; i++ )
{
if ( FindAry[i].length > longest )
longest = FindAry[i].length;
FindAry[i] = FindAry[i].replace(/(?!s)W/g, "\$&"); 
}
// Make 'longest' sub-expression
longest -= 1; 
var sLongest = '';
if ( longest > 0 )
sLongest = '.{0,' + longest.toString() + '}?';
// Join array using alternations
var sToFind = FindAry.join('|');
// Return new regex object
var rx =  new RegExp( '(?=((' + sLongest + ')(?:&(?:[a-z_:][a-zd_:.-]*|(?:#(?:[0-9]+|x[0-9a-f]+)))|%[a-z_:][a-zd_:.-]*);)?)(' + sToFind + ')|((?:&(?:[a-z_:][a-zd_:.-]*|(?:#(?:[0-9]+|x[0-9a-f]+)))|%[a-z_:][a-zd_:.-]*);)',  
'ig');
//console.log( rx);
return rx;
}  
function GetReplace( str, Rx )
{
var sResult = '';    // New modified string to return
var _M;              // Match object
var ndxLast = 0;     // Previous Rx.lastIndex value when valid match
// ( where next match starts )
Rx.lastIndex = 0;

while ( _M = Rx.exec( str ) )
{
// Alternation 1: (1) = container (optiopnal), p2 = pre-entity, entity, p3 = search string
// Alternation 2: p4 = entity
// Form:      
//     (?=
//          (                    # (1) start container
//            ( pre-entity )            # (2)
//            entity
//          )?                       # (1) end
//     )
//     ( search )                 # (3)
//  |  
//     ( entity )                 # (4)

if ( _M[4] )
{
// Entity, continue unchanged.
sResult += str.substr( ndxLast , _M.index - ndxLast ) + _M[4];
ndxLast = Rx.lastIndex;
continue;
}
// Check if entity container captured inside zero length assertion matched 
if ( _M[1] )
{
// Get some lengths 

var L1 = _M[1].length;
var L2 = _M[2].length;
var L3 = _M[3].length;
if ( L1 == L3 )
{
// Ok - This means it matched a trailing full entity
// Intended, modify the search string
sResult += str.substr( ndxLast , _M.index - ndxLast ) + '[' + _M[3] + ']' ;
ndxLast = Rx.lastIndex;
continue;
}
// Pre entity check  ( pre-entity ) 
if ( L2 > 0 )  
{
// This is a rare case and should not slow anything down.
// End entity condition to check
var sMatched = _M[3];
var mpartial;
RxEntPartial.lastIndex = 0;
// Verify the match had a partial entity at the end
if ( mpartial = RxEntPartial.exec( sMatched ) )
{
// Check partial entity is not at the  beginning                   
if ( mpartial.index > 0 )
{
// Location in target string to check
// for a full entity.
var loc = _M.index + mpartial.index;
// Assure there is no full entity
RxEntFull.lastIndex = loc;
var mfull;
if ( mfull = RxEntFull.exec( str ) )
{
if ( mfull.index == loc )
{
// Not valid, move past it
RxEntFull.lastIndex = 0;
Rx.lastIndex += (L1 - L3);
continue;
}
}
}
}
// Ok - This definitely passes.
// Intended, modify the search string
sResult += str.substr( ndxLast , _M.index - ndxLast ) + '[' + _M[3] + ']' ;
ndxLast = Rx.lastIndex;
continue;
}
// Normal checks
// -------------------------------------------------------
// If the length of the search >= the entity length
// then the search includes an entity at the begining

if ( L3 >= L1 )
{
// Intended, modify the search string
sResult += str.substr( ndxLast , _M.index - ndxLast ) + '[' + _M[3] + ']' ;
ndxLast = Rx.lastIndex;
continue;
}
// Uh oh, the search is a partial entity (from the beginning).
// Since we see that it is part of an entity, we have to go past it.
// The match position reflects the partial entity.
// Adjust (advance) the match position by the difference
// to go past the entity.
Rx.lastIndex += ( L1 - L3 );
continue;
}
// Here, the search string is pure, just modify it
sResult += str.substr( ndxLast , _M.index - ndxLast ) + '[' + _M[3] + ']' ;
ndxLast = Rx.lastIndex;
}
sResult += str.substr( ndxLast , str.length - ndxLast );
return sResult;
}
var TargetStr = "39&lt;img border=39&#039;&gt&gt&&#039 t; xx&gt x&gt; 0&# r&#039;&39; cad&#092; r&#FFd0&#22 /&gtttttt;  39; end";
console.log( 'Target:rn' + TargetStr );
// Always put the longest first of/if alphabetically sorted (ie. aaa|aa|a, etc ..)
var rx = MakeRegex( ['39', '&lt;img', 'cad&#092;', '&gt', 't;', 'x&', '0&#', 'r&#'] );
var NewString = GetReplace( TargetStr, rx );
console.log('Find any of:  39, &lt;img, cad&#092;, &gt, t;, x&, 0&#, r&#' );
console.log( NewString );

输出

Target:
39&lt;img border=39&#039;&gt&gt&&#039 t; xx&gt x&gt; 0&# r&#039;&39; cad&#092; r&#FFd0&#22 /&gtttttt;  39; end
Find any of:  39, &lt;img, cad&#092;, &gt, t;, x&, 0&#, r&#
[39][&lt;img] border=[39]&#039;[&gt][&gt]&&#0[39] [t;] x[x&]gt x&gt; [0&#] r&#039;&[39]; [cad&#092;] [r&#]FFd[0&#]22 /&gtttttt;  [39]; end

最新更新