背景
之前一直说要自己写个Python库,本来想写个简单的,比如说自动排序Python文件头的import语句,后来发现已经有现成的了,名为isort。最终实现了一个略微复杂一点的HTML转Markdown的库,名为ohHTMLToMarkdown。
为了学习一下如何发布一个Python包,我又厚颜无耻的把它传到了PyPI上。
实现思路
目前支持的HTML标签有:h1~h6
, p
, a
, img
, del
, b
, strong
, i
, em
, hr
, br
, ul
, ol
, table
, blockquote
, code
, pre
, span
, title
, time
, iframe
, section
, div
, html
, body
, head
。
处理主要靠的是BeautifulSoup
的html.parser
解析器和.descendants
对象。前者可以把html文档解析成一个树状模型,访问.contents
可以访问到当前层级的所有子树的根节点;后者可以返回一个生成器,它包含了当前子树的所有节点(包含本身)。
通过判断descendants
的长度可以知道该子树有没有子节点,之后根据当前tag的类型,可以选择继续进行深度遍历还是将它转换为Markdown的语义。
一个问题
>>> from bs4 import BeautifulSoup
>>> soup = BeautifulSoup("a
", 'html.parser')
>>> print (tuple(soup.descendants))
(a
, 'a')
>>> print (soup)
a
这里可是看出descendants
其实是包含其本身的,并且最底层的文字也会作为后代之一。这个问题在处理类似于
的时候不会有问题,但是在处理h1
boldita
就会有问题,我用了一个叫__special()
的方法解决了这个问题,但是看起来有点蠢。
测试
使用unittest
库写了19个测试用例,包含一些基本的功能测试。同时还从我的博客、、知乎上抓取了一些文章进行转换,总体效果还不错。
但是同样也有问题,就是每个网站在使用HTML时可能习惯不一样,比如说我的博客在渲染Markdown时,如果需要带行号显示,那么这块代码会被渲染成没有thead
的table
,而不是pre
;而在中我见到了类似
的代码。后者在处理的时候没问题,而前者会多出一大堆行号。
后来我就想这里其实可以定制一些子类,用来专门处理特定网站的特定标签格式,这些类继承Parser
类。
TODO
- 扩展子块用于解析特性网站的特定标签
- Nested列表(当前版本可以识别这种列表,但是无法输出正确格式的markdown,不会计算缩进)
- 其他未涉及的HTML标签
- 命令行工具
- 其他需要修改的问题
发布
如果想要把你的库发布到PyPI上,你至少需要一个PyPI账号。
这里还有一些其他步骤,我不确定这些是不是都是必须的,但是通过这些步骤,我上传成功了。有个详细的文档:
Python-OpenSource-Project-Developer-Guide’s documentation。
这里想提一句的是,PyPI似乎更新和简化了其项目发布的步骤,很多网上能搜到的教程都是老版的教程,我自己没试过。我这里所述的是我自己结合官方文档和同行项目之后,得到的精简步骤。
文件结构
因为我这个库只有一个py文件,只要把它放在同名目录下即可,同时可以新建其他三个文件setup.py
, README.md
,LICENSE
。其中后两者也不是必须的,但是一般都会有。如图:
>>> tree ohHtmlToMarkdown /F
D:\ONEDRIVE\DOCUMENTS\PYTHON和数据挖掘\CODE\OHHTMLTOMARKDOWN
LICENSE
ohHtmlToMarkdown.py
README.md
setup.py
这里提一句,虽然官方说模块名字最好用全小写,但其实带大写也不会有什么问题。
setup.py
一个setup的模板:
import setuptools
with open('README.md', 'r',encoding='utf-8') as fh:
long_description = fh.read()
setuptools.setup(
name="ohHtmlToMarkdown",
version="1.0.0",
description="A Python Library for converting HTML to Markdown.",
long_description=long_description,
long_description_content_type="text/markdown",
author="Ethan Huang",
author_email="[email protected]",
url='https://github.com/wfgydbu/ohHTML2Markdown',
packages=setuptools.find_packages(),
license='GNU GPL 3',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License (GPL)',
'Programming Language :: Python :: 3.6',
"Operating System :: OS Independent"
],
install_requires=['beautifulsoup4==4.6.0'],
py_modules=['ohHtmlToMarkdown'],
include_package_data=True,
zip_safe=False,
)
- 关于
classifiers
的选择,可以参考https://pypi.org/pypi?%3Aaction=list_classifiers。 - 关于
version
的填写,格式为major.minor[.patch[.sub]]. - 注意
py_modules
的使用,如果你的模块文件有点多,你可能会需要在根目录下在新建若干子目录,一般来说每个目录都会被认为是一个模块,通过modules.sub_module
调用,同时也要编写合适的__init__.py
文件。-
__init__.py
文件的作用是将文件夹变为一个Python模块,你可以把它当作模块的入口。
-
我这个setup.py
的模板是经过我反复锤炼过的(参考官方文档和同行的项目),它可以最小化的减少代码结构的调整。
LICENSE
假装会有人使用这个库,我这里选择了的License是GNU General Public License (GPL)
。关于LICENSE的选择可以参考choose licenses。
README.md
这个就不多说了,注意如果用的是MD的话,需要在setup.py
中配置,不然到时候显示的是MD的源码。
发布到PyPI
规范的发布过程在这里:Packaging Python Projects。但是它这里面的例子发布了一个没有实际代码的项目,导致少了一部分配置项,恰好这些配置项也是必要的。
PyPI会根据你在setup()中定义的配置项生成该项目在PyPI上的主页面,这是我发布的0.1版截图。
可以看到缺少项目描述,因为我的配置项的long_description写错了。改好之后,我发现我的描述是用中文写的。感觉把路走窄了。
指令
在setup.py
的同级目录下,执行:
# 安装twine
pip install --user --upgrade twine
# 打包
python setup.py sdist bdist_wheel
# 发布到测试服
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
# 发布到正式服
twine upload dist/*
执行完安装指令后,目录结构是这样的,我是在windows下做的:
D:\ONEDRIVE\DOCUMENTS\PYTHON和数据挖掘\CODE\OHHTMLTOMARKDOWN
│ LICENSE
│ ohHtmlToMarkdown.py
│ README.md
│ setup.py
│
├─build
│ ├─bdist.win-amd64
│ └─lib
│ ohHtmlToMarkdown.py
│
├─dist
│ ohHtmlToMarkdown-0.5.0-py3-none-any.whl
│ ohHtmlToMarkdown-0.5.0.tar.gz
│
└─ohHtmlToMarkdown.egg-info
dependency_links.txt
not-zip-safe
PKG-INFO
requires.txt
SOURCES.txt
top_level.txt
重要的补充
强烈建议在发布正式版本之前在测试服务器上试验一下,实际上,当生成了whl
文件之后,就可以直接使用pip install *.whl
安装这个包到本地了, 方便测试。
安装成功后,相关的代码和egg文件会被拷贝到${PATH}/Lib/site-packages
下(包括子目录),因此不管是模块名字还是子目录的名字,在世界范围内都应该是唯一的。
调用site-packages
下的py模块可以直接使用import module
,其他则是用.
连接,比如:import module.sub_module
。