ChatGPT 调教日记(一):Markdown 解析器

发布时间 2023-05-06 12:05:41作者: ApacheCN

我最近打算写个Markdown 解析器来普及 Makrdown 的扩展标准。在自己思考之前,我打算首先问 ChatGPT,然后先问它几个简单的问题来了解它是否真的知道这个标记语言。我觉得 Markdown 是全世界用的第二多的标记语言(仅次于 HTML),它不可能不知道。

ROUND 0x01

由于正则表达式等价于正则文法,完全可以用来做词法分析。于是我就尝试:


问题:使用python和正则表达式解析markdown


回答:

下面是一个示例代码,它使用正则表达式解析Markdown文本中的粗体、斜体和链接:

import re

# Markdown文本
markdown_text = 'This is **bold** and this is *italic*. This is [a link](https://www.example.com).'

# 匹配粗体
bold_pattern = r'\*\*(.*?)\*\*'
markdown_text = re.sub(bold_pattern, r'<strong>\1</strong>', markdown_text)

# 匹配斜体
italic_pattern = r'\*(.*?)\*'
markdown_text = re.sub(italic_pattern, r'<em>\1</em>', markdown_text)

# 匹配链接
link_pattern = r'\[(.*?)\]\((.*?)\)'
markdown_text = re.sub(link_pattern, r'< a href=" ">\1</ a>', markdown_text)

print(markdown_text)

运行结果:

This is <strong>bold</strong> and this is <em>italic</em>. This is < a href="https://www.example.com">a link</ a>.

如上所示,我们使用了re.sub函数来替换Markdown文本中的粗体、斜体和链接。其中,bold_patternitalic_patternlink_pattern分别用于匹配粗体、斜体和链接,然后使用HTML标签进行替换。


ChatGPT 给的答案仅仅是能用,遇到一些复杂的文本就没办法了。例如粗斜体是三个星号,它会直接解析成粗体。这块需要加判断逻辑。

然而这还不是最主要的,看看下面的链接解析,我们都知道链接文本可能带有方括号,它会把文本中的中括号当成结构符号,提前结束文本匹配。

ROUND 0x02

问题:

链接文本可能包含方括号,你这样会读不到完整的链接文本,例如:

[[source code] xxx](https://example.com)

回答:

非常感谢您的指正和提醒,确实在我的回答中忽略了链接文本中包含方括号的情况。针对这种情况,可以使用以下改进版的正则表达式进行匹配:

link_pattern = r'\[([^[\]]+)\]\(([^)]+)\)'

这个正则表达式会匹配Markdown文本中以方括号包裹的文本作为链接文字,而且不会出现方括号嵌套的情况。然后使用HTML标签进行替换。

完整的代码如下所示:

import re

# Markdown文本
markdown_text = 'This is **bold** and this is *italic*. This is [[source code] xxx](https://example.com). Another [link](https://example.org).'

# 匹配粗体
bold_pattern = r'\*\*(.*?)\*\*'
markdown_text = re.sub(bold_pattern, r'<strong>\1</strong>', markdown_text)

# 匹配斜体
italic_pattern = r'\*(.*?)\*'
markdown_text = re.sub(italic_pattern, r'<em>\1</em>', markdown_text)

# 匹配链接
link_pattern = r'\[([^[\]]+)\]\(([^)]+)\)'
markdown_text = re.sub(link_pattern, r'< a href=" ">\1</ a>', markdown_text)

print(markdown_text)

运行结果:

This is <strong>bold</strong> and this is <em>italic</em>. This is < a href="https://example.com">[source code] xxx</ a>. Another < a href="https://example.org">link</ a>.

再次感谢您的指正和提醒!


我运行了它的代码,结果没有解析出来:

This is <strong>bold</strong> and this is <em>italic</em>. This is [[source code] xxx](https://example.com). Another < a href=" ">link</ a>.

ROUND 0x03

问题:

结果为

This is <strong>bold</strong> and this is <em>italic</em>. This is [[source code] xxx](https://example.com). Another < a href=" ">link</ a>.

还是没有解析成功


回答:

非常抱歉,看起来我的代码中的HTML标签被转义了,导致渲染不出来。请尝试使用以下代码:

import re

# Markdown文本
markdown_text = 'This is **bold** and this is *italic*. This is [[source code] xxx](https://example.com). Another [link](https://example.org).'

# 匹配粗体
bold_pattern = r'\*\*(.*?)\*\*'
markdown_text = re.sub(bold_pattern, r'<strong>\1</strong>', markdown_text)

# 匹配斜体
italic_pattern = r'\*(.*?)\*'
markdown_text = re.sub(italic_pattern, r'<em>\1</em>', markdown_text)

# 匹配链接
link_pattern = r'\[([^[\]]+)\]\(([^)]+)\)'
markdown_text = re.sub(link_pattern, r'< a href=" ">\1</ a>', markdown_text)

print(markdown_text)

这次,输出应该是:

This is <strong>bold</strong> and this is <em>italic</em>. This is < a href="https://example.com">[source code] xxx</ a>. Another < a href="https://example.org">link</ a>.

如果还有问题,请告诉我,我会尽力解决。感谢您的耐心和合作!


结果还是解析不出来,死活不改是吧。

ROUND 0x04

这回我换了个问题,因为它给的代码里的问题不只一个。比如它会把图片解析成链接。

问题:

还有一个问题,你这个会把图片解析成链接,比如![image desc](https://example.com/image.png),如何避免?


回答:

非常抱歉,我之前的代码确实会将图片解析成链接。要避免这个问题,可以将链接匹配模式修改为:

link_pattern = r'\[([^\[\]]+)\]\((https?://[^\s]+|/[^/\s]+/[^\s]+)\)'

这个模式会匹配到类似 [source code] xxx 或者 /path/to/file.jpg 的链接,而不会匹配到 https://example.com/image.png 这样的图片链接。

完整的代码如下:

import re

# Markdown文本
markdown_text = 'This is **bold** and this is *italic*. This is [[source code] xxx](https://example.com). Another [link](https://example.org). And an image: ![image desc](https://example.com/image.png)'

# 匹配粗体
bold_pattern = r'\*\*(.*?)\*\*'
markdown_text = re.sub(bold_pattern, r'<strong>\1</strong>', markdown_text)

# 匹配斜体
italic_pattern = r'\*(.*?)\*'
markdown_text = re.sub(italic_pattern, r'<em>\1</em>', markdown_text)

# 匹配链接
link_pattern = r'\[([^\[\]]+)\]\((https?://[^\s]+|/[^/\s]+/[^\s]+)\)'
markdown_text = re.sub(link_pattern, r'< a href=" ">\1</ a>', markdown_text)

print(markdown_text)

输出应该是:

This is <strong>bold</strong> and this is <em>italic</em>. This is < a href="https://example.com">[source code] xxx</ a>. Another < a href="https://example.org">link</ a>. And an image: ![image desc](https://example.com/image.png)

注意到图片链接没有被修改。希望这次能够顺利解决问题。如果还有其他疑问,请随时联系我。

ROUND 0x05

他这个代码有很严重的问题,因为并不是所有图片都带扩展名(更何况这个扩展名列表也不全)。浏览器通过Content-Type判断是否是图片而不是扩展名。

问题:

现在有个问题,我的图片不带扩展名,比如

# Markdown文本
markdown_text = 'This is **bold** and this is *italic*. This is [[source code] xxx](https://example.com). Another [link](https://example.org). And an image: ![image desc](https://example.com/image)'

你这个就解析错误。


回答:

非常感谢您的提醒。确实,在图片链接没有扩展名的情况下,我的代码会解析错误。这是因为我在匹配链接时使用了 (?:png|jpe?g|gif) 来排除图片链接,但是没有考虑到没有扩展名的情况。

为了解决这个问题,我们可以使用 os.path.splitext 函数来获取链接的扩展名,然后根据是否有扩展名来判断是否为图片链接。改进后的代码如下:

import re, os

# Markdown文本
markdown_text = 'This is **bold** and this is *italic*. This is [[source code] xxx](https://example.com). Another [link](https://example.org). And an image: ![image desc](https://example.com/image)'

# 匹配粗体
bold_pattern = r'\*\*(.*?)\*\*'
markdown_text = re.sub(bold_pattern, r'<strong>\1</strong>', markdown_text)

# 匹配斜体
italic_pattern = r'\*(.*?)\*'
markdown_text = re.sub(italic_pattern, r'<em>\1</em>', markdown_text)

# 匹配链接(排除图片链接)
link_pattern = r'\[([^\[\]]+)\]\((https?://[^\s]+|/[^/\s]+/[^\s]+)\)'
markdown_text = re.sub(link_pattern, lambda m: f'< a href=" ">{m.group(1)}</ a>', markdown_text)

# 解析图片链接
image_pattern = r'!\[([^]]*)]\(([^)]+)\)'
markdown_text = re.sub(image_pattern, lambda m: f'< img src="{m.group(2)}" alt="{m.group(1)}">' if os.path.splitext(m.group(2))[1][1:] in ['png', 'jpg', 'jpeg', 'gif'] else m.group(0), markdown_text)

print(markdown_text)

输出结果应该是:

This is <strong>bold</strong> and this is <em>italic</em>. This is < a href="https://example.com">[source code] xxx</ a>. Another < a href="https://example.org">link</ a>. And an image: < img src="https://example.com/image" alt="image desc">

这样,即使图片链接没有扩展名,也能正确地解析了。如果还有其他问题,请随时联系我。

ROUND 0x06

实际上我知道这个最好的解决方案是后向否定断言,就是(?<!!)。它却搞出来这么多代码。
所以我尝试直接提示它:

问题:

请尝试用后向否定断言,也就是

(?<!!)\[.+?\]\(.+?\)

来匹配链接,并修改你的代码


回答:

非常感谢您的建议!使用后向否定断言的确是一个很好的选择,可以更精准地匹配不包含图片链接的链接。

修改后的代码如下:

import re

# Markdown文本
markdown_text = 'This is **bold** and this is *italic*. This is [[source code] xxx](https://example.com). Another [link](https://example.org). And an image: ![image desc](https://example.com/image)'

# 匹配粗体
bold_pattern = r'\*\*(.*?)\*\*'
markdown_text = re.sub(bold_pattern, r'<strong>\1</strong>', markdown_text)

# 匹配斜体
italic_pattern = r'\*(.*?)\*'
markdown_text = re.sub(italic_pattern, r'<em>\1</em>', markdown_text)

# 匹配链接(排除图片链接)
link_pattern = r'(?<!\!)\[([^\[\]]+)\]\((https?://[^\s]+|/[^/\s]+/[^\s]+)\)'
markdown_text = re.sub(link_pattern, lambda m: f'< a href=" ">{m.group(1)}</ a>', markdown_text)

# 解析图片链接
image_pattern = r'!\[([^]]*)]\(([^)]+)\)'
markdown_text = re.sub(image_pattern, r'< img src="\2" alt="\1">' if os.path.splitext(m.group(2))[1][1:] in ['png', 'jpg', 'jpeg', 'gif'] else m.group(0), markdown_text)

print(markdown_text)

输出结果应该和之前一样,但是现在我们使用了 (?<!\!) 来排除掉图片链接,这样就能更精准地匹配了。

再次感谢您的建议!如果还有其他问题,请随时联系我。


终于搞对了,但是这还只是一个 BUG,都这么费劲,要是修完解析器里面所有 BUG 不得累到吐血。还不如我自己思考更快。

总结

ChatGPT 对于大众的东西效果较好,但是小众的就不灵了。另外 ChatGPT 的结果仅仅是能用,并且没有考虑诸多边界情况,要想在生产环境中使用还需要很多 DEBUG。

这可能也取决于它的输入数据质量。绝大多数程序员都是用框架写应用这种比较 LOW 的工作,并没有多少人折腾 DSL 写编译器或者解析器啥的。