├── .gitignore ├── R ├── R.py ├── Result.py ├── __init__.py ├── cache.py └── util.py ├── README.md ├── cpp_gen.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /R/R.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from copy import copy 3 | from enum import Enum 4 | from itertools import chain 5 | from typing import Callable 6 | 7 | from .Result import Result, Success, Fail 8 | from .cache import cache_deco, cache_clear 9 | from .util import parse_n, make_gen, str_n, explain_n 10 | 11 | 12 | class RecursionWrapper: 13 | def __init__(self): 14 | self.val = None 15 | 16 | 17 | class Mode(Enum): 18 | ''' 19 | 贪心匹配 Mode.greedy 20 | 懒惰匹配 Mode.lazy 21 | ''' 22 | greedy = 'G' 23 | lazy = 'L' 24 | 25 | 26 | class R: 27 | ''' 28 | 正则表达式引擎 29 | ''' 30 | 31 | # --- basic --- 32 | def __init__(self, target, num=None, name: str = None, mode=Mode.greedy): 33 | self._target = target 34 | self.num_t = parse_n(num) 35 | self.name = name 36 | self.mode = mode 37 | 38 | # 逻辑关系单个实例中互斥 39 | self.and_r = None 40 | self.or_r = None 41 | self.invert = False 42 | self.xor_r = None 43 | self.next_r = None 44 | 45 | # 状态机 46 | self.gen = make_gen(self.target) if not isinstance(self.target, (R, RecursionWrapper)) else None 47 | 48 | @property 49 | def target(self): 50 | if isinstance(self._target, RecursionWrapper): 51 | return self._target.val if self._target.val is not None else self._target 52 | else: 53 | return self._target 54 | 55 | @target.setter 56 | def target(self, val): 57 | self._target = val 58 | 59 | def __and__(self, other: 'R'): 60 | this = self.clone() 61 | this.and_r = other 62 | return R(this) 63 | 64 | def __or__(self, other: 'R'): 65 | this = self.clone() 66 | this.or_r = other 67 | return R(this) 68 | 69 | def __invert__(self): 70 | this = self.clone() 71 | this.invert = True 72 | return R(this) 73 | 74 | def __xor__(self, other: 'R'): 75 | this = self.clone() 76 | this.xor_r = other 77 | return R(this) 78 | 79 | # @ 在 Python 中表示矩阵乘法, 非常近似于 next 80 | def __matmul__(self, other: 'R'): 81 | this = self.clone() 82 | this.next_r = other 83 | return R(this) 84 | 85 | def __repr__(self): 86 | if self.gen and isinstance(self.target, Callable): 87 | s = '%{}%'.format(self.target.__name__) 88 | elif isinstance(self._target, RecursionWrapper): 89 | return '' 90 | else: 91 | s = str(self.target) 92 | num_str = str_n(self.num_t) 93 | if num_str: 94 | s = '({}{}{})'.format(s, num_str, '?' if self.mode is Mode.lazy else '') 95 | 96 | if self.and_r: 97 | s = '({}&{})'.format(s, self.and_r) 98 | elif self.or_r: 99 | s = '({}|{})'.format(s, self.or_r) 100 | elif self.invert: 101 | s = '(~{})'.format(s) 102 | elif self.xor_r: 103 | s = '({}^{})'.format(s, self.xor_r) 104 | 105 | if self.next_r: 106 | s += str(self.next_r) 107 | return s 108 | 109 | def clone(self, num=None, name: str = None, mode: Mode = None): 110 | this = copy(self) 111 | if num is not None: 112 | this.num_t = parse_n(num) 113 | if name: 114 | this.name = name 115 | if mode: 116 | this.mode = mode 117 | return this 118 | 119 | # --- core --- 120 | @cache_deco 121 | def imatch(self, resource: str, prev_result: Result): 122 | ''' 123 | 参数: 字符串(resource), 上一个状态机的结果(prev_result) 124 | 返回 iter, 按模式 yield 所有结果 125 | 126 | 正则匹配可以看成图论, imatch 就像节点用 stream 或者说 pipe 连接 127 | ''' 128 | # 约定: from_num 和 to_num 在匹配开始时就已经确定 129 | from_num, to_num = explain_n(prev_result, self.num_t) 130 | 131 | prev_ed = prev_result.ed 132 | 133 | def capture_add(echo: Result): 134 | ''' 135 | 在 capture 添加 echo, 用于捕获组 136 | ''' 137 | nonlocal prev_ed 138 | if self.name and echo: 139 | group = echo.capture.get(self.name, ()) 140 | echo.capture = {**echo.capture, self.name: [*group, (prev_ed, echo.ed)]} 141 | prev_ed = echo.ed 142 | return echo 143 | 144 | if self.gen: 145 | # 已递归到最里层 146 | def stream4num(): 147 | if from_num == 0: 148 | # 可选匹配 149 | yield prev_result 150 | if to_num == 0: 151 | # 不会匹配到更多 152 | return 153 | 154 | counter = 0 155 | fa = self.gen(prev_result) 156 | next(fa) 157 | for char in resource[prev_result.ed:]: 158 | echo = fa.send(char) 159 | 160 | if echo == 'GO': 161 | continue 162 | elif isinstance(echo, Success): 163 | counter += 1 164 | capture_add(echo) 165 | if from_num <= counter <= to_num: 166 | yield echo 167 | if counter < to_num: 168 | # 未到达边界, 置换 fa 169 | fa = self.gen(echo) 170 | next(fa) 171 | else: 172 | return 173 | elif isinstance(echo, Fail): 174 | yield echo 175 | return 176 | 177 | stream4num = stream4num() if self.mode is Mode.lazy else reversed(tuple(stream4num())) 178 | else: 179 | def stream4num(): 180 | if to_num == 0: 181 | yield prev_result 182 | return 183 | if self.mode is Mode.lazy and from_num == 0: 184 | yield prev_result 185 | 186 | # DFS 187 | counter = 1 188 | curr_iter = self.target.imatch(resource, prev_result) 189 | while counter < from_num: 190 | counter += 1 191 | curr_iter = chain.from_iterable(self.target.imatch(resource, i) for i in curr_iter if i) 192 | 193 | q = deque() 194 | q.append((curr_iter, counter)) 195 | while q: 196 | last = q[-1] 197 | curr_iter, nth = last[:2] 198 | try: 199 | echo = capture_add(next(curr_iter)) 200 | 201 | if self.mode is Mode.lazy: 202 | yield echo 203 | if echo and nth < to_num: 204 | q.append((self.target.imatch(resource, echo), nth + 1)) 205 | else: 206 | if echo and nth < to_num: 207 | q.append((self.target.imatch(resource, echo), nth + 1, echo)) 208 | else: 209 | yield echo 210 | except StopIteration: 211 | q.pop() 212 | if len(last) == 3: 213 | yield last[-1] 214 | 215 | if self.mode is Mode.greedy and from_num == 0: 216 | yield prev_result 217 | 218 | stream4num = stream4num() 219 | # 数量关系处理完毕 220 | 221 | if self.and_r: 222 | def stream4logic(): 223 | for echo in stream4num: 224 | if not echo: 225 | yield echo 226 | else: 227 | echo.as_fail() 228 | for and_echo in self.and_r.imatch(resource[prev_result.ed:echo.ed], Result(0, 0)): 229 | if and_echo and and_echo.ed == echo.ed - prev_result.ed: 230 | echo.as_success() 231 | break 232 | yield echo 233 | 234 | elif self.or_r: 235 | def stream4logic(): 236 | yield from chain(stream4num, self.or_r.imatch(resource, prev_result)) 237 | 238 | elif self.invert: 239 | def stream4logic(): 240 | for echo in stream4num: 241 | yield echo.invert() 242 | 243 | elif self.xor_r: 244 | def stream4logic(): 245 | for echo in stream4num: 246 | demand_bool = not bool(echo) 247 | echo.as_fail() 248 | 249 | for xor_echo in self.xor_r.imatch(resource[prev_result.ed:echo.ed], Result(0, 0)): 250 | if bool(xor_echo) is demand_bool and xor_echo.ed == echo.ed - prev_result.ed: 251 | yield echo.as_success() 252 | break 253 | else: 254 | yield echo 255 | 256 | else: 257 | def stream4logic(): 258 | yield from stream4num 259 | # 逻辑关系处理完毕 260 | stream4logic = stream4logic() 261 | 262 | if self.next_r: 263 | yield from chain.from_iterable(self.next_r.imatch(resource, echo) for echo in filter(bool, stream4logic)) 264 | else: 265 | yield from stream4logic 266 | 267 | def match(self, resource: str): 268 | output_l = [] 269 | cursor = Result(0, 0) 270 | while cursor.ed < len(resource): 271 | for echo in self.imatch(resource, cursor): 272 | if echo: 273 | output_l.append(echo) 274 | op = max(echo.ed, cursor.ed + 1) 275 | cursor = Result(op, op) 276 | break 277 | else: 278 | cursor.op += 1 279 | cursor.ed += 1 280 | cache_clear() 281 | return output_l 282 | -------------------------------------------------------------------------------- /R/Result.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | from pprint import pformat 3 | 4 | 5 | class Result: 6 | ''' 7 | 记录字符串已匹配的开始和结束, 以及沿途捕获的组 8 | ''' 9 | 10 | def __init__(self, op: int, ed: int, capture: dict = None): 11 | self.op = op 12 | self.ed = ed 13 | # {..., name: [... (op, ed)]} 14 | self._capture = capture if capture is not None else {} 15 | 16 | self._hash = 0 17 | self.update = True 18 | 19 | def __repr__(self): 20 | return 'Result({}, {}, {})'.format(self.op, self.ed, pformat(self.capture)) 21 | 22 | @property 23 | def capture(self): 24 | return self._capture 25 | 26 | @capture.setter 27 | def capture(self, val: dict): 28 | self.update = True 29 | self._capture = val 30 | 31 | @property 32 | def hash(self): 33 | if self.update: 34 | self.update = False 35 | self._hash = str(sorted(self.capture.items())) 36 | return self._hash 37 | 38 | def clone(self, **kwargs): 39 | this = copy(self) 40 | for k, v in kwargs: 41 | setattr(this, k, v) 42 | return this 43 | 44 | # 由于需要对结果取 XOR, NOT, 所以有两个状态 Success 和 Fail, 并可以互相转化 45 | def as_success(self): 46 | self.__class__ = Success 47 | return self 48 | 49 | def as_fail(self): 50 | self.__class__ = Fail 51 | return self 52 | 53 | 54 | class Success(Result): 55 | def invert(self): 56 | return self.as_fail() 57 | 58 | 59 | class Fail(Result): 60 | def __bool__(self): 61 | return False 62 | 63 | def invert(self): 64 | return self.as_success() 65 | -------------------------------------------------------------------------------- /R/__init__.py: -------------------------------------------------------------------------------- 1 | from .R import R, Mode, RecursionWrapper 2 | from .util import BranchStop 3 | 4 | r = R 5 | -------------------------------------------------------------------------------- /R/cache.py: -------------------------------------------------------------------------------- 1 | if False: 2 | # 仅用于类型检查 3 | from .Result import Result 4 | from .R import R 5 | 6 | # {..., k: (share_l, share_iter)} 7 | cache = {} 8 | 9 | 10 | def cache_clear(): 11 | cache.clear() 12 | 13 | 14 | def cache_deco(imatch): 15 | ''' 16 | 缓存 R 中 imatch 的修饰器 17 | ''' 18 | 19 | def memo_imatch(self: 'R', resource: str, prev_result: 'Result'): 20 | def recursion_correct(result: 'Result'): 21 | result.op = max(result.op, prev_result.op) 22 | return result 23 | 24 | k = (id(self), prev_result.ed, prev_result.hash) 25 | share_l, share_iter = cache.setdefault(k, ([], imatch(self, resource, prev_result))) 26 | yield from map(lambda echo: recursion_correct(echo.clone(op=prev_result.ed)), share_l) 27 | 28 | while True: 29 | try: 30 | echo = next(share_iter) 31 | except StopIteration: 32 | break 33 | share_l.append(echo) 34 | yield recursion_correct(echo) 35 | 36 | return memo_imatch 37 | -------------------------------------------------------------------------------- /R/util.py: -------------------------------------------------------------------------------- 1 | from math import inf 2 | from typing import Callable 3 | 4 | if False: 5 | # 仅用于类型检查 6 | from .Result import Result 7 | 8 | 9 | def parse_n(num): 10 | ''' 11 | 将构造 R 时的 num 参数转化为统一的 tuple 12 | 约定: 如果是 num 本身就是 tuple, 那么默认长度为 2 且内容类型相同 13 | ''' 14 | if num is None: 15 | return 1, 1 16 | if isinstance(num, tuple): 17 | return num 18 | if isinstance(num, (int, Callable)): 19 | return num, num 20 | 21 | if isinstance(num, str): 22 | if num == '*': 23 | return 0, inf 24 | if num == '+': 25 | return 1, inf 26 | if num.startswith(':'): 27 | return num, num 28 | 29 | if num.startswith('{') and num.endswith('}'): 30 | num = tuple(map(int, num[1:-1].split(','))) 31 | if len(num) == 1: 32 | num *= 2 33 | return num 34 | raise TypeError 35 | 36 | 37 | def str_n(num_t: tuple): 38 | ''' 39 | 将 parse_t 得到的 num_t 转化为字符串 40 | ''' 41 | from_num, to_num = num_t 42 | if isinstance(from_num, Callable): 43 | tpl = '%{}%'.format 44 | from_num, to_num = tpl(from_num.__name__), tpl(to_num.__name__) 45 | 46 | if from_num == to_num: 47 | # {1,1}无需显示 48 | if from_num == 1: 49 | return '' 50 | return '{' + str(from_num) + '}' 51 | else: 52 | return '{' + str(from_num) + ',' + str(to_num) + '}' 53 | 54 | 55 | def explain_n(result: 'Result', num_t: tuple): 56 | ''' 57 | 如果 num 是由符号和函数定义的, 那么需要在运行时得到确定的 num_t 58 | ''' 59 | from_num, to_num = num_t 60 | 61 | # 符号定义, 则查询捕获组 62 | if isinstance(from_num, str): 63 | from_num = len(result.capture.get(from_num, ())) 64 | to_num = len(result.capture.get(to_num, ())) 65 | 66 | # 函数定义, 传入捕获组 67 | elif isinstance(from_num, Callable): 68 | from_num = from_num(result.capture) 69 | to_num = to_num(result.capture) 70 | 71 | assert 0 <= from_num <= to_num 72 | return from_num, to_num 73 | 74 | 75 | class BranchStop(Exception): 76 | pass 77 | 78 | 79 | def make_gen(target): 80 | ''' 81 | 返回一个 generator 来抽象状态机 82 | generator 接受一个 Result 来实例化 83 | ''' 84 | if isinstance(target, str): 85 | # 目标是 str, 则不断拿 send 来的 char 来比对目标中的 char 86 | def gen(prev_result: 'Result'): 87 | curr_result = prev_result.clone() 88 | 89 | for expect_char in target: 90 | recv_char = yield 'GO' 91 | curr_result.ed += 1 92 | 93 | if recv_char != expect_char: 94 | yield curr_result.as_fail() 95 | # 全匹配 96 | yield curr_result.as_success() 97 | 98 | elif isinstance(target, Callable): 99 | # 目标是函数, 传入 recv_char, 根据真假返回结果 100 | def gen(prev_result: 'Result'): 101 | curr_result = prev_result.clone() 102 | recv_char = yield 'GO' 103 | curr_result.ed += 1 104 | 105 | res = target(recv_char) 106 | if isinstance(res, BranchStop): 107 | res.args = (curr_result.op, curr_result.ed) 108 | raise res 109 | elif res: 110 | yield curr_result.as_success() 111 | else: 112 | yield curr_result.as_fail() 113 | 114 | else: 115 | raise TypeError 116 | return gen 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EasyRegex 2 | 一个符合程序员直觉的正则引擎 3 | 4 | MIT协议发布 5 | 6 | # 使用方法 7 | 拷贝 R 文件夹到程序的 root 8 | 9 | 依赖: Python 3.5+ 10 | 11 | ```Python 12 | from R import r, Mode, RecursionWrapper, BranchStop 13 | 14 | # 匹配'abc' 15 | m = r('abc') 16 | m.match('abcdabdabccc') 17 | # >> [Result(0, 3, {}), Result(7, 10, {})] 18 | # Result(0, 3, {}) 表示从位置0匹配到位置3, 捕获组为空 19 | 20 | # 用 @ 连接 R 对象, 以匹配更长的字符串 21 | m = r('abc') @ r('d') @ r('a') # 等价于 r('abcda') 22 | m.match('abcdabdabccc') 23 | # >> [Result(0, 5, {})] 24 | 25 | # 函数作为匹配目标 26 | m = r('1') @ r(str.isalpha) @ r('1') 27 | m.match('a1a1') 28 | # >> [Result(1, 4, {})] 29 | 30 | # 带数量条件的匹配 31 | m = r('b', '{1,2}') @ r('cd') 32 | m = r('b', (1, 2)) @ r('cd') # 二者等价 33 | m.match('bbcda') 34 | # >> [Result(0, 4, {})] 35 | 36 | # 懒惰模式(默认贪婪模式) 37 | m = r('ab') @ r('c', '*', Mode.lazy) 38 | m = r('ab') @ r('c', (0, inf), Mode.lazy) # 二者等价 39 | m.match('abcccc') 40 | # >> [Result(0, 2, {})] 41 | 42 | # 通配符 43 | dot = r(lambda char: True) 44 | m = r('a') @ dot.clone('*') @ r('a') 45 | m = r('a') @ r(lambda char: True, '*') @ r('a') # 二者等价 46 | m.match('123a123a123') 47 | # >> [Result(3, 8, {})] 48 | 49 | # 嵌套 50 | m = r(r('a'), 5) 51 | m = r('a', 5) # 二者等价 52 | m.match('qaaaaaq') 53 | # >> [Result(1, 6, {})] 54 | m = r('q') @ r(r('a'), '+', Mode.lazy) 55 | m = r('q') @ r('a', (1, inf), Mode.lazy) # 二者等价 56 | m.match('qaaaaaa') 57 | # >> [Result(0, 2, {})] 58 | 59 | # AND 60 | m = (r('abc') & r('abc')) @ r('d') 61 | m.match('abcd') 62 | # >> [Result(0, 4, {})] 63 | startswith_abc = r('abc') @ dot.clone('*') 64 | endswith_abc = dot.clone('*') @ r('abc') 65 | m = startswith_abc & endswith_abc # 等价于标准正则的 abc.*abc 66 | m.match('1abchhabc1') 67 | # >> [Result(1, 9, {})] 68 | 69 | # OR 70 | m = (r('a') | r('b')) @ r('bc') 71 | m.match('abcbbc') 72 | # >> [Result(0, 3, {}), Result(3, 6, {})] 73 | 74 | # NOT 75 | digit = r(str.isdigit) 76 | no_digit = ~digit # 非数字 77 | m = no_digit.clone('+') 78 | m.match('123yyyyy123') 79 | # >> [Result(3, 8, {})] 80 | 81 | # XOR 82 | m = (r('ab') ^ r('ab')) @ r('c') 83 | m.match('abc') 84 | # >> [] 85 | 86 | # 带捕获组的匹配 87 | m = r('b', '{1,2}', ':b') @ r('cd') 88 | m.match('bbcda') 89 | # >> [Result(0, 4, {':b': [(0, 1), (1, 2)]})] 90 | 91 | # 捕获组影响数量条件 92 | m = r('b', '+', ':b') @ r('cd', ':b') 93 | # 含义: 贪婪匹配一或多个'b'并以':b'为名字存入捕获组 94 | # 接下来需要'cd'的个数是名为':b'的捕获组的长度 95 | m.match('bbcdcd') 96 | # >> [Result(0, 6, {':b': [(0, 1), (1, 2)]})] 97 | 98 | # 函数数量条件 99 | m = r('a', name=':a') @ r('b', 100 | lambda capture: len(capture.get(':a',())) + 1) 101 | # 含义: 匹配1个'a'并存入捕获组, 接下来'b'的个数是名为':a'的捕获组的长度加一 102 | ``` 103 | 104 | # 进阶用法 105 | 以匹配嵌套DIV标签为例 106 | 107 | ```Python 108 | div_head = r('', name=':tail') 110 | no_head_tail = ~(div_head | div_tail) 111 | 112 | # 当捕获组内':head'和':tail'的长度相等时返回0, 否则为1 113 | def stop_head_tail_equal(capture: dict): 114 | head_group = capture.get(':head', ()) 115 | tail_group = capture.get(':tail', ()) 116 | return 1 if not head_group or not tail_group or len(head_group) != len(tail_group) else 0 117 | 118 | # '\0'是一个不可能存在的字符, 这里拿来当开关用 119 | sentinel = r('\0', stop_head_tail_equal) 120 | 121 | div = div_head @ r(div_head | div_tail | no_head_tail, '+') @ div_tail @ sentinel 122 | # 含义: 不断匹配 DIV 头标签和尾标签, 直到二者数量相同 123 | m.match('0
1
2
3
4') 124 | # >> [Result(1, 26, {':head': [(1, 5), (7, 11)], ':tail': [(13, 19), (20, 26)]})] 125 | # '0
1
2
3
4'[1:26] == '
1
2
3
' 126 | ``` 127 | 128 | # 终极用法 129 | 生成 AST 的编译器前端 130 | 131 | ```Python 132 | # 由于需要递归展开, rw 作为一个懒惰传值的容器 133 | rw = RecursionWrapper() 134 | block = (r('{') @ r(rw, '*') @ r('}')).clone(name=':block') 135 | rw.val = block 136 | # 意为 block = '{' + block + '}' 137 | 138 | block.match('{{{{{}{}}}') 139 | # >> [Result(2, 10, {':block': [(4, 6), (6, 8), (3, 9), (2, 10)]})] 140 | ``` 141 | 142 | 匹配终止异常 143 | 144 | ```Python 145 | # 当匹配 a 之后的 b 失败时, 抛出 BranchStop 异常 146 | path = r('a') @ (r('b') | r(lambda char: BranchStop())) 147 | try: 148 | path.match('ag') 149 | except BranchStop as bs: 150 | # args 为匹配终止时的区间 151 | # bs.args == (0, 2) 152 | ``` -------------------------------------------------------------------------------- /cpp_gen.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | from R import r, Mode 4 | 5 | input_str = ''' 6 | void PiXiuCtrl::init_prop() { 7 | Glob_Reinsert_Chunk = NULL; 8 | this->cbt.root = NULL; 9 | this->st.init_prop(); 10 | } 11 | 12 | $gen range_0_j(int j) { 13 | "'\\"'" 14 | '"\\'"' 15 | // 16 | /* 17 | * 18 | */ 19 | int j; 20 | j = 2; 21 | for (int i = 0; i < j; ++i) { 22 | $yield(i); 23 | } 24 | } 25 | 26 | void PiXiuCtrl::free_prop() { 27 | this->st.free_prop(); 28 | this->cbt.free_prop(); 29 | } 30 | ''' 31 | 32 | 33 | def make_default(d: dict): 34 | default_dict = defaultdict(lambda: (), d) 35 | return default_dict 36 | 37 | 38 | def success_get_0(capture: dict): 39 | capture = make_default(capture) 40 | if len(capture[':}']) == len(capture[':{']) and len(capture[':)']) == len(capture[':(']): 41 | return 0 42 | return 1 43 | 44 | 45 | sentinel = r('\0', success_get_0) 46 | 47 | spaces = r(str.isspace, '+') 48 | may_spaces = r(str.isspace, '*') 49 | 50 | char = r(lambda char: True) 51 | char_except_pair = r(lambda char: char not in '(){}') 52 | may_chars_except_pair = char_except_pair.clone('*') 53 | 54 | l_parentheses = r('(', name=':(') 55 | r_parentheses = r(')', name=':)') 56 | l_bracket = r('{', name=':{') 57 | r_bracket = r('}', name=':}') 58 | 59 | func_name = r(lambda char: str.isalpha(char) or str.isdigit(char) or char == '_', '+') 60 | type_name = r(func_name @ r(' *', (0, 1)), name=':type') 61 | var_name = r(func_name, name=':var') 62 | 63 | param = may_spaces @ type_name @ spaces @ var_name @ may_spaces @ r(',', (0, 1)) 64 | func_params = (l_parentheses @ param.clone('*', ':declaration') @ r_parentheses).clone() 65 | 66 | # body 67 | string_0 = may_spaces @ (r('"') @ (r('\\"') | (~r('"'))).clone('*') @ r('"')).clone(name=':string') 68 | string_1 = may_spaces @ (r("'") @ (r("\\'") | (~r("'"))).clone('*') @ r("'")).clone(name=':string') 69 | string = string_0 | string_1 70 | 71 | comment_0 = may_spaces @ (r('//') @ (~r('\n')).clone('*') @ r('\n')).clone(name=':comment') 72 | comment_1 = may_spaces @ (r('/*') @ char.clone('*', mode=Mode.lazy) @ r('*/')).clone(name=':comment') 73 | comment = comment_0 | comment_1 74 | 75 | declaration = (type_name @ spaces @ var_name @ may_spaces @ (r(';') | r('='))).clone(name=':declaration') 76 | code = char_except_pair | l_parentheses | r_parentheses | l_bracket | r_bracket 77 | 78 | func_body = (string | comment | declaration | code).clone('*', mode=Mode.lazy) 79 | # ~body 80 | 81 | matcher = ( 82 | r('$gen ') @ may_spaces 83 | @ func_name @ may_spaces # range_0_j 84 | @ func_params @ may_spaces # (int j) 85 | @ l_bracket @ may_spaces # { 86 | @ func_body @ may_spaces 87 | @ r_bracket # } 88 | @ sentinel 89 | ) 90 | 91 | result = matcher.match(input_str) 92 | print(result) 93 | for i in result: 94 | print() 95 | print(input_str[i.op:i.ed]) 96 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from R import r, Mode, RecursionWrapper, BranchStop 2 | 3 | # 通配符 4 | dot = r(lambda char: True) 5 | 6 | 7 | def t_str(): 8 | ''' 9 | R 的 stringify 是否符合预期 10 | ''' 11 | m = (r('abc') | r('cfg')) @ r('iop') @ r('iop') 12 | assert str(m) == '(abc|cfg)iopiop' 13 | 14 | m = (r('abc') | r('cfg', '{1}')) @ r('iop') @ r('iop', '{1}') 15 | assert str(m) == '(abc|cfg)iopiop' 16 | 17 | m = (r('abc') & r('abc') & r('abc')) @ r('d') 18 | assert str(m) == '((abc&abc)&abc)d' 19 | 20 | m = (r('abc') & r('abc') & r('abc')).clone(num='{1,2}', mode=Mode.lazy) @ r('d') 21 | assert str(m) == '(((abc&abc)&abc){1,2}?)d' 22 | 23 | m = r('a', '+', ':a') @ r('b', lambda capture: len(capture.get(':a', ()))) 24 | assert str(m) == '(a{1,inf})(b{%%})' 25 | 26 | m = (r('abc') | r('abb')) @ r('abc') 27 | assert str(m) == '(abc|abb)abc' 28 | 29 | alpha = r(str.isalpha, '+') 30 | m = (alpha & r('abc')) & (r('abc') & alpha) 31 | assert str(m) == '(((%isalpha%{1,inf})&abc)&(abc&(%isalpha%{1,inf})))' 32 | 33 | no_alpha = ~alpha 34 | assert str(no_alpha) == '(~(%isalpha%{1,inf}))' 35 | 36 | m = (r('a') ^ r('b')) @ r('c') 37 | assert str(m) == '(a^b)c' 38 | 39 | 40 | def t_simple(): 41 | ''' 42 | 没有条件的简单匹配 43 | ''' 44 | m = r('abc') 45 | assert str(m.match('abcdabdabccc')) == '[Result(0, 3, {}), Result(7, 10, {})]' 46 | 47 | for m in (r('abc') @ r('d') @ r('a'), r('abc') @ r(r('d') @ r('a'))): 48 | assert str(m.match('abcdabdabccc')) == '[Result(0, 5, {})]' 49 | assert str(m.match('aabcdabdabccc')) == '[Result(1, 6, {})]' 50 | 51 | # --- 函数匹配 52 | m = r('1') @ r(str.isalpha) @ r('1') 53 | assert str(m.match('a1a1')) == '[Result(1, 4, {})]' 54 | # --- 55 | 56 | 57 | def t_num(): 58 | ''' 59 | 数量条件的匹配 60 | ''' 61 | for m in (r('b', '{1,2}') @ r('cd'), r('b', '{2}') @ r('cd')): 62 | assert str(m.match('bbcda')) == '[Result(0, 4, {})]' 63 | 64 | m = r('b', '{0,1}') @ r('cd') 65 | assert str(m.match('cdabcd')) == '[Result(0, 2, {}), Result(3, 6, {})]' 66 | 67 | m = r('a') @ r('b') @ r('c', 0) @ r('d') 68 | assert str(m.match('abd')) == '[Result(0, 3, {})]' 69 | 70 | for m in (r('ab') @ r('c', '*'), r('ab') @ r('c', '+')): 71 | assert str(m.match('abcccc')) == '[Result(0, 6, {})]' 72 | 73 | # --- 懒惰模式 74 | m = (r('ab') @ r('c', '*', mode=Mode.lazy)) 75 | assert str(m.match('abcccc')) == '[Result(0, 2, {})]' 76 | 77 | m = (r('ab') @ r('c', (1, 2), mode=Mode.lazy)) 78 | assert str(m.match('abcccc')) == '[Result(0, 3, {})]' 79 | # --- 80 | 81 | # --- 通配符 82 | m = r('a') @ dot.clone('*') @ r('a') 83 | assert str(m.match('123a123a123')) == '[Result(3, 8, {})]' 84 | # --- 85 | 86 | # --- 嵌套 87 | for m in (r(r('b'), '*') @ r('cd'), r(r('b'), '*', mode=Mode.lazy) @ r('cd')): 88 | assert str(m.match('cd')) == '[Result(0, 2, {})]' 89 | 90 | m = r(r('a'), 5) 91 | assert str(m.match('qaaaaaq')) == '[Result(1, 6, {})]' 92 | 93 | m = r(r('a'), 0) @ r('q') 94 | assert str(m.match('qaaaaaq')) == '[Result(0, 1, {}), Result(6, 7, {})]' 95 | 96 | m = r('q') @ r(r('a'), '+', mode=Mode.lazy) 97 | assert str(m.match('qaaaaaa')) == '[Result(0, 2, {})]' 98 | # --- 99 | 100 | 101 | def t_and(): 102 | ''' 103 | and 条件的匹配 104 | ''' 105 | m = (r('abc') & r('abc')) @ r('d') 106 | assert str(m.match('abcd')) == '[Result(0, 4, {})]' 107 | assert str((m @ m).match('abcd' * 2)) == '[Result(0, 8, {})]' 108 | 109 | m = (r('a') & r('b')) @ r('d') 110 | assert str(m.match('ad')) == '[]' 111 | 112 | startswith_abc = r('abc') @ dot.clone('*') 113 | endswith_abc = dot.clone('*') @ r('abc') 114 | m = startswith_abc & endswith_abc 115 | assert str(m.match('1abchhabc1')) == '[Result(1, 9, {})]' 116 | 117 | 118 | def t_or(): 119 | ''' 120 | or 条件的匹配 121 | ''' 122 | m = (r('a') | r('b')) @ r('bc') 123 | assert str(m.match('abcbbc')) == '[Result(0, 3, {}), Result(3, 6, {})]' 124 | 125 | m = (r('abc') | r('cfg')) @ r('iop') @ r('iop') 126 | assert str(m.match('pppcfgiopiop')) == '[Result(3, 12, {})]' 127 | 128 | 129 | def t_not(): 130 | ''' 131 | not 条件的匹配 132 | ''' 133 | digit = r(str.isdigit) 134 | no_digit = ~digit 135 | m = no_digit.clone('+') 136 | assert str(m.match('123yyyyy123')) == '[Result(3, 8, {})]' 137 | 138 | 139 | def t_xor(): 140 | ''' 141 | xor 条件的匹配 142 | ''' 143 | m = (r('a') ^ r('b')) @ r('c') 144 | assert str(m.match('ac')) == '[Result(0, 2, {})]' 145 | assert str(m.match('bc')) == '[Result(0, 2, {})]' 146 | assert str(m.match('cc')) == '[]' 147 | 148 | m = (r('a') ^ r('ab')) @ r('c') 149 | assert str(m.match('ac')) == '[]' 150 | assert str(m.match('abc')) == '[]' 151 | 152 | m = (r('ab') ^ r('ab')) @ r('c') 153 | assert str(m.match('abc')) == '[]' 154 | 155 | 156 | def t_exception(): 157 | ''' 158 | 异常是否正常触发 159 | ''' 160 | counter = 0 161 | for func in (lambda: r(1), lambda: r('foo', 'bar')): 162 | try: 163 | func() 164 | except TypeError: 165 | counter += 1 166 | assert counter == 2 167 | 168 | 169 | def t_name(): 170 | ''' 171 | 带捕获组的匹配 172 | ''' 173 | m = r('b', '{1,2}', ':b') @ r('cd') 174 | assert str(m.match('bbcda')) == "[Result(0, 4, {':b': [(0, 1), (1, 2)]})]" 175 | 176 | # --- 捕获组影响数量条件 177 | for m in (r(r('b', 1, ':b'), 2) @ r('cd', ':b'), r('b', '+', ':b') @ r('cd', ':b'), 178 | r('b', '+', ':a').clone(name=':b') @ r('cd', ':b')): 179 | assert str(m.match('bbcdcd')) == "[Result(0, 6, {':b': [(0, 1), (1, 2)]})]" 180 | 181 | # ------ 函数数量条件 182 | m = r('a', name=':a') @ r('b', lambda capture: len(capture.get(':a', ())) + 1) 183 | assert str(m.match('aabbbbb')) == "[Result(1, 4, {':a': [(1, 2)]})]" 184 | # ------ 185 | # --- 186 | 187 | 188 | def t_div(): 189 | ''' 190 | 匹配嵌套 DIV 191 | ''' 192 | code = '0
1
2
3
4' 193 | div_head = r('', name=':tail') 195 | no_head_tail = ~(div_head | div_tail) 196 | 197 | def stop_head_tail_equal(capture: dict): 198 | head_group = capture.get(':head', ()) 199 | tail_group = capture.get(':tail', ()) 200 | return 1 if not head_group or not tail_group or len(head_group) != len(tail_group) else 0 201 | 202 | sentinel = r('\00', stop_head_tail_equal) 203 | 204 | div = r(div_head | div_tail | no_head_tail, '+') @ sentinel 205 | assert str(div.match(code)) == "[Result(0, 27, {':head': [(1, 5), (7, 11)], ':tail': [(13, 19), (20, 26)]})]" 206 | 207 | div = div_head @ r(div_head | div_tail | dot, '+') @ div_tail @ sentinel 208 | assert str(div.match(code)) == "[Result(1, 26, {':head': [(1, 5), (7, 11)], ':tail': [(13, 19), (20, 26)]})]" 209 | 210 | 211 | def t_recursive(): 212 | rw = RecursionWrapper() 213 | block = (r('{') @ r(rw, '*') @ r('}')).clone(name=':block') 214 | rw.val = block 215 | assert str(block.match('{{{{{}{}}}')) == "[Result(2, 10, {':block': [(4, 6), (6, 8), (3, 9), (2, 10)]})]" 216 | 217 | 218 | def t_branch_stop(): 219 | path = r('a') @ (r('b') | r(lambda char: BranchStop())) 220 | try: 221 | path.match('ag') 222 | except BranchStop as bs: 223 | assert bs.args == (0, 2) 224 | 225 | 226 | for func in ( 227 | t_str, 228 | t_simple, 229 | t_num, 230 | t_and, 231 | t_or, 232 | t_not, 233 | t_xor, 234 | t_exception, 235 | t_name, 236 | t_div, 237 | t_recursive, 238 | t_branch_stop, 239 | ): 240 | func() 241 | print('all pass') 242 | --------------------------------------------------------------------------------