筆記

引言

我很少用Jupyter Notebook,它最吸引我的地方,莫過於可以方便地顯示LaTeX公式,特別在做符號計算的時候。我主要用Matlab居多,而Matlab也有相應的符號計算功能,對我而言夠用了,雖然Matlab主要不是用來做這個事情。做符號計算最好的工具應該是Wolfram旗下的Mathematica,然而,它一來收費,二來我那個內存很有限的Macbook Air也裝不下這些巨大的軟件,僅僅是為了它的一個符號計算功能。所以我轉向SymPy,免費的,說實話,它不怎麼樣。

iPad上有一些很有趣的工具,比如Carnets,包含了常用庫的Jupyter Notebook,我用過幾次,感覺不錯,它也是免費的。無奈我幾年前買的那個iPad內存愈發捉襟見肘,Carnets卻越更新越大,只能刪掉了。實際上我一直在用的Pythonista可以安裝SymPy模塊,並且支持flask。我在此前注意到latex.css,如果加上MathJax渲染公式的話,可以有很漂亮的排版效果,相當於一個極簡版本的LaTeX文檔,很適合用於記錄計算過程和結果。這也是我準備自己寫一個工具的動機,但符號計算永遠不能代替紙筆推導,也是我不想在上面較真的原因。

代碼

我的文件使用iCloud同步,這樣我在電腦與平板上都可以直接編輯。離線版本需要將latex.cssMathJax保存到本地,我就不在這裡展示了。

Flask

由於主要的計算過程在Python Console內進行,因此我不打算加上圖形介面或是封裝成一個命令行軟件,轉以極簡為目的。你可以新建一個neoscript的目錄,裡面添加一個ns.py腳本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import os, json, flask
from sys import platform
from datetime import datetime

if not os.path.exists('projects/'):
os.mkdir('projects/')

PATH = os.getcwd()
HEAD = 'PUT YOUR TEXT BELOW\n___________________'
HTML = 'online.html'

def read_file(file):
ext = file.split('.')[-1]
with open(file, 'r') as f:
if ext == 'txt':
return f.read().replace(HEAD, '').strip('\n')
elif ext == 'json':
return json.load(f)
else:
return f.read()

def write_file(file, cont):
ext = file.split('.')[-1]
with open(file, 'w') as f:
if ext == 'txt':
f.write(HEAD+'\n\n'+cont)
elif ext == 'json':
json.dump(cont, f, indent=4, ensure_ascii=False)
else:
f.write(cont)

def time_now(exact_time=True):
now = datetime.now()
if exact_time:
return now.strftime('%B %d %Y %H:%M:%S')
else:
return now.strftime('%B %-d, %Y')

def generate_chapters(config):
chapters = []
for item in config['index']:
subchapters = []
for subitem in item['subindex']:
subsubtitle = subitem['subsubtitle']
filepath = subitem['filepath']
content = read_file(PATH+filepath)
subchapters.append({'subsubtitle': subsubtitle, 'content': content.replace('\n\n', '<br/>')})
subtitle = item['subtitle']
filepath = item['filepath']
content = read_file(PATH+filepath)
chapters.append({'subtitle': subtitle, 'content': content.replace('\n\n', '<br/>'), 'subchapters': subchapters})
return chapters

def view(title):
app = flask.Flask(__name__)
@app.route("/")
def index():
project = title.lower().replace(' ', '_')
code = read_file(PATH+'/projects/'+project+'/code.py')
config = read_file(PATH+'/projects/'+project+'/config.json')
abstract = read_file(PATH+'/projects/'+project+'/abstract.txt')
chapters = generate_chapters(config)
return flask.render_template(HTML, title=config['title'], date=config['date'], code=code, abstract=abstract, chapters=chapters)
app.run(use_reloader=False, debug=True)

def create(title):
project = title.lower().replace(' ', '_')
if not os.path.exists(PATH+'/projects/'+project+'/'):
os.mkdir(PATH+'/projects/'+project+'/')
config = {'title': title, 'date': time_now(False), 'index': []}
write_file(PATH+'/projects/'+project+'/config.json', config)
write_file(PATH+'/projects/'+project+'/abstract.txt', 'THIS IS THE ABSTRACT')
write_file(PATH+'/projects/'+project+'/code.py', '# Source code of the project: '+project+'.\n\nfrom sympy import *\n\n')
print('Done. Please check '+project+' in '+PATH.replace(' ', '\ ')+'/projects/')

def chapter(title, subtitle):
project = title.lower().replace(' ', '_')
if not os.path.exists(PATH+'/projects/'+project+'/chapters/'):
os.mkdir(PATH+'/projects/'+project+'/chapters/')
filename = subtitle.lower().replace(' ', '_')
filepath = '/projects/'+project+'/chapters/'+filename+'.txt'
write_file(PATH+filepath, 'THIS IS THE CHAPTER')

config = read_file(PATH+'/projects/'+project+'/config.json')
config['index'].append({'subtitle': subtitle, 'filepath': filepath, 'subindex': []})
write_file(PATH+'/projects/'+project+'/config.json', config)
print('Updated! You can edit new chapter '+filename+'.txt in '+PATH.replace(' ', '\ ')+'/projects/'+project+'/chapters/')

def subchapter(title, subtitle, subsubtitle):
project = title.lower().replace(' ', '_')
foldername = subtitle.lower().replace(' ', '_')
folderpath = '/projects/'+project+'/chapters/'+foldername+'/'
if not os.path.exists(PATH+folderpath):
os.mkdir(PATH+folderpath)
filename = subsubtitle.lower().replace(' ', '_')
filepath = folderpath+filename+'.txt'
write_file(PATH+filepath, 'THIS IS THE SUBCHAPTER')

config = read_file(PATH+'/projects/'+project+'/config.json')
idx = next((index for (index, d) in enumerate(config['index']) if d["filepath"].replace('.txt', '/') == folderpath), None)
config['index'][idx]['subindex'].append({'subsubtitle': subsubtitle, 'filepath': filepath})
write_file(PATH+'/projects/'+project+'/config.json', config)
print('Updated! You can edit new subchapter '+filename+'.txt in '+PATH.replace(' ', '\ ')+folderpath)

def markdown(title):
project = title.lower().replace(' ', '_')
config = read_file(PATH+'/projects/'+project+'/config.json')
abstract = read_file(PATH+'/projects/'+project+'/abstract.txt')
code = read_file(PATH+'/projects/'+project+'/code.py')
chapters = generate_chapters(config)

text = '# ' + config['title'] + '\n\n'
text += '<center>' + config['date'] + '</center>\n\n'
text += '## Abstract\n\n'
text += abstract + '\n\n'

for chapter in chapters:
text += '## ' + chapter['subtitle'] + '\n\n'
text += chapter['content'].replace('<br/>', '\n\n') + '\n\n'
for subchapter in chapter['subchapters']:
text += '### ' + subchapter['subsubtitle'] + '\n\n'
text += subchapter['content'].replace('<br/>', '\n\n') + '\n\n'

text += '## Code' + '\n\n'
text += '```python\n' + code + '\n```\n'
write_file(PATH+'/projects/'+project+'/'+project+'.md', text)
print('Done. New '+project+'.md is saved in '+PATH.replace(' ', '\ ')+'/projects/'+project+'/')

首先你需要確保你已經安裝了flasksympy,除此以外不依賴第三方模塊。

Templates

現在你需要在neoscript目錄下面再創建一個目錄templates,裡面添加一個online.html模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{{ title }}</title>
<link rel="stylesheet" href="https://latex.now.sh/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.5.0/themes/prism.min.css"/>
<script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/latest.js?config=TeX-MML-AM_CHTML' async></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/components/prism-python.min.js"></script>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
tex2jax: {
skipTags: ['script', 'noscript', 'style', 'textarea', 'pre'],
inlineMath: [['$','$']],
displayMath: [['$$','$$']]
}
});
</script>
</head>

<body>
<header>
<h1>{{ title }}</h1>
<p class="author">YOUR NAME<br/>{{ date }}</p>
</header>

{% if abstract %}
<div class="abstract">
<h2>Abstract</h2>
<p>{{ abstract|safe }}</p>
</div>
{% endif %}

{% for chapter in chapters %}
<div>
<h2>{{ chapter.subtitle }}</h2>
<p>{{ chapter.content|safe }}</p>
</div>

{% for subchapter in chapter.subchapters %}
<div>
<h3>{{ subchapter.subsubtitle }}</h3>
<p>{{ subchapter.content|safe }}</p>
</div>
{% endfor %}

{% endfor %}

{% if code %}
<div>
<h2>Code</h2>
<pre>
<code class="language-python">{{ code }}</code>
</pre>
</div>
{% endif %}
</body>
</html>

這樣就可以了,非常簡單。

Guide

首先你需要from ns import *,然後要起一個標題,譬如”User Guide”,用create("User Guide")的方式創建項目。同樣也可以用chapter("User Guide", "Add Chapters and Execute Code")的方式創建章節”Add Chapters and Execute Code”,以及subchapter("User Guide", "Add Chapters and Execute Code", "Add Subchapters")的方式在該章節下面創建子章節”Add Subchapters”,然後你需要在生成projects目錄下面的項目目錄中找到對應的文本文件,添加你需要記錄的內容。如果需要生成markdown文檔,則markdown("User Guide")即可。使用view("User Guide")用來查看效果,按Ctrl-C退出即可。計算代碼在項目目錄下的code.py文件中,借用上面的例子,from projects.user_guide.code import *即可載入。

顯示效果如圖,第一頁

user_guide_page_1

以及第二頁

user_guide_page_2

實際上是沒有分頁的,只是為了顯示方便。

後記

我對其效果還是很滿意的。類似的腳本我早就寫了,寫得十分混亂,我自己都不知道怎麼用它。後來我決定將內容編輯分開,放在單獨的文本文件當中,多餘的功能能刪就刪。其實嚴格說起來,那個章節我也可以用遞歸的方式寫下去,但我拒絕了。我分析了一下我需要的其實不過是符號計算+公式渲染+文本記錄,並附上及運行計算代碼,其他東西都是多餘的,一個子章節就夠了。其實我還可以往裡面加東西,譬如圖片和表格,但何必呢,你為什麼不直接用LaTeX呢。如果你真的要自己加上編輯器與圖形介面,你確定你做出來的東西會比現有的商用軟件好嗎。

這也是如果我只用到某個軟件的一小部分功能,我可能會按照自己的習慣親手寫一個。但複雜就沒有必要了,你也沒有這個時間。離線版本需要將latex.cssMathJax保存下來,文件有點多,方法都是類似的,只需要將html模板中的連結替換掉指向本地文件即可。