รหัสแหล่งที่มา
รหัสแหล่งที่มาสำหรับฟังก์ชั่นการเขียนที่ผมปรึกษาด้านล่างสามารถใช้ได้ที่นี่
อัปเดตใน Java 7
Pattern
คลาสที่อัปเดตของ Sun สำหรับ JDK7 มีแฟล็กใหม่ที่ยอดUNICODE_CHARACTER_CLASS
เยี่ยมซึ่งทำให้ทุกอย่างกลับมาใช้ได้อีกครั้ง สามารถใช้เป็นแบบฝังได้(?U)
สำหรับภายในรูปแบบดังนั้นคุณสามารถใช้กับString
Wrapper ของชั้นเรียนได้เช่นกัน นอกจากนี้ยังมีการแก้ไขคำจำกัดความสำหรับคุณสมบัติอื่น ๆ อีกมากมายด้วย ตอนนี้จะติดตามมาตรฐาน 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üß!