首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
88 阅读
2
nvim番外之将配置的插件管理器更新为lazy
73 阅读
3
2018总结与2019规划
55 阅读
4
PDF标准详解(五)——图形状态
37 阅读
5
为 MariaDB 配置远程访问权限
33 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE 运动
菜谱
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
linux
文本编辑器
Java
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
312
篇文章
累计收到
27
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE 运动
菜谱
页面
归档
友情链接
关于
搜索到
312
篇与
的结果
2024-09-06
PDF标准详解(四)——图形操作符
上一节,我们了解了PDF中cm操作符,它是定义变换矩阵的。同时也了解到re是创建一个矩阵的。上一节也说过,它用来构建一个路径,具体什么是路径,路径有什么作用呢?这些将在本节给出解释图形操作符是用来在pdf中构建内容并输出到相关设备上进行显示的。pdf中我们能看到的内容几乎都是由图形操作构成的。PDF中主要有6中图形操作符:图形状态操作符(Graphics state operator):CTM当前变换矩阵、 current color、 current clipping path。路径构造操作符(Path construction operators):线的轨迹,,各种图形。绘制路径操作符(Path-painting operators):填充, 描边, 或定义一个剪切区域。其他绘图操作符(自我描述图形对象): 图像(image),shading。文本操作符(Text operator):从字体(代表文本字符的字面/版式(TYPE-FACES)的描述)中选择,显示字符字形字符操作,例如前面显示hello,world 用到的Tj 操作符标记内容操作符(Marked-content Operator): Layers这一次我们主要介绍前两个,后面的等后续慢慢介绍路径路径构建操作符路径对象主要由直线、矩形框(re)、3次贝塞尔曲线构成。对于直线来说,我们需要先使用m(moveto) 来将画笔移动到指定位置,然后使用l(lineto) 来表示将画笔移动到某一个点。例如有下面的例子3 0 obj % 页面内容流 << >> stream % 流的开始 400 400 m 100 100 l s endstream % 流结束 endobj这里我们定义了一个从 (400, 400) 到(100, 100) 的直线。在画直线的时候,m只能有一个,作为起点,而l可以有多个,每有一个l都表示从画笔的上一个点画一条直线到新的位置。例如我们可以模拟一个画一个矩形3 0 obj % 页面内容流 << >> stream % 流的开始 400 400 m 100 100 l s 100 100 m 300 100 l 300 300 l 100 300 l 100 100 l S endstream % 流结束 endobj矩形的例子比较简单,这里就不给出了。我们只需要指定起点坐标并且加上长宽最后用re 操作符作为结束符即可构建对于贝塞尔曲线来说,我们需要4个点来画出一条曲线,它们的位置如下图所示我们需要一个起始和结束位置的点,并且加上两个控制点共同组成一条贝塞尔曲线。贝塞尔曲线我们使用c来作为操作符,在构建的时候需要使用m来规定起始位置的坐标,然后再跟上上图p1, p2, p3 的坐标来控制曲线。例如下面的例子3 0 obj % 页面内容流 << >> stream % 流的开始 100 100 m 200 300 300 400 400 200 c S endstream % 流结束 endobj这样我们构建了一条如下图所示的曲线我们对上面出现的操作符做一个总结操作符含义m设置点的起始位置(moveto)l从当前位置构建一条直线到对应位置 (lineto)re构建矩形路径c构建贝塞尔曲线路径显示操作符上述操作符只能构建一个路径,而这个路径究竟该如何显示,用作何种用途,需要另外给出操作,如果仅仅构建路径,那么页面上是不会有任何显示的,例如上述的内容流,我们稍微做一下更改,去掉最后的S 操作符,我们可以发现之前显示的内容现在不显示了3 0 obj % 页面内容流 << >> stream % 流的开始 100 100 m 200 300 300 400 400 200 c % 只构建路径,而不对路径做任何操作,页面不会有路径的内容 endstream % 流结束 endobj想要显示路径,我们需要使用 S 操作符。上面的路径,我们在最后加上S 就能显示出图形了。另外我们可以使用h操作符来构建一个闭合的路径,它是在原来图形的基础之上,使用一条直线将起始点到终点的两个点连接起来构成要给封闭的区间。例如上面使用直线画矩形的例子,我们可以删掉最后一个l 操作符,并使用h 闭合,照样能形成矩形3 0 obj % 页面内容流 << >> stream % 流的开始 400 400 m 100 100 l s 100 100 m 300 100 l 300 300 l 100 300 l h S endstream % 流结束 endobj去掉h 我们将得到一个开口的矩形。这个读者可以自行尝试,这里就不给出结果了。对于上面的贝塞尔曲线的例子3 0 obj % 页面内容流 << >> stream % 流的开始 100 100 m 200 300 300 400 400 200 c h S endstream % 流结束 endobj加上h 之后将得到下面的结果描边与填充操作这里我们采用S对路径勾画出了边框,也就是描边路径,它对应的英文单词是stroke,我们也可以使用f 或者F(fill)来对路径构成的封闭区间进行填充。默认采用黑色进行填充。3 0 obj % 页面内容流 << >> stream % 流的开始 100 100 m 200 300 300 400 400 200 c h f endstream % 流结束 endobj当然也可以提前指定画刷颜色,这个我们在后面介绍颜色空间的时候再介绍如何定义画刷和画笔。另外也可以使用b或者B(both) 来同时进行描边和填充操作。非0缠绕规则和奇偶绕组规则上述图形,我们很明确的仅定义了一个简单的区域,当出现重叠的复杂区域时,该如何进行填充呢?这里有两套不同的填充规则,即非0缠绕规则和奇偶绕组规则。3 0 obj % 页面内容流 << >> stream % 流的开始 100 350 200 200 re %生成矩形左上角坐标 (100, 350) 宽高都是200 120 370 160 160 re f %按照非0缠绕规则 400 350 200 200 re %生成矩形左上角坐标 (400, 350) 宽高都是200 420 370 160 160 re f* %按照奇偶缠绕规则 endstream % 流结束 endobj这里显示的效果如下我们在这里定义了两组矩形,每组有两个矩形路径进行了重叠。第一组采用非0缠绕规则,第二组采用奇偶规则来填充。我们先以这两个图形为例,来说明这两个规则非0规则:初始化环绕数到 0 。从图形中的任意一点 P 向外任意引一条射线。每遇到一条与该线的交叉线,如果射线与路劲的顺时针相交则计数加一,否则计数减一假如环绕数不等于 0 ,则点 P 在多边形内。但是这个方法有局限性 , 不适合相交 , 或者选一条正切的射线 . 因为射线的方向是任意的 , 这个规则简单的选用射线并不碰到这些情况例如上面我们定义了两个矩形,这两个矩形划分出了两个区域,也就是图中A和B所在区域。我们从A区域随意一点往外引一条射线。从图上看,射线与两条路劲相交,并且都是顺时针相交,所以这里的技术是2。同理,B点与一条顺时针路径路径计数是1。这两个区域的计数都不是0,所以他们都需要进行填充,因此它显示的是上图左侧的效果奇偶规则:从区域内某一点向外引一条射线。简单计算与该射线相交线的数量。如果这个数是奇数,则认为点在图形内。根据这个规则,我们看到A点的计数是2,是偶数,Bdian的计数是1,是奇数。按照奇偶规则,B点需要填充,而A点不需要进行填充。所以它显示的是上图右侧的效果我们再看一个例子3 0 obj % 页面内容流 << >> stream % 流的开始 150 50 m 150 250 l 250 50 l 50 150 l 350 150 l h f 550 50 m 550 250 l 650 50 l 450 150 l 750 150 l h f* endstream % 流结束 endobj这里我们画了两个五角星,线条的顺序按照m给出的为起点,每一个l代表笔画移动的一个端点,根据上述给出的值我们可以得到对应路径的环绕方向,具体的分析过程这里就不展开了,有兴趣的小伙伴可以自己尝试着画图分析一下,然后使用pdf阅读软件打开看看效果与预估的是否一样定义裁剪区域我们利用一些操作符来定义一个路径,这些路径可以作为图形显示出来,也可以作为一个裁剪区域,在该区域中的内容显示出来,不在该区域的内容则丢弃。对于给出的路径,我们使用W (非0缠绕)或者 W*(奇偶规则)来定义一个裁剪区域。例如下面有一个例子3 0 obj % 页面内容流 << >> stream % 流的开始 100 100 200 200 re h W %将上述路径设置为裁剪路径 150 150 m 200 200 l S %在裁剪路径中,所以会显示 0 0 m 500 800 l S %只显示裁剪路径中的内容 endstream % 流结束 endobj这里我们定义了一个长宽都为200的矩形,并且使用h 将矩形区域封闭,然后使用W来将矩形内部作为裁剪区域,然后在(150, 150) 的位置画一条直线到 (200, 200) 的位置。这两个点都在矩形内部,所以会显示出来,另外再画一条从 (0, 0) 到 (500, 800) 的线条,因为这条线有一部分在裁剪区域外,一部分在裁剪区域内,所以只会显示一部分线条。最终图形呈现的效果如下总结本文主要介绍PDF中基本的图形操作符。一般构建图形的操作符有3中使用m 定义画笔的起始位置,然后使用 l 来画一条直线或者直接使用 re 操作符来绘制一个矩形还可以使用c 来构建贝塞尔曲线对于构建的路径,可以使用 h 来进行画笔起始位置和终点位置的连线,这个连线一般是一条直线。对于这个路径我们可以使用 S (stroke) 来对路劲进行描边显示或者使用 f(非0缠绕) f*(奇偶规则)进行内容的填充,又或者使用 b(B) 来描边和填充。我们可以使用 W (非0缠绕)或者 W*(奇偶规则) 来将路径作为一个裁剪区域
2024年09月06日
3 阅读
0 评论
0 点赞
2024-06-14
PDF标准详解(三)—— PDF坐标系统和坐标变换
之前我们了解了PDF文档的基本结构,并且展示了一个简单的hello world。这个hello world 虽然只在页面中显示一个hello world 文字,但是包含的内容却是不少。这次我们仍然以它为切入点,来了解PDF的坐标系统以及坐标变换的相关知识图形学中二维图形变换中学我们学习了平面直角坐标系,x轴沿着水平方向从左往右递增,Y轴沿着竖直方向,从下往上坐标递增。而PDF的坐标系与数学中的坐标系相同。但是PDF的坐标是有单位的,PDF的坐标单位为磅,一般来说他们与英寸等的转化关系为1 磅 = 1/72 英寸因为PDF需要做到设备无关,也是就是在不同的显示像素和打印机上,显示的长度都一致,所以这里不能采用像素做单位。但是我们可以通过相关的接口来将这个单位转化为像素。例如在Windows平台可以通过下列的代码来获取一英寸有多少像素HDC hdc = GetDC(NULL); short cxInch = GetDeviceCaps(hdc, LOGPIXELSX); short cyInch = GetDeviceCaps(hdc, LOGPIXELSY); ReleaseDC(NULL, hdc);对于我的显示器来说,水平和竖直方向都是 1英寸=96像素有了这些概念之后,我们来看一个例子,下面是在页面的(200, 200) 位置画一个 长宽都为100的正方形3 0 obj % 页面内容流<< >>stream % 流的开始200 200 100 100 re Sendstream % 流结束endobj之前说过,页面显示内容在页面流中,因此这里我们将内容放置到页面流对象中。前面的200 200 是矩形的起始位置。后面的100 100 分别是长和宽。re 代表我们要构建一个矩形,最后的S表示要显示这个图形。严格意义上来说,re 和S都是路径构造所使用的操作符。这里的矩形也不单单是一个图形,它是一个路径。关于他们的概念将在后面继续介绍。下面我们来介绍基本的2D图形变换平移假设一个点原始坐标是(x1, x2),那么沿着x轴平移a,y轴平移b,那么平移之后点的坐标为 (x1 + a, x2 + b) ,转换成矩阵就是$$ \begin{bmatrix} x & y & 1\end{bmatrix} \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ a & b & 1 \end{bmatrix} = \begin{bmatrix}x + a & y + b & 1\end{bmatrix} $$旋转利用中学的知识可以知道$$ x_1 = r * cos(\theta+\psi) \ = r*(cos\theta*cos\psi-sin\theta*sin\psi) \ = x*cos\theta-ysin\theta $$同理,我们可以得到$$ y_1 = r * sin(\theta+\psi) \ = r * (sin\theta*cos\psi+cos\theta*sin\psi)\ = x*sin\theta+y*cos\theta $$转换成矩阵就是$$ \begin{bmatrix} x & y & 1\end{bmatrix} \begin{bmatrix} cos\theta & sin\theta & 0 \\ -sin\theta & cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix}x*cos\theta - y*sin\theta & x*sin\theta+y*cos\theta & 1\end{bmatrix} $$缩放缩放就是将坐标扩大或者缩小为原来的多少倍,我们可以很清楚的知道$$ x_1=x*a y_1=y*b $$这里的a和b都是缩放的系数利用矩阵表示就是$$ \begin{bmatrix} x & y & 1\end{bmatrix} \begin{bmatrix} a & 0 & 0 \\ 0 & b & 0 \\ 0 & 0 & 1\end{bmatrix} $$pdf 矩阵变换还有另外几种变换,这里就不一一列举了。现在我们知道二维图形的变换使用一个矩阵就能进行描述。所以PDF在变换图形的时候直接使用的是变换的矩阵。另外我们观察到对于二维变换来说,最后一列一直都是 0 0 1这三个数字。所以pdf中设置变换矩阵时忽略最后一列,仅仅保留前两列,采用6个数字$$ \begin{bmatrix}a & b & 0 \\ c & d & 0 \\ e & f & 1\end{bmatrix} $$这个矩阵在PDF中表现为 a b c d e f。回到我们之前hello的例子中,我们在 hello world 字符流开始的时候,给定了几个数字1. 0. 0. 1. 50. 700. cm各个数字之间采用空格隔开,这里数字后面跟的点表示它是一个浮点数。我们可以将这一列数字写成如下的矩阵$$ \begin{bmatrix}1.0 & 0.0 & 0 \\ 0.0 & 1.0 & 0 \\ 50.0 & 700.0 & 1 \end{bmatrix} $$这个矩阵我们叫做当前变换矩阵 (Current Transformation Matrix CTM),最后的cm表示使用该矩阵进行图形变换。它是current matrix 的缩写所以上述这一串数值的意思就是将 hello world 这个字符串平移到页面坐标 (50, 700) 的位置PDF 中控制图形变换的操作符现在我们利用这个上述知识来做一个小练习。我们将一个长宽都为100 的矩形在 (200, 200) 位置逆时针旋转45°绕任意点旋转,可以先将该点移动到坐标原点,然后按照坐标原点的进行旋转的公式进行计算,最后再将坐标点平移回原来的位置。这个过程产生3个变换矩阵平移矩阵$$ \begin{bmatrix}1 & 0 & 0 \\ 0 & 1 & 0 \\ -C_x & -C_y & 1\end{bmatrix} $$旋转矩阵$$ \begin{bmatrix}cos\theta & sin\theta & 0 \\ -sin\theta & cos\theta & 0 \\ 0 & 0 & 1\end{bmatrix} $$平移矩阵$$ \begin{bmatrix}1 & 0 & 0 \\ 0 & 1 & 0 \\ C_x & C_y & 1\end{bmatrix} $$我们将这三个矩阵相乘$$ \begin{bmatrix}1 & 0 & 0 \\ 0 & 1 & 0 \\ -C_x & -C_y & 1\end{bmatrix} * \begin{bmatrix}cos\theta & sin\theta & 0 \\ -sin\theta & cos\theta & 0 \\ 0 & 0 & 1\end{bmatrix} * \begin{bmatrix}1 & 0 & 0 \\ 0 & 1 & 0 \\ C_x & C_y & 1\end{bmatrix} $$最终得到这样一个矩阵$$ \begin{bmatrix}cos\theta & sin\theta & 0 \\ -sin\theta & cos\theta & 0 \\ C_x - C_xcos\theta+C_ysin\theta & C_y-C_xsin\theta-C_ycos\theta & 1\end{bmatrix} $$因此这里可以这样写3 0 obj % 页面内容流 << >> stream % 流的开始 200 200 100 100 re S %原始矩形 0.7 0.7 -0.7 0.7 200 -80 cm%进行坐标变换 200 200 100 100 re S %变换后的矩形 endstream % 流结束 endobj这样我们可以得到如下所示的图形这个时候我们会发现,同样是(200, 200) 的位置,在变换前和变换后,得到不一样的图形,这就说明我们的坐标系统被改变了。不再是水平和竖直方向的x y轴了。如果我们想要它变回原来的位置该怎么办?在GDI或者其他框架的图形编程中,在改变画笔、画刷等图形状态的时候,会首先保存原来的,然后更新,最后再还原。同样在PDF中,也存在有这样的保存和还原的操作符。我们使用q/Q这么一对操作符来完成保存和还原的操作。我在原来的基础上,再加一个矩形,在(400, 400) 位置画一个长宽都是100的矩形3 0 obj % 页面内容流 << >> stream % 流的开始 200 200 100 100 re S %原始矩形 0.7 0.7 -0.7 0.7 200 -80 cm%进行坐标变换 200 200 100 100 re S %变换后的矩形 400 400 100 100 re S % 这个矩形是相对于 (200, 200) 这个点旋转了45°的矩形 endstream % 流结束 endobj我们再采用q/Q这一对操作符来保存和还原图形状态3 0 obj % 页面内容流 << >> stream % 流的开始 200 200 100 100 re S %原始矩形 q 0.7 0.7 -0.7 0.7 200 -80 cm%进行坐标变换 200 200 100 100 re S %变换后的矩形 Q 400 400 100 100 re S endstream % 流结束 endobj这个时候我们发现它已经在(400, 400) 这个位置画了一个矩形。没有任何的图形变换PDF中将图形状态保存成一个栈结构,每次执行q就是将当前图形状态进行入栈,使用Q将之前保存在栈顶的图形状态进行出栈,并还原成当前图形状态。一般来说q/Q必须成对出现。好了,本节到这里就结束了。本节主要介绍了图形变换矩阵以及PDF中变换矩阵的操作符cm以及q/Q 这一对保存和还原图形状态的操作符
2024年06月14日
4 阅读
0 评论
0 点赞
2024-03-24
2024 年阅读清单
2024 年都过去这么久了才更新读书清单,我是在是有点过于懒惰了。我也忘记了去年最后一次更新读书清单是什么时候了,只知道我在看一套书,这套书花了差不多块5个月了。前几天刚刚看完,终于可以更新新一年的读书清单了《清明上河图密码》这本书就是我去年看了由个多月的一套书,本书有6部,读起来虽然不累,但是文字众多也算是比较花时间吧。本书是我最近几年第一次也是唯一一次看的大部头的书。书中人物众多,据说有名有姓的有800多人。对于一些小人物前面几部出现了几次后面再出现有时候确实不太记得,还好微信读书上有不少书友提醒。我对本书有一些看法,先来说说优点本书的优点一个是书中人物众多,特别是主线人物。每个人物都有名有姓,性格特征鲜明,许多人物都是让人印象深刻,京城十二绝,念奴十二娇,天工十二巧、各种店铺、官差、贩夫走卒等等。还有许多有名有姓的小人物。每个人物似乎都与主线有着某种联系,缺一不可在一个就是书中各种谜团永远牵动着人的好奇心,让我忍不住一直读下去。而且每部书中都有一个大案牵扯若干个小案,最终回这些大案又牵扯出一个惊天布局,我在阅读的过程中一直停不下来。而且作者一直采用各种人物视角切换的方式进行叙事,一直吊起人的好奇心。书中对小人物的心理描写极其细腻,读起来很是亲切。书中各种人物似乎都能在现实中找到一些影子。与其说在写故事,不如说在写现实中的芸芸众生。书中对于宋代的世代风貌描述的特别生动,特别是热闹的大街,大大小小的各种商贩。为我呈现了一个活灵活现的大宋都城汴京。对于缺点我觉得有下面两个明显的缺点人物故事切换太过频繁。这个是优点也是缺点,它确实可以调动读者的积极性,而且也体现了作者写故事,写人物的实力。但是过于频繁而且套路都一样,看多了容易让人视觉疲劳,这点在第5篇尤为明显。第5篇通过众多人物视角写了王家上上下下快60人与王小槐的种种恩怨和这些人之间的关系。都是一个套路,在阅读过程中,有几次都感觉读不下去第二个不能算是缺点吧,我个人认为书中的诡计或者作案手法过于随意,不够惊喜,没有那种让人眼前一亮的感觉。有些时候给出的线索不足,模糊。作为推理小说它总是差那么点意思。但是作为悬疑类小说,它确实足够优秀。书中最喜欢的人物应该是作绝——张用。他作为第四部的主角,与其他几部不同,他有种超脱俗世的洒脱,给人一种庄子的感觉。对于俗世,他洒脱、不拘小节、我行我素、有自己的一套行事标准,不被世俗规矩所束缚。对于朋友他总是热情、帮助朋友时是那种充分考虑朋友性格,让你觉得相处十分舒服的人。而且在第四部中,作者对人物性格描写惟妙惟肖,张用对人性的把握,对待不同人的不同态度,最终使与他接触的人都获得了心灵上的解脱。他算是我的人生榜样。最喜欢的应该是最后一部。对于最后一部的大揭秘没有让我眼前一亮的感觉。我最喜欢的应该是最后关于大宋灭亡的描写。在金人即将攻进来的时候,百姓的同仇敌忾与官方统治者的胆小懦弱、妥协形成鲜明的对比。大宋的统治者辜负了百姓、辜负了爱国将领,活该受此靖康之耻,被金人羞辱并封为昏德公纯粹是咎由自取。王朝灭亡最苦的还是百姓。前几部都城的繁荣与最后的人间地狱形成鲜明的反差。我觉得最后一部分是本书最有价值的内容《毛泽东传》这是我读的第二本毛泽东传纪。当初读毛选的时候,我对毛主席生平有了特别大的兴趣。特别是早期在中央几上几下,从被排挤到最后四渡赤水,到带领革命胜利,最后将中国从积贫积弱的农业国变成了一个世界强盛的工业国。这等丰功伟绩,古往今来也没有几个能做到。他的功绩哪怕减少一半在古代来说也可以被称之为千古一帝。他的人生是那样令人着迷但是这本书从某种意义上来说存在那种老外写传记的时的常规问题。书过于平淡,而且充斥了作者无端的联想。似乎老外可能不太懂得中国人为万世开太平那种胸襟。整本书有那么一种阴谋论的味道在里面。书中仅仅只是将毛主席的生平做了一个类似流水账似的讲述。故事性不是那么浓重,可能偏学术的传记都是这样的吧。总的来说我觉得这本书的可读性有,但是总体来说不太合我的胃口。想要了解主席生平事迹可能不能光看他一个人的传记。可能得从一些革命历史或者其他领导人的传记中找寻。《消失的第13级台阶》十几年前一对名叫佐津木的老夫妻被残忍杀害,而在凶案现场发生了一起交通事故。驾驶摩托车的树原亮刚好去拜访过佐津木夫妇,而且树原亮刚刚刑满释放,目前在佐津木夫妇的监护下,慢慢回归社会。在现场未发现其他可疑人员,而树原亮似乎与佐津木闹的不欢而散。而且树原亮自称因为事故而发生失意,不记得当天的情形,只记得当天经过一些台阶,但是现场都没有发现有台阶。就这样树原亮被认定是这起案件的凶手,被关押并等待执行死刑。主人公纯一在数年前因为过失致人死亡罪被判刑,但是因为在法庭上痛哭流涕,并且在狱中有悔改倾向,被提前释放,目前刚刚出狱,但是需要接受指定的监护,一旦发现有不良的表现就会被送回监狱。在回归家庭之后的纯一发现自己家因为需要偿还被害人一大笔钱,家里已经家徒四壁,目前需要一大笔钱。此时身为监狱死刑执行官的南乡约着纯一一起调查树原亮的案件。此时距离树原亮被执行死刑的时间所剩无几,但是案件又扑朔迷离,警察找到的所有线索似乎都指向树原亮,目前他们仅有的线索就是树原亮记得他曾今走在台阶上。在调查的过程中,纯一道出了他在十年前高中的时候曾今跟女友偷偷出去穷游,并且被警察教育过。根据警察的回忆,当时纯一与女友精神恍惚,纯一手臂上还有伤,并且手中还有大量的现金。这个数目不是穷游的高中生应该有的数目。纯一到底有着怎样的过去?在调查的过程中,委托人多次告诫,禁止纯一参与调查,但是都被南乡打哈哈给拒绝了,因为他觉得纯一天性善良,这是他回归社会的好机会。随着调查的深入,发现佐津木夫妇留有大量的遗产,这笔钱并不是作为普通教师所能挣到的数目,南乡猜测佐津木夫妇利用监护人的身份敲诈这些刑满释放人员而积累起来的不义之财。最终经中森检察官介绍,南乡与纯一知道了在当时凶案现场的山上存在一座佛寺,因为山体滑坡被掩埋起来。此时南乡与纯一在被掩埋的佛寺中发现了被藏起来的证物,证物中的存折可能记录了谁向佐津木转帐。最终在警方的鉴定下,存折出现了纯一的指纹。凶手会是纯一吗?纯一有着怎样的过去?委托人为何禁止纯一参加调查?真正的凶手是谁?这本推理小说居然是作者的处女作,实在是让人很难相信。我不得不感慨日本的推理作家横出,似乎总是后继有人。这本书这些谜题一直吸引我读下去,很久都没有读到这么酣畅淋漓的书了。但是比起常规的推理小说来说,他一直最后时刻还有人物登场,而且线索也不是在最终揭秘之前给到读者。可能这就是所谓的社会派推理小说吧,总是喜欢披着推理的皮讨论一些社会问题。但它并不是那种纯粹的诡计的比拼,不是那种纯粹的读者与作者智力比拼之类的小说。虽然有些不太满意但是并不影响本书的精彩程度。13级台阶有一个象征意义,据说在日本,从法院正式判决执行死刑到最后死刑实行需要经过13道审批手续。不知道这个说法对不对。这本书作者探讨了这样几个问题如何确定犯人是真心悔改还是仅仅希望减刑而作出的欺骗行为,进一步来说,刑法的意义到底是导人向善还是起到惩戒威慑犯人的作用?执行死刑是否有必要?杀死死刑犯的执行官是否需要为他们剥夺生命而产生负罪感?这个应该是日本文学作品的通病,对模糊的大义夸夸其谈,但是对眼前的苦难却视而不见。监护人制度是否应该存在,单凭监护人的监护记录是否就能断定犯人已经改过自新,或者重新将犯人送回监狱?是否值得为了复仇而影响自己的人生。针对问题每个人都有自己的答案,对我来说,刑法的作用肯定是威慑和报复,保护普通民众。毕竟犯罪已经造成很严重的后果,正常人几乎不会进行犯罪活动。对于犯罪分子应该以最严厉的刑罚进行惩罚。希望犯罪分子改过自新重新回归社会是对被害人的二次伤害。作为善良的普通人,我不希望日后当我成为被犯罪对象,并且这次犯罪活动对我造成巨大伤害之后,犯罪分子能在若干年后堂而皇之的融入社会,并且对之前对我造成的伤害只字不提,留我独自活在痛苦之中。《罗杰疑案》金斯艾伯特村落坐落着两座豪宅。皇家围场是其中之一,而居住在其中的弗拉尔斯太太刚刚过世。不久之后,她的情人,居住在另一座豪宅中的罗杰•艾克罗伊德先生便得知,弗拉尔斯太太是因为谋杀丈夫,并且最近被人勒索而心怀愧疚而自杀。罗杰当时邀请主人公也就是谢泼德医生共进晚餐,同时告知医生这件事,正当要揭秘勒索者是谁时,罗杰却拒绝告知医生信中后面关于勒索者的内容。当谢泼德医生离开后的当晚,罗杰被一把银剑插进了他的后颈而死亡。当时参加晚宴的人可谓各怀鬼胎,每个人都在警察的询问中隐瞒了一些事实。当调查陷入困境时,罗杰的侄女,邀请了谢泼德医生的邻居,已经退休隐居在此的大侦探波洛出马。随着调查,凶手以及当时的真相慢慢的浮出水面。虽然我爱推理小说,但是这还是第一次看阿婆的书。与福尔摩西不同的是,阿婆的书语言更加简练,笔下的侦探也更加朴实,他不是福尔摩斯那种天生的侦探形象,更像是身边不起眼的小人物一样。但是这种小人物一旦到他的领域就会发挥巨大的作用。阅读起来是另一种风格,整本书篇幅不大,但是铺垫特别多,比起福尔摩斯系列来说,阿婆的风格更像是一个舞台剧,或者一个箱庭模式。这部书中也可以看作是一个暴风雪山庄模式。我特别想多说一些书中的内容,但是感觉哪怕多说出任何一点都可能会造成剧透,给其他未读过此书的人造成不好的体验。这本书不适合写阅读体验,也不适合看剧情简介,它适合自己一页页的慢慢阅读,自己阅读的体验一定是好过读别人写的读后感或者评价的《小岛经济学》本书没有什么高深莫测的经济学概念,有的只是一个个小小的故事,通过故事来描述其中暗藏的经济学规律,对于没有经济学基础的读者来说,它很形象的讲解了经济活动是如何产生的,以及如何发展最后经济为什么会崩溃本书的故事从小岛出发,假设有两个人都以捕鱼为生,他们每天需要消耗一条鱼,并且只能补到一条鱼。但是随着其中一人省吃俭用,存下一笔供几天食用的鱼,并且利用这几天的时间发明了渔网,大大提升了捕鱼效率,从而产生了多余的鱼。也就是产生了资本的积累。随着技术的积累,岛上的鱼越来越多,也就产生了发展其他行业的可能。可以利用存储的鱼投资其他行业,例如自动捕鱼机,或者生产冲浪板以供娱乐,又或者与临近的岛屿交换它们的乐器等。总之随着生产效率的提高,个人财富会随着增加,个人娱乐需求也会产生,随着这些变化,经济会迎来正向的增长,普通人的生活也会越来越好。在某一时刻,岛上的居民会思考出制作自动捕鱼机,但是需要投入大量的时间和鱼,目前他手上没有这么多资源,而技术的发展会导致个人手中的鱼变多,而鱼的存储就会导致问题,这个时候银行就诞生了,银行利用利息来吸引居民将多余的鱼存储到银行,并且为需要大量鱼的人提供贷款。当有足够的鱼来支持研发自动捕鱼机,并且成功之后会导致捕鱼效率进一步提升,银行会因为这比成功的投资获得大量鱼的回报,并且也有多余的鱼来支持对应的居民存款利息。这一切都是这么这么自然并且都在像好的地方发展。随着交易的越来越频繁,采用真鱼来进行交易会显得越来越不方便,这个时候银行就发行纸币,并且规定一定的兑换比例,例如1元兑换1斤鱼。随着纸币的产生和发展,方便了岛上的交易。随着效率的提升,人口的增多,也需要一个组织来准备保护个人的财产以及保护岛上的居民。这个时候居民会自动的组织成立政府来完成诸如打击罪犯,保护个人和岛上的财产。政府是由居民组织成立,并且政府官员和工作人员也是由岛民选举产生。政府成立之后,政府工作人员不从事捕鱼工作,完全由居民纳税供养。随着政府部分越分越细,负责的事物越来越多,同时政客为了自己连任从而许诺给居民远超财力的福利,政府会越来越入不敷出。这个时候有专家就想到办法了,成立中央银行,并且掌握了发行纸币的权利。在早期靠着超发纸币,可以解决政府一部分的财力问题,因为纸币还不算太多,中央银行还有足够的鱼来完成部分居民的部分兑换需求。随着政府开销的进一步加大,纸币数量进一步加大,此时居民会发现有时候并不能从中央银行及时的兑换出需要的鱼。为了防止露馅,中央银行会规定每人每次只能兑换一定数量的鱼,并且超过这个数量需要提前进行预约。这暂时缓解了兑换的危机,但是鱼不足的情况依旧存在。然后又有专家有了新的注意,我们可以在鱼中加入其他物质,来填充重量。鱼的重量没变,但是鱼真正有效的部分却减少了。每个人每天都需要吃一定的鱼,往鱼里填充无效的部分会导致每个人需要鱼的重量增加,从而导致所有物价上涨。随着时间的推移,岛上的经济出现重大问题,例如房屋贷款的过度发放,房地产的野蛮生长导致了经济危机。这个时候居民看到隔壁岛上生产的便宜商品,并且岛上的政府早些年通过武力或者其他手段迫使周围岛屿都是用纸币进行交易,此时该岛是用不再那么值钱的纸币去购买其他岛屿廉价的物品,而周围岛屿则用收到的货币购买自动捕鱼机等高精尖的产品。随着其他岛屿捕鱼效率的提高,而且他们也发现这个纸币的价值越来越低,在机缘巧合之下,他们可能会考虑绕过该岛的纸币,利用鱼或者其他的纸币进行交易,这个时候该岛终于迎来了经济的彻底崩溃。总体而言,《小岛经济学》以简单易懂的故事形式,生动地阐述了奥地利经济学派的观点,强调生产、储蓄对于经济增长的重要性,批判了过度的《杀死一只知更鸟》这是一本关于爱和成长的故事,书中描述了一个伟大又充满爱的父亲——阿迪克斯。故事发生在一个美国的小镇——梅科姆。镇上有一些奇奇怪怪的,像刻薄的老太太——杜博斯太太,也有怪人拉德利,还有黑人。在那个黑人不被承认人权的年代,黑人一直被人看不起,视为社会的垃圾,有什么坏事都会栽赃给黑人。书中的阿迪克斯会陪着孩子一起读报,他会温柔的听孩子的倾诉,给孩子讲解道理。阿迪克丝对他的女儿说:“斯库特,当你最终了解他们时,你会发现,大多数人都是好人。”,他又说只有完全站在他人的角度看问题才能真正了解一个人。阿迪克斯一直隐瞒他是一个神枪手的事实,因为他意识到上帝给了他一个对其他动物不公平的优势,于是就把枪放下了。书中刻薄的杜波斯太太在临终前一直在戒毒瘾。对此阿迪克斯说“勇敢是:当你还未开始就已知道自己会输,可你依然要去做,而且无论如何都要把它坚持到底”。书中的怪人拉德利一直在他的屋子里面从没出过门,当时在邻居中流传着关于拉德利的恐怖传说。在孩子门去拉德利庭院探险时,吉姆的裤子被篱笆钩破并且无法及时取回。当吉姆再去取时发现裤子已经被补好并且整齐的放在椅子上。日后在树洞中也陆陆续续出现了送给孩子们的礼物。书中的第二部分是关于懒惰,卑鄙的尤厄尔家族家族控告黑人汤姆强奸的案子。在庭审上阿迪克斯通过多种证据已经证明这个是很明显的诬告,但只是因为被告汤姆是一个黑人而被判罚有罪,并面临着死刑。这个庭审在书中的描写特别精彩,算是书中的一个高潮部分。最终在庭审结束时,汤姆输掉了官司,阿迪克斯在明知汤姆无辜而瘫坐下来。之后成年人中间充斥了关于黑鬼的诅咒,认为汤姆罪有应得。只有孩子在为无辜的汤姆流泪。就像书中说的“杀死一只知更鸟就是一桩罪恶。知更鸟只唱歌给我们听,什么坏事也不做。它们不吃人们园子里的花果蔬菜,不再玉米仓里做窝,它们只是衷心地为我们唱歌。”因为不满阿迪克斯在法庭上揭露真相,尤厄尔选择在深夜袭击阿迪克斯的孩子但是最终死在自己的刀下,具体是如何被自己的刀杀死的,书中并没有给出。但是在黑夜中经历了搏斗,怪人拉德利也及时的出现保护了受伤的孩子。书中“我”——斯库特对着在墙角的拉德利微笑着说“你好,阿瑟”。我不知道作者想表达一种怎么的心情。但是我读到这里有一种多年老友重逢的感觉,特别奇妙,有一种感动,有一种喜悦,有一丝温暖。有一种心里的漏洞被填补的,暖心的感觉。我的孩子今年两岁了,我想以书中阿迪克斯为榜样,让我的孩子生活在充满父母关爱与包容的环境中。在未来我想跟我的孩子一起重读一下这本书。《怪诞行为学》这本书书名取的不太准确,乍一看以为是讲某些怪异行为的心理学书籍,但是实际上是讲行为经济学。传统经济学假设每个人在进行决策时会理性的考虑成本与收益的问题,但是实际上人都是非理性的,做决策时并没有考虑那么多,而是自己一时兴起冲动决定的。因此行为经济学诞生了,行为经济学利用心理学的相关知识和实验方法来研究人们日常生活中做决策时受到哪些因素的影响并以此来预测人的行为。本书是关于行为经济学的一本书。全书总共有4本,其中最有价值的应该是第一本,第一本列举了在实践中影响人们做决策的一些因素,后3本都是对前一本的补充。喜欢攀比,有时候我们并不知道自己需要哪个,但是我们可以知道哪个更好。有几种选择的时候,我们会选择相对比较好的,而忽略自己原本真正需要的。例如书中举了一个约会的例子,带上各方面都不如你的朋友,约会成功的可能性会大大提高,因为女生也不太清楚自己心中的白马王子具体是什么样子的,但是通过对比你与朋友,那么她可能会得出你是她心中向往的那个对象。书中还有一个例子,当我们买450美金的西服时,如果发现15分钟路程外的的一件服装店只卖435,能节省15美金。另一种情况下,我们要买一支钢笔25美金,但是另外一家店距离此处15分钟同样的钢笔只卖10美金。我们会如何选择,大部分人在面对西服时会选择购买450美金的。而在面对钢笔这种情况时会选择花15分钟节省这15美金。同样是花15分钟节省15美金,但是我们的选择却不同,因为通过比较前面一种请款是在450美金的情况下省15美金,而另外一种情况则是从25美金中省下15美金。所以商家一般会玩一个套路,就是降价不那么明显的会以折扣的形式给出。例如原来卖4块的东西现在卖2块,商家会标注降价50%,这样看起来就很诱人。人类行为的一个重要定律,就是要让人们渴望做一件事,只需使这件事的机会难以获得即可。例如所谓的饥饿营销,明明供货充足甚至面临仓库压仓的危险,商家仍然限购,甚至设计的一个等待的系统,只要让人知道想买到这个产品花一番功夫,自然就会勾起人们购物的狂热。有时候买房也是,房产中介会故意设置一些障碍,比如告诉你有很多人在等机会摇号。再比如北京的车牌摇号,有些人可能现在买不起车或者没有买车的计划,但是因为拿到车牌很困难。这算是人为的制造一些额外的需求免费的代价,人人都喜欢免费的东西,例如书中举例,15美元的电子版杂志,20美元的纸质版杂志,20美元的纸质版+电子版杂志。部分原本打算买电子版的,而且只需要电子版的即可,但是看到第三个套餐会觉得,20美元是白送了电子版的套餐,结果就选了第三个套餐,完全没有意识到自己只需要电子版的就够了。对应国内的例子就是各个购物网站会会推出免运费的套餐,例如购物满68免运费,本来就想买一个东西,但是一想到再买点别的就能免运费,可能就会多花钱买一些自己并不需要的东西。再或者连锁餐厅推出的充值100这顿饭免单或者充100送一百。后续不一定会再来,但是面对一顿免费的午餐我们有时候会抵抗不了这种诱惑。社会规范的成本:我们会免费的干一些志愿者活动,但是对同等工作量的工作有时候却嫌弃报酬少。我们的大脑中会有一个社会规范和市场规范,进入市场规范时会考虑成本与收益。但是在社会规范下会愿意不计成本的付出,例如去串亲戚时随便带点礼品的效果可能会比直接给钱要好,给礼品会让对方进入社会规范的考量,因为亲戚会原因给你提供丰盛的晚餐。直接给钱对方会进入市场规范,会计算晚餐的付出与得到的回报。另外公司一直想让员工为公司考虑,希望员工将公司当成自己的家,为自己的家全心全意的服务。如果在工资之外给钱的效果不如给一些小礼品或者给钱意外的关怀。提供各类福利,营造亲善的氛围。性兴奋的影响:在情绪亢奋下做出的决策与在平静心态下做出的决策不同,就像我们经常说的头脑发热一样。但是我们并没有意识到这一点。例如在面对性诱惑时,我们觉得自己会抵御住这种诱惑,但是实际上在处于这种诱惑状态下我们根本把持不住。最好的做法就是不让诱惑到来,当你意识到自己即将处于这种亢奋状态时,停止继续。对应国内的一些例子我觉得就是一些促销活动常常选择在深夜,在深夜的时候人们容易犯困,精神比较迷糊容易做出不理智的行为。拖沓的恶习与自我控制:我们经常信誓旦旦的做一些决定,例如早睡早起,减肥等,并且低估它们的难度,总是等最后时期再开始行动。最好的办法就是承认自己拖沓的习惯,不要总是把事情等到最后再做,而是指定严格的计划并提前按照计划进行。例如我们习惯将工作放到最后,总觉得时间来得及,后面做也没问题。但实际上最后再做效果并不好,应该按计划一点点的将工作往前推进所有权的个性:我们会依恋现在所拥有的一切。会过高的估计自己的房子,车子,甚至身边的亲人,例如总觉得自己的孩子比别人的可爱,聪明。我们总是把注意力集中到自己会失去什么,而不是会得到什么。我们对于损失有一种强烈的恐惧。这就是心理学中的“厌恶损失”原理。心理学上说我们损失造成的痛苦需要得到双倍才能挽回。商家对应的套路就是免费适用,或者首充优惠,让我们以极少的代价得到之后,因为我们在使用过程中已经产生依赖,觉得这是自己的东西,一旦到期或者需要返还时就会产生痛苦最终决定花大价钱给它“赎回”多种选择的困境,我们总希望选择越多越好。有时候维持这种选择要花费很大的代价。现代社会里,困扰人们的不是缺乏机会,而是机会太多,令人眼花缭乱。而我们可能往往认识不到,在面临机会选择的时候,妄图保留余地会让我们活得很累,而且最终的收获也不如坚持到底来的多。就像有些渣男渣女,背地里谈了好几个对象,明明知道该如何取舍,但是就是舍不得,想要都留着。最终一个都留不下来。老话讲当断不断,必受其乱。有时候应该果断放弃那些已经不是机会的机会。预期的效应:如果我们事先相信某种东西好,那么事后在实际使用时及时它没有那么好,我们也会提高对它的评价。预期改变品味。比如现在的小红书或者抖音流行什么精致女孩或者精致男孩。告诉你他们用了这个产品就变得精致,但是产品实际并没有多么出色,我们在实际使用中也有一种它使我们变得精致的错觉。但是这种错觉也有积极的用途,那就是善意的谎言,在面对一些病人时,如果告诉它某种药治好了同样的病症,那么在服药期间,安慰剂也将变得有效期来。价格的魔力,有些时候我们喜欢免费的东西,但是某些时候价格高反而会引起购买的欲望。 例如那些本来平平的商品经过华丽的包装卖高价,人们依然趋之若鹜。实际上去除包装它并不值这个价钱。人性的弱点:为什么我们不诚实?实验表明在某些情况下我们会选择作弊来提高收益,但是作弊的程度不取决于获得的收益也不取决于作弊的难易程度。简单来说就是人们不会因为给的报酬多而选择多作弊,他们会在某种程度上停下里,只靠作弊获取微薄的额外利益。另外我们也不会因为作弊比较简单而选择大量作弊,我们不会在不诚信的路上越走越远,大多数都只会浅尝辄止。另外我们在涉及金钱方面会额外谨慎,例如寝室的同学会顺走辣条但是不会顺走放在桌上的1块钱。尽管从经济上看顺走一包辣条比顺走1块钱要严重。想减少这种作弊或者不诚实的现象,就是将行为与金钱明显挂钩,书中举了一个例子,针对医院科室的笔总是丢失的情况下,可以考虑在笔上粘贴1块钱的纸币。啤酒与免费午餐:羊群效应,羊群会跟着领头羊走,我们的一些行为会受到其他人的影响,也就是从众心理。书中给出了点菜的例子,当所有人都向服务员大声说出自己想点的菜时,因为受到他人的影响,导致后面点菜的人要么想要避开前人的选择或者想要跟前面的人一样,最终得不到自己真实想吃的菜。而另一组采用的是将想点的菜写在纸条上的方式,结果就是这一组都对菜品有比较高的评价,认为他们都得到了自己想要的。商家对应的套路就是网红餐厅雇人排队,网红景区打卡,他们并没有那么值得去但是因为大家都去了,所以我也要去。想要知道网红餐厅是否值得去的标准就是等待一段时间,例如半年或者一年。如果餐厅照样人头攒动,人流不减的化,可能就真的值得一去。这本书分析一些影响人们决策的非理性因素。了解了这些因素之后我们对商家的一些套路有了理性的认识,但是是否在日后做决策时会自动避开这些呢,我自己的答案是未必,可能未来我会在又买了一堆垃圾之后抱着书直呼“又上当了!”
2024年03月24日
5 阅读
0 评论
0 点赞
2024-03-03
PDF标准详解(二)——PDF 对象
上一篇文章我们介绍了一个PDF文档应该包含的最基本的结构,并且手写了一个最简单的 “Hello World” 的PDF文档。后面我们介绍新的PDF标准给出示例时将以这个文档为基础,而不再给出完整的文档示例,小伙伴想自己测试可以根据上一节的文档来进行配置。对象上一节我们看到一个个奇奇怪怪的元素,可能也好奇它们的写法,现在我们来正式介绍它们的相关内容,它们就是PDF文档中一个个的对象。PDF 支持5种基本对象:整数和实数:例如43和12.2 这种数字字符串,PDF种字符串被包裹在小括号中,例如上一节中的 (hello world), 我们也可以给字符串制定编码,这个在后面介绍名称:一般用于字典中的键,以/ 开头,例如上一节中的 /Page 就是一个名称的对象布尔值: 由关键字 true 和 false表示null 对象,由关键字 null 表示PDF支持3种复合对象数组: 包含其他对象的有序集合,数组中的元素可以是其他任何类型的对象,例如可以像 [0 0 0 0 1] 这样只包含数字,也可以像上一节中的 [2 0 R] 包含其他对象的一个引用字典: 字典是由无序对的集合组成,将名称映射到对象。字典中的映射被包含在 <<>> 对中,例如 <</Kids [2 0 R]>> 就是一个字典,它将Kids这个名称映射到 [2 0 R] 这个间接引用的对象上流:流中一般包含二进制的数据流以及描述属性的字典,一般page中的content都是一个个的流对象。间接引用间接引用形成从一个对象到另一个对象的链接,为了将PDF拆分成一个个单独的对象,我们通过间接引用将它们链接在一起,例如上一篇文章中提到的1 0 obj << /Kids [2 0 R] /Count 1 /Type /Pages >>对象中就包含间接引用,PDF解析器,知道这个对象是一个Pages对象之后,可以通过Kids 对象指定的间接引用对象知道,当前PDF文档只有一页,这个页面对象就是2 0 这个对象。 这里的R 代表 reference 也就是引用,它是一个关键字,前面的 2 0 代表的是对象编号是2,世代号是0(这里我们不考虑世代号,默认的世代号都是0)流和过滤器流用于存储二进制数据,它们由字典和一大块二进制数据组成,字典根据流所放置的特定用途,列出数据的长度,以及可选的其他参数。从语法上将,流由字典组成,后跟 stream 关键字,换行符,0个或者多个字节的数据,另一个换行符,最后是一个endstream 关键字。根据上一篇文章中给出的页面流对象的定义来看4 0 obj << /Length 202 % 流的长度 >> stream %关键字 1. 0. 0. 1. 50. 700. cm % 202 字节的数据,这里是图形流,下面是图形流的数据 BT /F0 36. Tf (Hello, World!) Tj ET endstream % 流对象结束的关键字 endobj
2024年03月03日
19 阅读
0 评论
0 点赞
2024-01-18
PDF标准详解(一)——PDF文档结构
已经很久没有写博客记录自己学到的一些东西了。但是在过去一年的时间中自己确实又学到了一些东西。一直攒着没有系统化成一篇篇的文章,所以今年的博客打算也是以去年学到的一系列内容为主。通过之前Vim系列教程的启发,我发现还是写一些系列文章对自己的帮助最大。它能最大化自己的学习成果,并强迫自己深入了解一些内容。所以今年我想还是以系列文章为主,如果中间有需要穿插一些bug处理或者语言特性相关的,可能也会有这方面的内容吧。好了,废话就到这里,下面开始正式介绍PDF相关的内容PDF简介PDF的全称是 Portable document format(可移植文档格式),是描述打印页面的世界领先语言。最早于1990年代由Adobe Systems创造。早期是Adobe专有格式,直到2008年作为开放标准发布。后续经过一系列的发展,目前已经发展到了2.0版本,由于PDF完全向后兼容,并且大部分都是向前兼容的,因此,这里不打算固定在某个具体的版本,而是介绍一些PDF通用的标准和规则。PDF的文档结构PDF主要由四个部分构成,文件头、文件体、交叉引用表以及文件尾文件头将文件标识为PDF并给出它的版本号,例如%PDF-1.0 % PDF 版本号为 1.0 的文件头文件体是PDF文档的主体内容,主要由对象组成,它规定了页面信息和页面内容元素等信息交叉引用表给出了每个对象距离文件首部的地址偏移,这样在解析PDF的时候就不用从头到尾解析每个对象,而是根据需要通过交叉引用表来寻址到具体的对象地址,只单独解析某个对象,提高了解析效率文件尾给出交叉引用表的位置并且以%%EOF作为结尾PDF文件的逻辑结构一个标准的PDF文档需要在文件体中包含下列元素对象:根节点元素,类似于xml的根节点,它是整个文档的根节点对象Pages对象,它包含了PDF文档的页面信息,一般通过它来定义整个PDF文档有多少页Page 页面对象,它用来描述每个具体的页Page Content 对象,它来描述每个具体页中都有哪些对象,一般是一个字节流用来表示将在页面中显示哪些内容Page Resource 对象,它是内容的资源字典,供Content对象引用,资源包括字体、画刷、画笔等等trailer 字典,可以将它看作pdf文档对象的入口,通过它我们可以知道当前PDF文档的一些具体信息,例如根节点的位置,交叉引用表的大小它们之间的关系如下图:PDF版的Hello World说了这么多,我们来试试来自己编辑一个hello world文档,首先建立一个文本文件,将后缀改为.PDF 。我们先写上文件头:%PDF-1.0 % PDF 版本号为 1.0 的文件头主要对象我们按照之前的分析的PDF文档中需要包含的对象,来逐一定义首先给出Pages节点的定义1 0 obj % 对象1 << /Type /Pages % 这是一个页面列表 /Count 1 % 只有一页 /Kids [2 0 R] % 页面对象编号列表。这里只是对象2 >> endobj % 对象1结束对象的内容我们在后续会专门介绍,所以这里不需要额外关注它的语法,这里只需要知道1 0 obj定义了一个对象1,后续通过1 这个编号可以找到这个对象。这个对象中定义了他的类型是 Pages表示它是一个pages对象,/Count表示整个PDF文档只有一页,Kids是一个数组,表示每一页的页面对象,这里它只有一个页面对象,就是对象2接着我们定义页面对象2 0 obj << /Type /Page % 这是一个页面 /MediaBox [0 0 612 792] % 纸张尺寸为美国信肖像(612点x792点) /Resources 3 0 R % 对象3的资源引用 /Contents [4 0 R] % 图形内容在对象4中 >> endobj页面对象中我们定义了页面纸张的大小,单位是磅。因为PDF是可移植文档,它需要在不同设备上显示同样的内容,这里不能使用像素,如果使用像素,在同样尺寸的显示器上如果显示器的像素分辨率不同,那么显示的结果将会不同。所以这里一般使用磅作为单位。同时在页面对象中定义了页面中将要使用的资源以及将要显示的内容接着我们来定义资源对象3 0 obj << /Font % 字体字典 << /F0 % 只有一种字体,称为/F0 << /Type /Font % 这三行引用了内置字体Times Italic /BaseFont /Times-Italic /Subtype /Type1 >> >> >> endobj资源对象中,我们定义了一个字体资源,字体为 Times Italic,并且定义了这种字体资源的名称为 F0, 后面可以通过F0 这个名称来直接引用这个字体然后我们来定义页面内容对象4 0 obj % 页面内容流 << >> stream % 流的开始 1. 0. 0. 1. 50. 700. cm % 位置在(50,700) BT % 开始文本块 /F0 36. Tf % 在36pt选择/F0字体 (Hello, World!) Tj % 放置文本字符串 ET % 结束文本块 endstream % 流结束 endobj通过stream来定义一个流对象,在这个流对象中,我们定义它在页面的 (50, 700) 坐标位置显示字符,显示字符内容通过后面的 (Hello, World!) Tj来定义,并且定义了字符采用F0 字体,也就是上面定义的Times-Italic字体页面相关的内容我们已经定义完了,接着我们需要定义一些结构相关的对象,方便PDF解析器找到并解析页面内容。我们来定义根节点5 0 obj << /Type /Catalog %文件目录 /Pages 1 0 R %参考页面列表 >> endobj根节点包含了一个Pages定义,通过根节点就可以找到Pages节点接着我们来定义交叉引用表xref %这里我们跳过了交叉引用表的开始 0 6交叉引用表包含一些偏移地址信息,我们单纯的通过文本文档很难计算各个对象的偏移,所以这里我们只给出文档中对象数量为6,具体的地址我们先不给出,这样PDF解析器也能解析出各个对象之前我们给出了5个对象的定义,但是交叉引用表的条目却是6,这是因为交叉引用表的第一条一般是一个没有什么用处的,有效的对象从第二条定义开始。下面给出 Trailer 字典的定义trailer << /Size 6 %交叉引用表的行数 /Root 5 0 R % 参考文档目录 >>Trailer 字典以 trailer关键字开始。条目下面包括了交叉引用表的行数以及根节点的对象最后我们给出交叉引用表在PDF文档中的偏移,由于交叉引用表的内容为空,所以这里我们直接给0startxref 0 %xref表开始的字节偏移量,这里设置成0最后我们以%%EOF结尾来表示整个PDF文档结束到这里我们已经得到了一个PDF阅读器可以打开的PDF文档。我们使用PDF阅读器可以得到如下的页面PDF文档一般的读取过程不知道各位小伙伴们是否能看懂上面 Hello World 文档的定义。下面我们通过一个完整的 PDF文档来将上面所有定义的对象串起来,希望各位能对PDF文档有一个完整的认识。我们不用纠结各个部分的写法,以及为什么要这么写,只需要明白各个对象的功能即可。具体对象定义相关的语法和每个对象的详细解释将会在后面一系列文章中给出,相信那个时候再来看这个 Hello Word 文档一定会有一个更清晰的认识。再说明文档读取的过程前,我们先使用一些工具来补全这个文档,这里使用 pdftk 工具。可以在这里 进行下载,完成之后,使用如下命令进行补全pdftk hello.pdf output hello-full.pdf成功后会得到如下内容%PDF-1.0 %忏嫌 1 0 obj << /Kids [2 0 R] /Count 1 /Type /Pages >> endobj 2 0 obj << /Resources 3 0 R /MediaBox [0 0 612 792] /Contents [4 0 R] /Type /Page >> endobj 3 0 obj << /Font << /F0 << /BaseFont /Times-Italic /Subtype /Type1 /Type /Font >> >> >> endobj 4 0 obj << /Length 202 >> stream % 娴佺殑寮€濮? 1. 0. 0. 1. 50. 700. cm % 浣嶇疆鍦紙50,700锛? BT % 寮€濮嬫枃鏈潡 /F0 36. Tf % 鍦?6pt閫夋嫨/F0瀛椾綋 (Hello, World!) Tj % 鏀剧疆鏂囨湰瀛楃涓? ET % 缁撴潫鏂囨湰鍧? endstream endobj 5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj xref 0 6 0000000000 65535 f 0000000015 00000 n 0000000074 00000 n 0000000168 00000 n 0000000267 00000 n 0000000523 00000 n trailer << /Root 5 0 R /Size 6 >> startxref 573 %%EOF 这个我将整个PDF文档都粘贴了出来,从这里我们可以看到,它已经为我们补全了交叉引用表。下面通过整个文档来说明一般读取过程PDF解析程序,先通过文件头来确定是否是PDF文件,并且得到PDF文件的版本在文件末尾找到%%EOF 关键子,确定文件尾。接着向上查找到 startxref 关键字,该关键字后面将会给出交叉引用表的偏移,通过这个偏移地址可以找到交叉引用表接着查找trailer关键字,通过trailer关键字可以得到文档的一些信息,这里关键的是得到 Root 节点的对象。根据交叉引用表可以很块定位到Root 节点对象,也就是对象5根据Root 对象中的 Pages属性可以找到Pages对象,也就是PDF页面信息对象根据Pages对象中的Kids 数组,可以找到PDF包含的所有页面对象,这个文档只有一个页面对象找到Page 对象后可以根据 Resources 和Contents属性可以找到页面内容和页面引用的资源。例如该文档就可以使用Times-Italic字体显示 hello world字符串
2024年01月18日
5 阅读
0 评论
0 点赞
2024-01-13
2023 年度回顾与2024 年展望
时间如白驹过隙,转眼已经2024年了,本来打算2024年元旦那天写写年度回顾的,但是因为一些琐事耽误了,平时上班路程远回来也就懒得动了,一直就拖到今天才开始着手这个每年的例行公事。2023年的回顾回顾整个2023年,从我自己来说并没有什么特别大的事情发生。年初我把自己的小孩接到身边来,由家里的老人过来帮忙带孩子,我则是和媳妇上班。每天下班回来都逗小孩,确实挺快乐的。随着孩子慢慢长大,慢慢会走路,会叫爸爸妈妈。现在每天下班开门都有一个小朋友晃晃悠悠的朝你身边走过来伸手让你抱抱,并且笑着喊你爸爸。看到这一幕上班一整天的疲惫似乎一扫而空。瞬间又充满了力量,随时为了这个小宝宝去冲锋陷阵。有了一个小孩,家里欢乐的时光多了不少。这个可能是我这一整年每天都能感到的幸福时光。对于工作上的事情,22年年末刚入职,很多事情并不那么熟悉,去年写总结的时候没怎么总结在新公司的一些工作。现在我已经在公司工作一年多了,随着公司给予的培训和同事领导平时的照顾,我已经慢慢的从刚入公司接受一些边角料的活到现在已经能独立负责项目中一整个大的模块,在项目组中我自认为已经成为整个团队非常重要的一个组成部分。回顾这一整年在新公司的工作,工作相对比较清闲,没有互联网公司那么卷,每天按时上下班,各个任务给的时间相对也比较充足,新的项目仍在开发中,目前项目已经持续了一年多了,这在以前的公司是无法想象的。通过这段时间的工作经历我突然意识到,也许只有在一个不那么卷的公司自己才能有足够多的成长。想象以前做项目都是用开源项目直接套,似乎并不关注它怎么实现,整个团队都追求快速出成果。遇到段时间内无法攻克的难题,加班加点,改方案,改架构几乎是家常便饭。根本没有时间考虑使用合适的架构,合适的语言,甚至加上单元测试,编写完善的文档都是奢望。似乎每个难点在领导眼里解决时间都不超过2小时,不然就是能力问题。有时候我也怀疑自己的能力,难道我真的没有独立解决问题的能力?我没有独立带队完成可商用项目的能力?我的眼光不够长远,没有预先想到客户的使用场景和使用需求?而在新的公司中,这些问题完全不存在。首先一个是,项目开始前都会反复论证,并且给予充分的时间。而且遇到问题,给出合理的理由项目时间可以适当往后延长。我们经常开发到一半会过来重新考虑之前的架构是否合理,并且给予时间进行重构,或者进行文档的补充,又或者在某个程度团队成员都会花时间讲解自己这部分的架构以便让整个团队对整个项目都有完整的概念。甚至有想法的话可以在跟对应的人员充分沟通后一起重构之前不属于你的代码。所以在这一年中我感觉自己完全融入整个团队,我不仅能改自己的bug,甚至当别人无法抽身时我也能试着挑战别人的bug。我想这才是软件开发应该有的团队氛围和管理方式。总之目前来说我还是比较喜欢当前的工作环境和氛围的。虽说工资无法与互联网大厂相比,但是我待在这里还是蛮舒服的。在学习方面,我的博客已经断更很久了。主要是因为周末都是在家陪孩子玩,没怎么抽出时间来看书或者学习。有时候看着身边的小伙伴都在趁着周末要么接项目搞钱,要么学习新的知识。我心里也有点慌,那些人学历比我好,工资比我高,能力比我强,周末也比我努力。我在现在的公司虽说待着比较舒服,但是毕竟手里没有多少钱,当孩子大了,我也超过35岁了不知道能不能继续给她提供一个稳定的生活环境。毕竟北京这边未来会面临很多问题。但是我作为打工人每天只用工作8小时,一周也就上5天班,带孩子却是需要7*24小时,周末在家时我想稍微给家里的老人减轻点负担。现在也是充满矛盾。还有一个就是读书方面,虽然现在在地铁通勤时间不少,但是大部分时间都拿去玩手机了,看书的时间并没有多少,满打满算也就8本书左右。现在还在读一个长篇小说大概是6本,现在读到第3本,所以之前读完的篇章并没有往读书记录里面加。果然手机是罪恶之源。心里虽然有些愧疚但是手机真的是一拿起来就放不下去。2024年计划在制定计划这个方面,我想制定一些靠谱的或者说对自己有吸引力的目标,以便自己能够完成。我参考了一些如何制定计划的书和文章,现在制定出下面几个计划,我想暂时分成这么几类吧。生活今年带着老婆孩子出去旅游一次,暂时就定在:上海、长沙、三亚、青岛这么几个城市,到时候再一起商量一下春天带孩子去公园踏青,去一次大兴的野生动物圆健康给父母买一个体检套餐,并陪着他们去体检一次去年自己的体检结果已经出现轻度脂肪肝和稍微的超重,所以今年的一个目标就是控制饮食加稍微的锻炼,使体重回归到正确的水平参加公司组织的体检并努力做到今年的体检结果都正常工作上尝试着引入新的工作方式,以及多调试一下项目代码,多做做笔记抽时间更新一下项目文档。特别是自己维护的那部分代码个人学习方面仍然坚持更新博客,我想今年以系列为主,就像之前更新vim系列一样,公司毕竟是做Office和PDF的,那就以这两个部分为主读书方面,今年给自己定的目标是读完12本书,平均每个月一本理财方面今年开始记录家庭支出和收入情况每个月流出一笔钱用作定投纳指和存银行定期个人娱乐方面《王国之泪》 玩100个小时,尽量不看攻略,看看今年能不能通关
2024年01月13日
5 阅读
0 评论
0 点赞
2023-04-18
从0开始自制解释器——重构代码
在上一篇文章中,完成了对括号的支持,这样整个程序就可以解析普通的算术表达式了。但是在解析两个括号的过程中发现有大量的地方需要进行索引的回退操作,索引的操作应该保证能得到争取的token,这个步骤应该放在词法分析的阶段,如果在语法分析阶段还要考虑下层词法分析的过程,就显得有些复杂了。而且随着后续支持的符号越来越多,可能又得在大量的地方进行这种索引变更的操作,代码将难以理解和维护。因此这里先停下来进行一次代码的重构。基本架构这里的代码我按照教程里面的结构进行组织。将按照程序的逻辑分为3层,最底层负责操作字符串的索引保证下次获取token的时候索引能在正确的位置。第二层是词法分析部分,负责给字符串的每个部分都打上对应的token。第三个部分是语法分析的部分,它负责解析之前设计的BNF范式,并计算对应的结果。详细的代码上面给出模块划分的概要可能没怎么说清楚,下面将通过代码来进行详细的说明。Token 模块为了支持这个设计,首先变更一下全局变量的定义,现在定义的全局变量如下所示extern Token g_currentToken; //当前token extern int g_nPosition; //当前字符索引的位置 extern char g_currentChar; //当前字符串之前通过 get_next_char() 来返回当前指向的token并变更索引的时候发现我们在任何时候想获取当前指向的字符时永远要变更索引,这样就不得不考虑在某些时候要进行索引的回退。比如在解析整数退出的时候,此时当前字符已经指向下一个字符了,但是我们在接下来解析其他符号的时候调用 get_next_char() 导致索引多增加了一个。这种情况经常出现,因此这里使用全局变量保存当前字符,只在需要进行索引增加的时候进行增加。另外我们不希望上层来直接操作这个索引,因此在最底层的Token模块提供一个名为 advance() 的函数用于将索引加一,并获取之后的字符。它的定义如下void advance() { g_nPosition++; // 如果到达字符串尾部,索引不再增加 if (g_nPosition >= strlen(g_pszUserBuf)) { g_currentChar = '\0'; } else { g_currentChar = g_pszUserBuf[g_nPosition]; } }这样在对应需要用到当前字符的位置就不再使用 get_next_char() , 而是改用全局变量 g_currentChar。例如现在的 skip_whitespace 函数现在的定义如下void skip_whitespace() { while (is_space(g_currentChar)) { advance(); } }这样我们在获取下一个token的时候只在必要的时候进行索引的递增。lex 模块由于打标签的工作交个底层的Token模块了,该模块主要用来实现词法分析的功能,也就是给各个部分打上标签,根据之前Token部分提供的接口,需要对 get_next_token 函数进行修改。bool get_next_token() { dyncstring_reset(&g_currentToken.value); while (g_currentChar != '\0') { if (is_digit(g_currentChar)) { g_currentToken.type = CINT; parser_number(&g_currentToken.value); return true; } else if (is_space(g_currentChar)) { skip_whitespace(); } else { switch (g_currentChar) { case '+': g_currentToken.type = PLUS; dyncstring_catch(&g_currentToken.value, '+'); advance(); break; case '-': g_currentToken.type = MINUS; dyncstring_catch(&g_currentToken.value, '-'); advance(); break; case '*': g_currentToken.type = DIV; dyncstring_catch(&g_currentToken.value, '*'); advance(); break; case '/': g_currentToken.type = MUL; dyncstring_catch(&g_currentToken.value, '/'); advance(); break; case '(': g_currentToken.type = LPAREN; dyncstring_catch(&g_currentToken.value, '('); advance(); break; case ')': g_currentToken.type = RPAREN; dyncstring_catch(&g_currentToken.value, ')'); advance(); break; case '\0': g_currentToken.type = END_OF_FILE; break; default: return false; } return true; } } return true; }在这个函数中,将不再通过输出参数来返回当前的token,而是直接修改全局变量。同时也不再使用get_next_char 函数来获取当前指向的字符,而是直接使用全局变量。并且在适当的时机调用advance 来实现递增。另外在上层我们直接使用 g_currentToken 拿到当前的token,而在适当的时机调用新增的eat() 函数来实现更新token的操作。bool eat(LPTOKEN pToken, ETokenType eType) { if (pToken->type == eType) { get_next_token(); return true; } return false; }该函数接受两个参数,第一个是当前token的值,第二个是我们期望当前token是何种类型。如果当前token的类型与期望的不符则报错,否则更新token。interpreter 模块该模块主要负责解析根据前面的BNF范式来完成计算并解析内容。这个模块提供三个函数get_factor、get_term、expr。这三个函数的功能没有变化,只是在实现上依靠lex 模块提供的功能。主要思路是直接使用 g_currentToken 这个全局变量来获得当前的token,使用 eat() 来更新并获得下一个token的值。这里我们以get_factor() 函数为例int get_factor(bool* pRet) { int value = 0; if (g_currentToken.type == CINT) { value = atoi(g_currentToken.value.pszBuf); *pRet = eat(&g_currentToken, CINT); } else { if (g_currentToken.type == LPAREN) { bool bValid = true; bValid = eat(&g_currentToken, LPAREN); value = expr(&bValid); bValid = eat(&g_currentToken, RPAREN); *pRet = bValid; } } return value; }与前面分析的相同,该函数主要负责获取整数和计算括号中子表达式的值。在解析完整数和括号中的子表达式之后,需要调用eat分别跳过对应的值。只是在识别到括号之后需要跳过左右两个括号。这样就完成了对应的分层,每层只负责自己该做的事。不用在上层考虑修改索引的问题,结构也更加清晰,未来在添加功能的时候也更加方便。剩下几个函数就不再贴出代码了,感兴趣的小伙伴可以去对应的GitHub仓库上查阅相关代码。
2023年04月18日
7 阅读
0 评论
0 点赞
2023-03-24
从0开始自制解释器——添加对括号的支持
在上一篇我们添加了对乘除法的支持,也介绍了BNF范式,并且针对当前的算术表达式写出了对应的范式,同时根据范式给出相应的代码实现。这篇我们将继续为算数表达式添加对括号的支持。对应的BNF 范式在上一篇我们给出了乘除法对应的范式<expr>::=<term>{(PLUS|MINUS)<term>} <term>::=<factor>{(DIV|MUL)<factor>} <factor>::={(0|1|2|3|4|5|6|7|8|9)}针对乘除法的优先级比加减法高,我们的做法是将乘除法单独作为一个部分,然后在最外层表达式中只处理加减法。基于这种思路,我们来看如何处理括号的问题。例如下面的算数表达式((1+2)*3+4) - (5 - 6 / 3)这里我们直接给出对应的文法,然后再来分析一下该如何由这个文法得到对应的表达式<expr>::=<term>{(PLUS|MINUS)<term>} <term>::=<factor>{(DIV|MUL)<factor>} <factor>::=({(0|1|2|3|4|5|6|7|8|9|)})|LPAREN<expr>RPAREN首先根据表达式,它应该由两个term来组成 expr = term - term接着看看两个term,它们并不是单纯的加法运算,所以两个term应该只有单纯的一个factor,也就是 expr = factor - factor因为最外层都有括号,所以再次展开 expr = (expr1) - (expr2)这时就又到了分析expr的过程了,左侧的expr最外层是一个加法,所以这里可以得到 expr1 = term + term右侧的expr 最外层是一个减法,也就是 expr2 = term - term结合最外层的表达式可以得到 expr = (term1 + term2) - (term3 - term4)term1 部分有一个乘法,所以它可以解析为 term1 = factor * factorterm2 部分就是单独的数字所以可以得到 term2 = factor,并且进一步得到 term2=4term3 部分就是单纯的数字,可以得到 term3 = factor,并且进一步得到 term3=5term4 部分有一个除法,所以它可以解析为 term3 = factor / factor此时整个表达式可以表示为 expr = (factor1 * factor2 + 4) - (5 - factor3 / factor4)factor1 本身也是一个括号,加表达式,所以它可以表示为 factor1 = (expr)factor2 是一个数字,所以它表示为 factor2 = 3factor3 是一个数字,所以它表示为 factor3 = 6factor4 是一个数字,所以它表示为 factor4 = 3此时表达式可以是 expr = ((expr1) * 3 + 4) - (5 - 6 / 3)此时再次分析这个 expr1 可以得到 expr1 = 1+2这个时候,整个表达式就出来了 expr = ((1+2) * 3 + 4) - (5 - 6 / 3)用图来表示大概可以表示如下代码实现有了范式,我们就可以按照范式来组织代码实现。首先我们先在 ETokenType 中添加针对括号的标签typedef enum e_TokenType { CINT = 0, //整数 PLUS, //加法 MINUS, //减法 DIV, //乘法 MUL, //除法 LPAREN, //左括号 RPAREN, //右括号 END_OF_FILE // 字符串末尾结束符号 }ETokenType;然后在 get_next_token 函数中添加对括号进行词法分析并打标签的功能bool get_next_token(LPTOKEN pToken) { char c = get_next_char(); dyncstring_reset(&pToken->value); if (is_digit(c)) { dyncstring_catch(&pToken->value, c); pToken->type = CINT; parser_number(&pToken->value); } else if(is_space(c)) { skip_whitespace(); return get_next_token(pToken); } else { switch (c) { case '+': pToken->type = PLUS; break; case '-': pToken->type = MINUS; break; case '*': pToken->type = DIV; break; case '/': pToken->type = MUL; break; case '(': pToken->type = LPAREN; break; case ')': pToken->type = RPAREN; break; case '\0': pToken->type = END_OF_FILE; break; default: return false; } } return true; }这里我对这个函数进行了一些改写,针对依靠单个字符就能打上标签的采用switc来进行处理,像空白字符、数字这种有多种字符类型的就采用普通的if处理。然后在get_oper 中添加对括号的识别 if (get_next_token(&token) && (token.type == PLUS || token.type == MINUS || token.type == DIV || token.type == MUL || token.type == LPAREN || token.type == RPAREN)) { oper = token.type; if (pRet) *pRet = true; }然后根据文法,get_factor 需要能够返回一个 expr的结果,所以这里需要添加以下代码 if (token.type == LPAREN) { bool bValid = true; value = expr(&bValid); if (!bValid) *pRet = false; if (get_next_token(&token) && token.type == RPAREN) *pRet = true; else *pRet = false; }如果我们得到的标签不为括号则按照原来的处理方式来处理,如果是括号,则将括号中的内容作为表达式并计算表达式的值,作为整数来返回。之前的expr 函数我们仅仅将结果打印并返回是否解析成功,这里需要做一些改进。我们使用一个传出参数来返回解析是否成功,而将计算结果作为值进行返回。另外需要特别注意的是,我们将反括号的判断放到了 get_factor 函数中,所以在 get_term 和 expr 中,遇到反括号应该考虑对位置索引进行递减,并且遇到反括号应该认为到达末尾并推出。这里的代码就不贴出来了。有兴趣的小伙伴可以看github上上传的代码。地址
2023年03月24日
6 阅读
0 评论
0 点赞
2023-03-22
从0开始自制解释器——添加对乘除法的支持
在上一篇中,我们实现了对减法的支持,并且介绍了语法图。针对简单的语法进行描述,用语法图描述当然是没问题的。但是针对一些复杂的语法进行描述,如果每个部分都通过语法图来描述就显得有些繁琐了。这篇我们先介绍另一种描述语法的方式,并进一步介绍一些关于语法分析的知识。BNF范式与上下文无关文法巴科斯范式 以美国人巴科斯(Backus)和丹麦人诺尔(Naur)的名字命名的一种形式化的语法表示方法,用来描述语法的一种形式体系,是一种典型的元语言。又称巴科斯-诺尔形式(Backus-Naur form)。它不仅能严格地表示语法规则,而且所描述的语法是与上下文无关的。它以递归方式描述语言中的各种成分,凡遵守其规则的程序就可保证语法上的正确性。它具有语法简单,表示明确,便于语法分析和编译的特点。BNF表示语法规则的方式为:非终结符用尖括号括起。每条规则的左部是一个非终结符,右部是由非终结符和终结符组成的一个符号串,中间一般以“::=”分开。具有相同左部的规则可以共用一个左部,各右部之间以直竖“|”隔开。所谓非终结符就是语言中某些抽象的概念不能直接出现在语言中的符号,终结符就是可以直接出现在语言中的符号。其实这些都是一些官话,初看起来只觉得拗口和难以理解,但是它的形式非常简单。它主要是用下面几个符号来表达含义使用<>来表示必须包含的部分使用[]来表示可选部分使用{}来表示可以重复0次或者无数次使用|来表示左右两边任选一部分,相当于OR使用::=来表示被定义为现在来给出具体的例子,我们都看过《西游记》,里面的取经4人组包括唐僧、孙悟空、猪八戒和沙僧。使用BNF范式进行定义,可以写成 <取经团队>::=<唐僧><孙悟空><猪八戒><沙僧>。我们再来举一个例子,我们知道一个文章由若干个段落组成、一个段落由若干个句子组成、一个句子由符合一定语法规则的汉字组成并且以句号作为结尾。我们简单的将句子的语法规则定义为主谓宾三个部分。而这里的主谓宾我们简单的用一些名词和动词来定义。因此这里的一系列结构可以定义为如下内容<文章>::={<段落>}<段落>::={<句子>}<句子>::=<主语><谓语><宾语>。<主语>::=人|狗|猫|天<谓语>::=吃|抓|下<宾语>::=饭|雨|肉|鱼根据这个表达式我们很容易的推出类似 人吃饭。、天下雨。、猫抓鱼。 这样的句子。相信到这里小伙伴应该明白BNF范式的一些基本概念和使用方式了。我们再来插入一个题外话,既然这里提到BNF范式是一种上下文无关文法,那什么是上下文、什么是上下文无关。先别着急了解概念,我们仍然通过例子来说明。在上述的句子的定义中,我们一共可以生成 4 * 3 * 4 = 48 种 结果,我们可以获得类似 人吃饭。、猫抓鱼。这种有意义的句子,也可能产生像天吃鱼。、人下雨 这种读起来感觉别扭的非正常语句。但是在上下文无关的语法中,主语宾语和谓语的内容没有相互关联,也就是说谓语和宾语的产生与主语无关。那上下文有关的文法呢?这里为了产生一些有意义的句子,我们给它加上一些限定。例如人后面只能接 吃 抓作为谓语、而当吃作为谓语时只能将 饭、肉、鱼作为宾语。针对这种需求,我们可以进行如下定义<句子>::=<主语><谓语><宾语>。<主语>::=人|狗|猫|天人<谓语>::=人(吃|抓)吃<宾语>::=吃(饭|肉|鱼)这样我们对这个产生式进行了一些限定,当主语是人的时候,谓语只能产生吃和抓这样的宾语。这种情况下的描述就被称之为上下文有关。上下文无关我自己的理解就是后续表达式的产生不依赖前面已产生的内容。而上下文有关的含义则与之相法。这个上下文就跟我们这么多年阅读理解题里面写的“请根据上下文来理解某个词表达了作者怎样的心情”这里的上下文类似。当然更加规范的说法就是,在应用一个产生式进行推导时,前后已经推导出的部分结果就是上下文。上下文无关就是只要文法的定义里面有一个定义,不管前面的产生串是什么都可以应用相应的产生推导后面的内容。代码编写上面的定义只是开胃菜,希望通过上面的描述,小伙伴能够理解BNF范式的应用,至于上下文无关和上下文有关。这些暂时不用考虑,毕竟我们目前还是在做上下文无关文法相关的内容。这里我们要支持乘法和除法,首先要做的就是在 ETokenType 结构中添加对乘法和除法相关的定义typedef enum e_TokenType { CINT = 0, //整数 PLUS, //加法 MINUS, //减法 DIV, //乘法 MUL, //除法 END_OF_FILE // 字符串末尾结束符号 }ETokenType;接着在 get_next_token和 get_oper() 函数中添加对这两个运算符的支持// get_next_token else if (c == '*') { pToken->type = DIV; dyncstring_catch(&pToken->value, '*'); } else if (c == '/') { pToken->type = MUL; dyncstring_catch(&pToken->value, '/'); } // get_oper if (get_next_token(&token) && (token.type == PLUS || token.type == MINUS || token.type == DIV || token.type == MUL)) { oper = token.type; if (pRet) *pRet = true; }现在词法分析部分已经可以支持乘除法的符号解析了。接着来完成语法分析的部分。首先我们来定义一下这个简单计算器的文法。<expr>::=<term>{<oper><term>} <term>::={0|1|2|3|4|5|6|7|8|9} <oper>::=PLUS|MINUS|DIV|MUL回忆一下上一节给出的语法图,理解这个表达式并不算困难。但是这里我们定义的文法有一个问题,就是从文法上体现不出运算的优先级。学过小学数学的都知道算数运算中优先计算乘除法,最后算加减法。但是根据这个文法我们无法体现出乘除法的优先级。因此这里我们需要修改定义。优先计算乘除法在文法上可以理解成,乘除法单独成一个部分,我们获取这个部分的计算结果最后与其他部分一起计算加减法。用BNF范式来体现就是<expr>::=<term>{(PLUS|MINUS)<term>} <term>::=<factor>{(DIV|MUL)<factor>} <factor>::={0|1|2|3|4|5|6|7|8|9}与语法图类似,范式也可以很容易转化为代码。允许出现多次的我们在代码实现上体现为循环。而文法中相关的定义我们直接采用一些get方式来获取对应被打上标记的值即可。上述文法描述可以转化为如下的c 代码int expr() { bool bRet = false; int result = get_term(&bRet); int bEOF = false; do { ETokenType oper = get_oper(&bRet); switch (oper) { case PLUS: { int num = get_term(&bRet); if(bRet) result += num; } break; case MINUS: { int num = get_term(&bRet); if(bRet) result -= num; } break; case END_OF_FILE: printf("%d\n", result); bEOF = true; break; default: bRet = false; break; } } while (bRet && !bEOF); if (!bRet) { printf("Syntax Error!\n"); } return 0; }上述expr的定义就是由一个term加若干个 +|- 和后面的若干个term 来组成,因此这里有一个循环。来取出所有term 和所有加减法,并进行计算。int get_term(bool* pValid) { int result = get_factor(pValid); int bEOF = false; do { ETokenType oper = get_oper(pValid); switch (oper) { case DIV: { int num = get_factor(pValid); if (*pValid) result *= num; } break; case MUL: { int num = get_factor(pValid); if (*pValid) result /= num; } break; case PLUS: case MINUS: { g_pPosition--; bEOF = true; } break; case END_OF_FILE: { g_pPosition--; bEOF = true; } } } while (pValid && !bEOF); return result; }而term 则是由整数以及若干个乘除法和另一个整数组成,所以代码中也用循环来取一直到取到不是这个term 定义所组成的部。注意这里与之前一样,当取到term的结束部分,我们仍然需要将索引进行递减。而最终的oper 和 factor 则保持原来的算法不变。好了,本篇到此也就结束了,小伙伴可以到该位置 取出代码来进行阅读和修改。
2023年03月22日
4 阅读
0 评论
0 点赞
2023-03-14
从0开始自制解释器——实现多个整数的加减法
在上一篇我们实现了一个可以计算两个多位整数加减法的计算器。本章我们继续来给这个计算器添加功能,这次要给它添加可以连续计算多个整数相加减的功能。例如我们可以计算 1 + 2 + 3 这样的表达式。语法图在正式写代码之前让我们先来学习一下一些基本的理论知识。这次要介绍的理论是语法图。什么是语法图呢?语法图是编程语言语法语法规则的图形表示。它体现了词法分析的运行规则。语法图直观的展示了在编程语言中哪些语句是符合语法的,哪些是不符合语法规范的。语法图的阅读非常容易,它类似于程序的流程图,只要顺着箭头指向的路径来读即可。与程序流程图类似,语法图中有些路径表示选择,有些表示循环。我们试着来读一下下面的语法图这张语法图表示的含义是,一个术语(term) 可选的跟上一个加号或者减号,而后面又需要跟上另一个术语。接着又可以有选择的跟上另一个加号或者减号。但是加号或者减号后面必须跟上另一个术语。这里又提到另一个单词,term 它的中文意思是术语。似乎很难用其他文字来解释何为术语。你只需要知道在这里它代表的是一个整数,它并不影响我们阅读这个语法图代码展示在上一篇中我们提到,将Token流识别为对应结构的过程被称之为词法分析,我们代码中的词法分析的实现主要在函数 expr 中。在这个函数中我们主要实现了词法分析以及最后的解释执行。我们按照语法图修改一下词法分析的代码我们先给出下面的伪代码获取第一个整数作为计算结果保存 while(解析到最后一个字符) { 获取操作符(+/-) switch(操作符) { case +: 获取下一个整数,如果不是整数则退出并报错 与结果相加 break; case -: 获取下一个整数,如果不是整数则退出并报错 与结果相减 break; } } 最终打印计算结果或者打印语法错误基于这个思路我们给出具体的实现代码int expr() { bool bRet = false; int result = get_term(&bRet); int bEOF = false; do { ETokenType oper = get_oper(&bRet); switch (oper) { case PLUS: { int num = get_term(&bRet); if(bRet) result += num; } break; case MINUS: { int num = get_term(&bRet); if(bRet) result -= num; } break; case END_OF_FILE: printf("%d\n", result); bEOF = true; break; default: bRet = false; break; } } while (bRet && !bEOF); if (!bRet) { printf("Syntax Error!\n"); } }这里为了便于理解,我将获取整数和操作符的模块又进行了一次封装,提供了两个函数分别是 get_term() 和 get_oper()。它们的代码如下int get_term(bool *pRet) { Token token = { 0 }; dyncstring_init(&token.value, DEFAULT_BUFFER_SIZE); int value = 0; if (get_next_token(&token) && token.type == CINT) { value = atoi(token.value.pszBuf); if (pRet) *pRet = true; } else { if (pRet) *pRet = false; } dyncstring_free(&token.value); return value; }ETokenType get_oper(bool* pRet) { Token token = { 0 }; dyncstring_init(&token.value, DEFAULT_BUFFER_SIZE); int oper = 0; if (get_next_token(&token) && (token.type == PLUS || token.type == MINUS)) { oper = token.type; if (pRet) *pRet = true; } else if (token.type == END_OF_FILE) { oper = END_OF_FILE; if (pRet) *pRet = true; } else { oper = -1; if (pRet) *pRet = false; } dyncstring_free(&token.value); return oper; }到此为止,就实现了多个整数的算术运算。整个实现过程的代码我都放到该位置。有兴趣的小伙伴可以自己对照着代码跟着我一起来实现属于自己的解释器。
2023年03月14日
5 阅读
0 评论
0 点赞
1
...
4
5
6
...
32