整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

使用Python构建Markdown到HTML的转换管道

markdown中写下你的文章,并使用Python将它们转换成HTML-作者Florian Dahlitz,于2020年5月18日(15分钟)

介绍

几个月前,我想开通自己的博客,而不是使用像Medium这样的网站。这是一个非常基础的博客,所有的文章都是HTML形式的。然而,有一天,我突然产生了自己编写Markdown到HTML生成器的想法,最终这将允许我用markdown来编写文章。此外,为它添加诸如估计阅读时间之类的扩展特性会更容易。长话短说,我实现了自己的markdown到HTML生成器,我真的很喜欢它!

在本系列文章中,我想向您展示如何构建自己的markdown到HTML生成器。该系列由三部分组成:

  • 第一部分(本文)介绍了整个管线的实现。

  • 第二部分通过一个模块扩展了实现的管线,该模块用于计算给定文章的预计阅读时间。

  • 第三部分演示如何使用管线生成自己的RSS摘要。

这三部分中使用的代码都可以在GitHub上找到。

备注:我的文章中markdown到HTML生成器的想法基于Anthony Shaw文章中的实现。

项目构建

为了遵循本文的内容,您需要安装几个软件包。我们把它们放进requirements.txt文件。

Markdown是一个包,它允许您将markdown代码转换为HTML。之后我们用Flask产生静态文件。

但在安装之前,请创建一个虚拟环境,以避免Python安装出现问题:

激活后,您可以使用pip安装requirements.txt中的依赖。

很好!让我们创建几个目录来更好地组织代码。首先,我们创建一个app目录。此目录包含我们提供博客服务的Flask应用程序。所有后续目录都将在app目录内创建。其次,我们创建一个名为posts的目录。此目录包含要转换为HTML文件的markdown文件。接下来,我们创建一个templates目录,其中包含稍后使用Flask展示的模板。在templates目录中,我们再创建两个目录:

posts包含生成的HTML文件,这些文件与应用程序根目录中posts目录中的文件相对应。

shared包含在多个文件中使用的HTML文件。

此外,我们还创建了一个名为services的目录。该目录将包含我们在Flask应用程序中使用的模块,或者为它生成某些东西。最后,创建一个名为static的目录带有两个子目录images和css。自定义CSS文件和文章的缩略图将存储在此处。

您的最终项目结构应如下所示:

令人惊叹!我们完成了一般的项目设置。我们来看看Flask的设置。

Flask设置

路由

我们在上一节安装了Flask。但是,我们仍然需要一个Python文件来定义用户可以访问的端点。在app目录中创建main.py并将以下内容复制到其中。

该文件定义了一个具有两个端点的基础版Flask应用程序。用户可以使用/route访问第一个端点返回索引页,其中列出了所有文章。

第二个端点是更通用的端点。它接受post的名称并返回相应的HTML文件。

接下来,我们通过向app目录中添加一个__init__.py,将其转换为一个Python包。此文件为空。如果您使用UNIX计算机,则可以从项目的根目录运行以下命令:

模板

现在,我们创建两个模板文件index.html以及layout.html,都存储在templates/shared目录中。这个layout.html模板将用于单个博客条目,而index.html模板用于生成索引页,从中我们可以访问每个帖子。让我们从index.html模板开始。

它是一个基本的HTML文件,其中有两个元标记、一个标题和两个样式表。注意,我们使用一个远程样式表和一个本地样式表。远程样式表用于启用Bootstrap[1]类。第二个是自定义样式。我们晚点再定义它们。

HTML文件的主体包含一个容器,其中包含Jinja2[2]逻辑,用于为每个post生成Bootstrap卡片[3]。您是否注意到我们不直接基于变量名访问这些值,而是需要将[0]添加到其中?这是因为文章中解析的元数据是列表。实际上,每个元数据元素都是由单一元素组成的列表。我们稍后再看。到目前为止,还不错。让我们看看layout.html模板。

如你所见,它比前一个短一点,简单一点。文件头与index.html文件很相似,除了我们有不同的标题。当然,我们可以共用一个模板,但是我不想让事情变得更复杂。

body中的容器仅定义一个h1标记。然后,我们提供给模板的内容被插入并呈现。

样式

正如上一节所承诺的,我们将查看自定义CSS文件style.css. 我们在static/css中找到该文件,并根据需要自定义页面。下面是我们将用于基础示例的内容:

我不喜欢Bootstrap中blockquotes的默认外观,所以我们在左侧添加了一点间距和边框。此外,blockquote段落底部的页边空白将被删除。不删除的话看起来很不自然。

最后但并非最不重要的是,左右两边的填充被删除。由于两边都有额外的填充,缩略图没有正确对齐,所以在这里删除它们。

到现在为止,一直都还不错。我们完成了关于Flask的所有工作。让我们开始写一些帖子吧!

写文章

正如标题所承诺的,你可以用markdown写文章-是的!在写文章的时候,除了保证正确的markdown格式外,没有其他需要注意的事情。

在完成本文之后,我们需要在文章中添加一些元数据。此元数据添加在文章之前,并由三个破折号分隔开来---。下面是一个示例文章(post1.md)的摘录:

注意:您可以在GitHub库的app/posts/post1.md中找到完整的示例文章。

在我们的例子中,元数据由标题、副标题、类别、发布日期和index.html中卡片对应缩略图的路径组成.

我们在HTML文件中使用了元数据,你还记得吗?元数据规范必须是有效的YAML。示例形式是键后面跟着一个冒号和值。最后,冒号后面的值是列表中的第一个也是唯一的元素。这就是我们通过模板中的索引运算符访问这些值的原因。

假设我们写完了文章。在我们可以开始转换之前,还有一件事要做:我们需要为我们的帖子生成缩略图!为了让事情更简单,只需从你的电脑或网络上随机选取一张图片,命名它为placeholder.jpg并把它放到static/images目录中。GitHub存储库中两篇文章的元数据包含一个代表图像的键值对,值是placeholder.jpg。

注意:在GitHub存储库中,您可以找到我提到的两篇示例文章。

markdown到HTML转换器

最后,我们可以开始实现markdown to HTML转换器。因此,我们使用我们在开始时安装的第三方包Markdown。我们先创建一个新模块,转换服务将在其中运行。因此,我们在service目录中创建了converter.py。我们一步一步看完整个脚本。您可以在GitHub存储库中一次查看整个脚本。

首先,我们导入所需的所有内容并创建几个常量:

ROOT指向我们项目的根。因此,它是包含app的目录。

POSTS_DIR是以markdown编写的文章的路径。

TEMPLATE_DIR分别指向对应的templates目录。

BLOG_TEMPLATE_文件存储layout.html的路径。

INDEX_TEMPLATE_FILE是index.html

BASE_URL是我们项目的默认地址,例如。https://florian-dahlitz.de.默认值(如果不是通过环境变量DOMAIN提供的话)是http://0.0.0.0:5000。

接下来,我们创建一个名为generate_entries的新函数。这是我们定义的唯一一个转换文章的函数。

在函数中,我们首先获取POSTS_DIR目录中所有markdown文件的路径。pathlib的awesome glob函数帮助我们实现它。

此外,我们定义了Markdown包需要使用的扩展。默认情况下,本文中使用的所有扩展都随它的安装一起提供。

注意:您可以在文档[4]中找到有关扩展的更多信息。

此外,我们实例化了一个新的文件加载程序,并创建了一个在转换项目时使用的环境。随后,将创建一个名为all_posts的空列表。此列表将包含我们处理后的所有帖子。现在,我们进入for循环并遍历POSTS_DIR中找到的所有文章。

我们启动for循环,并打印当前正在处理的post的路径。如果有什么东西出问题了,这尤其有用。然后我们就知道,哪个文章的转换失败了。

接下来,我们在默认url之后增加一部分。假设我们有一篇标题为“面向初学者的Python”的文章。我们将文章存储在一个名为python-for-beginners.md,的文件中,因此生成的url将是http://0.0.0.0:5000/posts/python-for-beginners。

变量url_html存储的字符串与url相同,只是我们在末尾添加了.html。我们使用此变量定义另一个称为target_file.的变量。变量指向存储相应HTML文件的位置。

最后,我们定义了一个变量md,它表示markdown.Markdown的实例,用于将markdown代码转换为HTML。您可能会问自己,为什么我们没有在for循环之前实例化这个实例,而是在内部实例化。当然,对于我们这里的小例子来说,这没有什么区别(只是执行时间稍微短一点)。但是,如果使用诸如脚注之类的扩展来使用脚注,则需要为每个帖子实例化一个新实例,因为脚注添加后就不会从此实例中删除。因此,如果您的第一篇文章使用了一些脚注,那么即使您没有明确定义它们,所有其他文章也将具有相同的脚注。

让我们转到for循环中的第一个with代码块。

实际上,with代码块打开当前post并将其内容读入变量content。之后调用_md.convert将以markdown方式写入的内容转换为HTML。随后,env环境根据提供的模板BLOG_TEMPLATE_FILE(即layout.html如果你还记得的话)渲染生成的HTML。

第二个with 代码块用于将第一个with 代码块中创建的文档写入目标文件。

以下三行代码从元数据中获取发布日期(被发布的日期),将其转换为正确的格式(RFC 2822),并将其分配回文章的元数据。此外,生成的post_dict被添加到all_posts列表中。

我们现在出了for循环,因此,我们遍历了posts目录中找到的所有posts并对其进行了处理。让我们看看generate_entries函数中剩下的三行代码。

我们按日期倒序对文章进行排序,所以首先显示最新的文章。随后,我们将文章写到模板目录一个新创建的index.html文件中。别把index.html错认为templates/shared目录中的那个。templates/shared目录中的是模板,这个是我们要使用Flask服务的生成的。

最后我们在函数generate_entries之后添加以下if语句。

这意味着如果我们通过命令行执行文件,它将调用generate_entries函数。

太棒了,我们完成了converter.py脚本!让我们从项目的根目录运行以下命令来尝试:

您应该看到一些正在转换的文件的路径。假设您编写了两篇文章或使用了GitHub存储库中的两篇文章,那么您应该在templates目录中找到三个新创建的文件。首先是index.html,它直接位于templates目录中,其次是templates/posts目录中的两个HTML文件,它们对应于markdown文件。

最后启动Flask应用程序并转到http://0.0.0.0:5000。

总结

太棒了,你完成了这个系列的第一部分!在本文中,您已经学习了如何利用Markdown包创建自己的Markdown to HTML生成器。您实现了整个管线,它是高度可扩展的,您将在接下来的文章中看到这一点。

希望你喜欢这篇文章。一定要和你的朋友和同事分享。如果你还没有,考虑在Twitter上关注我@DahlitzF或者订阅我的通知,这样你就不会错过任何即将发表的文章。保持好奇心,不断编码!

参考文献

Bootstrap (http://getbootstrap.com/)

Primer on Jinja Templating (https://realpython.com/primer-on-jinja-templating/)

Bootstrap Card (https://getbootstrap.com/docs/4.4/components/card/)

Python-Markdown Extensions (https://python-markdown.github.io/extensions/)

Tweet

英文原文:https://florian-dahlitz.de/blog/build-a-markdown-to-html-conversion-pipeline-using-python
译者:阿布铥


本文接着上文(Golang GinWeb框架6-绑定请求字符串/URI/请求头/复选框/表单类型)继续探索GinWeb框架


静态文件服务

package main
​
import (
  "github.com/gin-gonic/gin"
  "log"
  "net/http"
  "os"
)
​
func main() {
  router := gin.Default()
  cwd, _ := os.Getwd()  //获取当前文件目录
  log.Printf("当前项目路径:%s", cwd)
  router.Static("/static", cwd) //提供静态文件服务器, 第一个参数为相对路径,第二个参数为根路径, 这个路径一般放置css,js,fonts等静态文件,前端html中采用/static/js/xxx或/static/css/xxx等相对路径的方式引用
  router.StaticFS("/more_static", http.Dir("./")) //将本地文件树结构映射到前端, 通过浏览器可以访问本地文件系统, 模拟访问:http://localhost:8080/more_static
  router.StaticFile("/logo.png", "./resources/logo.png")  //StaticFile提供单静态单文件服务, 模拟访问:http://localhost:8080/log.png
​
  // Listen and serve on 0.0.0.0:8080
  router.Run(":8080")
}


返回文件数据

package main
​
import (
  "github.com/gin-contrib/cors"
  "github.com/gin-gonic/gin"
  "net/http"
)
​
func main() {
  router := gin.Default()
  router.Use(cors.Default())
​
  router.GET("/local/file", func(c *gin.Context) {
    c.File("./main.go")
  })
​
​
  // A FileSystem implements access to a collection of named files.
  // The elements in a file path are separated by slash ('/', U+002F)
  // characters, regardless of host operating system convention.
  // FileSystem接口, 要求实现文件的访问的方法, 提供文件访问服务根路径的HTTP处理器
  var fs http.FileSystem = http.Dir("./")  //将本地目录作为文件服务根路径
  router.GET("/fs/file", func(c *gin.Context) {
    c.FileFromFS("main.go", fs)  //将文件服务系统下的文件数据返回
  })
  router.Run(":8080")
}
/*
模拟访问文件数据:
curl http://localhost:8080/local/file
​
模拟访问文件系统下的文件数据:
curl http://localhost:8080/fs/file
*/


用文件读出器提供文件数据服务

package main
​
import (
  "github.com/gin-gonic/gin"
  "net/http"
)
​
func main() {
  router := gin.Default()
  router.GET("/someDataFromReader", func(c *gin.Context) {
    response, err := http.Get("https://raw.githubusercontent.com/gin-gonic/logo/master/color.png")
    if err != nil || response.StatusCode != http.StatusOK {  //请求链接中的文件出现错误时, 直接返回服务不可用
      c.Status(http.StatusServiceUnavailable)
      return
    }
​
    reader := response.Body  //用响应体内容构造一个文件读出器
    defer reader.Close()
    contentLength := response.ContentLength
    contentType := response.Header.Get("Content-Type")
​
    extraHeaders := map[string]string{
      "Content-Disposition": `attachment; filename="gopher.png"`,
    }
    // DataFromReader writes the specified reader into the body stream and updates the HTTP code.
    // func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string) {}
    // DataFromReader方法将指定的读出器reader中的内容, 写入http响应体流中, 并更新响应码, 响应头信息等
    c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
  })
  router.Run(":8080")
}
/*
模拟访问:
curl http://localhost:8080/someDataFromReader
*/


HTML渲染


使用LoadHTMLGlob()方法或LoadHTMLFiles()方法

package main
​
import (
  "github.com/gin-gonic/gin"
  "net/http"
)
​
func main() {
  router := gin.Default()
  //LoadHTMLGlob方法以glob模式加载匹配的HTML文件, 并与HTML渲染器结合
  router.LoadHTMLGlob("templates/*")
  //router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
  router.GET("/index", func(c *gin.Context) {
    //HTML方法设置响应码, 模板文件名, 渲染替换模板中的值, 设置响应内容类型Content-Type "text/html"
    c.HTML(http.StatusOK, "index.tmpl", gin.H{
      "title": "Main website",
    })
  })
  router.Run(":8080")
}
/*
模拟测试:
curl http://localhost:8080/index
*/

增加模板文件, templates/index.tmpl

<html>
  <h1>
    {{ .title }}
  </h1>
</html>

使用不同文件夹下的相同文件名的模板文件

func main() {
  router := gin.Default()
  router.LoadHTMLGlob("templates/**/*")
  router.GET("/posts/index", func(c *gin.Context) {
    c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
      "title": "Posts",
    })
  })
  router.GET("/users/index", func(c *gin.Context) {
    c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
      "title": "Users",
    })
  })
  router.Run(":8080")
}

posts目录下添加模板文件, templates/posts/index.tmpl

{{ define "posts/index.tmpl" }}
<html><h1>
  {{ .title }}
​</h1>
<p>Using posts/index.tmpl</p>
</html>
{{ end }}

users目录下添加模板文件, templates/users/index.tmpl

{{ define "users/index.tmpl" }}
<html><h1>
  {{ .title }}
</h1>
<p>Using users/index.tmpl</p>
</html>
{{ end }}

自定义模板渲染器


你也可以使用你自定义的HTML模板渲染器, 需要自定义模板文件file1, file2等

package main
​
import (
​  "github.com/gin-gonic/gin"
  "html/template"
  "net/http"
)
​
func main() {
  router := gin.Default()
  //template.ParseFiles(文件1,文件2...)创建一个模板对象, 然后解析一组模板,使用文件名作为模板的名字
  // Must方法将模板和错误进行包裹, 返回模板的内存地址 一般用于变量初始化,比如:var t = template.Must(template.New("name").Parse("html"))
  html := template.Must(template.ParseFiles("file1", "file2"))
  router.SetHTMLTemplate(html) //关联模板和HTML渲染器
​
  router.GET("/index", func(c *gin.Context) {
    //HTML方法设置响应码, 模板文件名, 渲染替换模板中的值, 设置响应内容类型Content-Type "text/html"
    c.HTML(http.StatusOK, "file1", gin.H{
      "title": "Main website",
    })
  })
  router.Run(":8080")
}


自定义分隔符


你可以自定义分隔符, 模板中默认的分隔符是{{ }}, 我们也可以修改, 比如下面增加一对中括号

  r := gin.Default()
  r.Delims("{[{", "}]}")
  r.LoadHTMLGlob("/path/to/templates")


自定义模板方法


详见 示例代码.

模板中与后端都定义好模板方法, 模板渲染时执行该方法, 类似过滤器方法, 比如时间格式化操作

package main
​
import (
  "fmt"
  "html/template"
  "net/http"
  "time"
​
  "github.com/gin-gonic/gin"
​)
​
func formatAsDate(t time.Time) string {
  year, month, day := t.Date()  //Date方法返回年,月,日
  return fmt.Sprintf("%d%02d/%02d", year, month, day)  //格式化时间
}
​
func main() {
  router := gin.Default()
  router.Delims("{[{", "}]}") //自定义模板中的左右分隔符
  //SetFuncMap方法用给定的template.FuncMap设置到Gin引擎上, 后面模板渲染时会调用同名方法
  //FuncMap是一个map,键名关联方法名, 键值关联方法, 每个方法必须返回一个值, 或者返回两个值,其中第二个是error类型
  router.SetFuncMap(template.FuncMap{
    "formatAsDate": formatAsDate,
  })
  router.LoadHTMLFiles("./testdata/template/raw.tmpl") //加载单个模板文件并与HTML渲染器关联
​
  router.GET("/raw", func(c *gin.Context) {
    c.HTML(http.StatusOK, "raw.tmpl", gin.H{
      "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
    })
  })
​
  router.Run(":8080")
}
​
/*
模拟测试:
curl http://localhost:8080/raw
*/

定义模板文件: raw.tmpl

Date: {[{.now | formatAsDate}]}

时间格式化结果:

Date: 2017/07/01


多个模板


Gin默认只使用一个html.Template模板引擎, 也可以参考多模板渲染器使用类似Go1.6的块级模板block template功能.

模板相关详情请参考官方template包


参考文档


Gin官方仓库:https://github.com/gin-gonic/gin



END已结束

欢迎大家留言, 订阅, 交流哦!


往期回顾


Golang GinWeb框架6-XML/JSON/YAML/ProtoBuf等渲染

Golang GinWeb框架5-绑定请求字符串/URI/请求头/复选框/表单类型

Golang GinWeb框架4-请求参数绑定和验证

Golang GinWeb框架3-自定义日志格式和输出方式/启禁日志颜色

Golang GinWeb框架2-文件上传/程序panic崩溃后自定义处理方式

Golang GinWeb框架-快速入门/参数解析

Golang与亚马逊对象存储服务AmazonS3快速入门

Golang+Vue实现Websocket全双工通信入门

GolangWeb编程之控制器方法HandlerFunc与中间件Middleware

Golang连接MySQL执行查询并解析-告别结构体

Golang的一种发布订阅模式实现

Golang 并发数据冲突检测器(Data Race Detector)与并发安全

Golang"驱动"MongoDB-快速入门("快码加鞭")

互式文档是一种创建Shiny apps的新途径。交互式文档是一种包含Shiny控件与输出的 R Markdown文件, 你可以在 markdown中写报告,并且作为app来启动它。

本文主要阐述如何使用R Markdown写报告。

与本文配套的文献 Introduction to interactive documents, 将向你展示如何通过将R Markdown 报告转变成为带有Shiny组件的交互式文档。

R Markdown

R Markdown是通过R语言制作动态文档的文件格式。R Markdown文档在markdown中完成,其中包含嵌入的R代码,如下图:

---
title: R Markdown
output: html_document
---

This is an R Markdown document. Markdown is a simple formatting syntax which allows you to author HTML, PDF, and MS Word documents. For more details on how to use R Markdown, see <http://rmarkdown.rstudio.com>.

When you click the **Knit** button a document will be generated that includes both content as well as the output of any embedded R code chunks within the document. You can embed an R code chunk like this:

```{r}
summary(cars)
```

You can also embed plots:

```{r, echo=FALSE}
plot(cars)
```

Note that the `echo = FALSE` parameter was added to the code chunk to prevent printing of the R code that generated the plot.

R Markdown文档编辑需要 rmarkdown包,rmarkdown安装需要RStudio编辑器环境,但是你可以以github途径来下载rmarkdown,并安装。

devtools:install_github("rmarkdown", "rstudio")

R Markdown是资源代码丰富并高可用的文件,你可以将通过一下两种方式改变R Markdown文件格式。

  1. knit