基于alpine搭建python及wkhtmltopdf环境

1.引言

最近项目中遇到一个PDF相关的问题,需要通过程序生成PDF文件,PDF中包含较为复杂的内容(可能有表格、表单、文本、图片)。

一开始想到的方案是通过调用一些pdf库根据需要生成的内容手动绘制pdf文件,几年前使用java调用iText这个库做过类似的工作,但内容比较简单,而且效果不佳,不够美观。内容比较多的情况下,这种硬编码绘制pdf界面的工作量有点不敢想象,于是放弃了这个念头。

于是抱着找个现成的pdf工具直接拿来用的心态,打开github,搜索pdf,Language选择Python,按stars排序,结果可以分成以下几类:读写PDF文件内容、PDF操作(旋转、分割、合并、水印)、HTML转PDF、OCR文字图像识别...

到这里,初步选择“HTML转PDF”作为方向定位到WeasyPrint [★3.2k]、xhtml2pdf [★ 1.6k]、python-pdfkit [★ 1.1k] 这几个库。

依次尝试了这几个库发现只有★最少的python-pdfkit生成的pdf内容最为接近原始页面,没有很多动态效果的网页几乎是完全一样。并且使用起来也非常pythonic,一行代码就能完成转换动作。其它2个要么转换效果不够完美,要么使用过于复杂,没有太多尝试。

下图是将kotlin官网主页转成pdf后的效果,除页面上需要二次请求的部分图片和视频未显示出来,几乎跟原始页面完全一致。

wkhtmltopdf https://kotlinlang.org/ kotlin.pdf

python-pdfkit是用Python对wkthmltopdf [★ 9k]这个工具的封装,所以需要在系统上安装wkthmltopdf。

2.本地安装wkhtmltopdf及pdfkit

2.1 安装wkhtmltopdf

本地安装wkhtmltopdf比较简单,各大Linux操作系统的包管理工具都支持。

2.1.1 RedHat/CentOS/Fedora:
sudo dnf install wkhtmltopdf
2.1.2 Debian/Ubuntu:
$ sudo apt-get install wkhtmltopdf
2.1.3 macOS:
$ brew install caskroom/cask/wkhtmltopdf
2.1.4 windows

Windows平台可以去官网下载.exe文件安装,不过由于是国外网站,下载基本是龟速。

wkhtmltopdf安装完成以后,就可以在命令行测试是否安装成功了。

wkhtmltopdf https://www.baidu.com/ baidu.pdf

如果安装成功并且各种依赖没有问题,会在当前路径下生成一个pdf文件。

当然wkhtmltopdf支持许多参数,具体的用法可以查看帮助文档。

2.2 安装pdfkit

pdfkit可以在python环境下通过pip快速安装。

pip3 install pdfkit

3.服务器上安装wkhtmltopdf

服务器上安装wkhtmltopdf与本地Linux环境安装基本没有区别,使用包管理工具安装,千万别不自在尝试用.deb或.rpm包安装,否则依赖问题可能会让你不得不熬夜...

另外,还有一个问题,由于服务器一般没有图形界面,也就是X Server,而wkhtmltopdf依赖X Server,所以需要通过虚拟X Server来解决这个问题,具体的设置可以在pdfkit的wiki页面看到。

Using wkhtmltopdf without X server

4.Docker容器中安装wkhtmltopdf

到这里并没有结束,我们的目标在Docker容器中运行应用程序,所以需要在Docker容器中安装wkhtmltopdf。

之前为了省事,直接用python:3.8作为基础镜像,创建了应用程序的镜像。于是进入到该容器中尝试性地安装wkhtmltopdf,安装很顺利,结局很意外。在命令行运行wkhtmltopdf的时候找不到Qt相关的一个库文件。

# wkhtmltopdf -V
wkhtmltopdf: error while loading shared libraries: libQt5Core.so.5: cannot open shared object file: No such file or directory

一开始的思路是把这库文件加上,既然缺少,那么加上不就完事了嘛。结果四处搜索解决办法,百度、github issues、stackoverflow找到了同样的问题,按照热心网友提供的答案折腾了两个晚上才放弃...对,两个晚上还是没搞定,没办法只能换个思路来解决问题了。

在这期间,又尝试了使用ubuntu、centos等基础镜像,出现了上文提到的X Server相关的问题,一顿操作后wkhtmltopdf终于能正常运行了。

不过又发现了一个问题,发现之前构建的镜像(安装wkhtmltopdf后)大小达到了惊人的1.1G~1.2G...
其实这个问题以前也有注意到,只不过好歹能正常运行,就没有考虑性能和容量问题,哈哈!!

于是就想着今天就自己来构建一个小巧一些的镜像,所以就想到了这篇文章中的主角:Alpine

5.通过alpine构建最小镜像

到这里就直接放出Dockerfile了。

5.1 python 3.8环境

FROM alpine:edge
ENV LANG=C.UTF-8
RUN apk update
# 安装python、pip、pipenv
RUN apk add --no-cache python3=3.8.0-r0 && \
    ln -fs /usr/include/locale.h /usr/include/xlocale.h && \
    ln -fs /usr/bin/python3 /usr/local/bin/python && \
    ln -fs /usr/bin/pip3 /usr/local/bin/pip && \
    pip install pipenv

通过alpine安装python 3.8构建的镜像大小大概在80M左右。

5.2 python3.8 + wkhtmltopdf + 中文字体(思源黑体)

构建完python镜像后,就想能不能把wkhtmltopdf也加上呢?于是去alpine的安装包仓库查找了一下,幸运的是wkhtmltopdf已被收录进来,在community分区下。

https://pkgs.alpinelinux.org/packages?name=wkhtmltopdf&branch=edge

http://dl-cdn.alpinelinux.org/alpine/edge/community/

那么只要加上一行apk add wkhtmltopdf应该就可以了,最后添加中文字体。

FROM alpine:edge
ENV LANG=C.UTF-8
RUN apk update
# 安装python、pip、pipenv
RUN apk add --no-cache python3=3.8.0-r0 && \
    ln -fs /usr/include/locale.h /usr/include/xlocale.h && \
    ln -fs /usr/bin/python3 /usr/local/bin/python && \
    ln -fs /usr/bin/pip3 /usr/local/bin/pip && \
    pip install pipenv
RUN apk add --no-cache wkhtmltopdf
# 复制字体到字体目录,如果宿主机没有准备好字体文件,这里可以换成使用wget去下载
COPY fonts/SourceHanSansCN/SourceHanSansCN-Normal.otf /usr/share/fonts/

通过alpine安装python 3.8、wkhtmltopdf,并且安装中文字体后,镜像大小也仅有200M左右。

# 这里我将镜像命名为pypdf (python + wkhtmltopdf)
$ docker images
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
pypdf                latest              2bd31821d26a        31 minutes ago      212MB

5.3 打包应用程序镜像

到这一步,在上面的Dockerfile内容后面加上自己的应用程序的相关内容就可以了,大致为复制代码 > 设置环境变量 > 安装依赖 > 启动应用程序等操作。

但是过程中会使用pip安装依赖,需要用到gcc等工具,所以先通过apk安装这些库,pip安装完成依赖后再将这些库移除以缩小镜像体积。

FROM alpine:edge
ENV LANG=C.UTF-8
RUN apk update
RUN apk add --no-cache python3=3.8.0-r0 \
        python3-dev libgfortran build-base libstdc++ libpng libpng-dev freetype freetype-dev && \
    ln -fs /usr/include/locale.h /usr/include/xlocale.h && \
    ln -fs /usr/bin/python3 /usr/local/bin/python && \
    ln -fs /usr/bin/pip3 /usr/local/bin/pip && \
    pip install pipenv
RUN apk add --no-cache wkhtmltopdf
COPY fonts/SourceHanSansCN/SourceHanSansCN-Normal.otf /usr/share/fonts/

# 复制代码 > 设置环境变量 > 安装依赖 > 启动应用程序
# 这里只是为了验证程序能否正常运行,简单起见,没有加uWSGI
ADD ./ /app/
WORKDIR /app/
ENV FLASK_DEBUG=0
ENV FLASK_HOST="0.0.0.0"
ENV FLASK_APP="run_app.py"
RUN pipenv install
EXPOSE 5000
CMD pipenv run python run_app.py

RUN apk del --purge build-base libgfortran libpng-dev freetype-dev python3-dev && rm -vrf /var/cache/apk/*

6.收工

镜像创建成功后,启动容器,运行程序,把生成pdf的逻辑走了一遍,一切正常,nice!!!

今天终于可以早点睡觉了,哈哈哈!!!

7.后记

本以为一切顺利、万事大吉的时候,又发生意外了!wkhtmltopdf这个工具居然还区分with patched qt版本和with unpatched qt 版本,只有patched qt版本支持诸如页眉、页脚、边距等高级操作。很不幸,在alpine系统上通过apk安装的是wkhtmltopdf 0.12.5 (with unpatched qt)。

业务需求无法实现自然是不行的,只得继续在github上寻找答案。

首先找到一种方案:在装好wkhtmltopdf 0.12.5 (with unpatched qt)的基础上安装及配置qt,由于对qt并不了解,看到洋洋洒洒几十行操作步骤,只能另寻他法。

第二种方案:事先在其它的系统上编译安装配置好wkhtmltopdf 0.12.5 (with patched qt),将最终得到的可执行文件移植到alpine系统上不就可以了,不过wkhtmltopdf依赖的库当然必不可少需要一并安装。至于可执行程序我自己就不去编译了,已经有热心人分享出来了,直接拿来用即可。最后我将这个方案写成了Dockerfile,wkhtmltopdf可执行程序也上传到github,另外我还一并添加了谷歌思源字体,毕竟要处理中文字体。

FROM alpine:edge
# 1.add dependencies for wkhtmltopdf
RUN apk add --update --no-cache \
    libgcc libstdc++ libx11 glib libxrender libxext libintl \
    ttf-dejavu ttf-droid ttf-freefont ttf-liberation ttf-ubuntu-font-family
# 2.copy executable wkhtmltopdf to the container's `/bin` folder
COPY wkhtmltopdf /bin/wkhtmltopdf
# 3.add chinese font `SourceHanSansCN` to the container
COPY fonts/SourceHanSansCN-Normal.otf /usr/share/fonts/SourceHanSansCN-Normal.otf

最后我做成了一个wkhtmltopdf的Docker镜像,额外的Python环境就在这个镜像基础上继续构建。

到这里,曲折的过程才算画上句号!