Javascript: มองหาเชิงลบเทียบเท่า


142

มีวิธีการที่จะบรรลุเทียบเท่ากับการมองเชิงลบในการแสดงออกปกติจาวาสคริปต์? ฉันต้องการจับคู่สตริงที่ไม่ได้เริ่มต้นด้วยชุดอักขระเฉพาะ

ดูเหมือนว่าฉันไม่สามารถหา regex ที่ทำได้โดยไม่ล้มเหลวหากพบส่วนที่ตรงกันที่จุดเริ่มต้นของสตริง คำค้นหาเชิงลบดูเหมือนจะเป็นคำตอบเดียว แต่จาวาสคริปต์ไม่มี

แก้ไข: นี่คือ regex ที่ฉันต้องการทำงาน แต่ไม่:

(?<!([abcdefg]))m

ดังนั้นมันจะตรงกับ 'm' ใน 'jim' หรือ 'm' แต่ไม่ใช่ 'jam'


พิจารณาโพสต์ regex ตามที่มันจะดูมีลักษณะเชิงลบ; ที่อาจทำให้ตอบสนองได้ง่ายขึ้น
Daniel LeCheminant

1
ผู้ที่ต้องการติดตาม lookbehind และการรับเลี้ยงบุตรบุญธรรมโปรดดูตารางความเข้ากันได้ ECMAScript 2016+
Wiktor Stribiżew

@ WiktorStribiżew: มีการเพิ่มการมองหลังในข้อมูลจำเพาะ 2018 Chrome สนับสนุนพวกเขา แต่Firefox ยังไม่ได้ดำเนินการข้อมูลจำเพาะ
Lonnie Best

สิ่งนี้จำเป็นต้องดูเบื้องหลังหรือไม่? เกี่ยวกับ(?:[^abcdefg]|^)(m)อะไร เช่นเดียวกับใน"mango".match(/(?:[^abcdefg]|^)(m)/)[1]
slebetman

คำตอบ:


58

Lookbehind Assertionsได้รับการยอมรับในข้อกำหนดของ ECMAScriptในปี 2561

การใช้งาน lookbehind เชิงบวก:

console.log(
  "$9.99  €8.47".match(/(?<=\$)\d+(\.\d*)?/) // Matches "9.99"
);

การใช้งานที่มองเชิงลบ:

console.log(
  "$9.99  €8.47".match(/(?<!\$)\d+(?:\.\d*)/) // Matches "8.47"
);

การสนับสนุนแพลตฟอร์ม:


2
มี polyfill ไหม?
Killy

1
@Killy มีไม่เท่าที่ผมรู้และผมสงสัยที่เคยมีจะได้รับการสร้างหนึ่งอาจจะเป็นทำไม่ได้มาก (IE การเขียนการดำเนินงานเต็มรูปแบบใน Regex JS)
Okku

สิ่งที่เกี่ยวกับการใช้ปลั๊กอิน babel เป็นไปได้ที่จะรวบรวมเป็น ES5 หรือสนับสนุน ES6 แล้ว?
สเตฟานเจ

1
@IlpoOksanen ฉันคิดว่าคุณหมายถึงการขยายการใช้งาน RegEx .. ซึ่งเป็นสิ่งที่ polyfills ทำ .... และไม่มีอะไรผิดปกติกับการเขียนตรรกะใน JavaScript
neaumusic

1
คุณกำลังพูดเรื่องอะไร ข้อเสนอเกือบทั้งหมดได้รับแรงบันดาลใจจากภาษาอื่นและพวกเขามักจะชอบจับคู่ไวยากรณ์และความหมายของภาษาอื่น ๆ ที่มันเหมาะสมในบริบทของ JS สำนวนและความเข้ากันได้ย้อนหลัง ฉันคิดว่าฉันค่อนข้างชัดเจนว่าทั้งในแง่บวกและแง่ลบได้รับการยอมรับในสเปค 2561 ในปี 2560 และฉันให้ลิงก์ไปยังแหล่งข้อมูล นอกจากนี้ฉันอธิบายในรายละเอียดว่าแพลตฟอร์มใดที่ใช้ข้อกำหนดดังกล่าวและสถานะของแพลตฟอร์มอื่น ๆ คืออะไร - และยังได้รับการอัปเดตตั้งแต่นั้นมา โดยปกติแล้วนี่ไม่ใช่คุณสมบัติ Regexp ล่าสุดที่เราจะได้เห็น
Okku

83

ตั้งแต่ปี 2018 Lookbehind ยืนยันเป็นส่วนหนึ่งของสเปคภาษา ECMAScript

// positive lookbehind
(?<=...)
// negative lookbehind
(?<!...)

ตอบก่อนปี 2018

เนื่องจาก Javascript รองรับlookahead เชิงลบวิธีหนึ่งที่ทำได้คือ:

  1. ย้อนกลับสตริงการป้อนข้อมูล

  2. จับคู่กับ regex ที่กลับด้าน

  3. ย้อนกลับและจัดรูปแบบการแข่งขัน


const reverse = s => s.split('').reverse().join('');

const test = (stringToTests, reversedRegexp) => stringToTests
  .map(reverse)
  .forEach((s,i) => {
    const match = reversedRegexp.test(s);
    console.log(stringToTests[i], match, 'token:', match ? reverse(reversedRegexp.exec(s)[0]) : 'Ø');
  });

ตัวอย่างที่ 1:

กำลังติดตามคำถามของ @ andrew-ensley:

test(['jim', 'm', 'jam'], /m(?!([abcdefg]))/)

ขาออก:

jim true token: m
m true token: m
jam false token: Ø

ตัวอย่างที่ 2:

กำลังติดตาม @neaumusic ความคิดเห็น ( max-heightไม่ตรงกันแต่line-heightเป็นโทเค็นheight):

test(['max-height', 'line-height'], /thgieh(?!(-enil))/)

ขาออก:

max-height true token: height
line-height false token: Ø

36
ปัญหาของวิธีนี้คือมันใช้ไม่ได้เมื่อคุณมีทั้ง lookahead และ
lookbehind

3
คุณช่วยกรุณาแสดงตัวอย่างการทำงานว่าฉันต้องการจับคู่max-heightแต่ไม่line-heightและฉันต้องการเพียงการแข่งขันจะheight
neaumusic

ไม่ช่วยถ้างานคือการแทนที่สัญลักษณ์ที่เหมือนกันสองลำดับติดต่อกัน (และไม่เกิน 2) ที่ไม่ได้นำหน้าด้วยสัญลักษณ์บางอย่าง ''(?!\()จะเข้ามาแทนที่ใน apostrophes ''(''test'''''''testจากส่วนอื่น ๆ จึงออกมากกว่า(''test'NNNtest (''testNNN'test
Wiktor Stribiżew

61

สมมติว่าคุณต้องการค้นหาทั้งหมดที่intไม่ได้นำหน้าด้วยunsigned:

ด้วยการสนับสนุนการมองเชิงลบ:

(?<!unsigned )int

โดยไม่สนับสนุนการมองที่ไม่ดี:

((?!unsigned ).{9}|^.{0,8})int

แนวคิดพื้นฐานคือการคว้าตัวละครก่อนหน้าและไม่รวมการจับคู่กับการค้นหาเชิงลบ แต่ยังตรงกับกรณีที่ไม่มีตัวละคร n นำหน้า (โดยที่ n คือความยาวของการดูด้านหลัง)

ดังนั้น regex ในคำถาม:

(?<!([abcdefg]))m

จะแปลเป็น:

((?!([abcdefg])).|^)m

คุณอาจต้องเล่นกับกลุ่มจับเพื่อหาจุดที่แน่นอนของสตริงที่คุณสนใจหรือคุณต้องการแทนที่ส่วนที่เฉพาะเจาะจงด้วยอย่างอื่น


2
นี่ควรเป็นคำตอบที่ถูกต้อง ดู: "So it would match the 'm' in 'jim' or 'm', but not 'jam'".replace(/(j(?!([abcdefg])).|^)m/g, "$1[MATCH]") คืน"So it would match the 'm' in 'ji[MATCH]' or 'm', but not 'jam'" มันค่อนข้างง่ายและใช้งานได้!
Asrail

41

กลยุทธ์ของ Mijoja ใช้ได้กับกรณีเฉพาะของคุณ แต่ไม่ใช่โดยทั่วไป:

js>newString = "Fall ball bill balll llama".replace(/(ba)?ll/g,
   function($0,$1){ return $1?$0:"[match]";});
Fa[match] ball bi[match] balll [match]ama

นี่คือตัวอย่างที่มีเป้าหมายเพื่อให้ตรงกับ double-l แต่ไม่ใช่ถ้ามันนำหน้าด้วย "ba" หมายเหตุคำว่า "balll" - ลุคที่แท้จริงควรมีการระงับ 2 l แรก แต่ตรงกับคู่ที่สอง แต่ด้วยการจับคู่ 2 l แรกจากนั้นให้ละเว้นการจับคู่นั้นเป็นบวกปลอมเอ็นจิ้น regexp สร้างรายได้จากจุดสิ้นสุดของการจับคู่นั้นและละเว้นอักขระใด ๆ ภายในบวกเท็จ


5
อ่าคุณถูกต้อง อย่างไรก็ตามนี่ใกล้มากขึ้นกว่าเดิม ฉันสามารถยอมรับสิ่งนี้ได้จนกว่าจะมีบางสิ่งที่ดีขึ้นมาพร้อม (เช่นจาวาสคริปต์ใช้งาน lookbehinds)
Andrew Ensley

33

ใช้

newString = string.replace(/([abcdefg])?m/, function($0,$1){ return $1?$0:'m';});

10
นี้ไม่ได้ทำอะไร: เสมอจะเท่ากับnewString stringทำไม upvotes มากมาย?
MikeM

@MikeM: เพราะประเด็นก็คือเพื่อแสดงให้เห็นถึงเทคนิคการจับคู่
ข้อผิดพลาด

57
@bug การสาธิตที่ไม่ทำอะไรเลยเป็นการสาธิตที่แปลก คำตอบตรงข้ามราวกับว่ามันเป็นเพียงแค่คัดลอกและวางโดยไม่เข้าใจว่ามันทำงานอย่างไร ดังนั้นการขาดคำอธิบายประกอบและความล้มเหลวในการแสดงให้เห็นว่ามีสิ่งใดเข้าคู่กัน
MikeM

2
@MikeM: กฎของ SO คือถ้าตอบคำถามตามที่เขียนไว้มันถูกต้อง OP ไม่ได้ระบุกรณีการใช้งาน
ข้อผิดพลาด

7
แนวคิดนี้ถูกต้อง แต่ใช่ว่าจะสาธิตไม่ดีนัก ลองใช้นี้ในคอนโซล JS "Jim Jam Momm m".replace(/([abcdefg])?m/g, function($0, $1){ return $1 ? $0 : '[match]'; });... Ji[match] Jam Mo[match][match] [match]มันควรจะกลับ แต่โปรดทราบด้วยว่าตามที่ Jason พูดถึงด้านล่างมันอาจล้มเหลวได้ในบางกรณี
Simon East

11

คุณสามารถกำหนดกลุ่มที่ไม่ได้ดักจับได้โดยลบชุดอักขระของคุณ:

(?:[^a-g])m

... ซึ่งจะจับคู่กับจดหมายที่m ไม่ได้นำหน้าทุกตัว


2
ฉันคิดว่าการแข่งขันจะครอบคลุมถึงตัวละครก่อนหน้าด้วย
Sam

4
^ นี่เป็นเรื่องจริง คลาสตัวละครหมายถึง ... ตัวละคร! กลุ่มที่ไม่ได้จับภาพทั้งหมดของคุณกำลังทำอยู่ไม่ได้ทำให้ค่านั้นพร้อมใช้งานในบริบทการแทนที่ การแสดงออกของคุณไม่ได้พูดว่า "ทุก ๆ m ไม่ได้นำหน้าด้วยตัวอักษรเหล่านั้น" มันกำลังพูดว่า "ทุก ๆ m นำหน้าด้วยตัวละครที่ไม่ใช่ตัวอักษรเหล่านั้น"
theflowersoftime

5
สำหรับคำตอบที่ยังแก้ปัญหาเดิม (จุดเริ่มต้นของสตริง) ก็ยังต้องมีตัวเลือกเพื่อให้ regex (?:[^a-g]|^)mจะส่งผลให้ ดูregex101.com/r/jL1iW6/2สำหรับตัวอย่างการรัน
Johny Skovdal

การใช้ช่องว่างแบบลอจิกอาจไม่ได้ผลที่ต้องการเสมอไป
GoldBishop

2

นี่คือวิธีที่ฉันประสบความสำเร็จstr.split(/(?<!^)@/)สำหรับ Node.js 8 (ซึ่งไม่สนับสนุน lookbehind):

str.split('').reverse().join('').split(/@(?!$)/).map(s => s.split('').reverse().join('')).reverse()

กิจ? ใช่ (unicode ยังไม่ได้ทดสอบ) ที่ไม่พึงประสงค์? ใช่.


1

ตามความคิดของ Mijoja และการวาดภาพจากปัญหาที่เปิดเผยโดย JasonS ฉันมีความคิดนี้; ฉันตรวจสอบนิดหน่อย แต่ฉันก็ไม่แน่ใจในตัวเองดังนั้นการยืนยันโดยใครบางคนที่มีความเชี่ยวชาญมากกว่าฉันใน js regex จะดีมาก :)

var re = /(?=(..|^.?)(ll))/g
         // matches empty string position
         // whenever this position is followed by
         // a string of length equal or inferior (in case of "^")
         // to "lookbehind" value
         // + actual value we would want to match

,   str = "Fall ball bill balll llama"

,   str_done = str
,   len_difference = 0
,   doer = function (where_in_str, to_replace)
    {
        str_done = str_done.slice(0, where_in_str + len_difference)
        +   "[match]"
        +   str_done.slice(where_in_str + len_difference + to_replace.length)

        len_difference = str_done.length - str.length
            /*  if str smaller:
                    len_difference will be positive
                else will be negative
            */

    }   /*  the actual function that would do whatever we want to do
            with the matches;
            this above is only an example from Jason's */



        /*  function input of .replace(),
            only there to test the value of $behind
            and if negative, call doer() with interesting parameters */
,   checker = function ($match, $behind, $after, $where, $str)
    {
        if ($behind !== "ba")
            doer
            (
                $where + $behind.length
            ,   $after
                /*  one will choose the interesting arguments
                    to give to the doer, it's only an example */
            )
        return $match // empty string anyhow, but well
    }
str.replace(re, checker)
console.log(str_done)

ผลผลิตส่วนตัวของฉัน:

Fa[match] ball bi[match] bal[match] [match]ama

หลักการคือการโทรหาcheckerแต่ละจุดในสายอักขระระหว่างอักขระสองตัวเมื่อใดก็ตามที่ตำแหน่งนั้นเป็นจุดเริ่มต้นของ:

--- สตริงย่อยใด ๆ ที่มีขนาดของสิ่งที่ไม่ต้องการ (ที่นี่'ba'ดังนั้น..) (หากเป็นที่รู้จักขนาดนั้นมิฉะนั้นจะต้องยากกว่าที่จะทำ)

--- --- หรือเล็กกว่านั้นถ้ามันเป็นจุดเริ่มต้นของสตริง: ^.?

และติดตามสิ่งนี้

--- สิ่งที่จะต้องตามหาจริง (ที่นี่'ll')

ที่การเรียกแต่ละครั้งcheckerจะมีการทดสอบเพื่อตรวจสอบว่าค่าก่อนllไม่ใช่สิ่งที่เราไม่ต้องการ ( !== 'ba'); ถ้าเป็นกรณีนี้เราเรียกฟังก์ชันอื่นและมันจะต้องเป็นอันนี้ ( doer) ที่จะทำการเปลี่ยนแปลงใน str ถ้าวัตถุประสงค์นี้หรือโดยทั่วไปที่จะได้รับการป้อนข้อมูลที่จำเป็นในการประมวลผลด้วยตนเอง strผลการสแกนของ

ที่นี่เราเปลี่ยนสตริงดังนั้นเราจำเป็นต้องติดตามความแตกต่างของความยาวเพื่อชดเชยตำแหน่งที่replaceคำนวณโดยทั้งหมดที่เปิดstrซึ่งตัวมันเองไม่เคยเปลี่ยนแปลง

เนื่องจากสตริงดั้งเดิมนั้นไม่สามารถเปลี่ยนแปลงได้เราสามารถใช้ตัวแปรstrเพื่อเก็บผลลัพธ์ของการดำเนินการทั้งหมด แต่ฉันคิดว่าตัวอย่างที่ซับซ้อนแล้วโดยการแทนที่จะมีความชัดเจนกับตัวแปรอื่น ( str_done)

ฉันเดาว่าในแง่ของการแสดงมันจะต้องค่อนข้างรุนแรง: การเปลี่ยนที่ไม่มีจุดหมายของ '' เป็น '', this str.length-1ครั้ง, และที่นี่การแทนที่ด้วยมือโดยผู้กระทำซึ่งหมายถึงการหั่นเป็นจำนวนมาก ... อาจเป็นในกรณีข้างต้นนี้ ถูกจัดกลุ่มโดยการตัดสตริงเพียงครั้งเดียวเป็นชิ้น ๆ ที่เราต้องการแทรก[match]และ.join()ไอเอ็นจีด้วย[match]ตัวเอง

อีกอย่างคือฉันไม่รู้ว่ามันจะจัดการกับกรณีที่ซับซ้อนมากขึ้นได้อย่างไรนั่นคือค่าที่ซับซ้อนสำหรับลุคปลอม ๆ ... ความยาวอาจเป็นข้อมูลที่มีปัญหามากที่สุด

และcheckerในกรณีที่มีความเป็นไปได้หลายอย่างของค่าที่ไม่ต้องการสำหรับ $ behind เราจะต้องทำการทดสอบกับ regex อื่น (ที่จะถูกแคช (สร้าง)) นอกที่checkerดีที่สุดเพื่อหลีกเลี่ยงวัตถุ regex เดียวกันที่จะสร้าง ในแต่ละครั้งที่โทรหาchecker) เพื่อทราบว่าเป็นสิ่งที่เราพยายามหลีกเลี่ยงหรือไม่

หวังว่าฉันจะชัดเจน; ถ้าไม่ลังเลฉันจะพยายามให้ดีขึ้น :)


1

ใช้กรณีของคุณหากคุณต้องการแทนที่ mด้วยบางสิ่งเช่นแปลงเป็นตัวพิมพ์ใหญ่Mคุณสามารถคัดค้านชุดในกลุ่มการจับภาพได้

จับคู่([^a-g])mแทนที่ด้วย$1M

"jim jam".replace(/([^a-g])m/g, "$1M")
\\jiM jam

([^a-g])จะตรงกับถ่านใด ๆ ที่ไม่ ( ^) ใน ช่วงและเก็บไว้ในกลุ่มการจับครั้งแรกเพื่อให้คุณสามารถเข้าถึงได้ด้วยa-g$1

ดังนั้นเราจึงพบimในjimและแทนที่ด้วยซึ่งผลในการiMjiM


1

ดังที่ได้กล่าวไว้ก่อนหน้านี้ JavaScript อนุญาตให้ lookbehinds ตอนนี้ ในเบราว์เซอร์รุ่นเก่าคุณยังต้องการวิธีแก้ปัญหา

ฉันเดิมพันหัวของฉันไม่มีวิธีการค้นหา regex โดยไม่ต้องมองหาที่ให้ผลลัพธ์ที่แน่นอน สิ่งที่คุณทำได้คือทำงานกับกลุ่ม สมมติว่าคุณมี regex (?<!Before)Wantedอยู่ที่ไหนWantedregex ที่คุณต้องการจับคู่และBeforeเป็น regex ที่นับสิ่งที่ไม่ควรนำหน้าการแข่งขัน ดีที่สุดที่คุณสามารถทำได้คือการลบล้าง regex ไม่Beforeและใช้ NotBefore(Wanted)regex $1ผลที่ต้องการคือกลุ่มแรก

ในกรณีของคุณซึ่งเป็นเรื่องง่ายที่จะปฏิเสธBefore=[abcdefg] NotBefore=[^abcdefg]ดังนั้น regex [^abcdefg](m)จะเป็น หากคุณต้องการตำแหน่งWantedคุณจะต้องจัดกลุ่มNotBeforeด้วยเพื่อให้ผลลัพธ์ที่ต้องการคือกลุ่มที่สอง

หากการจับคู่Beforeรูปแบบมีความยาวคงnที่นั่นคือถ้ารูปแบบนั้นไม่มีโทเค็นซ้ำคุณสามารถหลีกเลี่ยงการลบBeforeรูปแบบและใช้นิพจน์ทั่วไป(?!Before).{n}(Wanted)แต่ยังคงต้องใช้กลุ่มแรกหรือใช้นิพจน์ทั่วไป(?!Before)(.{n})(Wanted)และใช้วินาที กลุ่ม. ในตัวอย่างนี้รูปแบบBeforeจริงมีความยาวคงที่ ได้แก่ 1 เพื่อใช้ regex ไม่หรือ(?![abcdefg]).(m) (?![abcdefg])(.)(m)หากคุณมีความสนใจในการแข่งขันทั้งหมดเพิ่มgธงดูข้อมูลรหัสของฉัน:

function TestSORegEx() {
  var s = "Donald Trump doesn't like jam, but Homer Simpson does.";
  var reg = /(?![abcdefg])(.{1})(m)/gm;
  var out = "Matches and groups of the regex " + 
            "/(?![abcdefg])(.{1})(m)/gm in \ns = \"" + s + "\"";
  var match = reg.exec(s);
  while(match) {
    var start = match.index + match[1].length;
    out += "\nWhole match: " + match[0] + ", starts at: " + match.index
        +  ". Desired match: " + match[2] + ", starts at: " + start + ".";   
    match = reg.exec(s);
  }
  out += "\nResulting string after statement s.replace(reg, \"$1*$2*\")\n"
         + s.replace(reg, "$1*$2*");
  alert(out);
}

0

สิ่งนี้ทำอย่างมีประสิทธิภาพ

"jim".match(/[^a-g]m/)
> ["im"]
"jam".match(/[^a-g]m/)
> null

ค้นหาและแทนที่ตัวอย่าง

"jim jam".replace(/([^a-g])m/g, "$1M")
> "jiM jam"

โปรดทราบว่าสตริงที่มีลักษณะลบเชิงลบต้องมีความยาว 1 อักขระเพื่อให้ทำงานได้


1
ไม่มาก ใน "jim" ฉันไม่ต้องการ "i"; เพียงแค่ "m" และ"m".match(/[^a-g]m/)ก็nullเช่นกัน ฉันต้องการ "m" ในกรณีนั้นด้วย
Andrew Ensley

-1

/(?![abcdefg])[^abcdefg]m/gi ใช่นี่เป็นกลลวง


5
การตรวจสอบ(?![abcdefg])ซ้ำซ้อนโดยสิ้นเชิงเนื่องจาก[^abcdefg]มีหน้าที่ป้องกันไม่ให้ตัวละครเหล่านั้นตรงกัน
พฤศจิกายน

2
สิ่งนี้จะไม่ตรงกับ 'm' ที่ไม่มีตัวอักษรนำหน้า
แอนดรูว์ Ensley
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.