关于本书的编写
为了编写此书,我评价了许多写书的软件,最终决定使用 Sphinx 和 reStructuredText 作为写书的工具。随着章节的逐渐增加,我越来越觉得当初的选择是正确的。
本书的编写工具
本书采用 reStructuredText (rst) 格式的文本编写,然后用 Sphinx 将 reStructuredText 文件自动转换为 html 格式的文件。采用 Leo 管理和组织所有的文档。用 proTeXt 将 latex 格式的数学公式转换为 PNG 图片。
- reStructuredText : 一种结构化文本格式,它提供了对写书所需的各种元素的支持。例如章节、链接、格式、图片以及语法高亮等等。
- Sphinx : 将一系列 reStructuredText 文本转换成各种不同的输出格式,并自动制作交叉引用(cross-references)、索引等。也就是说,如果某目录中有一系列的 reStructuredText 格式的文档, Sphinx 可以制作一份组织得非常完美的 HTML 文件。
- Leo : 以树状结构管理文本、代码的编辑器,可以用它来进行数据组织和项目管理。我使用它管理构成本书的所有 rst 文档、python 程序以及图片和笔记。下面是使用 Leo 编写本书时的一个例子:
编写本书所使用的 Leo 编辑器的界面
- PicPick , Greenshot : 界面截图工具。
问题与解决方案
在使用上述工具编写本书时,为了达到完美的效果,我对这些工具做了一些配置和修改的工作。
代码中的注释
Sphinx 使用 Pygments 进行代码高亮的处理,在 Pygments 的缺省样式中,代码注释部分是采用斜体字表示的,斜体的汉字看起来十分别扭,因此需要将缺省样式的斜体改为正体。在 conf.py 文件中有如下配置:
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
它指定 pygments 使用 sphinx 样式对代码进行高亮处理,我没有弄明白如何添加自己定义的样式,因此直接手工修改定义此样式的文件:
%Python 安装目录%\Lib\site-packages\sphinx\highlighting.py
将其中的 Comment 的样式改为 noitalic:
...
styles.update({
Generic.Output: '#333',
Comment: 'noitalic #408090',
Number: '#208050',
})
...
修改 Sphinx 的主题
为了给文档添加评论功能,必须添加一部分 javascript 代码,因此需要修改 Shpinx 的主题。
- 首先编辑 conf.py 文件中如下的两个配置:
# The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'pydoc' # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ["./theme"]
- 然后在 conf.py 文件所在的目录下创建一个子目录 theme,将 sphinx 安装目录下的 themes\sphinxdoc 文件夹复制到 theme 文件夹下,并重命名为 pydoc,目录结构如下图所示:
theme 文件夹的结构
- 编辑 layout.html 文件。此文件是一个模板,Sphinx 最终使用此模板生成每个 rst 文件所对应的 html 文件。因此我在其中添加了对我自己的 css 和 js 文件的引用:
<link type="text/css" href="_static/jquery-ui-1.7.2.custom.css" rel="stylesheet" />
<link type="text/css" href="_static/comments.css" rel="stylesheet" />
<script type="text/javascript" src="_static/jquery-ui-1.7.2.custom.min.js"></script>
<script type="text/javascript" src="_static/pydoc.js"></script>
- 在 theme\pydoc\static 目录下添加相应的 css 和 js 文件。为了固定 html 页面左侧的目录栏,可以配置 theme\pydoc\theme.conf 中的 stickysidebar=True,不过好像 IE7.0 下无法正常显示,因此在 css 文件中添加如下代码,除了 IE6.0 以外其它的浏览器(Firefox,IE7, Chrome) 都能够正常固定目录栏:
div.sphinxsidebar{ position : fixed; left : 0px; top : 30px; margin-left : 0px !important; }
关闭引号自动转换
在输出 html 的时候,如果使用 Sphinx 缺省的配置,会对引号进行自动转换:将标准的单引号和双引号转换为 unicode 中的全角引号。为了关闭此项功能,需要编辑 conf.py,进行如下设置:
html_use_smartypants = False
用 latex 编写数学公式
Sphinx 支持将 latex 编写的数学公式转换为 png 图片。为了在 windows 下使用 latex,我下载了 proTeXt ,这个 tex 软件包的大小有 700M 左右,安装之后占用 1.3G。为了告诉 Sphinx 工具 latex 的安装位置,如下修改 make.bat 文件:
%SPHINXBUILD% -D pngmath_latex="..\latex.exe" -b html %ALLSPHINXOPTS% build/html
然后就可以如下使用 latex:
X_k = \sum_{n=0}^{N-1} x_n e^{-{i 2\pi k \frac{n}{N}}} \qquad k = 0,\dots,N-1.
得到的输出图片如下:
Leo 的配置
Leo 的缺省配置用起来很不习惯:它的树状目录在上方,而且字体很小。下面是对 Leo 的一些修改和配置:
- Leo 现在可以使用 tk 和 qt 两个库。使用 tk 库的界面用起来不习惯,因此通过在启动 Leo 时添加参数强制使用 qt 库的界面:launchLeo.py --gui=qt 。
- 我个人很喜欢微软雅黑的汉字字体,但是由于雅黑字体的英文不是等宽的,因此用它来编辑代码的话就很不爽了。于是上网找到了一个雅黑和 Consolas 的复合字体:
YaHei Mono 字体下载地址: http://hyry.dip.jp/files/yahei_mono.7z
- 复制一份 leo\config\leoSettings.leo 到同一目录,改名为 myLeoSettings.leo。用 Leo 编辑此文件,在目录树中找到节点:qtGui plugin-->@data qt-gui-plugin-style-sheet,修改此样式表中的字体的定义,使用新安装的 Yahei Mono 字体。
QTextEdit#richTextEdit { ... font-family: Yahei Mono; font-size: 17px; ... }
- 修改 @settings-->Window-->@string initial_split_orientation 节点和 @settings-->Window-->Options for new windows-->@strings[vertical,horizontal] initial_splitter_orientation 节点的值为 horizontal。这样目录树和编辑框就是左右分栏的了。
- 在 Leo 中用 @auto-rst 输出 rst 文件时,会自动的将目录树中的节点名转换为 rst 文件中的标题。在 rst 中标题都是由下划线标出的。下划线的长度要求和文本的长度一致。由于 Leo 采用 unicode 表示文本,因此汉字的长度为 1,但是 rst 编译器似乎要求汉字的长度为 2,因此对于 Leo 的配置 这样的标题,rst 要求用 9 个下划线符号标识,而 Leo 只用 6 个,造成在编译时出现许多警告信息,为了解决这个问题,编辑 leo\core\leoRst.py 文件中的 underline 函数如下,并且将其后的所有 len(s) 都改为 len(ss):
def underline (self,s,p): ... try: ss = s.encode("gbk") except: try: ss = s.encode("shiftjis") except: ss = s trace = False and not g.unitTesting ...
让 Matplotlib 显示中文
将中文字体文件复制到:
%PythonPath%\Lib\site-packages\matplotlib\mpl-data\fonts\ttf\
下,这里以上一节介绍的 Yahei Mono 字体为例。
找到 Matplotlib 的配置文件 matplotlibrc,全局配置文件的路径:
%PythonPath%\Lib\site-packages\matplotlib\mpl-data\matplotlibrc
用户配置文件路径:
c:\Documents and Settings\%UserName%\.matplotlib\matplotlibrc
用文本编辑器打开此文件,进行如下编辑:
- 找到设置 font.family 的行,改为 font.family : monospace,注意去掉前面的#号。
- 在下面添加一行:font.monospace : Yahei Mono。
在 matplotlib 中使用中文字符串时记住要用 unicode 格式,例如:u"测试中文显示"。
用 Matplotlib 生成图片
matplotlib 提供了一个 Sphinx 的扩展插件,可以使用..plot 命令自动生成图片。可是这个插件生成的图片的路径和本书所采用的路径不符合,无法在 HTML 文件中显示最终生成的图。因此我直接复制下面两个文件:
c:\Python26\Lib\site-packages\matplotlib\sphinxext\plot_directive.py
c:\Python26\Lib\site-packages\matplotlib\sphinxext\only_directives.py
到 sourceexts 下,命名为 plot_directive.py。然后编辑 conf.py,修改下面两行:
sys.path.append(os.path.abspath('exts'))
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest',
'sphinx.ext.pngmath', 'plot_directive']
这样就可以载入 extsplot_directive.py 扩展插件了。然后编辑 plot_directive.py 文件,使得它的输出符合本书的路径,并且除去大图和 PDF 输出。
在 rst 文件中使用:
import matplotlib.pyplot as plt
import numpy as np
x = np.random.randn(1000)
plt.hist( x, 20)
plt.grid()
plt.title(r'Normal: $\mu=%.2f, \sigma=%.2f$'%(x.mean(), x.std()))
plt.show()
用 Graphviz 绘图
Sphinx 可以调用 Graphviz 绘制流程图,首先下载 Graphviz 的 Windows 安装包进行安装,假设安装目录为 c:\graphviz。
Graphviz 的下载地址: http://www.graphviz.org
编辑 conf.py 配置文件,在 extensions 列表定义的最后添加一项:'sphinx.ext.graphviz'。
如下编辑 make.bat 文件,配置 dot.exe 的执行路径:
.. graphviz::
digraph GraphvizDemo{
node [fontname="Yahei Mono" shape="rect"];
edge [fontname="Yahei Mono" fontsize=10];
node1[label="开始"];
node2[label="正常"];
node1->node2[label="测试"];
}
输出图为:
![digraph GraphvizDemo{ node [fontname="Yahei Mono" shape="rect"]; edge [fontname="Yahei Mono" fontsize=10];
node1[label="开始"];
node2[label="正常"];
node1->node2[label="测试"];
}](img/graphviz-691597b9de6125817b93aaad942bf30f1e3d5346.png)
制作 CHM 文档
Sphinx 支持输出为 CHM 文档格式,只需要运行 make htmlhelp 即可。但是此命令输出的目录文件(扩展名为.hhc),却不支持中文。为了解决这个问题,我进行了如下修改:
- sphinx 的安装目录下找到 buildershtmlhelp.py,将其复制一份,改名为 htmlhelpcn.py。输出 CHM 文档的程序都在这里面。
- 修改 builders__init__.py 文件,在其最后的 BUILTIN_BUILDERS 字典定义中添加一行:
'htmlhelpcn': ('htmlhelpcn', 'HTMLHelpBuilder')
- 修改 make.bat 文件,在其中添加:
if "%1" == "htmlhelpcn" ( %SPHINXBUILD% -b htmlhelpcn %ALLSPHINXOPTS% build/htmlhelpcn echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in build/htmlhelpcn. goto end )
- 编辑 htmlhelpcn.py 文件,找到 project_template 字符串的定义,修改其中的 Language 定义为 Language=0x804。
- 反复运行 make.bat htmlhelpcn 命令,根据输出的错误提示修改 htmlhelpcn.py,将其中几处编码错误的地方都添加.encode("gb2312")。其中有一处:
f.write(item.encode('ascii', 'xmlcharrefreplace')) # 改为--> f.write(item.encode('gb2312'))
- 如果在 rst 文档中给图片添加了中文说明的话,有可能输出的 CHM 文件中看不到图片。
- make.bat htmlhelpcn 正常运行之后,运行下面的命令输出制作 CHM 文件:
"C:\Program Files\HTML Help Workshop\hhc.exe" htmlhelpcn\scipydoc.hhp
CHM 中嵌入 Flash 动画
用如下的 reStructuredText 的 raw 指令可以在 html 中嵌入 Flash 动画:
<OBJECT CLASSID="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" WIDTH="589" HEIGHT="447"
CODEBASE="http://active.macromedia.com/flash5/cabs/swflash.cab#version=7,0,0,0">
<PARAM NAME="movie" VALUE="img/fft_study_04.swf">
<PARAM NAME="play" VALUE="true">
<PARAM NAME="loop" VALUE="false">
<PARAM NAME="wmode" VALUE="transparent">
<PARAM NAME="quality" VALUE="high">
<EMBED SRC="img/fft_study_04.swf" width="589" HEIGHT="447" quality="high"
loop="false" wmode="transparent" TYPE="application/x-shockwave-flash"
PLUGINSPAGE=
"http://www.macromedia.com/shockwave/download/index.cgi?P1_Prod_Version=ShockwaveFlash">
</EMBED>
</OBJECT>
由于 Html Help Workshop 不会将 swf 文件打包进 CHM,因此 CHM 中看不到 flash 动画,只需要在嵌入 flash 动画的 html 之后添加一条:
<img src="img/fft_study_04.swf" style="visibility:hidden"/>
这样 Html Help Workshop 就会把 fft_study_04.swf 文件添加进去,由于使用隐藏的 CSS,页面中也不会把它当作图片显示出来。
制作 PDF 文档
调用 make latex 命令可以输出为 latex 格式的文件,然后调用 xelatex scipydoc.tex 即可将其转换为 PDF 文件,xelatex 是 proTeXt 自带的命令。制作 PDF 文档时同样有中文无法显示的问题,按照以下步骤解决:
- 编辑文档的配置文件 conf.py,在最后的 Options for LaTeX output 定义处,添加如下代码,这段文字将添加到最终输出的 tex 文件中,这里的 Yahei Mono 可以修改为你想要的中文字体名:
latex_preamble = r"""
\usepackage{float}
\textwidth 6.5in
\oddsidemargin -0.2in
\evensidemargin -0.2in
\usepackage{ccaption}
\usepackage{fontspec,xunicode,xltxtra}
\setsansfont{Microsoft YaHei}
\setromanfont{Microsoft YaHei}
\setmainfont{Microsoft YaHei}
\setmonofont{Yahei Mono}
\XeTeXlinebreaklocale "zh"
\XeTeXlinebreakskip = 0pt plus 1pt
\renewcommand{\baselinestretch}{1.3}
\setcounter{tocdepth}{3}
\captiontitlefont{\small\sffamily}
\captiondelim{ - }
\renewcommand\today{\number\year 年\number\month 月\number\day 日}
\makeatletter
\renewcommand*\l@subsection{\@dottedtocline{2}{2.0em}{4.0em}}
\renewcommand*\l@subsubsection{\@dottedtocline{3}{3em}{5em}}
\makeatother
\titleformat{\chapter}[display]
{\bfseries\Huge}
{\filleft \Huge 第 \hspace{2 mm} \thechapter \hspace{4 mm} 章}
{4ex}
{\titlerule
\vspace{2ex}%
\filright}
[\vspace{2ex}%
\titlerule]
%\definecolor{VerbatimBorderColor}{rgb}{0.2,0.2,0.2}
\definecolor{VerbatimColor}{rgb}{0.95,0.95,0.95}
""".decode("utf-8")
通过 renewcommand 命令将输出的 PDF 文档中的一部分英文修改为中文。
不知何故,在 latex_preamble 中添加修改插图标题前缀的命令没有作用,因此通过下面的命令在正文中添加转换前缀的 renewcommand:
.. raw:: latex
\renewcommand\partname{部分}
\renewcommand{\chaptermark}[1]{\markboth{第 \thechapter\ 章 \hspace{4mm} #1}{}}
\fancyhead[LE,RO]{用 Python 做科学计算}
\renewcommand{\figurename}{\textsc{图}}
- 调整 conf.py 中的其它选项:
latex_paper_size = 'a4' latex_font_size = '11pt' latex_use_modindex = False
- 运行下面的命令输出 PDF 文档,使用 nonstopmode,即使出现错误也不暂停运行。
xelatex -interaction=nonstopmode scipydoc.tex
还有一些 latex 配置没有找到如何使用 reStructuredText 进行设置,因此写了一个 Python 的小程序读取输出的 tex 文件,替换其中的一些 latex 命令:
- 将 begin{figure}[htbp]改为 begin{figure}[H},这样能保证图和文字保持 tex 中的前后关系,而不会对图进行自动排版
- 在\tableofcontents 之前添加\renewcommand\contentsname{目 录},将目录标题的英文改为中文,此段配置在 latex_preamble 中定义无效
添加 PDF 封面
使用作图软件设计封面图片之后,使用图片转 PDF 工具将其转换为一个只有一页的 PDF 文档 cover.pdf:
图片转 PDF 工具下载地址: http://www.softinterface.com
然后使用 PDF 合并工具将 cover.pdf 和正文的 PDF 文件进行合并。我在网络上找了很久,终于找到了下面这个能够维持内部链接和书签的免费的合并工具:
PDF 工具 PDFsam 下载地址: http://www.pdfsam.org
PDFsam 提供了界面和命令行方式,界面方式很容易使用,但是为了一个批处理产生最终 PDF 文档我需要使用命令行方式,下面是使用命令行进行 PDF 文档合并的批处理程序:
set MERGE=java -jar "c:\Program Files\pdfsam\lib\pdfsam-console-2.2.0e.jar"
%MERGE% -f cover.pdf -f scipydoc.pdf -o %CD%\scipydoc2.pdf concat
- -f 参数指定输入的 PDF 文件名
- -o 参数指定输出的 PDF 文件名,注意必须使用绝对路径,因此这里使用%CD%将相对路径转换为绝对路径。
输出打包的批处理
下面是同时输出 zip, chm, pdf 文件的批处理命令:
rename html scipydoc
"c:\Program Files\7-Zip\7z.exe" a scipydoc.zip scipydoc
rename scipydoc html
"C:\Program Files\HTML Help Workshop\hhc.exe" htmlhelpcn\scipydoc.hhp
copy htmlhelpcn\scipydoc.chm . /y
cd latex
xelatex -interaction=nonstopmode scipydoc.tex
cd ..
copy latex\scipydoc.pdf . /y
HTML 的中文搜索
由于 Sphinx 不懂中文分词,因此它所生成的搜索索引文件 searchindex.js 中的中文单词分的不正确。为了修正这个问题,我写了一个 Sphinx 扩展 chinese_search.py,使用中文分词库 smallseg 生成索引文件中的中文单词。
smallseg 中文分词库地址: http://code.google.com/p/smallseg
下面是这个扩展的完整源程序:
from os import path
import re
import cPickle as pickle
from docutils.nodes import comment, Text, NodeVisitor, SkipNode
from sphinx.util.stemmer import PorterStemmer
from sphinx.util import jsdump, rpartition
from smallseg import SEG
DEBUG = False
word_re = re.compile(r'\w+(?u)')
stopwords = set("""
a and are as at
be but by
for
if in into is it
near no not
of on or
such
that the their then there these they this to
was will with
""".split())
if DEBUG:
testfile = file("testfile.txt", "wb")
class _JavaScriptIndex(object):
"""
The search index as javascript file that calls a function
on the documentation search object to register the index.
"""
PREFIX = 'Search.setIndex('
SUFFIX = ')'
def dumps(self, data):
return self.PREFIX + jsdump.dumps(data) + self.SUFFIX
def loads(self, s):
data = s[len(self.PREFIX):-len(self.SUFFIX)]
if not data or not s.startswith(self.PREFIX) or not \
s.endswith(self.SUFFIX):
raise ValueError('invalid data')
return jsdump.loads(data)
def dump(self, data, f):
f.write(self.dumps(data))
def load(self, f):
return self.loads(f.read())
js_index = _JavaScriptIndex()
class Stemmer(PorterStemmer):
"""
All those porter stemmer implementations look hideous.
make at least the stem method nicer.
"""
def stem(self, word):
word = word.lower()
return word
#return PorterStemmer.stem(self, word, 0, len(word) - 1)
class WordCollector(NodeVisitor):
"""
A special visitor that collects words for the `IndexBuilder`.
"""
def __init__(self, document):
NodeVisitor.__init__(self, document)
self.found_words = []
def dispatch_visit(self, node):
if node.__class__ is comment:
raise SkipNode
if node.__class__ is Text:
words = seg.cut(node.astext().encode("utf8"))
words.reverse()
self.found_words.extend(words)
class IndexBuilder(object):
"""
Helper class that creates a searchindex based on the doctrees
passed to the `feed` method.
"""
formats = {
'jsdump': jsdump,
'pickle': pickle
}
def __init__(self, env):
self.env = env
self._stemmer = Stemmer()
# filename -> title
self._titles = {}
# stemmed word -> set(filenames)
self._mapping = {}
# desctypes -> index
self._desctypes = {}
def load(self, stream, format):
"""Reconstruct from frozen data."""
if isinstance(format, basestring):
format = self.formats[format]
frozen = format.load(stream)
# if an old index is present, we treat it as not existing.
if not isinstance(frozen, dict):
raise ValueError('old format')
index2fn = frozen['filenames']
self._titles = dict(zip(index2fn, frozen['titles']))
self._mapping = {}
for k, v in frozen['terms'].iteritems():
if isinstance(v, int):
self._mapping[k] = set([index2fn[v]])
else:
self._mapping[k] = set(index2fn[i] for i in v)
# no need to load keywords/desctypes
def dump(self, stream, format):
"""Dump the frozen index to a stream."""
if isinstance(format, basestring):
format = self.formats[format]
format.dump(self.freeze(), stream)
def get_modules(self, fn2index):
rv = {}
for name, (doc, _, _, _) in self.env.modules.iteritems():
if doc in fn2index:
rv[name] = fn2index[doc]
return rv
def get_descrefs(self, fn2index):
rv = {}
dt = self._desctypes
for fullname, (doc, desctype) in self.env.descrefs.iteritems():
if doc not in fn2index:
continue
prefix, name = rpartition(fullname, '.')
pdict = rv.setdefault(prefix, {})
try:
i = dt[desctype]
except KeyError:
i = len(dt)
dt[desctype] = i
pdict[name] = (fn2index[doc], i)
return rv
def get_terms(self, fn2index):
rv = {}
for k, v in self._mapping.iteritems():
if len(v) == 1:
fn, = v
if fn in fn2index:
rv[k] = fn2index[fn]
else:
rv[k] = [fn2index[fn] for fn in v if fn in fn2index]
return rv
def freeze(self):
"""Create a usable data structure for serializing."""
filenames = self._titles.keys()
titles = self._titles.values()
fn2index = dict((f, i) for (i, f) in enumerate(filenames))
return dict(
filenames=filenames,
titles=titles,
terms=self.get_terms(fn2index),
descrefs=self.get_descrefs(fn2index),
modules=self.get_modules(fn2index),
desctypes=dict((v, k) for (k, v) in self._desctypes.items()),
)
def prune(self, filenames):
"""Remove data for all filenames not in the list."""
new_titles = {}
for filename in filenames:
if filename in self._titles:
new_titles[filename] = self._titles[filename]
self._titles = new_titles
for wordnames in self._mapping.itervalues():
wordnames.intersection_update(filenames)
def feed(self, filename, title, doctree):
"""Feed a doctree to the index."""
self._titles[filename] = title
visitor = WordCollector(doctree)
doctree.walk(visitor)
def add_term(word, prefix='', stem=self._stemmer.stem):
word = stem(word)
word = word.strip(u"!@#$%^&*()_+-*/\\\";,.[]{}<>")
if len(word) <= 1: return
if word.encode("utf8").isalpha() and len(word) < 3: return
if word.isdigit(): return
if word in stopwords: return
try:
float(word)
return
except:
pass
if DEBUG:
testfile.write("%s\n" % word.encode("utf8"))
self._mapping.setdefault(prefix + word, set()).add(filename)
words = seg.cut(title.encode("utf8"))
for word in words:
add_term(word)
for word in visitor.found_words:
add_term(word)
def load_indexer(self):
def func(docnames):
print "############### CHINESE INDEXER ###############"
self.indexer = IndexBuilder(self.env)
keep = set(self.env.all_docs) - set(docnames)
try:
f = open(path.join(self.outdir, self.searchindex_filename), 'rb')
try:
self.indexer.load(f, self.indexer_format)
finally:
f.close()
except (IOError, OSError, ValueError):
if keep:
self.warn('search index couldn\'t be loaded, but not all '
'documents will be built: the index will be '
'incomplete.')
# delete all entries for files that will be rebuilt
self.indexer.prune(keep)
return func
def builder_inited(app):
if app.builder.name == 'html':
print "****************************"
global seg
seg = SEG()
app.builder.load_indexer = load_indexer(app.builder)
def setup(app):
app.connect('builder-inited', builder_inited)
PDF 的页码和图编号参照
Sphinx 生成的 tex 文件没有使用\label 和\ref 进行编号引用,而是生成一些链接,这些链接虽然方便电子版的阅读,可是打印出来之后就毫无用处了,因此我写了一个扩展 latex_ref.py 为最终生成的 PDF 添加编号引用功能,这个扩展添加了三个 role:tlabel, tref, tpageref,分别对应 tex 的\label, \ref, \pageref。
下面是完整的源程序:
# -*- coding: utf-8 -*-
from docutils import nodes, utils
class tref(nodes.Inline, nodes.TextElement):
pass
class tlabel(nodes.Inline, nodes.TextElement):
pass
class tpageref(nodes.Inline, nodes.TextElement):
pass
def tref_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
data = text.split(",")
if u"图" in data[0]:
name = u"图"
pos = data[0][0]
ref = data[1]
return [tref(name=name, ref=ref, pos=pos)], []
return [],[]
def tlabel_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
return [tlabel(latex=text)], []
def tpageref_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
return [tpageref(latex=text)], []
def latex_visit_ref(self, node):
self.body.append(r"%s\ref{%s}" % (node['name'], node['ref']))
raise nodes.SkipNode
def html_visit_ref(self, node):
self.body.append(r'<a href="#%s">%s%s</a>' % (node['ref'], node['pos'], node['name']))
raise nodes.SkipNode
def latex_visit_label(self, node):
self.body.append(r"\label{%s}" % node['latex'])
raise nodes.SkipNode
def latex_visit_pageref(self, node):
self.body.append(r"\pageref{%s}" % node['latex'])
raise nodes.SkipNode
def empty_visit(self, node):
raise nodes.SkipNode
def setup(app):
app.add_node(tref,latex=(latex_visit_ref, None),text=(empty_visit, None),html=(html_visit_ref, None))
app.add_node(tlabel,latex=(latex_visit_label, None),text=(empty_visit, None),html=(empty_visit, None))
app.add_node(tpageref,latex=(latex_visit_pageref, None),text=(empty_visit, None),html=(empty_visit, None))
app.add_role('tref', tref_role)
app.add_role('tlabel', tlabel_role)
app.add_role('tpageref', tpageref_role)
ReST 使用心得
添加图的编号和标题
使用 figure 命令插入带编号和标题的插图:
.. _pythonxyhome:
.. figure:: images/pythonxy_home.png
Python(x,y) 的启动画面
PDF 文字包围图片
当给 figure 添加 figwidth 和 align 属性之后,在生成的 latex 文档中,将使用 wrapfigure 生成图。为了和前面的段落之间添加一个换行符,使用一个斜杠空格。
.. literalinclude:: examples/tvtk_cone.example.py
.. literalinclude:: example.c
:language: c
未解决的问题
数学公式输出不正确
有时候数学公式的输出不正确,某些数学符号不能显示,可是多试几次之后就正常了,不知道是什么原因。
Leo 不能配置目录树和编辑框的宽度比例
每次 Leo 开启之后目录树和编辑框的宽度是相等的,看上去很不协调。而且修改 mySettings.leo 中的相关配置也不能解决,不明白是什么问题。目前的解决方法是添加两个工具按钮:show-tree 和 hide-tree,这样点击一下 show-tree 就会将目录树和编辑框改为 1:3 的比例;而点击 hide-tree 则能隐藏目录树:
# -*- coding: utf-8 -*-
from enthought.traits.api import \
Str, Float, HasTraits, Property, cached_property, Range, Instance, on_trait_change, Enum
from enthought.chaco.api import Plot, AbstractPlotData, ArrayPlotData, VPlotContainer
from enthought.traits.ui.api import \
Item, View, VGroup, HSplit, ScrubberEditor, VSplit
from enthought.enable.api import Component, ComponentEditor
from enthought.chaco.tools.api import PanTool, ZoomTool
import numpy as np
# 鼠标拖动修改值的控件的样式
scrubber = ScrubberEditor(
hover_color = 0xFFFFFF,
active_color = 0xA0CD9E,
border_color = 0x808080
)
# 取 FFT 计算的结果 freqs 中的前 n 项进行合成,返回合成结果,计算 loops 个周期的波形
def fft_combine(freqs, n, loops=1):
length = len(freqs) * loops
data = np.zeros(length)
index = loops * np.arange(0, length, 1.0) / length * (2 * np.pi)
for k, p in enumerate(freqs[:n]):
if k != 0: p *= 2 # 除去直流成分之外,其余的系数都*2
data += np.real(p) * np.cos(k*index) # 余弦成分的系数为实数部
data -= np.imag(p) * np.sin(k*index) # 正弦成分的系数为负的虚数部
return index, data
class TriangleWave(HasTraits):
# 指定三角波的最窄和最宽范围,由于 Range 似乎不能将常数和 traits 名混用
# 所以定义这两个不变的 trait 属性
low = Float(0.02)
hi = Float(1.0)
# 三角波形的宽度
wave_width = Range("low", "hi", 0.5)
# 三角波的顶点 C 的 x 轴坐标
length_c = Range("low", "wave_width", 0.5)
# 三角波的定点的 y 轴坐标
height_c = Float(1.0)
# FFT 计算所使用的取样点数,这里用一个 Enum 类型的属性以供用户从列表中选择
fftsize = Enum( [(2**x) for x in range(6, 12)])
# FFT 频谱图的 x 轴上限值
fft_graph_up_limit = Range(0, 400, 20)
# 用于显示 FFT 的结果
peak_list = Str
# 采用多少个频率合成三角波
N = Range(1, 40, 4)
# 保存绘图数据的对象
plot_data = Instance(AbstractPlotData)
# 绘制波形图的容器
plot_wave = Instance(Component)
# 绘制 FFT 频谱图的容器
plot_fft = Instance(Component)
# 包括两个绘图的容器
container = Instance(Component)
# 设置用户界面的视图, 注意一定要指定窗口的大小,这样绘图容器才能正常初始化
view = View(
HSplit(
VSplit(
VGroup(
Item("wave_width", editor = scrubber, label=u"波形宽度"),
Item("length_c", editor = scrubber, label=u"最高点 x 坐标"),
Item("height_c", editor = scrubber, label=u"最高点 y 坐标"),
Item("fft_graph_up_limit", editor = scrubber, label=u"频谱图范围"),
Item("fftsize", label=u"FFT 点数"),
Item("N", label=u"合成波频率数")
),
Item("peak_list", style="custom", show_label=False, width=100, height=250)
),
VGroup(
Item("container", editor=ComponentEditor(size=(600,300)), show_label = False),
orientation = "vertical"
)
),
resizable = True,
width = 800,
height = 600,
title = u"三角波 FFT 演示"
)
# 创建绘图的辅助函数,创建波形图和频谱图有很多类似的地方,因此单独用一个函数以
# 减少重复代码
def _create_plot(self, data, name, type="line"):
p = Plot(self.plot_data)
p.plot(data, name=name, title=name, type=type)
p.tools.append(PanTool(p))
zoom = ZoomTool(component=p, tool_mode="box", always_on=False)
p.overlays.append(zoom)
p.title = name
return p
def __init__(self):
# 首先需要调用父类的初始化函数
super(TriangleWave, self).__init__()
# 创建绘图数据集,暂时没有数据因此都赋值为空,只是创建几个名字,以供 Plot 引用
self.plot_data = ArrayPlotData(x=[], y=[], f=[], p=[], x2=[], y2=[])
# 创建一个垂直排列的绘图容器,它将频谱图和波形图上下排列
self.container = VPlotContainer()
# 创建波形图,波形图绘制两条曲线: 原始波形(x,y) 和合成波形(x2,y2)
self.plot_wave = self._create_plot(("x","y"), "Triangle Wave")
self.plot_wave.plot(("x2","y2"), color="red")
# 创建频谱图,使用数据集中的 f 和 p
self.plot_fft = self._create_plot(("f","p"), "FFT", type="scatter")
# 将两个绘图容器添加到垂直容器中
self.container.add( self.plot_wave )
self.container.add( self.plot_fft )
# 设置
self.plot_wave.x_axis.title = "Samples"
self.plot_fft.x_axis.title = "Frequency pins"
self.plot_fft.y_axis.title = "(dB)"
# 改变 fftsize 为 1024,因为 Enum 的默认缺省值为枚举列表中的第一个值
self.fftsize = 1024
# FFT 频谱图的 x 轴上限值的改变事件处理函数,将最新的值赋值给频谱图的响应属性
def _fft_graph_up_limit_changed(self):
self.plot_fft.x_axis.mapper.range.high = self.fft_graph_up_limit
def _N_changed(self):
self.plot_sin_combine()
# 多个 trait 属性的改变事件处理函数相同时,可以用 @on_trait_change 指定
@on_trait_change("wave_width, length_c, height_c, fftsize")
def update_plot(self):
# 计算三角波
global y_data
x_data = np.arange(0, 1.0, 1.0/self.fftsize)
func = self.triangle_func()
# 将 func 函数的返回值强制转换成 float64
y_data = np.cast["float64"](func(x_data))
# 计算频谱
fft_parameters = np.fft.fft(y_data) / len(y_data)
# 计算各个频率的振幅
fft_data = np.clip(20*np.log10(np.abs(fft_parameters))[:self.fftsize/2+1], -120, 120)
# 将计算的结果写进数据集
self.plot_data.set_data("x", np.arange(0, self.fftsize)) # x 坐标为取样点
self.plot_data.set_data("y", y_data)
self.plot_data.set_data("f", np.arange(0, len(fft_data))) # x 坐标为频率编号
self.plot_data.set_data("p", fft_data)
# 合成波的 x 坐标为取样点,显示 2 个周期
self.plot_data.set_data("x2", np.arange(0, 2*self.fftsize))
# 更新频谱图 x 轴上限
self._fft_graph_up_limit_changed()
# 将振幅大于-80dB 的频率输出
peak_index = (fft_data > -80)
peak_value = fft_data[peak_index][:20]
result = []
for f, v in zip(np.flatnonzero(peak_index), peak_value):
result.append("%s : %s" %(f, v) )
self.peak_list = "\n".join(result)
# 保存现在的 fft 计算结果,并计算正弦合成波
self.fft_parameters = fft_parameters
self.plot_sin_combine()
# 计算正弦合成波,计算 2 个周期
def plot_sin_combine(self):
index, data = fft_combine(self.fft_parameters, self.N, 2)
self.plot_data.set_data("y2", data)
# 返回一个 ufunc 计算指定参数的三角波
def triangle_func(self):
c = self.wave_width
c0 = self.length_c
hc = self.height_c
def trifunc(x):
x = x - int(x) # 三角波的周期为 1,因此只取 x 坐标的小数部分进行计算
if x >= c: r = 0.0
elif x < c0: r = x / c0 * hc
else: r = (c-x) / (c-c0) * hc
return r
# 用 trifunc 函数创建一个 ufunc 函数,可以直接对数组进行计算,不过通过此函数
# 计算得到的是一个 Object 数组,需要进行类型转换
return np.frompyfunc(trifunc, 1, 1)
if __name__ == "__main__":
triangle = TriangleWave()
triangle.configure_traits()
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论