[Python-checkins] cpython: Issue #19380: Optimized parsing of regular expressions.

serhiy.storchaka python-checkins at python.org
Fri Oct 10 10:16:17 CEST 2014


https://hg.python.org/cpython/rev/1adeac2a8714
changeset: 92911:1adeac2a8714
user: Serhiy Storchaka <storchaka at gmail.com>
date: Fri Oct 10 11:14:49 2014 +0300
summary:
 Issue #19380: Optimized parsing of regular expressions.
files:
 Lib/sre_parse.py | 270 +++++++++++++++-------------------
 Misc/NEWS | 4 +-
 2 files changed, 123 insertions(+), 151 deletions(-)
diff --git a/Lib/sre_parse.py b/Lib/sre_parse.py
--- a/Lib/sre_parse.py
+++ b/Lib/sre_parse.py
@@ -18,12 +18,15 @@
 SPECIAL_CHARS = ".\\[{()*+?^$|"
 REPEAT_CHARS = "*+?{"
 
-DIGITS = set("0123456789")
+DIGITS = frozenset("0123456789")
 
-OCTDIGITS = set("01234567")
-HEXDIGITS = set("0123456789abcdefABCDEF")
+OCTDIGITS = frozenset("01234567")
+HEXDIGITS = frozenset("0123456789abcdefABCDEF")
 
-WHITESPACE = set(" \t\n\r\v\f")
+WHITESPACE = frozenset(" \t\n\r\v\f")
+
+_REPEATCODES = frozenset((MIN_REPEAT, MAX_REPEAT))
+_UNITCODES = frozenset((ANY, RANGE, IN, LITERAL, NOT_LITERAL, CATEGORY))
 
 ESCAPES = {
 r"\a": (LITERAL, ord("\a")),
@@ -153,11 +156,9 @@
 self.data.append(code)
 def getwidth(self):
 # determine the width (min, max) for this subpattern
- if self.width:
+ if self.width is not None:
 return self.width
 lo = hi = 0
- UNITCODES = (ANY, RANGE, IN, LITERAL, NOT_LITERAL, CATEGORY)
- REPEATCODES = (MIN_REPEAT, MAX_REPEAT)
 for op, av in self.data:
 if op is BRANCH:
 i = MAXREPEAT - 1
@@ -176,11 +177,11 @@
 i, j = av[1].getwidth()
 lo = lo + i
 hi = hi + j
- elif op in REPEATCODES:
+ elif op in _REPEATCODES:
 i, j = av[2].getwidth()
 lo = lo + i * av[0]
 hi = hi + j * av[1]
- elif op in UNITCODES:
+ elif op in _UNITCODES:
 lo = lo + 1
 hi = hi + 1
 elif op == SUCCESS:
@@ -191,34 +192,31 @@
 class Tokenizer:
 def __init__(self, string):
 self.istext = isinstance(string, str)
+ if not self.istext:
+ string = str(string, 'latin1')
 self.string = string
 self.index = 0
 self.__next()
 def __next(self):
- if self.index >= len(self.string):
+ index = self.index
+ try:
+ char = self.string[index]
+ except IndexError:
 self.next = None
 return
- char = self.string[self.index:self.index+1]
- # Special case for the str8, since indexing returns a integer
- # XXX This is only needed for test_bug_926075 in test_re.py
- if char and not self.istext:
- char = chr(char[0])
 if char == "\\":
+ index += 1
 try:
- c = self.string[self.index + 1]
+ char += self.string[index]
 except IndexError:
 raise error("bogus escape (end of line)")
- if not self.istext:
- c = chr(c)
- char = char + c
- self.index = self.index + len(char)
+ self.index = index + 1
 self.next = char
- def match(self, char, skip=1):
+ def match(self, char):
 if char == self.next:
- if skip:
- self.__next()
- return 1
- return 0
+ self.__next()
+ return True
+ return False
 def get(self):
 this = self.next
 self.__next()
@@ -232,6 +230,17 @@
 result += c
 self.__next()
 return result
+ def getuntil(self, terminator):
+ result = ''
+ while True:
+ c = self.next
+ self.__next()
+ if c is None:
+ raise error("unterminated name")
+ if c == terminator:
+ break
+ result += c
+ return result
 def tell(self):
 return self.index, self.next
 def seek(self, index):
@@ -270,7 +279,7 @@
 if code:
 return code
 code = CATEGORIES.get(escape)
- if code and code[0] == IN:
+ if code and code[0] is IN:
 return code
 try:
 c = escape[1:2]
@@ -279,7 +288,7 @@
 escape += source.getwhile(2, HEXDIGITS)
 if len(escape) != 4:
 raise ValueError
- return LITERAL, int(escape[2:], 16) & 0xff
+ return LITERAL, int(escape[2:], 16)
 elif c == "u" and source.istext:
 # unicode escape (exactly four digits)
 escape += source.getwhile(4, HEXDIGITS)
@@ -325,7 +334,7 @@
 escape += source.getwhile(2, HEXDIGITS)
 if len(escape) != 4:
 raise ValueError
- return LITERAL, int(escape[2:], 16) & 0xff
+ return LITERAL, int(escape[2:], 16)
 elif c == "u" and source.istext:
 # unicode escape (exactly four digits)
 escape += source.getwhile(4, HEXDIGITS)
@@ -347,11 +356,11 @@
 elif c in DIGITS:
 # octal escape *or* decimal group reference (sigh)
 if source.next in DIGITS:
- escape = escape + source.get()
+ escape += source.get()
 if (escape[1] in OCTDIGITS and escape[2] in OCTDIGITS and
 source.next in OCTDIGITS):
 # got three octal digits; this is an octal escape
- escape = escape + source.get()
+ escape += source.get()
 c = int(escape[1:], 8)
 if c > 0o377:
 raise error('octal escape value %r outside of '
@@ -370,22 +379,18 @@
 pass
 raise error("bogus escape: %s" % repr(escape))
 
-def _parse_sub(source, state, nested=1):
+def _parse_sub(source, state, nested=True):
 # parse an alternation: a|b|c
 
 items = []
 itemsappend = items.append
 sourcematch = source.match
- while 1:
+ while True:
 itemsappend(_parse(source, state))
- if sourcematch("|"):
- continue
- if not nested:
+ if not sourcematch("|"):
 break
- if not source.next or sourcematch(")", 0):
- break
- else:
- raise error("pattern not properly closed")
+ if nested and source.next is not None and source.next != ")":
+ raise error("pattern not properly closed")
 
 if len(items) == 1:
 return items[0]
@@ -394,7 +399,7 @@
 subpatternappend = subpattern.append
 
 # check if all items share a common prefix
- while 1:
+ while True:
 prefix = None
 for item in items:
 if not item:
@@ -414,16 +419,12 @@
 
 # check if the branch can be replaced by a character set
 for item in items:
- if len(item) != 1 or item[0][0] != LITERAL:
+ if len(item) != 1 or item[0][0] is not LITERAL:
 break
 else:
 # we can store this as a character set instead of a
 # branch (the compiler may optimize this even more)
- set = []
- setappend = set.append
- for item in items:
- setappend(item[0])
- subpatternappend((IN, set))
+ subpatternappend((IN, [item[0] for item in items]))
 return subpattern
 
 subpattern.append((BRANCH, (None, items)))
@@ -433,21 +434,16 @@
 item_yes = _parse(source, state)
 if source.match("|"):
 item_no = _parse(source, state)
- if source.match("|"):
+ if source.next == "|":
 raise error("conditional backref with more than two branches")
 else:
 item_no = None
- if source.next and not source.match(")", 0):
+ if source.next is not None and source.next != ")":
 raise error("pattern not properly closed")
 subpattern = SubPattern(state)
 subpattern.append((GROUPREF_EXISTS, (condgroup, item_yes, item_no)))
 return subpattern
 
-_PATTERNENDERS = set("|)")
-_ASSERTCHARS = set("=!<")
-_LOOKBEHINDASSERTCHARS = set("=!")
-_REPEATCODES = set([MIN_REPEAT, MAX_REPEAT])
-
 def _parse(source, state):
 # parse a simple pattern
 subpattern = SubPattern(state)
@@ -457,32 +453,35 @@
 sourceget = source.get
 sourcematch = source.match
 _len = len
- PATTERNENDERS = _PATTERNENDERS
- ASSERTCHARS = _ASSERTCHARS
- LOOKBEHINDASSERTCHARS = _LOOKBEHINDASSERTCHARS
- REPEATCODES = _REPEATCODES
+ _ord = ord
+ verbose = state.flags & SRE_FLAG_VERBOSE
 
- while 1:
+ while True:
 
- if source.next in PATTERNENDERS:
- break # end of subpattern
- this = sourceget()
+ this = source.next
 if this is None:
 break # end of pattern
+ if this in "|)":
+ break # end of subpattern
+ sourceget()
 
- if state.flags & SRE_FLAG_VERBOSE:
+ if verbose:
 # skip whitespace and comments
 if this in WHITESPACE:
 continue
 if this == "#":
- while 1:
+ while True:
 this = sourceget()
- if this in (None, "\n"):
+ if this is None or this == "\n":
 break
 continue
 
- if this and this[0] not in SPECIAL_CHARS:
- subpatternappend((LITERAL, ord(this)))
+ if this[0] == "\\":
+ code = _escape(source, this, state)
+ subpatternappend(code)
+
+ elif this not in SPECIAL_CHARS:
+ subpatternappend((LITERAL, _ord(this)))
 
 elif this == "[":
 # character set
@@ -494,39 +493,38 @@
 setappend((NEGATE, None))
 # check remaining characters
 start = set[:]
- while 1:
+ while True:
 this = sourceget()
+ if this is None:
+ raise error("unexpected end of regular expression")
 if this == "]" and set != start:
 break
- elif this and this[0] == "\\":
+ elif this[0] == "\\":
 code1 = _class_escape(source, this)
- elif this:
- code1 = LITERAL, ord(this)
 else:
- raise error("unexpected end of regular expression")
+ code1 = LITERAL, _ord(this)
 if sourcematch("-"):
 # potential range
 this = sourceget()
+ if this is None:
+ raise error("unexpected end of regular expression")
 if this == "]":
 if code1[0] is IN:
 code1 = code1[1][0]
 setappend(code1)
- setappend((LITERAL, ord("-")))
+ setappend((LITERAL, _ord("-")))
 break
- elif this:
- if this[0] == "\\":
- code2 = _class_escape(source, this)
- else:
- code2 = LITERAL, ord(this)
- if code1[0] != LITERAL or code2[0] != LITERAL:
- raise error("bad character range")
- lo = code1[1]
- hi = code2[1]
- if hi < lo:
- raise error("bad character range")
- setappend((RANGE, (lo, hi)))
+ if this[0] == "\\":
+ code2 = _class_escape(source, this)
 else:
- raise error("unexpected end of regular expression")
+ code2 = LITERAL, _ord(this)
+ if code1[0] != LITERAL or code2[0] != LITERAL:
+ raise error("bad character range")
+ lo = code1[1]
+ hi = code2[1]
+ if hi < lo:
+ raise error("bad character range")
+ setappend((RANGE, (lo, hi)))
 else:
 if code1[0] is IN:
 code1 = code1[1][0]
@@ -541,7 +539,7 @@
 # XXX: <fl> should add charmap optimization here
 subpatternappend((IN, set))
 
- elif this and this[0] in REPEAT_CHARS:
+ elif this in REPEAT_CHARS:
 # repeat previous item
 if this == "?":
 min, max = 0, 1
@@ -552,20 +550,20 @@
 min, max = 1, MAXREPEAT
 elif this == "{":
 if source.next == "}":
- subpatternappend((LITERAL, ord(this)))
+ subpatternappend((LITERAL, _ord(this)))
 continue
 here = source.tell()
 min, max = 0, MAXREPEAT
 lo = hi = ""
 while source.next in DIGITS:
- lo = lo + source.get()
+ lo += sourceget()
 if sourcematch(","):
 while source.next in DIGITS:
- hi = hi + sourceget()
+ hi += sourceget()
 else:
 hi = lo
 if not sourcematch("}"):
- subpatternappend((LITERAL, ord(this)))
+ subpatternappend((LITERAL, _ord(this)))
 source.seek(here)
 continue
 if lo:
@@ -587,7 +585,7 @@
 item = None
 if not item or (_len(item) == 1 and item[0][0] == AT):
 raise error("nothing to repeat")
- if item[0][0] in REPEATCODES:
+ if item[0][0] in _REPEATCODES:
 raise error("multiple repeat")
 if sourcematch("?"):
 subpattern[-1] = (MIN_REPEAT, (min, max, item))
@@ -604,18 +602,14 @@
 if sourcematch("?"):
 group = 0
 # options
- if sourcematch("P"):
+ char = sourceget()
+ if char is None:
+ raise error("unexpected end of pattern")
+ if char == "P":
 # python extensions
 if sourcematch("<"):
 # named group: skip forward to end of name
- name = ""
- while 1:
- char = sourceget()
- if char is None:
- raise error("unterminated name")
- if char == ">":
- break
- name = name + char
+ name = source.getuntil(">")
 group = 1
 if not name:
 raise error("missing group name")
@@ -623,14 +617,7 @@
 raise error("bad character in group name %r" % name)
 elif sourcematch("="):
 # named backreference
- name = ""
- while 1:
- char = sourceget()
- if char is None:
- raise error("unterminated name")
- if char == ")":
- break
- name = name + char
+ name = source.getuntil(")")
 if not name:
 raise error("missing group name")
 if not name.isidentifier():
@@ -647,27 +634,25 @@
 if char is None:
 raise error("unexpected end of pattern")
 raise error("unknown specifier: ?P%s" % char)
- elif sourcematch(":"):
+ elif char == ":":
 # non-capturing group
 group = 2
- elif sourcematch("#"):
+ elif char == "#":
 # comment
- while 1:
- if source.next is None or source.next == ")":
+ while True:
+ if source.next is None:
+ raise error("unbalanced parenthesis")
+ if sourceget() == ")":
 break
- sourceget()
- if not sourcematch(")"):
- raise error("unbalanced parenthesis")
 continue
- elif source.next in ASSERTCHARS:
+ elif char in "=!<":
 # lookahead assertions
- char = sourceget()
 dir = 1
 if char == "<":
- if source.next not in LOOKBEHINDASSERTCHARS:
+ char = sourceget()
+ if char is None or char not in "=!":
 raise error("syntax error")
 dir = -1 # lookbehind
- char = sourceget()
 p = _parse_sub(source, state)
 if not sourcematch(")"):
 raise error("unbalanced parenthesis")
@@ -676,16 +661,9 @@
 else:
 subpatternappend((ASSERT_NOT, (dir, p)))
 continue
- elif sourcematch("("):
+ elif char == "(":
 # conditional backreference group
- condname = ""
- while 1:
- char = sourceget()
- if char is None:
- raise error("unterminated name")
- if char == ")":
- break
- condname = condname + char
+ condname = source.getuntil(")")
 group = 2
 if not condname:
 raise error("missing group name")
@@ -705,12 +683,14 @@
 raise error("bad group number")
 if condgroup >= MAXGROUPS:
 raise error("the group number is too large")
+ elif char in FLAGS:
+ # flags
+ state.flags |= FLAGS[char]
+ while source.next in FLAGS:
+ state.flags |= FLAGS[sourceget()]
+ verbose = state.flags & SRE_FLAG_VERBOSE
 else:
- # flags
- if not source.next in FLAGS:
- raise error("unexpected end of pattern")
- while source.next in FLAGS:
- state.flags = state.flags | FLAGS[sourceget()]
+ raise error("unexpected end of pattern " + char)
 if group:
 # parse group contents
 if group == 2:
@@ -728,7 +708,7 @@
 state.closegroup(group)
 subpatternappend((SUBPATTERN, (group, p)))
 else:
- while 1:
+ while True:
 char = sourceget()
 if char is None:
 raise error("unexpected end of pattern")
@@ -742,10 +722,6 @@
 elif this == "$":
 subpattern.append((AT, AT_END))
 
- elif this and this[0] == "\\":
- code = _escape(source, this, state)
- subpatternappend(code)
-
 else:
 raise error("parser error")
 
@@ -776,11 +752,11 @@
 p = _parse_sub(source, pattern, 0)
 p.pattern.flags = fix_flags(str, p.pattern.flags)
 
- tail = source.get()
- if tail == ")":
- raise error("unbalanced parenthesis")
- elif tail:
- raise error("bogus characters at end of regular expression")
+ if source.next is not None:
+ if source.next == ")":
+ raise error("unbalanced parenthesis")
+ else:
+ raise error("bogus characters at end of regular expression")
 
 if flags & SRE_FLAG_DEBUG:
 p.dump()
@@ -817,13 +793,7 @@
 if c == "g":
 name = ""
 if s.match("<"):
- while True:
- char = sget()
- if char is None:
- raise error("unterminated group name")
- if char == ">":
- break
- name += char
+ name = s.getuntil(">")
 if not name:
 raise error("missing group name")
 try:
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -166,7 +166,9 @@
 Library
 -------
 
-- Issue 1519638: Now unmatched groups are replaced with empty strings in re.sub()
+- Issue #19380: Optimized parsing of regular expressions.
+
+- Issue #1519638: Now unmatched groups are replaced with empty strings in re.sub()
 and re.subn().
 
 - Issue #18615: sndhdr.what/whathdr now return a namedtuple.
-- 
Repository URL: https://hg.python.org/cpython


More information about the Python-checkins mailing list

AltStyle によって変換されたページ (->オリジナル) /