我最近打算写个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_pattern
、italic_pattern
和link_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 写编译器或者解析器啥的。