正则表达式,看完这一篇博客即可

背景

做了一段时间的genai相关应用.遇到如下的一种场景:一次大模型调用输出多个模块代码,从输出内容里面提取这些模块代码然后保存成多个文件,组合成一个可运行的项目或者完整的功能模块. 这里就涉及到对大模型返回的原始文本结果用正则表达式进行处理的场景了.本文详细回顾一下python和java里面正则表达式的用法以及技巧,并探究了正则表达式在大模型场景下的应用.

正则表达式回顾

以python语言中的re模块为例,回顾python中正则表达式中的特殊符号.

. 默认情况下匹配任意非换行符单个字符串, python re中可以使用 re.DOTALL (简写为 re.S)flag 让它能匹配任意字符串包含换行符号.

>>> re.search(r'abc.',"""abc
... """)
>>> re.search(r'abc.',"""abc
... """,re.DOTALL)
<re.Match object; span=(0, 4), match='abc\n'>

^ 默认表示在整个目标字符串上匹配以指定字符串开始的模式,配合re.MULTILINE (简写为re.M) 可以在多行上进行匹配.比如下面的例子,如果不用re.M flag 整个字符串不是以abc开始,所以失败,如果使用re.M 可以发字符串第二行以abc开始,匹配成功.

>>> re.search(r'^abc',"""dbf
... abc
... dsd""")
>>> re.search(r'^abc',"""dbf
... abc
... dsd""",re.M)
<re.Match object; span=(4, 7), match='abc'>

$^类似, 默认在整个目标字符串上匹配以指定字符串结尾的模式,配合re.re.MULTILINE实现在多行上进行匹配.

>>> re.search(r'abc$',"""sdsdsd
... abc
... dsdds""")
>>> re.search(r'abc$',"""sdsdsd
... abc
... dsdds""",re.M)
<re.Match object; span=(7, 10), match='abc'>

量词特殊字符
* 0次或者多次匹配前面的字符串或者模式

>>> re.search(r'abc*',"ab")
<re.Match object; span=(0, 2), match='ab'>

+ 1次或者多次匹配前面的字符串或者模式

re.search(r'abc+',"abcd")
<re.Match object; span=(0, 3), match='abc'>

? 0次或者1次匹配前面的字符串或者模式

>>> re.search(r'abc?',"abcd")
<re.Match object; span=(0, 3), match='abc'>

贪婪量词(greedy quantifier),非贪婪量词(non quantifier)以及占用量词(possessive quantifiers)
默认的量词匹配是贪婪的,即是尽可能多的进行匹配,如果匹配不上则选择减少匹配长度进行匹配.
在特殊量词字符后加上?可以变成非贪婪量词匹配.非贪婪匹配和贪婪相反,先尽可能少的匹配,如果匹配不上,才增加匹配长度.
如下模式,默认情况下使用贪婪匹配,加上?则可以非贪婪匹配.

>>> re.search(r'<.*>',"<a> <b>")
<re.Match object; span=(0, 7), match='<a> <b>'>
>>> re.search(r'<.*?>',"<a> <b>")
<re.Match object; span=(0, 3), match='<a>'>

占用量词(possessive quantifiers): 尽可能多的匹配,行为和贪婪量词匹配近似,但是占用量词匹配不进行回溯,而贪婪量词匹配则会进行回溯,如果匹配不上则减少匹配长度进行回溯,占用量词匹配是在量词匹配的特殊字符后加一个+号,但是它不会进行回溯.这个功能是python 3.11 版本之后才引入的,其行为和贪婪量词匹配以及非贪婪量词匹配同.举个例子:

# 贪婪匹配会回溯
>>> re.search(r'a+a','aaaaaa')
<re.Match object; span=(0, 6), match='aaaaaa'>
# 非贪婪匹配,所以只匹配两个a字符
>>> re.search(r'a+?a','aaaaaa')
# 贪婪匹配但是不回溯,就匹配失败
<re.Match object; span=(0, 2), match='aa'>
>>> re.search(r'a++a','aaaaaa')

第一个是贪婪量词匹配,匹配字符+会尽可能多的匹配a字符,但是这里最后还需要匹配一个字符a,所以这里贪婪匹配会有回溯行为,最终匹配成功.
第三个例子是占用量词匹配,不回溯,因此a++这个模式就直接匹配完全部字符串了,最后一个a字符就无法匹配上,由于此模式不进行回溯,所以整个模式匹配失败.

{m} 前面字符串或者模式精准匹配m次. 次数不对则匹配不上

re.search(r'a{4}',"aaaa")
<re.Match object; span=(0, 4), match='aaaa'>
>>> re.search(r'a{4}',"aaa")
>>> 

{m,n}前面字符串或者模式匹配m到n次, 默认情况下是贪婪匹配.

>>> re.search(r'a{3,}b','aaaaab')
<re.Match object; span=(0, 6), match='aaaaab'>
>>> re.search(r'a{3,5}b','aaaab')
<re.Match object; span=(0, 5), match='aaaab'>

同理{m,n}?则是非贪婪版本,{m,n}+则是占用量词版本.
如下正则匹配的例子可以比较非贪婪匹配和辆次占用

>>> re.search(r'a{1,3}','aaaaa')
<re.Match object; span=(0, 3), match='aaa'>
>>> re.search(r'a{1,3}?','aaaaa')
<re.Match object; span=(0, 1), match='a'>
>>> re.search(r'a{1,3}+','aaaaa')
<re.Match object; span=(0, 3), match='aaa'>

只有中间第二个例子使用非贪婪匹配,匹配到一个字符.

\转意字符,可以实现匹配字面量的正则表达式特殊字符(比如.,*以及()).

>>> re.search(r'\(\.\*\)','adc (.*) bdc')
<re.Match object; span=(4, 8), match='(.*)'>

[]方括号, 匹配或者匹配非([^])方括号中指定的字符. 这个特殊符号是一个比较复杂的字符.

  • 方括号中列出待匹配字符列表:
>>> re.search(r'[abcd]+','ab')
<re.Match object; span=(0, 2), match='ab'>
  • 方括号中可以罗列一个字符范围用于匹配,通常是26个字母以及数字范围,使用-符号放置于范围上限下限之间,如果要匹配特殊字符-,那么可以加上斜线\转意,或者将-放在括号的开头或者结尾
    如下例子:
普通的范围匹配
>>> re.search(r'[a-z]','ab')
<re.Match object; span=(0, 1), match='a'>
匹配特殊字符-的写法
>>> re.search(r'[a\-z]+','-az')
<re.Match object; span=(0, 3), match='-az'>
>>> re.search(r'[a\-z]+','-ab')
<re.Match object; span=(0, 2), match='-a'>
>>> re.search(r'[a-z-]+','-ab')
<re.Match object; span=(0, 3), match='-ab'>
  • 一些正则特殊字符在方括号里面只有字面量意义没有特殊意义,这里的一些字符指的是. * ()
>>> re.search(r'[.*()]+', '.*()abc')
<re.Match object; span=(0, 4), match='.*()'>

但是有一些特殊符号(\开头的特殊正则符号)却是保留其特有的意义,不是字面量
比如

>>> re.search(r'[\w]+', 'abcde')
<re.Match object; span=(0, 5), match='abcde'>
  • 排除列表中字符进行匹配[^]
    例子:
>>> re.search(r'[^abc]+','eda')
<re.Match object; span=(0, 2), match='ed'>
如果要排除^可以放括号内最前面或者最后面
>>> re.search(r'[^^abc]+','edm^a')
<re.Match object; span=(0, 3), match='edm'>
>>> re.search(r'[^a-c]','edmc')
<re.Match object; span=(0, 1), match='e'>
  • 匹配方括号[]字符本身
    主要是右边括号(左边括号可以放任意位置)容易产生歧义,要么放方括号内部开头,要么不放开头用\进行转意
>>> re.search(r'[]a-c[]+', 'ab[abb]cd')
<re.Match object; span=(0, 8), match='ab[abb]c'>
>>> re.search(r'[a-c\][]+', 'ab[abb]cd')
<re.Match object; span=(0, 8), match='ab[abb]c'>

|: 或匹配 A|B |两边可以是任意的正则表达式, 匹配时从左到右尝试,一旦一个正则匹已经配上,则不再进行其他正则表达式的匹配.如果本身就要匹配|那么使用转意字符\|. 放置在方括号中的|不存在任何特殊意义

>>> re.search(r'abb|abc',"abbbc")
<re.Match object; span=(0, 3), match='abb'>
转意
>>> re.search(r'abb\|abc',"abb|abc")
<re.Match object; span=(0, 7), match='abb|abc'>

() 匹配任何在括号内的正则表达式,括号引入了捕获组的概念,一个括号表示一个捕获组的开始和结束.匹配成功后返回的match对象上可以用组相关的function(group,groups)获取每一个捕获组的组数据.

(...) 扩展模式

(?:...) 非捕获组, 括号内正则表达式能进行匹配,但是不会被视作一个捕获组,返回的match对象里面获取不到这个组的数据

>>> res = re.search(r'(?:abc)','abcd')
>>> res.group()
'abc'
由于使用非捕获组,所以groups()输出为空的tuple
>>> res.groups()
()
>>> res = re.search(r'(abc)',"abcd")
>>> res.group()
'abc'
使用正常的捕获组,groups()能打印groupid为1的捕获组
>>> res.groups()
('abc',)

(?>...) 独立非捕获组.不可回溯的匹配

(?P<name>...) 命名的组,组的名字必须是唯一的,可以通过组的名字访问组的内容. 同时可以通过(?p=)来引用前面指定名称组的匹配内容. 除此之外也可以使用\number来实现同样的效果
(?P=name) 获取命名的组,配合(?p<name>)使用.

比如匹配两个引号之间的字符,包含两个引号
>>> res=re.search(r"""(?P<q>['"]).*?(?P=q)""","""abc 'a'""")
>>> res
<re.Match object; span=(4, 7), match="'a'">
可以传递group name 给 group函数 获取指定命名的group值
>>> res.group('q')
"'"
>>> res.groups()
("'",)
groupdict 返回以group name 为key, group value为值的字典
>>> res.groupdict()
{'q': "'"}

(?=...): 位置匹配, 匹配的位置后面要求满足括号中的模式.

这里要注意 括号内的内容不算捕获组,所以groups()不会有结果
>>> res = re.search(r'abc(?=\d+)','abc abc123')
>>> res.group()
'abc'
>>> res.groups()
()

(?!...): 位置匹配,匹配位置之要求不满足括号中的模式.

>>> res = re.search(r'abc(?!\d+)','abc abc123')
>>> res.group()
'abc'
>>> res.groups()
()

(?<=...): 同理位置匹配, 匹配位置前面要求满足括号中的模式.此处对模式有要求,必须要求前置的匹配模式是定长的.比如a|b这种匹配模式是允许的,但是a*,a+,a{3,4}这种匹配模式是不允许的,使用不定长的匹配模式会报错

re.search(r'(?<=a|b)mn','amn')
<re.Match object; span=(1, 3), match='mn'>
不定长直接报错, 提示匹配模式需要定长
>>> re.search(r'(?<=a+)mn','amn')
re.error: look-behind requires fixed-width pattern

(?<!...): 位置匹配,匹配位置前面要求不能满足括号中的模式,也要求前置的匹配模式是定长的.

>>> re.search(r'(?<!a|b)mn','amnbmncmn')
<re.Match object; span=(7, 9), match='mn'>

\number: 匹配前面指group number的组的相同内容(不是匹配相同模式), 组编号从1开始.
举个例子

这里要求\1内容和前面组内正则匹配内容完全一致, 所以最终只能匹配上`c c`.
groups()返回值是 `(c,)`
>>> re.search(r'(\w+) \1','abc cde')
<re.Match object; span=(2, 5), match='c c'>
这里是相同的模式,但是匹配到的值不一样,最终匹配结果是`abc cde`,
groups()返回值为`(abc,cde)`
>>> re.search(r'(\w+) (\w+)','abc cde')
<re.Match object; span=(0, 7), match='abc cde'>

\A 匹配字符串的开始,不管是否为多行(re.M)模式. 非多行模式下和^字符功能一样,多行模式下,\A不收到影响.

\A不收多行模式影响,只匹配字符开头
>>> re.search(r'\Aabc',"""mnf
... abc""",re.M)
^收到多行匹配行为影响.如下例子可以匹配到第二行开头.
>>> re.search(r'^abc',"""mnf
... abc""", re.M)
<re.Match object; span=(4, 7), match='abc'>

\b: 匹配单词边界,表示匹配单词以指定字符串开始或者结尾.

匹配的是第二个acta子字符串中的act
>>> re.search(r'\bact', 'sact acta actb')
<re.Match object; span=(5, 8), match='act'>

\B: 与\b相反,匹配非单词边界,单词不以指定字符串开始或者结尾.

匹配的是第一个sact中的act.
>>> re.search(r'\Bact', 'sact acta actb')
<re.Match object; span=(1, 4), match='act'>

\d: [0-9] 匹配数字
\D: [^0-9]匹配非数字

\s: [ \t\n\r\f\v] 匹配空白
\S: [^ \t\n\r\f\v]匹配非除空白

\w: [a-zA-Z0-9_]匹配任何单词字符,字母数字下划线
\W: [^a-zA-Z0-9_]匹配非单词字符

python正则表达式

re模块中的正则方法

先详细回顾re包中的类和方法.
re.compile 构建一个 re.Pattern 对象
和其他模块便捷方法re.function相比, 适合在同一个匹配模式反复且较多次使用的场景. 如果只是同一个正则模式几次使用的话, 使用包级别的函数和pattern对象性能相差微乎其微.

生成 re.Match 对象的方法

re.search 扫描整个字符串, 寻找第一个模式匹配位置(且只找第一个匹配位置), 如果匹配上则返回一个Match对象,如果没匹配上,则返回None.
re.finditer 扫描整个字符串, 寻找所有可以和模式匹配的位置, 返回一个Match的迭代对象. 如果匹配上,迭代对象可以通过迭代操作每一个match对象,如果没匹配上,则迭代对象内部为空. 这里无论匹配上还是未匹配上,返回结果都是一个迭代对象.

re.match: 从字符串开始位置进行模式匹配,如果匹配上,则返回一个match对象,未匹配上则返回None.

re.fullmatch: 整个字符串进行模式匹配,匹配上则返回match对象,未匹配上则返回None. re.match只是部分匹配即可,但是re.fullmatch要求完全匹配

不生成re.Match 对象的方法
re.split 根据指定的模式对字符串进行分割, 返回分割的子字符串列表. 如果没匹配上,则返回原字符串的列表.
re.findall re.finditer简化版, 在扫描整个字符串,进行模式匹配,返回匹配上的所有子字符串的列表,如果没匹配上返回一个空列表.

re.sub(p, repl, s, count=0) 字符串替换, 满足模式的所有子字符串都会被替换成repl, 如果没有匹配上,则返回原字符串
re.subn, 功能与sub类似,但是返回的是替换后字符串匹配上子字符串数目构成的元祖.

>>> re.sub('abc','ABC','mpq abc abc')
'mpq ABC ABC'
>>> re.subn('abc','ABC','mpq abc abc')
('mpq ABC ABC', 2)
count决定替换的子字符串个数,默认是0表示匹配上的子字符串全部替换
>>> re.subn('abc','ABC','mpq abc abc', count=1)
('mpq ABC abc', 1)
>>> re.subn('abc','ABC','mpq abc abc', count=10)
('mpq ABC ABC', 2)

re.split(pattern, string, maxsplit=0, flags=0) 匹配的模式作为分割位置对字符串进行分割, maxsplit 决定分割次数,默认是全部分割

>>> re.split(r'\W+','Word,  word,word')
['Word', 'word', 'word']
>>> re.split(r'\W+','Word,  word,word', maxsplit=1)
['Word', 'word,word']

re.escape(pattern): 转义给定的字符串中正则表达式相关的特殊字符为普通字符. 比如(.*) 传递给escape之后得到的结果是\\(\\.\\*\\). 这里(),.以及*都是正则表达式中的特殊字符,所以结果都加上了\\.

re.purge 清空正则表达式缓存, 很少使用. 一般不主动调用此方法. re模块中的函数都可以缓存编译的正则表达式, 这个方法就是清空此缓存.

python re function中的flag 详解

前面提到re.MULTILINE(re.M) 即在多行情况下改变^ $两个字符的匹配行为.下面详解一下python正则表达式函数里面常用的flag.
python的re模块中 存在 inline flag
re.DOTALL(re.S) 让 .可以匹配换行符

re.ASCII(re.A): 让匹配模式的元字符只匹配ascii字符.默认情况下是匹配unicode字符.

>>> re.findall(r'\w+', 'helloworld_你好')
['helloworld_你好']
>>> re.findall(r'\w+','helloworld_你好',re.A)
['helloworld_']
inline flag 语法
>>> re.findall(r'(?a:\w+)','helloworld_你好')
['helloworld_']

re.DEBUG 显示此正则表达式编译的debug信息.

>>> re.findall(r'\w+', 'helloworld',re.DEBUG)
MAX_REPEAT 1 MAXREPEAT
  IN
    CATEGORY CATEGORY_WORD

 0. INFO 4 0b0 1 MAXREPEAT (to 5)
 5: REPEAT_ONE 9 1 MAXREPEAT (to 15)
 9.   IN 4 (to 14)
11.     CATEGORY UNI_WORD
13.     FAILURE
14:   SUCCESS
15: SUCCESS
['helloworld']
>>> help(re.compile)

>>> re.compile(r'\w+',re.DEBUG)
MAX_REPEAT 1 MAXREPEAT
  IN
    CATEGORY CATEGORY_WORD

 0. INFO 4 0b0 1 MAXREPEAT (to 5)
 5: REPEAT_ONE 9 1 MAXREPEAT (to 15)
 9.   IN 4 (to 14)
11.     CATEGORY UNI_WORD
13.     FAILURE
14:   SUCCESS
15: SUCCESS
re.compile('\\w+', re.DEBUG)

re.IGNORECASE(re.I) 忽略大小写进行匹配

>>> re.search('[a-z]+','abcdEf')
<re.Match object; span=(0, 4), match='abcd'>
>>> re.search('[a-z]+','abcdEf',re.I)
<re.Match object; span=(0, 6), match='abcdEf'>
inline flag
>>> re.search(r'(?i:[a-z]+)', 'abcdEf')
<re.Match object; span=(0, 6), match='abcdEf'>

re.locale(re.L) 基于当前区域设置的来进行匹配, 一般比较少使用

re.NOFLAG 默认标志, 取值为0

>>> re.NOFLAG == 0
True

re.UNICODE(re.U) unicode字符匹配, sring模式默认匹配方式. 对应re.ASCII

>>> re.search(r'\w+','hello_你好',re.A)
<re.Match object; span=(0, 6), match='hello_'>
>>> re.search(r'\w+','hello_你好',re.U)
<re.Match object; span=(0, 8), match='hello_你好'>

re.VERBOSE(re.X) 让正则表达式在使用时更具备可读性. 允许在pattern中添加空格以及注释,实现对逻辑块进行分离,可读性提高,本质还是比较鸡肋的flag.
总结下来存在一定使用频率的flag有
re.MULTILINE(re.M), re.DOTALL(re.S), re.IGNORECASE(re.I),

inline flag.除了在正则function的 flag参数上设置flag之外,还可以在正则表达式的pattern中使用flag,这种语法被称为inline flag,
具体有两种(?aiLmsux)(?aiLmsux:)
区别: 不带冒号,设置的flag对整个正则表达式都生效,带冒号的只对正则表达式括号内部分生效,括号外不生效,举个例子

注意用法上的区别
>>> re.search(r'(?i)abcdef', 'abcDEF')
<re.Match object; span=(0, 6), match='abcDEF'>
>>> re.search(r'(?i:abc)def','abcDEF')

类型 re.Pattern 和 re.Match
re.Match的 group和 groups函数
group([gid,gid,gid]) 默认是返回整个匹配的字符串,
里面参数可以是单个整数或者一个array, 表示groupid
groups 返回id 从1 开始的所有group.
group() 和 groups()是使用最多的两个方法.

java 正则表达式

java正则表达式主要是 java.util.regex 下的两个关键类PatternMatcher. java 处理正则表达式的过程属于经典的过程,先要构建一个Pattern实例(使用Pattern.compile方法), 其次在实例上调用matcher方法获得一个Matcher对象, 然后在这个matcher对象上使用多样的方法来完成正则表达式在给定字符串上进行匹配,分割以及替换任务.

public static void test0() {
    // 构建pattern实例
    Pattern pattern = Pattern.compile("hello");

    // 构建matcher实例
    Matcher matcher = pattern.matcher("hello world");

    // 使用find方法查询每一个模式匹配的子字符串
    while (matcher.find()) {
      System.out.println(matcher.group());
    }
  }

相比于python的re模块, java 在匹配这一部分只有完全匹配(re.fullmatch)和模式在字符串上遍历查询每一个匹配的子字符串(re.finditer)方法.

两个核心类中的方法.
matcher: 返回一个Matcher对象, Matcher对象提供了丰富对象来处理正则匹配以及字符串替换的场景.

asPredicate 返回一个函数,用于判断给定字符串是否存在匹配模式的子字符串
asMatchPredicate 返回一个函数, 用于判断给定字符串是否完全匹配给定模式

Pattern pattern = Pattern.compile("abc");
    List<String> arr = List.of("abc", "abcd", "mabc");
    // 过滤出至少一个子字符串匹配的元素
    List<String> res1 = arr.stream().filter(pattern.asPredicate()).collect(Collectors.toList());
    System.out.println(res1);

    // 过滤出完全匹配给定模式的字符串的元素
    List<String> res2 = arr.stream().filter(pattern.asMatchPredicate()).collect(Collectors.toList());
    System.out.println(res2);

输出结果

[abc, abcd, mabc]
[abc]

split: 用于处理字符串分割的场景(Java里面分割场景是在pattern实例上面).

Matcher对象: 正则匹配, 子字符串提取 以及 替换的场景.
匹配find和 matches方法, 替换 replace 和 replaceAll

正则表达式举例

  1. 正则表达式匹配中国内地手机号码
    国内手机号码是11位数字,某些场景下是需要输入中国国际区码(+86)现在实现一个正则表达式验证输入手机号(可能带有中国国际区码)是否是正确的.
>>> re.fullmatch(r'(\+86)?\d{11}','+8613333333333')
<re.Match object; span=(0, 14), match='+8613333333333'>
>>> re.fullmatch(r'(\+86)?\d{11}','13333333333')
<re.Match object; span=(0, 11), match='13333333333'>
  1. 提取文本中的代码片段
    比如有如下的原始文本,
generated sql content:

```sql
select * from test
```
and python code:

```python
if __name__ == '__main__':
	print('hello')
```

需要提取其中的sql代码片段以及python代码片段提取出来.
本质上是需要一个正则表达式提取markdown 围栏代码块 中的内容以及此语法块对应的 语言标识符
这里采用的正则表达式为:

r'```\s*([^\n]*)\s*\n*(.+?)```'

这个正则表达式有两个sub group 第一个提取的是语言标识符,第二个提取的是代码块. 整个表达式需要配合re.DOTALL flag 使用.

# -*- coding:utf8 -*-
import re

def parse_code(text: str) -> list:
    # 提取代码块语言标识和代码块中的代码
    reg_tmp = r'```\s*([^\n]*)\s*\n*(.+?)```'
    return [(item[0].strip(), item[1].strip()) for item in re.findall(reg_tmp, text, re.DOTALL)]

if __name__ == '__main__':
    text = """
        generated sql content:

        ```sql
        select * from test
        ```
        and python code:

        ```python
        if __name__ == '__main__':
            print('hello')
        ```
    """
    res = parse_code(text)
    print(res)

输出为一个tuple的list,其中tuple第一个元素是语言标识符,第二个元素是对应的代码片段:

[('sql', 'select * from test'), ('python', "if __name__ == '__main__':\n            print('hello')")]

可见此正则表达式能实现代码的正确提取.

GenAI 场景使用正则表达式举例

模拟如下场景,让大模型作为一个算法工程师的agent,根据用户的输入需求requirement和相应的算法实现代码lang生成结果,然后用正则表达式提取其中的语言标识符和代码片段,并把代码片段保存成可执行文件.
实现如下

package org.mylearn.reg.service.impl;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.mylearn.reg.service.AlgorithmService;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Slf4j
@Service
public class AlgorithmServiceImpl implements AlgorithmService {

    @Autowired
    private ChatModel chatModel;

    // 提取代码以及语言标识符的正则表达式
    private final static Pattern CODE_EXTRACT = Pattern.compile("```\\s*([^\\n]*)\\s*\\n*(.+?)```", Pattern.DOTALL);
    private final static Map<String, String> SUFFIX = Map.of(
        "java", "java",
        "python", "py",
        "js", "js"
    );

    @SneakyThrows
    @Override
    public String genAlgo(String requirement, String lang, String baseDir) {
        String systemTemp = """
            你是一个高级算法工程师,你会根据用户的输入的算法问题以及要求的输出代码语言生成对应算法实现的code
            输出结果有如下要求:
            1. 输出结果里面代码放在```code```围栏代码块中.
            2. 每一个代码块要有语言代码标示,比如python生成的代码块则是
            ```python
            python code
            ```
            """;
        String userTemp = """
            算法问题: {requirement}
            输出代码语言: {lang}
            """;
        SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemTemp);
        PromptTemplate userPromptTemplate = new PromptTemplate(userTemp);
        Message systemMessage = systemPromptTemplate.createMessage();
        Message userMessage = userPromptTemplate.createMessage(Map.of("requirement", requirement, "lang", lang));
        Prompt prompt = new Prompt(Arrays.asList(systemMessage, userMessage));
        String srcContent = chatModel.call(prompt).getResult().getOutput().getContent();
        Matcher matcher = CODE_EXTRACT.matcher(srcContent);
        Map<String, String> res = new HashMap<>();
        while(matcher.find()) {
            String algo = matcher.group(1);
            String content = matcher.group(2);
            res.put(algo, content);
        }
        for (Map.Entry<String, String> entry : res.entrySet()) {
            String fileType = entry.getKey();
            String content = entry.getValue();
            String fileName = fileType + "." + SUFFIX.get(fileType);
            Path path = Paths.get(baseDir).resolve(fileName);
            Files.writeString(path, content);
        }
        return "ok";
    }
}

模型采用ollama本地运行的 phi4:14b,相应的application.yml文件配置段:

spring:
  application:
    name: java-reg
  ai:
    ollama:
      chat:
        options:
          model: 'phi4:14b'

curl 调用api

curl -X 'POST' \
  'http://localhost:8080/algo/demo' \
  -H 'accept: */*' \
  -H 'Content-Type: application/json' \
  -d '{
  "requirement": "kahn拓扑排序",
  "lang": "java"
}'

大模型输出结果:


import java.util.*;

public class KahnTopologicalSort {

    public List<Integer> topologicalSort(int numCourses, int[][] prerequisites) {
        // Create a graph representation and track in-degrees of each node
        Map<Integer, List<Integer>> adjacencyList = new HashMap<>();
        int[] inDegree = new int[numCourses];

        for (int i = 0; i < numCourses; i++) {
            adjacencyList.put(i, new ArrayList<>());
        }

        // Fill the graph and compute initial in-degrees
        for (int[] prerequisite : prerequisites) {
            int course = prerequisite[0];
            int dependency = prerequisite[1];
            adjacencyList.get(dependency).add(course);
            inDegree[course]++;
        }

        // Initialize a queue with nodes having zero in-degree
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }

        List<Integer> sortedOrder = new ArrayList<>();

        // Process the graph using Kahn's algorithm
        while (!queue.isEmpty()) {
            int course = queue.poll();
            sortedOrder.add(course);

            for (int neighbor : adjacencyList.get(course)) {
                inDegree[neighbor]--;
                if (inDegree[neighbor] == 0) {
                    queue.offer(neighbor);
                }
            }
        }

        // Check if we have a valid topological ordering
        if (sortedOrder.size() != numCourses) {
            return Collections.emptyList(); // Return an empty list if there's a cycle
        }

        return sortedOrder;
    }

    public static void main(String[] args) {
        KahnTopologicalSort kts = new KahnTopologicalSort();
        int numCourses = 4;
        int[][] prerequisites = {{1, 0}, {2, 0}, {3, 1}, {3, 2}};

        List<Integer> result = kts.topologicalSort(numCourses, prerequisites);
        System.out.println(result);
    }
}

对应DAG:
dag

运行代码输出结果如下,可见大模型输出结果正确.

[0, 1, 2, 3]

小结

本文回顾了正则表达式的常用的语法,以及和大模型结合的一些案例,希望能帮助读者更好地熟悉正则表达式.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值