มีเหตุผลที่เฉพาะเจาะจงหรือไม่ในการอ่านการออกแบบไวยากรณ์นิพจน์ปกติหรือไม่?


160

โปรแกรมเมอร์ทุกคนดูเหมือนจะยอมรับว่าการอ่านโค้ดมีความสำคัญมากกว่าการใช้ซินเทอร์แบบสั้นซึ่งทำงานอยู่ แต่ต้องการผู้พัฒนาระดับสูงในการตีความด้วยความถูกต้องในระดับใด ๆ มีเหตุผลสำหรับสิ่งนี้หรือไม่?

เราทุกคนยอมรับว่าดีกว่าselfDocumentingMethodName() e()เหตุใดจึงไม่ควรใช้กับนิพจน์ทั่วไปด้วย

ดูเหมือนว่าแทนที่จะออกแบบไวยากรณ์ของตรรกะหนึ่งบรรทัดโดยไม่มีโครงสร้างองค์กร:

var parse_url = /^(?:([A-Za-z]+):)?(\/{0,3})(0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/;

และนี่ไม่ใช่การแยก URL อย่างเข้มงวดแม้แต่!

แต่เราสามารถสร้างโครงสร้างไปป์ไลน์ที่จัดระเบียบและสามารถอ่านได้ตัวอย่างเช่น:

string.regex
   .isRange('A-Z' || 'a-z')
   .followedBy('/r');

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


ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
maple_shaft

1
ฉันพยายามจัดการปัญหาการอ่านนี้กับห้องสมุดที่ชื่อว่า RegexToolbox เพื่อให้ห่างไกลมันรังเพลิง C #, Java และ JavaScript - ดูgithub.com/markwhitaker/RegexToolbox.CSharp
Mark Whitaker

มีความพยายามหลายครั้งในการแก้ไขปัญหานี้ แต่วัฒนธรรมยากที่จะเปลี่ยนแปลง ดูคำตอบของฉันเกี่ยวกับการแสดงออกทางวาจาที่นี่ ผู้คนเข้าถึงเครื่องมือทั่วไปที่มีอยู่น้อยที่สุด
Parivar Saraff

คำตอบ:


178

มีเหตุผลสำคัญอย่างหนึ่งที่ทำไมนิพจน์ทั่วไปได้รับการออกแบบให้กระชับตามที่พวกเขาออกแบบมาเพื่อใช้เป็นคำสั่งในการแก้ไขโค้ดไม่ใช่ภาษาที่ใช้ในการเขียนรหัสอย่างแม่นยำยิ่งขึ้นedเป็นหนึ่งในโปรแกรมแรกที่ใช้นิพจน์ทั่วไป และจากที่นั่นการแสดงออกปกติเริ่มต้นการพิชิตเพื่อครอบงำโลก ตัวอย่างเช่นedคำสั่งg/<regular expression>/pในไม่ช้าก็เป็นแรงบันดาลใจให้โปรแกรมแยกต่างหากgrepซึ่งยังคงใช้งานอยู่ในปัจจุบัน เนื่องจากพลังของพวกเขาพวกเขาจึงได้มาตรฐานและใช้ในเครื่องมือต่าง ๆ เช่นsedและvim

แต่พอสำหรับเรื่องไม่สำคัญ แล้วทำไมต้นกำเนิดนี้ถึงชอบไวยากรณ์สั้น ๆ ? เพราะคุณไม่ได้พิมพ์คำสั่งบรรณาธิการเพื่ออ่านมันอีกครั้ง มันพอเพียงที่คุณสามารถจำได้ว่าจะนำมันมารวมกันอย่างไรและคุณสามารถทำสิ่งต่าง ๆ ด้วยสิ่งที่คุณต้องการทำ อย่างไรก็ตามตัวละครทุกตัวที่คุณต้องพิมพ์จะทำให้การแก้ไขความคืบหน้าช้าลง ไวยากรณ์นิพจน์ทั่วไปได้รับการออกแบบเพื่อเขียนการค้นหาที่ค่อนข้างซับซ้อนในแบบที่ไม่ใช้แล้วและนั่นคือสิ่งที่ทำให้ผู้คนปวดหัวที่ใช้รหัสเพื่อแยกวิเคราะห์อินพุตบางส่วนไปยังโปรแกรม


5
regex ไม่ได้หมายถึงการแยกวิเคราะห์ มิฉะนั้นstackoverflow.com/questions/1732348/... และปวดหัว
njzk2

19
@ njzk2 คำตอบนั้นผิดจริง เอกสาร HTML ไม่ใช่ภาษาปกติ แต่เป็นแท็กเปิด HTML ซึ่งเป็นคำถามที่ถามจริงคือ
Random832

11
นี่เป็นคำตอบที่ดีที่จะอธิบายว่าทำไม regex ดั้งเดิมจึงเป็นความลับเหมือนเดิม แต่ไม่ได้อธิบายว่าทำไมปัจจุบันไม่มีมาตรฐานทางเลือกที่สามารถอ่านได้เพิ่มขึ้น
Doc Brown

13
ดังนั้นสำหรับความคิดที่ว่าgrep"คว้า" ผิดมันมาในความเป็นจริงจากg/ re(สำหรับการแสดงออกปกติ) / p?
Hagen von Eitzen

6
@DannyPflughoeft ไม่มันไม่ได้ แท็กเปิดเป็นเพียง<aaa bbb="ccc" ddd='eee'>ไม่มีแท็กซ้อนอยู่ภายใน คุณไม่สามารถซ้อนแท็กสิ่งที่คุณซ้อนคือองค์ประกอบ (แท็กเปิดเนื้อหารวมถึงองค์ประกอบย่อยแท็กปิด) ซึ่งคำถามไม่ได้ถามเกี่ยวกับการแยกวิเคราะห์ แท็ก HTML เป็นภาษาปกติ - การปรับสมดุล / การซ้อนเกิดขึ้นที่ระดับเหนือแท็ก
Random832

62

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

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

แม้จะมีการแสดงออกปกติมักจะใช้มากเกินไปนั่นคือนำไปใช้แม้ว่าเครื่องมืออื่นจะดีกว่ามาก ฉันไม่คิดว่าไวยากรณ์ของ regex แย่มาก แต่เห็นได้ชัดว่าดีกว่ามากในรูปแบบที่สั้นและเรียบง่าย: ตัวอย่างแบบฉบับของตัวบ่งชี้ในภาษา C-like [a-zA-Z_][a-zA-Z0-9_]*สามารถอ่านได้ด้วยความรู้ regex ขั้นต่ำและเมื่อพบว่าแถบนั้นมีทั้งชัดเจนและชัดเจน ต้องการตัวละครน้อยลงไม่ได้เลวร้ายโดยธรรมชาติค่อนข้างตรงข้าม การรัดกุมคือคุณธรรมหากคุณยังเข้าใจได้

มีอย่างน้อยสองเหตุผลว่าทำไมไวยากรณ์นี้จึงมีรูปแบบเรียบง่ายเช่นนี้: มันไม่ต้องการการหลบหนีสำหรับตัวละครส่วนใหญ่ดังนั้นมันจึงอ่านได้ค่อนข้างเป็นธรรมชาติและใช้เครื่องหมายวรรคตอนที่มีอยู่ทั้งหมด บางทีที่สำคัญที่สุดคือมันไม่ต้องการอะไรเลยสำหรับการหาลำดับ คุณเขียนสิ่งแรกแล้วสิ่งที่เกิดขึ้นหลังจากนั้น เปรียบเทียบความแตกต่างนี้กับของคุณfollowedByโดยเฉพาะเมื่อรูปแบบต่อไปนี้ไม่ใช่ตัวอักษร แต่เป็นการแสดงออกที่ซับซ้อนมากขึ้น

แล้วทำไมพวกเขาถึงขาดความเข้าใจในกรณีที่ซับซ้อนกว่านี้? ฉันเห็นปัญหาหลักสามข้อ:

  1. ไม่มีความสามารถที่เป็นนามธรรม Grammars ที่เป็นทางการซึ่งมีต้นกำเนิดมาจากสาขาวิชาวิทยาการคอมพิวเตอร์เชิงทฤษฎีเดียวกันกับ regexes มีชุดโปรดักชั่นดังนั้นพวกเขาจึงสามารถให้ชื่อกับส่วนกลางของรูปแบบ:

    # This is not equivalent to the regex in the question
    # It's just a mock-up of what a grammar could look like
    url      ::= protocol? '/'? '/'? '/'? (domain_part '.')+ tld
    protocol ::= letter+ ':'
    ...
    
  2. ดังที่เราได้เห็นข้างต้นช่องว่างที่ไม่มีความสำคัญเป็นพิเศษมีประโยชน์ในการอนุญาตการจัดรูปแบบที่ง่ายต่อสายตา สิ่งเดียวกันกับความคิดเห็น ' 'การแสดงออกปกติไม่สามารถทำได้เพราะพื้นที่เป็นเพียงที่ตัวอักษร หมายเหตุแม้ว่า: การใช้งานบางอย่างจะอนุญาตให้ใช้โหมด "verbose" ซึ่งช่องว่างถูกละเว้นและความคิดเห็นเป็นไปได้

  3. ไม่มี meta-language เพื่ออธิบายรูปแบบทั่วไปและตัวรวม ตัวอย่างเช่นหนึ่งสามารถเขียนdigitกฎหนึ่งครั้งและใช้มันต่อไปในไวยากรณ์อิสระบริบท แต่หนึ่งไม่สามารถกำหนด "ฟังก์ชั่น" เพื่อพูดว่าจะได้รับการผลิตpและสร้างการผลิตใหม่ที่ทำสิ่งพิเศษด้วยเช่นสร้าง pการผลิตสำหรับเครื่องหมายจุลภาคคั่นรายการของการเกิดขึ้นของ

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

การแก้ปัญหาสำหรับกรณีเหล่านั้นมีอยู่ มีไลบรารี combinator parser ประมาณหมื่นที่ทำสิ่งที่คุณเสนอเพียงกับชุดการดำเนินงานที่แตกต่างกันมักจะแตกต่างกันไวยากรณ์และมักจะมีพลังในการแยกวิเคราะห์มากกว่านิพจน์ปกติ (เช่นพวกเขาจัดการกับภาษาบริบทฟรีหรือบางขนาดใหญ่) ส่วนย่อยของสิ่งเหล่านั้น) จากนั้นมีตัวแยกวิเคราะห์ซึ่งใช้วิธี "ใช้ DSL ที่ดีกว่า" ที่อธิบายไว้ข้างต้น และมีตัวเลือกในการเขียนบางส่วนของการแยกวิเคราะห์ด้วยมือในรหัสที่เหมาะสม คุณยังสามารถมิกซ์แอนด์แมทช์โดยใช้นิพจน์ทั่วไปสำหรับงานย่อยง่าย ๆ และทำสิ่งที่ซับซ้อนในโค้ดที่เรียกใช้ regexes

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


9
I don't know enough about the early years of computing to explain how regular expressions came to be so popular.เราอาจเสี่ยงต่อการคาดเดา: เอ็นจิ้นนิพจน์ทั่วไปขั้นพื้นฐานนั้นใช้งานง่ายมากง่ายกว่าตัวแยกวิเคราะห์ที่ไม่มีบริบทอย่างมีประสิทธิภาพ
biziclop

15
@biziclop ฉันจะไม่ประเมินค่าสูงไปตัวแปรนี้ Yacc ซึ่งเห็นได้ชัดว่ามีรุ่นก่อนมากพอที่จะเรียกว่า " ยังรวบรวมคอมไพเลอร์อื่น " ถูกสร้างขึ้นในช่วงต้นยุค 70 และถูกรวมอยู่ใน Unix รุ่นก่อนหน้าgrepนี้คือ (รุ่น 3 vs รุ่น 4) ดูเหมือนว่าการใช้งานที่สำคัญครั้งแรกของ regex คือในปี 1968

ฉันทำได้เฉพาะสิ่งที่ฉันพบใน Wikipedia (ดังนั้นฉันจะไม่เชื่อเลย 100%) แต่ตามที่yaccสร้างขึ้นในปี 1975 ความคิดทั้งหมดของตัวแยกวิเคราะห์ LALR (ซึ่งอยู่ในประเภทแรกของตัวแยกวิเคราะห์ที่ใช้งานได้จริงของพวกเขา ชนิด) กำเนิดขึ้นในปี 1973 ในขณะที่การใช้งานเครื่องมือ regexp ครั้งแรกที่ JIT รวบรวมการแสดงออก (!) ถูกตีพิมพ์ในปี 1968 แต่คุณพูดถูกมันยากที่จะพูดว่าสิ่งที่เปลี่ยนไปในความเป็นจริงมันยากที่จะบอกว่า ปิด" แต่ฉันสงสัยว่าเมื่อพวกเขาใส่ตัวแก้ไขข้อความที่ผู้พัฒนาใช้พวกเขาต้องการใช้พวกเขาในซอฟต์แวร์ของพวกเขาเองเช่นกัน
biziclop

1
@ jpmc26 เปิดหนังสือของเขา, JavaScript The Good Parts to the Regex Chapter
Viziionary

2
with very few differences between dialectsฉันจะไม่พูดว่ามัน "น้อยมาก" คลาสอักขระที่กำหนดไว้ล่วงหน้าใด ๆ มีคำจำกัดความหลายอย่างระหว่างภาษาถิ่นต่างๆ และยังมีการแยกวิเคราะห์นิสัยที่เฉพาะเจาะจงสำหรับแต่ละภาษา
nhahtdh

39

มุมมองทางประวัติศาสตร์

บทความ Wikipediaค่อนข้างละเอียดเกี่ยวกับต้นกำเนิดของนิพจน์ทั่วไป (Kleene, 1956) ไวยากรณ์เดิมค่อนข้างง่ายมีเพียง*, +, ?, และการจัดกลุ่ม| (...)มันสั้น ( และสามารถอ่านได้ทั้งสองไม่จำเป็นต้องคัดค้าน) เพราะภาษาทางการมักจะแสดงด้วยสัญกรณ์คณิตศาสตร์สั้น ๆ

ต่อมาไวยากรณ์และความสามารถในการพัฒนากับบรรณาธิการและขึ้นอยู่กับPerlซึ่งได้รับการพยายามที่จะรวบรัดโดยการออกแบบ ( "การก่อสร้างทั่วไปควรจะสั้น" ) สิ่งนี้ทำให้ไวยากรณ์ซับซ้อนขึ้นมาก แต่โปรดทราบว่าขณะนี้ผู้คนคุ้นเคยกับการแสดงออกปกติและสามารถเขียนได้ดี (ถ้าไม่อ่าน) ความจริงที่ว่าบางครั้งพวกเขาเขียนเท่านั้นแนะนำว่าเมื่อพวกเขายาวเกินไปพวกเขามักจะไม่ใช่เครื่องมือที่เหมาะสม นิพจน์ทั่วไปมักไม่สามารถอ่านได้เมื่อถูกทารุณกรรม

เกินกว่าการแสดงออกปกติตามสตริง

พูดเกี่ยวกับไวยากรณ์ทางเลือกลองดูที่มีอยู่แล้ว ( cl-ppcreในCommon LISP ) นิพจน์ทั่วไปที่ยาวของคุณสามารถแยกวิเคราะห์ได้ppcre:parse-stringดังนี้:

(let ((*print-case* :downcase)
      (*print-right-margin* 50))
  (pprint
   (ppcre:parse-string "^(?:([A-Za-z]+):)?(\\/{0,3})(0-9.\\-A-Za-z]+)(?::(\\d+))?(?:\\/([^?#]*))?(?:\\?([^#]*))?(?:#(.*))?$")))

... และผลลัพธ์ในรูปแบบต่อไปนี้:

(:sequence :start-anchor
 (:greedy-repetition 0 1
  (:group
   (:sequence
    (:register
     (:greedy-repetition 1 nil
      (:char-class (:range #\A #\Z)
       (:range #\a #\z))))
    #\:)))
 (:register (:greedy-repetition 0 3 #\/))
 (:register
  (:sequence "0-9" :everything "-A-Za-z"
   (:greedy-repetition 1 nil #\])))
 (:greedy-repetition 0 1
  (:group
   (:sequence #\:
    (:register
     (:greedy-repetition 1 nil :digit-class)))))
 (:greedy-repetition 0 1
  (:group
   (:sequence #\/
    (:register
     (:greedy-repetition 0 nil
      (:inverted-char-class #\? #\#))))))
 (:greedy-repetition 0 1
  (:group
   (:sequence #\?
    (:register
     (:greedy-repetition 0 nil
      (:inverted-char-class #\#))))))
 (:greedy-repetition 0 1
  (:group
   (:sequence #\#
    (:register
     (:greedy-repetition 0 nil :everything)))))
 :end-anchor)

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

อย่างไรก็ตามหากคุณเริ่มมีปัญหากับนิพจน์ทั่วไปการเปลี่ยนเป็นรูปแบบนี้อาจช่วยให้คุณถอดรหัสและตรวจแก้รหัสของคุณได้ นี่เป็นข้อดีอย่างหนึ่งของรูปแบบสตริงที่ข้อผิดพลาดของอักขระเดียวสามารถมองเห็นได้ยาก ประโยชน์หลักของไวยากรณ์นี้คือการจัดการกับนิพจน์ทั่วไปโดยใช้รูปแบบที่มีโครงสร้างแทนการเข้ารหัสแบบสตริง ที่ช่วยให้คุณสามารถเขียนและสร้างนิพจน์เช่นโครงสร้างข้อมูลอื่น ๆ ในโปรแกรมของคุณ เมื่อฉันใช้ไวยากรณ์ข้างต้นนี้เป็นเพราะฉันต้องการสร้างการแสดงออกจากส่วนเล็ก ๆ (ดูคำตอบ CodeGolf ของฉัน ) สำหรับตัวอย่างของคุณเราอาจเขียน1 :

`(:sequence
   :start-anchor
   ,(protocol)
   ,(slashes)
   ,(domain)
   ,(top-level-domain) ... )

นอกจากนี้ยังสามารถสร้างนิพจน์ทั่วไปที่ใช้สตริงได้โดยใช้การต่อสตริงและหรือการแก้ไขในฟังก์ชันตัวช่วย แต่มีข้อ จำกัด ที่มีกิจวัตรสตริงซึ่งมีแนวโน้มที่จะถ่วงรหัส (คิดว่าเกี่ยวกับปัญหาการทำรังไม่แตกต่างจาก backticks เทียบกับ$(...)ในทุบตี; ยังหลบหนีตัวละครอาจทำให้คุณปวดหัว)

โปรดทราบว่าแบบฟอร์มด้านบนอนุญาตให้ใช้(:regex "string")แบบฟอร์มเพื่อให้คุณสามารถผสมเครื่องหมายคำย่อกับต้นไม้ ทั้งหมดนี้นำไปสู่ ​​IMHO ในการอ่านและเรียงความที่ดี มันระบุถึงปัญหาสามข้อที่แสดงโดย delnanทางอ้อม (กล่าวคือไม่ได้อยู่ในภาษาของการแสดงออกปกติ)

สรุป

  • เพื่อจุดประสงค์ส่วนใหญ่สัญกรณ์สั้น ๆ ในความเป็นจริงสามารถอ่านได้ มีปัญหาเมื่อต้องรับมือกับความหมายที่เพิ่มขึ้นซึ่งเกี่ยวข้องกับการย้อนรอย ฯลฯ แต่การใช้ของพวกเขานั้นไม่ค่อยเป็นธรรม การใช้นิพจน์ทั่วไปที่ไม่มีการรับประกันสามารถนำไปสู่นิพจน์ที่ไม่สามารถอ่านได้

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

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


1.คุณอาจต้องการสร้างนิพจน์ของคุณในเวลาอ่านเนื่องจากนิพจน์ทั่วไปมักจะเป็นค่าคงที่ในแอปพลิเคชัน ดูcreate-scannerและload-time-value:

'(:sequence :start-anchor #.(protocol) #.(slashes) ... )

5
บางทีฉันเพิ่งคุ้นเคยกับไวยากรณ์ RegEx แบบดั้งเดิม แต่ฉันไม่แน่ใจว่า 22 บรรทัดที่อ่านได้ค่อนข้างเข้าใจง่ายกว่า regex หนึ่งบรรทัดที่เทียบเท่า

3
@ dan1111 "ค่อนข้างอ่าน" ;-) เอาล่ะ แต่ถ้าคุณต้องการที่จะมี regex นานจริงๆก็จะทำให้ความรู้สึกที่จะกำหนดย่อยเช่นdigits, identและพวกเขาเขียน วิธีที่ฉันเห็นมันทำโดยทั่วไปคือการจัดการสตริง (การต่อข้อมูลหรือการแก้ไข) ซึ่งนำปัญหาอื่น ๆ เช่นการหลบหนีที่เหมาะสม ค้นหาสิ่งที่\\\\`อยู่ในแพ็คเกจ emacs Btw นี้จะทำให้แย่ลงเพราะตัวหนีเช่นเดียวกับที่ใช้ทั้งสำหรับตัวอักษรพิเศษเช่น\nและและไวยากรณ์\" regex \(ตัวอย่างที่ไม่ใช่เสียงกระเพื่อมของไวยากรณ์ที่ดีคือการprintfที่ไม่มีความขัดแย้งกับ%d \d
coredump

1
จุดยุติธรรมเกี่ยวกับชุดย่อยที่กำหนด นั่นทำให้รู้สึกมาก ฉันแค่สงสัยว่าการใช้คำฟุ่มเฟือยเป็นการปรับปรุง อาจเป็นเรื่องง่ายสำหรับผู้เริ่มต้น (แม้ว่าแนวคิดเช่นgreedy-repetitionนั้นจะไม่ง่ายและยังต้องเรียนรู้) อย่างไรก็ตามมันเสียสละการใช้งานสำหรับผู้เชี่ยวชาญเนื่องจากเป็นการยากที่จะมองเห็นและเข้าใจรูปแบบทั้งหมด

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

@ dan1111 บางทีฉันควรเสนอการแก้ไขโดยใช้ Haskell? Parsec ทำเพียงเก้าบรรทัด เป็นสายการบินเดียว: do {optional (many1 (letter) >> char ':'); choice (map string ["///","//","/",""]); many1 (oneOf "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-."); optional (char ':' >> many1 digit); optional (char '/' >> many (noneOf "?#")); optional (char '?' >> many (noneOf "#")); optional (char '#' >> many (noneOf "\n")); eof}. ด้วยบางบรรทัดเช่นกำหนดสายยาวเป็นdomainChars = ...และsection start p = optional (char start >> many p)มันดูเรียบง่าย
CR Drost

25

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

เปรียบเทียบกับ regex BNF ไวยากรณ์ของมันนั้นไม่ค่อยสะอาดกว่า regex แต่มันถูกใช้แตกต่างกัน คุณเริ่มต้นด้วยการกำหนดสัญลักษณ์ที่เรียบง่ายที่มีชื่อและเขียนมันจนกว่าคุณจะมาถึงสัญลักษณ์ที่อธิบายรูปแบบทั้งหมดที่คุณต้องการจับคู่

ตัวอย่างเช่นดูที่ไวยากรณ์ URI ในrfc3986 :

URI           = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
scheme        = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
hier-part     = "//" authority path-abempty
              / path-absolute
              / path-rootless
              / path-empty
...

คุณสามารถเขียนได้เกือบเหมือนกันโดยใช้ตัวแปรของไวยากรณ์ regex ที่สนับสนุนการฝังนิพจน์ย่อยที่มีชื่อ


โดยส่วนตัวแล้วฉันคิดว่า terse regex เช่น syntax เป็นสิ่งที่ดีสำหรับคุณสมบัติที่ใช้กันทั่วไปเช่นตัวละคร - คลาส, การต่อข้อมูล, ตัวเลือกหรือการทำซ้ำ แต่สำหรับคุณสมบัติที่ซับซ้อนและหายากมากขึ้น ค่อนข้างคล้ายกับวิธีที่เราใช้ตัวดำเนินการเช่น+หรือ*ในการเขียนโปรแกรมปกติและเปลี่ยนไปใช้ฟังก์ชั่นที่มีชื่อสำหรับการดำเนินงานที่หายาก


12

selfDocumentingMethodName () ดีกว่า e ()

ใช่ไหม? มีเหตุผลที่ภาษาส่วนใหญ่มี {และ} เป็นตัวคั่นบล็อกมากกว่า BEGIN และ END

คนชอบความอดทนและเมื่อคุณรู้ไวยากรณ์แล้วคำศัพท์สั้น ๆ ก็ดีกว่า ลองนึกภาพตัวอย่าง regex ของคุณถ้า d (สำหรับหลัก) เป็น 'หลัก' regex จะยิ่งน่าอ่านมากขึ้น หากคุณทำให้แยกวิเคราะห์ได้ง่ายขึ้นด้วยตัวควบคุมแล้วมันจะมีลักษณะเหมือน XML ไม่ว่าจะดีเมื่อคุณรู้ไวยากรณ์

เพื่อตอบคำถามของคุณอย่างถูกต้องคุณต้องตระหนักว่า regex นั้นมาจากยุคที่จำเป็นต้องใช้ความพยายามอย่างยิ่งในการคิดว่าเอกสาร XML ขนาด 1 MB ไม่ใช่เรื่องใหญ่วันนี้ แต่เรากำลังพูดถึงวันที่ 1 MB นั้นค่อนข้างดี พื้นที่เก็บข้อมูลทั้งหมดของคุณ นอกจากนี้ยังมีภาษาที่ใช้น้อยลงและ regex ไม่ได้อยู่ห่างจาก perl หรือ C เป็นล้านไมล์ดังนั้นไวยากรณ์จะคุ้นเคยกับโปรแกรมเมอร์ของวันที่จะมีความสุขกับการเรียนรู้ไวยากรณ์ ดังนั้นจึงไม่มีเหตุผลที่จะทำให้มัน verbose มากขึ้น


1
selfDocumentingMethodNameมีการตกลงกันโดยทั่วไปจะดีกว่าeเพราะโปรแกรมเมอร์สัญชาตญาณไม่สอดคล้องกับความเป็นจริงในแง่ของสิ่งที่จริงถือว่าการอ่านหรือโค้ดที่มีคุณภาพที่ดี คนที่ทำข้อตกลงนั้นผิด แต่นั่นเป็นวิธี
Leushenko

1
@ Leushenko: คุณอ้างว่าe()มันดีกว่าselfDocumentingMethodName()หรือเปล่า?
JacquesB

3
@JacquesB อาจไม่ได้อยู่ในบริบททั้งหมด (เช่นชื่อทั่วโลก) แต่สำหรับสิ่งที่มีขอบเขตแคบ? เกือบจะแน่นอน แน่นอนบ่อยกว่าภูมิปัญญาดั้งเดิมพูดว่า
Leushenko

1
@ Leushenko: ฉันมีเวลายากที่จะจินตนาการบริบทเป็นชื่อฟังก์ชั่นตัวอักษรเดียวดีกว่าชื่ออธิบายเพิ่มเติม แต่ฉันคิดว่านี่เป็นความเห็นที่บริสุทธิ์
JacquesB

1
@MilesRout: ตัวอย่างนี้ใช้e()กับชื่อวิธีการบันทึกข้อมูลด้วยตนเอง คุณสามารถอธิบายในบริบทที่เป็นการปรับปรุงให้ใช้ชื่อวิธีการตัวอักษรเดี่ยวมากกว่าชื่อวิธีการอธิบายได้อย่างไร
JacquesB

6

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

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

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

คุณสามารถพบคำถาม regex นับหมื่นรายการบน SO และคำถามเหล่านี้มักถูกทำเครื่องหมายว่าซ้ำกัน เพียงอย่างเดียวนี้แสดงให้เห็นถึงการใช้งานที่ไม่เหมือนใครซึ่งแตกต่างจากกันมาก

และไม่ใช่เรื่องง่ายที่จะเสนอวิธีการที่กำหนดไว้ล่วงหน้าเพื่อจัดการกับงานพิเศษที่แตกต่างกันนี้ คุณมีฟังก์ชั่นสตริงสำหรับงานประเภทนั้น แต่ถ้าฟังก์ชั่นเหล่านั้นไม่เพียงพอสำหรับงาน specifix ของคุณแสดงว่าถึงเวลาที่ต้องใช้ regex


2

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

ตัวอย่างเช่นสตริง regex สำหรับ URL อาจลดลงจากประมาณ:

UriRe = [scheme][hier-part][query][fragment]

ไปที่:

UriRe = UriSchemeRe + UriHierRe + "(/?|/" + UriQueryRe + UriFragRe + ")"
UriSchemeRe = [scheme]
UriHierRe = [hier-part]
UriQueryRe = [query]
UriFragRe = [fragment]

การแสดงออกปกติเป็นสิ่งที่ดี แต่พวกเขามีแนวโน้มที่จะถูกกระทำทารุณโดยผู้ที่หมกมุ่นอยู่กับความซับซ้อนที่ชัดเจน การแสดงออกที่เกิดขึ้นเป็นสำนวนขาดค่าระยะยาว


2
น่าเสียดายที่ภาษาการเขียนโปรแกรมส่วนใหญ่ไม่มีฟังก์ชั่นที่ช่วยในการเขียน regexes และวิธีการทำงานของการจับภาพกลุ่มนั้นไม่เป็นมิตรต่อการเขียน
CodesInChaos

1
ภาษาอื่น ๆ จำเป็นต้องใช้ Perl 5 ในการสนับสนุน "การแสดงผลปกติที่เข้ากันได้กับ Perl" นิพจน์ย่อยไม่เหมือนกับการเชื่อมสตริงสเปคของ regex การจับภาพควรมีชื่อไม่ใช่การใช้หมายเลขโดยนัย
JDługosz

0

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

Perl ทำให้ความพยายามที่ค่อนข้างอ่อนแอในการทำให้พวกเขาอ่านง่ายขึ้นโดยให้ช่องว่างและความคิดเห็น แต่ไม่ได้ทำอะไรจากจินตนาการจากระยะไกล

มีไวยากรณ์อื่น ๆ สิ่งที่ดีคือไวยากรณ์ scsh สำหรับ regexpsซึ่งจากประสบการณ์ของฉันผลิต regexps ซึ่งพิมพ์ได้ง่ายพอสมควร แต่ยังคงสามารถอ่านได้หลังจากความจริง

[ scshยอดเยี่ยมด้วยเหตุผลอื่น ๆ เพียงหนึ่งในนั้นคือข้อความตอบรับที่มีชื่อเสียง]


2
Perl6 ทำ! ดูที่ไวยากรณ์
JDługosz

@ JDługoszเท่าที่ฉันเห็นนั่นดูเหมือนจะเป็นกลไกสำหรับตัวแยกวิเคราะห์แทนที่จะเป็นไวยากรณ์ทางเลือกสำหรับนิพจน์ทั่วไป แต่ความแตกต่างอาจจะไม่ลึก
Norman Grey

มันสามารถทดแทนได้ แต่ไม่ จำกัด อยู่ในกำลังเดียวกัน คุณสามารถแปล regedp เป็นไวยากรณ์แบบอินไลน์ด้วยการโต้ตอบแบบ 1 ต่อ 1 ของโมดิฟายเออร์ แต่ในไวยากรณ์ที่อ่านง่ายขึ้น ตัวอย่างการส่งเสริมดังกล่าวอยู่ในคติ Perl เดิม
JDługosz

0

ฉันเชื่อว่านิพจน์ทั่วไปได้รับการออกแบบให้เป็น 'ทั่วไป' และเรียบง่ายที่สุดเท่าที่จะเป็นไปได้เพื่อให้สามารถใช้ (แบบคร่าวๆ) ในแบบเดียวกันทุกที่

คุณเป็นตัวอย่างของการregex.isRange(..).followedBy(..)เชื่อมต่อกับทั้งไวยากรณ์ของภาษาโปรแกรมเฉพาะและสไตล์เชิงวัตถุ (การโยงเมธอด)

ตัวอย่าง 'regex' ที่แน่นอนนี้จะดูเป็นอย่างไรใน C? รหัสจะต้องมีการเปลี่ยนแปลง

วิธีการทั่วไปที่สุดคือการกำหนดภาษาที่กระชับง่ายซึ่งสามารถฝังในภาษาอื่นได้อย่างง่ายดายโดยไม่มีการเปลี่ยนแปลง และนั่นคืออะไร (เกือบ) regex คืออะไร


0

เอ็นจินนิพจน์ทั่วไปที่ใช้ร่วมกับ Perl ได้ถูกนำมาใช้อย่างกว้างขวางทำให้เกิดไวยากรณ์ของนิพจน์ทั่วไปที่ผู้แก้ไขและภาษาจำนวนมากเข้าใจ ดังที่ @ JDługoszชี้ให้เห็นในความคิดเห็นPerl 6 (ไม่ใช่แค่เวอร์ชันใหม่ของ Perl 5 แต่เป็นภาษาที่แตกต่างกันทั้งหมด) ได้พยายามที่จะทำให้การแสดงออกปกติอ่านได้มากขึ้นโดยการสร้างพวกเขาจากองค์ประกอบที่กำหนดเอง ตัวอย่างเช่นนี่คือตัวอย่างไวยากรณ์สำหรับการแยกวิเคราะห์ URL จาก Wikibooks :

grammar URL {
  rule TOP {
    <protocol>'://'<address>
  }
  token protocol {
    'http'|'https'|'ftp'|'file'
  }
  rule address {
    <subdomain>'.'<domain>'.'<tld>
  }
  ...
}

แยกการแสดงออกปกติเช่นนี้ช่วยให้แต่ละบิตจะกำหนดเป็นรายบุคคล (เช่นข้อ จำกัดdomainที่จะเป็นตัวอักษรและตัวเลข) หรือขยายผ่าน subclassing (เช่นFileURL is URLที่ จำกัดprotocolเพียงเป็น"file")

ดังนั้น: ไม่มีไม่มีเหตุผลทางเทคนิคสำหรับความเข้มงวดของการแสดงออกปกติ แต่วิธีที่ใหม่กว่าสะอาดและอ่านง่ายขึ้นเพื่อเป็นตัวแทนของพวกเขาอยู่ที่นี่แล้ว! ดังนั้นหวังว่าเราจะเห็นแนวคิดใหม่ ๆ ในสาขานี้

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