รหัสแหล่งที่มา
รหัสแหล่งที่มาสำหรับฟังก์ชั่นการเขียนที่ผมปรึกษาด้านล่างสามารถใช้ได้ที่นี่
อัปเดตใน Java 7
Patternคลาสที่อัปเดตของ Sun สำหรับ JDK7 มีแฟล็กใหม่ที่ยอดUNICODE_CHARACTER_CLASSเยี่ยมซึ่งทำให้ทุกอย่างกลับมาใช้ได้อีกครั้ง สามารถใช้เป็นแบบฝังได้(?U)สำหรับภายในรูปแบบดังนั้นคุณสามารถใช้กับStringWrapper ของชั้นเรียนได้เช่นกัน นอกจากนี้ยังมีการแก้ไขคำจำกัดความสำหรับคุณสมบัติอื่น ๆ อีกมากมายด้วย ตอนนี้จะติดตามมาตรฐาน Unicode ทั้งในRL1.2และRL1.2aจากUTS # 18: Unicode นิพจน์ปกติ นี่เป็นการปรับปรุงที่น่าตื่นเต้นและน่าทึ่งและทีมพัฒนาต้องได้รับการยกย่องสำหรับความพยายามครั้งสำคัญนี้
ปัญหา Regex Unicode ของ Java
ปัญหากับ Java regexes คือว่า Perl 1.0 หนี charclass - ความหมาย\w, \b, \s, \dและการเติมเต็มของพวกเขา - ไม่ได้อยู่ใน Java ขยายไปถึงการทำงานร่วมกับ Unicode คนเดียวในหมู่เหล่านี้\bมีความสุขความหมายขยายบางอย่าง แต่เหล่านี้ไม่ว่าจะเป็นการแผนที่\wหรือเพื่อตัวระบุ Unicodeหรือเพื่อUnicode คุณสมบัติเส้นแบ่ง
นอกจากนี้คุณสมบัติ POSIX ใน Java สามารถเข้าถึงได้ด้วยวิธีนี้:
POSIX syntax Java syntax
[[:Lower:]] \p{Lower}
[[:Upper:]] \p{Upper}
[[:ASCII:]] \p{ASCII}
[[:Alpha:]] \p{Alpha}
[[:Digit:]] \p{Digit}
[[:Alnum:]] \p{Alnum}
[[:Punct:]] \p{Punct}
[[:Graph:]] \p{Graph}
[[:Print:]] \p{Print}
[[:Blank:]] \p{Blank}
[[:Cntrl:]] \p{Cntrl}
[[:XDigit:]] \p{XDigit}
[[:Space:]] \p{Space}
นี้เป็นระเบียบจริงเพราะมันหมายถึงว่าสิ่งที่ชอบAlpha, LowerและSpaceทำไม่ได้ในแผนที่ Java กับ Unicode Alphabetic, LowercaseหรือWhitespaceคุณสมบัติ นี่เป็นเรื่องที่น่ารำคาญอย่างยิ่ง การสนับสนุนคุณสมบัติ Unicode ของ Java นั้นถือเป็นการรักษาวัยก่อนวัยอย่างเคร่งครัดซึ่งฉันหมายความว่ามันไม่สนับสนุนคุณสมบัติ Unicode ที่ออกมาในทศวรรษที่ผ่านมา
การไม่สามารถพูดถึงช่องว่างได้อย่างถูกต้องเป็นเรื่องที่น่ารำคาญมาก พิจารณาตารางต่อไปนี้ สำหรับแต่ละจุดโค้ดเหล่านั้นมีทั้งคอลัมน์ J-results สำหรับ Java และคอลัมน์ P-results สำหรับ Perl หรือเอนจิ้น regex ที่ใช้ PCRE อื่น ๆ :
Regex 001A 0085 00A0 2029
J P J P J P J P
\s 1 1 0 1 0 1 0 1
\pZ 0 0 0 0 1 1 1 1
\p{Zs} 0 0 0 0 1 1 0 0
\p{Space} 1 1 0 1 0 1 0 1
\p{Blank} 0 0 0 0 0 1 0 0
\p{Whitespace} - 1 - 1 - 1 - 1
\p{javaWhitespace} 1 - 0 - 0 - 1 -
\p{javaSpaceChar} 0 - 0 - 1 - 1 -
เห็นมั้ย?
ผลลัพธ์ของพื้นที่สีขาว Java เกือบทุกรายการคือ ̲w̲r̲o̲n̲g̲ ตาม Unicode มันเป็นปัญหาใหญ่จริงๆ Java นั้นยุ่งเหยิงโดยให้คำตอบที่“ ผิด” ตามแนวทางปฏิบัติที่มีอยู่และตาม Unicode ด้วย แถม Java ยังไม่ให้คุณเข้าถึงคุณสมบัติ Unicode จริงด้วยซ้ำ! ในความเป็นจริง Java ไม่สนับสนุนคุณสมบัติใด ๆที่สอดคล้องกับช่องว่าง Unicode
ทางออกสำหรับปัญหาเหล่านั้นทั้งหมดและอื่น ๆ
เพื่อจัดการกับปัญหานี้และปัญหาอื่น ๆ ที่เกี่ยวข้องเมื่อวานนี้ฉันได้เขียนฟังก์ชัน Java เพื่อเขียนสตริงรูปแบบใหม่ที่เขียนค่า charclass 14 รายการเหล่านี้ใหม่:
\w \W \s \S \v \V \h \H \d \D \b \B \X \R
โดยแทนที่ด้วยสิ่งที่ใช้งานได้จริงเพื่อให้ตรงกับ Unicode ในรูปแบบที่คาดเดาได้และสอดคล้องกัน มันเป็นเพียงต้นแบบอัลฟาจากเซสชันการแฮ็กเดียว แต่ใช้งานได้อย่างสมบูรณ์
เรื่องสั้นคือรหัสของฉันเขียนใหม่ทั้ง 14 ดังต่อไปนี้:
\s => [\u0009-\u000D\u0020\u0085\u00A0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]
\S => [^\u0009-\u000D\u0020\u0085\u00A0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]
\v => [\u000A-\u000D\u0085\u2028\u2029]
\V => [^\u000A-\u000D\u0085\u2028\u2029]
\h => [\u0009\u0020\u00A0\u1680\u180E\u2000-\u200A\u202F\u205F\u3000]
\H => [^\u0009\u0020\u00A0\u1680\u180E\u2000\u2001-\u200A\u202F\u205F\u3000]
\w => [\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]
\W => [^\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]
\b => (?:(?<=[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])(?![\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])|(?<![\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])(?=[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]))
\B => (?:(?<=[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])(?=[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])|(?<![\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])(?![\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]))
\d => \p{Nd}
\D => \P{Nd}
\R => (?:(?>\u000D\u000A)|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029])
\X => (?>\PM\pM*)
สิ่งที่ต้องพิจารณา ...
การใช้งานที่สำหรับ\Xนิยามสิ่งUnicode ตอนนี้หมายถึงการเป็นคลัสเตอร์มรดกอักษรไม่ใช่คลัสเตอร์อักษรขยายเป็นหลังค่อนข้างซับซ้อนมากขึ้น ตอนนี้ Perl เองใช้เวอร์ชันที่น่าสนใจกว่า แต่เวอร์ชันเก่ายังคงใช้งานได้อย่างสมบูรณ์แบบสำหรับสถานการณ์ที่พบบ่อยที่สุด แก้ไข:ดูภาคผนวกที่ด้านล่าง
สิ่งที่ต้องทำ\dขึ้นอยู่กับเจตนาของคุณ แต่ค่าเริ่มต้นคือนิยาม Uniode ผมสามารถมองเห็นคนที่ไม่เคยต้องการ\p{Nd}แต่บางครั้งทั้งสองหรือ[0-9]\pN
นิยามขอบเขตทั้งสอง\bและ\Bถูกเขียนขึ้นโดยเฉพาะเพื่อใช้\wนิยาม
ว่า\wคำนิยามกว้างมากเกินไปเพราะมันคว้าตัวอักษร parenned ไม่ได้เป็นเพียงคนที่วงกลม Other_AlphabeticคุณสมบัติUnicode ไม่พร้อมใช้งานจนกว่า JDK7 นั่นคือสิ่งที่ดีที่สุดที่คุณสามารถทำได้
การสำรวจขอบเขต
เขตแดนมีปัญหานับตั้งแต่ Larry กำแพงประกาศเกียรติคุณแรก\bและ\Bไวยากรณ์สำหรับการพูดคุยเกี่ยวกับพวกเขาสำหรับ Perl 1.0 ย้อนกลับไปในปี 1987 กุญแจสำคัญในการทำความเข้าใจวิธีการ\bและ\Bการทำงานของทั้งสองคือการปัดเป่าสองตำนานแพร่หลายเกี่ยวกับพวกเขา:
- พวกเขาจะไม่เคยมองสำหรับ
\wตัวอักษรคำไม่เคยสำหรับอักขระที่ไม่ใช่คำพูด
- พวกเขาไม่ได้มองหาขอบของสตริงโดยเฉพาะ
\bหมายถึงเขตแดน:
IF does follow word
THEN doesn't precede word
ELSIF doesn't follow word
THEN does precede word
และทั้งหมดนี้ถูกกำหนดอย่างตรงไปตรงมาอย่างสมบูรณ์แบบว่า:
- ต่อไปนี้คำ
(?<=\w)มี
- คำแจ๋ว
(?=\w)คือ
- ไม่ปฏิบัติตามคำ
(?<!\w)มี
- ไม่ได้คำ Precede
(?!\w)คือ
ดังนั้นนับตั้งแต่IF-THENมีการเข้ารหัสเป็นand เอ็ดร่วมกันABใน regexes การorเป็นX|Yและเพราะandเป็นที่สูงขึ้นในลำดับความสำคัญกว่าที่เป็นเพียงor AB|CDดังนั้นทุก\bสิ่งที่หมายถึงเขตแดนสามารถถูกแทนที่ได้อย่างปลอดภัยด้วย:
(?:(?<=\w)(?!\w)|(?<!\w)(?=\w))
ด้วยการ\wกำหนดด้วยวิธีที่เหมาะสม
(คุณอาจคิดว่ามันแปลกที่ส่วนประกอบAและCส่วนตรงข้ามกันในโลกที่สมบูรณ์แบบคุณควรจะเขียนสิ่งAB|Dนั้นได้ แต่ในขณะที่ฉันกำลังไล่ตามความขัดแย้งในการกีดกันซึ่งกันและกันในคุณสมบัติของ Unicode ซึ่งฉันคิดว่าฉันได้ดูแล แต่ฉันทิ้งเงื่อนไขสองชั้นไว้ในขอบเขตในกรณีนี้นอกจากนี้ยังทำให้ขยายได้มากขึ้นหากคุณได้รับแนวคิดเพิ่มเติมในภายหลัง)
สำหรับสิ่งที่\Bไม่มีขอบเขตตรรกะคือ:
IF does follow word
THEN does precede word
ELSIF doesn't follow word
THEN doesn't precede word
อนุญาตให้\Bแทนที่อินสแตนซ์ทั้งหมดด้วย:
(?:(?<=\w)(?=\w)|(?<!\w)(?!\w))
นี่คือวิธี\bและ\Bพฤติกรรมจริงๆ รูปแบบที่เทียบเท่ากันสำหรับพวกเขาคือ
\bโดยใช้((IF)THEN|ELSE)โครงสร้างคือ(?(?<=\w)(?!\w)|(?=\w))
\Bโดยใช้((IF)THEN|ELSE)โครงสร้างคือ(?(?=\w)(?<=\w)|(?<!\w))
แต่เวอร์ชันที่มีก็AB|CDใช้ได้ดีโดยเฉพาะอย่างยิ่งหากคุณไม่มีรูปแบบเงื่อนไขในภาษา regex ของคุณเช่น Java ☹
ฉันได้ตรวจสอบพฤติกรรมของขอบเขตโดยใช้คำจำกัดความที่เท่ากันทั้งสามแล้วกับชุดทดสอบที่ตรวจสอบการจับคู่ 110,385,408 รายการต่อการวิ่งและฉันได้เรียกใช้การกำหนดค่าข้อมูลที่แตกต่างกันหลายสิบรายการตาม:
0 .. 7F the ASCII range
80 .. FF the non-ASCII Latin1 range
100 .. FFFF the non-Latin1 BMP (Basic Multilingual Plane) range
10000 .. 10FFFF the non-BMP portion of Unicode (the "astral" planes)
อย่างไรก็ตามผู้คนมักต้องการขอบเขตที่แตกต่างกัน พวกเขาต้องการบางสิ่งที่เป็นช่องว่างและตระหนักถึงขอบของสตริง:
- ขอบซ้าย เป็น
(?:(?<=^)|(?<=\s))
- ขอบขวาเป็น
(?=$|\s)
แก้ไข Java ด้วย Java
รหัสที่ฉันโพสต์ไว้ในคำตอบอื่นของฉันให้สิ่งนี้และสิ่งอำนวยความสะดวกอื่น ๆ อีกเล็กน้อย ซึ่งรวมถึงคำจำกัดความสำหรับคำภาษาธรรมชาติขีดกลางยัติภังค์และอะพอสทรอฟีและอื่น ๆ อีกเล็กน้อย
นอกจากนี้ยังช่วยให้คุณสามารถระบุอักขระ Unicode ในจุดโค้ดตรรกะไม่ใช่ในตัวแทน UTF-16 ที่งี่เง่า มันยากที่จะเอาชนะว่ามันสำคัญแค่ไหน! และนั่นเป็นเพียงการขยายสตริง
สำหรับ regex charclass ทดแทนที่ทำให้ charclass ใน Java ของคุณ regexes ที่สุดทำงานบน Unicode, และการทำงานอย่างถูกต้อง คว้าแหล่งที่มาเต็มรูปแบบจากที่นี่ คุณสามารถทำได้ตามที่คุณต้องการแน่นอน หากคุณแก้ไขปัญหานี้ฉันชอบที่จะได้ยินเรื่องนี้ แต่คุณไม่จำเป็นต้องทำ ค่อนข้างสั้น ความกล้าของฟังก์ชันการเขียน regex หลักนั้นง่ายมาก:
switch (code_point) {
case 'b': newstr.append(boundary);
break; /* switch */
case 'B': newstr.append(not_boundary);
break; /* switch */
case 'd': newstr.append(digits_charclass);
break; /* switch */
case 'D': newstr.append(not_digits_charclass);
break; /* switch */
case 'h': newstr.append(horizontal_whitespace_charclass);
break; /* switch */
case 'H': newstr.append(not_horizontal_whitespace_charclass);
break; /* switch */
case 'v': newstr.append(vertical_whitespace_charclass);
break; /* switch */
case 'V': newstr.append(not_vertical_whitespace_charclass);
break; /* switch */
case 'R': newstr.append(linebreak);
break; /* switch */
case 's': newstr.append(whitespace_charclass);
break; /* switch */
case 'S': newstr.append(not_whitespace_charclass);
break; /* switch */
case 'w': newstr.append(identifier_charclass);
break; /* switch */
case 'W': newstr.append(not_identifier_charclass);
break; /* switch */
case 'X': newstr.append(legacy_grapheme_cluster);
break; /* switch */
default: newstr.append('\\');
newstr.append(Character.toChars(code_point));
break; /* switch */
}
saw_backslash = false;
อย่างไรก็ตามรหัสนั้นเป็นเพียงรุ่นอัลฟ่าสิ่งที่ฉันแฮ็กในช่วงสุดสัปดาห์ มันจะไม่อยู่อย่างนั้น
สำหรับเบต้าฉันตั้งใจจะ:
พับการทำสำเนารหัสเข้าด้วยกัน
จัดเตรียมอินเทอร์เฟซที่ชัดเจนขึ้นเกี่ยวกับการหลีกเลี่ยงสตริงที่ไม่ใช้ Escape เทียบกับการเพิ่มการหลบหนี regex
ให้ความยืดหยุ่นในการ\dขยายตัวและอาจเป็นไฟล์\b
จัดหาวิธีการอำนวยความสะดวกที่จัดการการหมุนและเรียก Pattern.compile หรือ String.matches หรืออะไรก็ได้สำหรับคุณ
สำหรับรุ่นที่ใช้งานจริงควรมี javadoc และชุดทดสอบ JUnit ฉันอาจรวม gigatester แต่ไม่ได้เขียนเป็นการทดสอบ JUnit
ภาคผนวก
ฉันมีข่าวดีและข่าวร้าย
ข่าวดีก็คือที่ผมได้ในขณะนี้มีมากประมาณใกล้เคียงกับคลัสเตอร์อักษรขยาย\Xการใช้งานสำหรับการปรับปรุงให้ดีขึ้น
ข่าวร้าย☺คือรูปแบบนั้นคือ:
(?:(?:\u000D\u000A)|(?:[\u0E40\u0E41\u0E42\u0E43\u0E44\u0EC0\u0EC1\u0EC2\u0EC3\u0EC4\uAAB5\uAAB6\uAAB9\uAABB\uAABC]*(?:[\u1100-\u115F\uA960-\uA97C]+|([\u1100-\u115F\uA960-\uA97C]*((?:[[\u1160-\u11A2\uD7B0-\uD7C6][\uAC00\uAC1C\uAC38]][\u1160-\u11A2\uD7B0-\uD7C6]*|[\uAC01\uAC02\uAC03\uAC04])[\u11A8-\u11F9\uD7CB-\uD7FB]*))|[\u11A8-\u11F9\uD7CB-\uD7FB]+|[^[\p{Zl}\p{Zp}\p{Cc}\p{Cf}&&[^\u000D\u000A\u200C\u200D]]\u000D\u000A])[[\p{Mn}\p{Me}\u200C\u200D\u0488\u0489\u20DD\u20DE\u20DF\u20E0\u20E2\u20E3\u20E4\uA670\uA671\uA672\uFF9E\uFF9F][\p{Mc}\u0E30\u0E32\u0E33\u0E45\u0EB0\u0EB2\u0EB3]]*)|(?s:.))
ซึ่งใน Java คุณจะเขียนเป็น:
String extended_grapheme_cluster = "(?:(?:\\u000D\\u000A)|(?:[\\u0E40\\u0E41\\u0E42\\u0E43\\u0E44\\u0EC0\\u0EC1\\u0EC2\\u0EC3\\u0EC4\\uAAB5\\uAAB6\\uAAB9\\uAABB\\uAABC]*(?:[\\u1100-\\u115F\\uA960-\\uA97C]+|([\\u1100-\\u115F\\uA960-\\uA97C]*((?:[[\\u1160-\\u11A2\\uD7B0-\\uD7C6][\\uAC00\\uAC1C\\uAC38]][\\u1160-\\u11A2\\uD7B0-\\uD7C6]*|[\\uAC01\\uAC02\\uAC03\\uAC04])[\\u11A8-\\u11F9\\uD7CB-\\uD7FB]*))|[\\u11A8-\\u11F9\\uD7CB-\\uD7FB]+|[^[\\p{Zl}\\p{Zp}\\p{Cc}\\p{Cf}&&[^\\u000D\\u000A\\u200C\\u200D]]\\u000D\\u000A])[[\\p{Mn}\\p{Me}\\u200C\\u200D\\u0488\\u0489\\u20DD\\u20DE\\u20DF\\u20E0\\u20E2\\u20E3\\u20E4\\uA670\\uA671\\uA672\\uFF9E\\uFF9F][\\p{Mc}\\u0E30\\u0E32\\u0E33\\u0E45\\u0EB0\\u0EB2\\u0EB3]]*)|(?s:.))";
¡Tschüß!