首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
269 阅读
2
从零开始配置 vim(15)——状态栏配置
159 阅读
3
nvim番外之将配置的插件管理器更新为lazy
145 阅读
4
2018总结与2019规划
137 阅读
5
PDF标准详解(五)——图形状态
116 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
emacs
读书笔记
elisp
VimScript
linux
文本编辑器
投资理财
Java
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
392
篇文章
累计收到
32
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
90
篇与
的结果
2026-07-18
股票投资学习记录(九)——二级市场的定价机制
在对一家公司进行深入分析之后,了解它的业务并且深入理解了公司的商业模式和护城河之后,对于它是否是一个好公司已经有一个大概的观念了。好公司还要有好的价格才值得买入。价值投资强调对于好公司要在价格远低于价值的买入,那么二级市场是如何对它进行定价?每天看到股票价格的涨涨跌跌是如何形成的呢?二级市场股票交易市场又被称为二级市场。这里的二指的是二手货的意思,因为我们买入的股票不是直接在上市公司手中购买的原始股,而是其他拥有这支股票的人转手卖给我们的二手货。而根据前面关于公司融资步骤我们可以知道:公司公开的向社会募集资金实际上是在IPO阶段。IPO阶段的定价由公司和IPO的承接商根据具体行情、公司融资目标等因素确定一个报价。之后进入到打新环节。所以从这个角度看,股权的一手交易实际是在打新环节。此时是我们直接从上市公司买入股权。我们可以形象的将二级市场理解为一个买卖股权的咸鱼平台,只不过只能买正规的上市公司股权,而无法买入体量小的小公司股权。买卖二手货的一个重要特点就是商品质量我们无法确定,买卖价格自由,没有所谓的官方指导价或者建议零售价,全部由卖家和买家进行自由报价。同时二级市场交易的钱不会流入上市公司的口袋,在打新阶段上市公司实际已经完成了融资。股票价格是如何决定的股票价格的一个重要特征是价格由少数交易者决定。比如说某个小区有1000套房,而其中990套都有业主在稳定的居住将其作为刚需住房,而只有10套挂牌出售。那么此时该小区二手房的价格主要由这10套房的成交价来决定。所以如果想操纵价格,那么可以从限制流通量和修改报价这方面入手。例如可以将所有小区待售房源集中起来一起拉高报价,或者采取假交易的手段,左手倒右手不断提交交易价格。这个时候小区房子的成交价格提高了,但是小区的居住价值却没有变化。理解了这个情况,再分析公司股价的变化就比较容易了,无非是从供需关系来分析。比如新股上市为什么会迎来一次涨幅或者说打新为什么总能赚钱。因为上市公司刚刚上市此时大量的股权都在公司自己和机构手中,只有少量股权能自由交易,流动性不足。卖家都知道新股会迎来一波上涨不愿意低价卖,而买家又非常想要买就只能不断加价。一旦机构和公司手中的股权解禁能自由买卖,那么股价将会迎来一次价值回归。对于普通散户来说,一般的建议是只买入上市超过3年,具有一定流动性的公司的股票。而我以前读价值投资相关的书籍时,作为稳健性投资者,我个人的指标是连续5年稳定分红、PE的5年历史平均值不超过20倍。所以我个人是拔高到至少上市5年以上。市场先生市场先生是格雷厄姆的一个经典比喻。市场先生是一个情绪不稳定,容易剧烈波动的人。他每天跑到你面前向你报价希望你能买入他手上的东西或者能将你手上的东西卖给他。有时候他极度乐观给这个东西一个很高的价格,有时候又极度悲观,给这个东西定一个很低的价格。我们要做的是不被市场先生影响,在他给出的价格过高时卖出在价格过低时买入。市场先生是一个很经典的比喻。那么市场先生的报价是如何形成的?或者说为什么市场容易出现暴涨暴跌呢?《乌合之众》这本书中讲,人在处于群体中时容易产生极端情绪丧失理智。股民在炒股过程中虽然物理上并没有一大群人在身边但是他们被交易软件上的波动各种弹窗消息以及自媒体、电视的疯狂轰炸联系到一起形成群体。在这个群体中人容易产生非理性行为。任何消息都会被无限放大,极端的乐观和极端的悲观很容易在这个群体中出现。从而使股票价格在他内在价值之间疯狂波动。产生波动的另一个原因是基金投资者。基金投资者本质也是在投资股票但是他们没有选股能力所以将钱交给基金,有基金经理帮他们选股进行投资。这类人也与普通个股投资者一样一旦出现风吹草动就容易进行买入卖出操作。而基金经理为了应对大额赎回必须要要贱卖手中的优质股票将钱还给投资者。所以某些时候出现千股跌停的现象。有时候二者互相影响,某个消息导致一些个股跌,从而导致基金投资者丧失信心,出现大批量赎回。而为了应对赎回基金经理不得不贱卖其中的优质股。这就导致大量毫不相关的股票出现大跌,进一步摧毁投资者信心。此时市场陷入恐慌,不管是个股投资者还是基金投资者都忙着割肉退出想要少点损失。此时对于价值投资者来说,如果他熟悉的某家公司出现大跌,此时就是进场的时机了。所以巴菲特说“别人恐惧时我贪婪”最后的总结二级市场的报价主要是供需关系决定的,与股票本身的价值无关。二级市场因为市场先生的存在大部分的时间都不能正确的做到价格和价值相匹配,可能在上市公司公布财报时短暂的出现价格正确的反应价值的时候。但是大部分时间市场先生的报价都是杂音。作为理性的投资者应该着眼于公司自身的价值,只在二级市场给出的报价合理时买入公司股权。
2026年07月18日
3 阅读
0 评论
0 点赞
2026-07-12
股票投资学习记录(八)——如何给公司估值
格雷厄姆在《聪明的投资者》这本书中提到,买股票应该买股价远低于估值的公司,需要给足安全边际。我们在考虑购买一个公司股票,通过分析发现它是一家有着优秀商业模式,深厚护城河,管理层也注重股东回报。那么下一步就是给公司进行估值,然后等待股价远低于我们的估值的时候买入。然后耐心持有,持续跟踪,等待股价价值回归,远高于估值的时候卖出。常用的估值方法常用的估值方法有绝对估值法和相对估值法。绝对估值法是采用对应的计算模型输入相应的值计算出一个准确的价格。绝对估值法的计算模型有:自由现金流折现法股利折现模型净资产分析法相对估值法是参考市场上其他同类型的公司,或者跟过去的自己相比,分析二者的区别得出一个大致的区间。相对估值法很像我们日常购物,比如买手机我们也不知道某款手机的具体价格,但是如果你一个杂牌手机的价格比苹果这种手机还贵,那肯定是贵了。自由现金流折现法自由现金流是指公司可以自由自由支配的现金流。如果以个人来类比的话,个人收入主要是工资收入,但是工资收入不能随便乱花,每个需要有一些固定支出,例如:水电费、房租、宽带、话费、交通费、伙食费等等,剩余的钱才是可以随便花的。一个企业也是同样的,赚的钱不能随便花,扣除经营企业必要投入后的钱才是可以自由支配的现金。自由现金流的计算可以简单的使用 自由现金流 = 经营活动产生的现金流净额 - 资本支出资本支出可以简单的采用现金流量表中:购买固定资产、厂房、设备等长期资产的现金流出这一项。接下来再来描述折现的概率。折现基于这样一个事实:货币具有时间成本,今天的100元购买力与10年后的100元不一样。折现的作用就是用来计算未来的100元等于现在的多少元。假设按照固定理财的3%收益进行计算,那么今天的97.08元就相当于明年的100元。我们可以将公司抽象成一台印钞机,每年印钞机会印出一定金额的现金,现在让你买断未来10年的使用权,那么这十年能产生的现金流折现到今天的总和就是它的价格。此时可以使用这样一个公式来计算:$$ \sum_{n=0}^{10}\frac{FCF}{({1+r})^{n}} $$我们可以简化为$$ FCF \frac{1-\frac{1}{({1+r})^{n}}}{r} $$从上述公式可以看到,在该模型中影响估值的主要有折现率和参与折现的年份。回到这个印钞机的例子,假设10年时间,折现率为3%,那么它的估值为 853.02元。对于企业来说,企业每年的自由现金流不是一成不变的,一般在实际估值中使用戈登公式:$$ V = \sum_{t=1}^{n} \frac {FCF_0 (1+g_1)^t}{(1+r)^t} + \frac {FCF_n (1+g_2)}{(r-g_2)(1+r)^n} $$FCF0:表示当前时点刚刚实现的自由现金流g1: 高速增长阶段的增长率g2: n 年之后进入永续低增长,需要满足g2 < r 公式才能成立r: 表示折现率n: 表示高速增长的年份戈登公式使用的前提是未来能永续维持低增长,并且增长率要小于我们给出的折现率,相当于要假定企业永远存在并且永远低速增长。虽然我们知道企业不可能永远存在,也不可能永远增长,但是一般来讲保证后续低速阶段给出的增长率小于折现率,企业后续能低速增长30到40年,这个结果已经很接近了。另外估值永远只能得到一个大概的区间,无法得到精确值,估值的艺术在于确定折现率、两个阶段如何划分以及如何确定两个阶段的增长率。这些数据的设定基于我们对企业、行业的理解程度。现在我们来实际计算两个例子:对于长江电力来说,它的主要业绩来源于发电站,而现在它提升业绩的方式主要也是后面收购新的水电站。我个人认为现在的长江电力对水电站的发电量已经开发到了极致,所以假定它未来自由现金流0增长。但是水电站的经营时间较长,假定是100年。我们可以从财报中查到自由现金流约为405亿元,另外我采用5%的折现率,带入公式计算,最终的结果大概为:8038.4亿元而长江电力的总股数为244.7亿股,所以得到最终股价约为 32.82元。按照安全边际,可以在此基础上打7到8折,也就是股价为22.92 或者 26.25 元时拥有安全边际如果我们按前10年3%的增长,后10年到100年的区间0增长,带入公式计算,我们得到它的价值大概为 7835.5亿元。除以总股数得到最终股价为 32.07我们发现采用不同的估值计算方式,得出的每股内在价值差距不大,因为它是这种没什么增长潜力的公司。我们的增长率取的比较低。股利折线模型它是自由现金流折现法的简化模型,它适用于红利型公司。红利型公司的确定型都比较强。它们业务稳定,也不需要大笔的资本开支,赚的钱很大一部分可以用来分红,未来也不太可能有高增长。对于这种类型的公司,我们可以使用它的分红来计算对应的内在价值。因为对于红利股,就像一个典型的印钞机,分发的红利就是印钞机印出的钱。所以我们取每年分红作为印出来的钱,但是每年分红会缓慢增长的,假设增长率为g、贴现率为r,那么可以有这样的公式$$ V = \frac {D_1}{r-g} $$按照这个公式我们来计算长江电力:长江电力26的分红已经结束了,每股分红1元,带入公式计算每股价值为33元。净资产估值法这个方式主要适用于一些小生意,因为大型上市公司的价值是无法使用净资产来衡量的。对于重资产企业可以采用这个标准。我们可以使用市净率这个指标,查看公司当前市净率处于历史的哪个区间来间接的判断它的估值相对估值法相对估值法比较简单,没有公式,我们只比较于它类似的公司价值大概是多少。我们可以对比同样公司的市盈率、市净率、总营收、净利润、ROE等指标来综合判断。使用类比估值法需要保证用来作为参考的公司不能太高估,如果参考的公司当前处于高估状态,那么最终我们对比得出的估值可能也是偏高的。除了于公司相比,还可以于整个行业相比。如果公司各项参考指标都优于行业平均水平但是市盈率、市净率等指标都小于行业的平均水平,那么当前公司可能处于低估状态。总结在进行公司估值时,我们可以使用上述方式进行交叉验证,防止在估值时某些指标取错造成估值不准确的问题。
2026年07月12日
3 阅读
0 评论
0 点赞
2026-07-05
股票投资学习记录(七)——公司战略分析
本来这一篇应该承接上一篇写财务报表的笔记,但是我写过一些教人如何读财报的书籍,我觉得那些读书笔记已经可以作为财报方面的笔记,所以本篇直接绕过关于财报科目的笔记,进入下一轮知识的学习,也就是公司战略的分析## 什么是公司战略我们在维基百科和一些百科中查到公司战略的官方定义是:确定企业的基本长期目标,选择行动途径和为实现这些目标将进行资源分配的活动。用通俗一点的话来说,公司战略主要回答3个问题:公司的目标和愿景是什么,公司在行业中的竞争优势是什么,以及基于公司的目标和愿景,执行路径和行动计划是什么公司的目标和愿景我们一般在公司的官网上就能看到,例如阿里巴巴的马云当初给阿里定下的愿景是“让中国没有难做的生意”。依托电商平台搭建、物流体系布局、数字化基础设施投入,真正实现了赋能中小商家、助力乡村农产品上行,是战略目标落地的经典案例。分析公司战略的作用作为股票投资者,对于公司战略的分析是我们找准优秀公司,获得超额利润的必要条件。有一句话是“做好人、买好股、得好报”。公司战略分析可以帮我们站在更高的维度审视公司经营方向是否准确。公司战略决定了公司的方向和天花板战略不是空洞的口号,而是企业最落地的经营决策,直接敲定企业的主营赛道、竞争模式、资源倾斜方向。企业是聚焦主业还是多元扩张、是技术领先还是成本领先、是深耕细分市场还是全面布局,所有选择都会直接锁定企业的竞争圈层与成长上限。呷哺呷哺早期凭借“平价单人小火锅、高频刚需、高性价比”的精准战略,差异化避开传统正餐火锅赛道,快速抢占大众消费市场,实现规模化扩张与盈利增长。后期管理层为追求更高毛利,战略定位大幅偏移:持续提高客单价、大力推出高端火锅品牌凑凑、发力高端餐饮赛道。这一战略调整直接导致品牌定位混乱:原有平价核心用户流失,而高端火锅赛道竞争激烈、消费者认知固化,无法抢占高端市场,最终陷入“两头不讨好”的困境,业绩大幅下滑、门店大规模闭店,充分印证了战略跑偏直接击穿企业原有基本盘、锁定发展颓势。公司战略是未来盈利能力的蓝图财报体现的是过去公司赚了多少钱。而战略决定了公司未来能赚多少钱。投资中一个很重要的概念是护城河,大多数企业的护城河是在激烈的市场竞争中产生的。而竞争中能否正确的构建和长期执行自己的战略是形成护城河的关键。乔布斯为苹果定下核心战略:聚焦核心智能硬件、以用户体验为核心、拒绝无意义的业务扩张。基于这一战略,苹果始终坚持软硬件一体化闭环模式,重视产品设计、系统优化、用户生态搭建,打造出差异化的产品竞争力。为坚守核心战略,苹果主动砍掉与核心生态不符的业务:放弃iPod、终止造车项目。并非没有技术能力,而是造车业务无法匹配苹果“极致用户体验、高壁垒、高溢价”的核心定位,会分散核心资源。长期聚焦的战略,让苹果积累了海量忠实用户,搭建起App Store、iTunes等平台化盈利体系,形成难以复制的生态护城河,支撑其长期高毛利率、高现金流的盈利状态。而微软与苹果的战略正好相反,苹果推行软硬件一体。而微软则主要推行软件,聚焦系统、办公、云服务等标准化软件产品。通过公司战略执行,判断公司的管理水平巴菲特本身多次说过,他主要投资的是“公司、好管理、好价格”。可以通过公司管理对于战略执行来判断公司管理层的水平。战略的制定体现了管理层的高瞻远瞩和水平,但是执行也能体现管理层的水平。我们可以在年报中观察他们是否兑现在了之前的业绩承诺或者管理方向有没有跑偏。例如东方雨虹是做建筑防水材料的龙头公司。它早期的战略是:大力发展零售渠道,以降低对大型工程的依赖;多元化和全球化的战略;人力方面搞合伙人制度,给合伙人和供应商股权激励以便达成销售业绩。在房地产最后的景气点,公司为了做大业绩,大量给供应商赊账,虽然做大了业绩但是背离了当初高质量发展目标,累计了大量的应收账款,为未来埋下了巨大的风险。这就给我们提醒了,如果公司的战略规划和财务数据不匹配,说明要么是财务造假要么是公司战略执行出了问题。这种公司我们应该远离。通过战略分析提前识别潜在风险和机会好的战略配合好的执行是未来盈利的保证,而混乱或者错误的战略可能造成企业未来经营的不利,这类公司现在看起来虽然便宜但是可能未来会陷入低估值陷阱。例如前面的呷哺呷哺明明应该合适在低端赛道多多发展用户就能赚钱,但是它去卷高端赛道被刺了原来的用户,而高端赛道人们也就认那么几家,结果就是两边不讨好。另外一个例子就是格力电器,这家公司早些年靠着硬核的空调和一句“好空调,格力造”、“掌握核心科技”的广告词深入人心。平心而论格力空调的质量确实不错。早期它的销售额和分红都比较好,属于价值投资的典型案例。但是随着房地产的遇冷,空调行业的竞争越来越激烈,格力的业绩受到了很大的影响。格力早期虽然提出了多元化的战略,例如格力做过手机、也想做新能源汽车,但是这些目前看都遥遥无期。所以格力目前看是没有成长性,并且随着其他品牌的竞争它未来的日子可能会更加难过。公司战略分析,给我们提供决策锚点虽然多元化对于有些公司来说是好事,但是也不是所有的公司都应该搞多元化。例如茅台,一般的产品例如手机或者电脑可能会给消费者不同的配置提供不同价位的产品。但是对于奢侈品来说,搞低端化就是在自杀,茅台作为白酒类的奢侈品就深谙这种道理,这么多年了一直是飞天茅台和王子酒。不乱搞跨界,也不盲目降价。只要茅台能一直维持这个奢侈品属性,保持现有战略,短期内的下跌反而是投资的机会,虽然整个白酒行业受实体经济低迷的影响,业绩也都一落千丈,但是随着未来实体经济的回暖,它未来的业绩可能也会增长。如何了解公司的战略对于普通没有人脉的投资者来说,公司的战略会纰漏在这么几个未知:年报中的董事长致辞、经营情况的讨论和分析、公司未来发展的展望一些临时公告、重大事项公告和业务说明会如何分析公司战略单纯看懂战略文字没有投资价值,核心是落地分析、验证真伪、预判价值。以下为普通投资者可直接套用的五步分析框架,简单、高效,可复刻一. 定赛道,看战略是否贴合行业趋势企业战略不能脱离行业大环境,逆势战略大概率长期失效。核心通过PEST基础判断,快速校验战略合理性:政策:是否符合行业监管、产业扶持政策?是否存在政策受限风险?经济:是否匹配消费升级/降级、行业增量/存量趋势?社会:是否贴合用户需求变化、消费习惯迭代?技术:是否适配行业技术迭代节奏,是否存在被技术颠覆的风险?例如,家具家电行业主要依靠房地产行业的发展。如果某个家电、家具、装修行业向上面的东方雨虹一样继续重仓工程渠道、盲目扩产,就是逆势战略,长期必然承压。这些行业合适的战略应该是聚焦存量翻新需求、建立或者扩展零售渠道才更符合产业规律二. 拆定位,明确企业战略类型将企业战略落地为具体类型,快速判断优劣,核心有四类:聚焦主业战略:深耕单一核心赛道,打磨产品与壁垒,适合龙头、细分龙头(茅台、海天),优势是壁垒稳固、现金流稳定,风险最低。差异化竞争战略:避开同质化内卷,走小众、高端、性价比差异化路线(早期呷哺呷哺),适合中小成长企业。成本领先战略:依托规模化、供应链优势降本,走平价放量路线,适合大众刚需行业。多元化扩张战略:分为相关多元化(贴合主业延伸,风险低)和无关多元化(跨界陌生赛道,风险极高),多数企业失败均源于盲目无关多元化。三. 验匹配,验证战略与企业禀赋是否相配好战略一定是力所能及的战略,而非空中楼阁。重点校验三点:资源匹配:企业的资金、技术、渠道、人才是否支撑战略落地?小企业盲目布局重资产赛道,必然失败。优势匹配:战略是否依托企业原有核心竞争力?放弃自身优势跨界,大概率跑偏。风险匹配:战略扩张节奏是否匹配企业抗风险能力?景气周期激进扩张、下行周期不收缩,极易爆雷。四. 核落地,用财务数据验证战略执行真伪这是投资分析的核心:不听企业说什么战略,只看财报做了什么动作,实现定性战略与定量数据交叉验证。主要的验证指标有:渠道战略:承诺发力零售,就看零售营收占比、渠道费用率是否提升,工程渠道占比是否下降。研发战略:承诺技术创新,就看研发费用率、专利数量、新品营收占比是否持续提升。扩张战略:承诺稳健经营,就看资产负债率、应收账款、存货增速是否可控。盈利战略:追求高质量增长,就看净利率、现金流、毛利率是否稳步优化,而非单纯营收扩张。五. 判持续性,评估战略壁垒与长期价值短期落地不代表长期值钱,最后一步判断战略能否构筑可持续的护城河:战略是否可复制:竞争对手能否轻易模仿?茅台的品牌聚焦战略、苹果的生态战略,均难以复制。是否持续加宽壁垒:战略落地后,市场份额、毛利率、用户粘性、品牌力是否持续提升?是否适配长期趋势:未来3-5年行业变化下,战略是否依然有效?是否存在被颠覆风险?案例下面将用海天味业来进行具体的战略分析首先判断海天味业的战略是否符合行业发展的。海天主营酱油、蚝油、酱料等餐饮调味品,属于刚需必选消费赛道。从行业趋势来看,行业整体从「增量扩张」进入「存量竞争、集中度提升」阶段,行业核心逻辑为:中小杂牌出清、头部品牌依托渠道与品质抢占市场、行业稳健低速增长,无爆发性增长,但具备极强的抗周期属性。政策端支持消费品制造业规范化、品牌化发展,无限制性政策;消费端,餐饮连锁化、家庭消费品质升级是长期趋势。战略结论:海天长期坚持「深耕调味品主业、推进渠道下沉、品类延伸」的战略,完全贴合行业存量集中、品牌升级的大趋势,不属于逆势扩张。1. 定赛道:校验战略是否贴合行业趋势(外部校验)海天主营酱油、蚝油、酱料等餐饮调味品,属于刚需必选消费赛道。从行业趋势来看,行业整体从「增量扩张」进入「存量竞争、集中度提升」阶段,行业核心逻辑为:中小杂牌出清、头部品牌依托渠道与品质抢占市场、行业稳健低速增长,无爆发性增长,但具备极强的抗周期属性。政策端支持消费品制造业规范化、品牌化发展,无限制性政策;消费端,餐饮连锁化、家庭消费品质升级是长期趋势。战略结论:海天长期坚持「深耕调味品主业、推进渠道下沉、品类延伸」的战略,完全贴合行业存量集中、品牌升级的大趋势,不属于逆势扩张。2. 拆定位:明确企业核心战略类型对照前文四类战略,海天的战略可精准定义为:聚焦主业+成本领先+渠道差异化的组合战略。坚决不做无关多元化,长期扎根调味品赛道,拒绝跨界炒作;依托极致规模化产能、全国供应链布局,实现行业成本领先,具备定价容错空间;渠道差异化布局,同步深耕餐饮B端(核心基本盘)+家庭C端下沉市场,区别于小众品牌单一渠道的短板。整体战略稳健、清晰、无激进扩张,完全匹配消费龙头的经营特质。3. 验匹配:战略与企业自身禀赋高度适配校验资源、优势、风险三重匹配度:资源匹配:企业常年现金流充裕、资产负债率极低、无重资产压力,可持续支撑产能扩建、渠道铺设、品牌投放;优势匹配:依托几十年酿造工艺、品牌口碑、全国经销商网络,深耕调味品主业是扬长避短,而非跨界冒险;风险匹配:赛道刚需属性强、业绩波动小,企业战略始终以稳健经营为核心,不盲目加杠杆、不追逐热点,抗风险能力极强。核心亮点:海天的战略没有脱离自身能力圈,是典型的「力所能及、稳步深耕」的优质战略。4. 核落地:用财务数据交叉验证战略真伪(核心实操)企业年报中长期披露战略:深耕主业、优化渠道结构、丰富品类矩阵、稳健扩张。我们用核心财务、经营数据验证执行落地情况,杜绝“口头战略”:渠道战略落地:长期维持B端餐饮渠道高占比,同时持续提升下沉市场、线上渠道、家庭零售渠道覆盖率,渠道结构持续优化,单一渠道依赖风险逐步降低,与战略表述一致;品类战略落地:从单一酱油,稳步延伸至蚝油、料酒、酱料、复合调味料,新品类营收占比持续提升,实现主业范围内的相关多元化,无盲目跨界;盈利战略落地:行业竞争加剧阶段,企业始终维持稳健的毛利率、净利率,经营性现金流常年覆盖净利润,不靠赊销冲业绩,杜绝了东方雨虹式“规模虚增、质量下滑”的问题,完美落地高质量发展战略;风险管控落地:应收账款、存货周转长期稳定,无异常攀升,杠杆率维持低位,战略稳健性完全兑现。关键判断:战略话术与经营数据高度匹配,管理层知行合一,无执行跑偏、无财务修饰隐患。5. 判持续性:评估战略壁垒与长期投资价值从可复制性、壁垒加宽、长期趋势三个维度终审:战略难以复制:普通中小企业无法复刻海天的全国供应链、经销商网络、品牌心智、规模化成本优势;壁垒持续加宽:随着行业出清,头部品牌市场份额持续提升,渠道、品牌、成本壁垒逐年加固,护城河越做越厚;长期趋势有效:调味品刚需属性永久存在,消费升级、行业集中是长期趋势,未来3-5年企业稳健深耕的战略依然成立,无颠覆式风险。最终的结论:海天味业属于战略清晰、贴合赛道、能力匹配、执行落地、壁垒持续的标准优质企业。企业没有花哨的扩张故事,但战略定力极强、经营极度稳健,适合长期价值投资。同时也可通过战略判断其估值特征:稳健深耕的主业战略,决定了企业无超高成长性、但有极强确定性,只能赚稳健业绩增长的钱,无法赚题材炒作、高速扩张的钱,投资预期需要与企业战略匹配。
2026年07月05日
4 阅读
0 评论
0 点赞
2026-05-03
海天味业(603288)初步分析
基本信息公司名称: 佛山市海天调味食品股份有限公司所处行业:调味发酵品核心业务:主要产品为酱油、耗油、调味酱、食醋、料酒、复合调味料特点:拥有极大的用户市场,几乎家家户户、与餐饮相关的一日三餐都有使用海天的调味品。商业模式靠什么挣钱:核心靠“酱油、蚝油等调味品的销售”赚钱,线上+线下双渠道覆盖(线下为主,占比90%以上),依托C端家庭、B端餐饮的刚性需求,实现稳定营收护城河是什么: 庞大的销售网络,全国任何超市、小卖部几乎都有它的产品。价格便宜,差不多能用买可乐的价钱买到它的调味品。质量靠谱,没有特殊调味需求的它基本上是最安心的选择。核心护城河是“渠道壁垒+品牌壁垒+规模效应”——渠道覆盖密度全行业第一,品牌家喻户晓,规模大导致生产成本低,性价比高,中小品牌难以竞争未来5到10年分红会消失吗:不会,只要中国人的饮食方式不变,海天调味品必然存在分红历史连续多少年分红不间断: 分红连续12年不间断近5年分红是否稳定:分红稳定,最近几年略有增长最近12个月股息率大概是多少:股息率大概为2.74%左右分红率 (分红/净利润):112.95%累计派现大于累计融资: 融资派现比 2015.98% 满足条件盈利稳定性5 年净利润: 近5年净利润稳定在50-60亿区间5-10 年营业额: 近10年营业额从100亿增长到250亿左右,2023年后增长放缓,2024-2025年基本维持在250亿左右连续3年公司扣非净利润为正、净利润是否为正:连续10年以上净利润、扣非净利润均为正,无任何一年亏损,盈利稳定性极强特点: 业务目前似乎增长到极限,未来不大可能长期增长,利润基本维持在一定水平现金流经营活动现金流是否稳健:近5年经营活动现金流净额稳定在60-70亿区间,2025年约65亿,现金流充沛、稳定近5年营业现金流大于净利润: 近5年经营活动现金流净额均大于净利润连续3年自由现金流为正:连续10年以上自由现金流为正财务安全资产负债率: 12% 没有偿债压力流动资产与流动负债的比: 基本维持稳定并且略有增加,最新流动比率约7.8倍,短期偿债能力极强,完全不用担心短期还债问题是否有大的风险:财务上没有大的风险赚钱能力近3年 ROE:常年维持在20%以上,赚钱能力优秀估值与安全边界股息率: 当前股息率约2.74%,处于合理偏低水平PB: 当前处于合理区间PE: 当前处于合理区间核心风险人口下降导致需求减缓,未来没有增长空间随着食品安全和健康饮食的观念慢慢深入人心,未来可能主打健康的调味产品会侵蚀它的市场空间行业竞争加剧(如李锦记、千禾味业的竞争),可能导致毛利率小幅下滑结论商业模式: 庞大的销售网络,深入人心的品牌力,价格便宜,C端+B端需求刚性财务状况: 财务状况优秀分红情况:分红长期稳定估值:PE、PB均处于自身历史及行业合理区间,股息率处于合理偏低区间,安全边际不足当前股息率小于4%,估值也未到低估区间, 本身具备投资价值但是当前不是买入时机。最近我发现它派现金额远大于净利润,有透支公司未来的嫌疑。但是无论从产品、和商业模式来看,它未来可以稳定的经营并长期分红,可以放入股票池观察,后续如果长期出现这种透支型的分红则拉入黑名单
2026年05月03日
23 阅读
0 评论
0 点赞
2026-04-19
股票投资学习记录(三)——快速筛选公司
价值投资有一个非常重要的概念,就是能力圈,投资那些你能看懂的公司。投资之前需要详细的分析,但是A股有5000多家上市公司,要是每一家公司都详细的分析,可能大部分的都被浪费在了垃圾公司里面,A股大量是平庸的公司,值得投资的好公司和特别垃圾的公司占少数。我们在平庸公司中投入大量时间分析、投资,结果可能不如投资少数值得投资的优秀公司。所以在了解公司之前需要快速过滤垃圾和平庸的公司,只留下少数值得投入精力研究和分析的公司。我们可以通过基本的财务数据筛选值得后续关注的公司。公司的经营活动围绕着如何赚钱、如何赚更多的钱、以及如何搜集更多的资源赚钱。这就对应着公司的营运、投资和筹资三大活动。财务报表可以反应这三种活动具体的效果。财务指标上可以分为:成长类指标、资产指标和效率类的指标成长类指标成长类指标:主要有营业收入、主营业务现金流、净利润;它们主要衡量企业的赚钱能力。营业收入营业收入是指公司做生意赚的钱,一般在利润表中可以找到。做生意也好、投资也好肯定想要投资那种业务越做越大,赚的钱越来越多的公司,对应的也就是营业收入越来越高的公司。我们可以拉长时间到5年或者10年,观察营业数据是否稳定增长,有没有大幅下跌。企业的营业收入是比较好调节的,有时候发现企业营业收入大幅度上涨,觉得它是好公司,但是它的上涨可能是造假,营业收入不等于真实收到的钱,只要签了合同就算营业收入。具体这个收入的增长是不是真的可以从现金流量表中的销售商品、提供劳务收到的现金这一个科目的增长趋势看出来。当然这一个科目也有造假的可能,但是比只盯一个指标要保险的多。我们可以将营业收入和主营业务的现金流叠加到一个图表中查看二者趋势是否一致。有的时候发现主营业务的现金流要大于营业收入,这主要是公司在整个生态中处于优势地位可以先收钱后发货。相对应的主营业务现金流小于营业收入的一般是处于弱势,需要先发货后收尾款,这种企业虽然也能增长但是一旦行业出现变故容易收不回尾款造成财务危机。当然也有二者几乎相等的企业,这种一般是面向个人消费者的公司,个人消费者购买产品一手交钱一手交货。总结一下这部分,选股的一个标准应该是长期来看营业收入有增长并且主营业务的现金流不小于营业收入。利润增长同样的,看利润也是要看几年的利润增长。一般来讲营业收入与利润应该是同步增长的,如果营业收入是增长的但是利润没有什么起色,可能是公司财务造假又或者是成本上来了,收入无法转化为利润,这个时候就应该留一个心眼了。资产类的指标我之前不知道在哪看的一个定义,企业的资产是能带来实际收益的东西。大致意思就是能给企业赚钱的就是资产,所以我们分析企业的资产质量就能知道企业未来能不能继续赚钱。也能与之前的营业收入相对照。在快速排查企业的时候我们只需要关注几个指标就行:现金、短期借款、应付账款。现金在财报中对应的是现金和短期金融资产。但是现金太少有些急于用钱的场合就拿不出钱来,比如员工的工资和福利保障、给供应商结尾款、还欠款的利息。账上的现金太多了也不好,多的钱一来会贬值,而来无法产生效益可能降低企业未来的增长。但看某一年的现金是无法体现出价值的。我们仍然需要看多年的现金情况,企业应该将现金控制在一个合理的范围。我们可以看公司的现金能否覆盖公司的短期负债。可以用现金和现金等价物 与 短期借款、应付票据、应付账款、预收账款、合同负债、其他应付账款、一年内到期的非流动负债简单对比一下,看看规模是否可控。并不是说现金需要大于它们的和,因为有时候后面经营持续产生现金流,可以用后面的钱来还债。但是要保证它们一直处于可控范围,如果现金没有增加但是短期负债大量增加就需要小心了。预售账款、合同负债属于收钱但是没有发货,这种不需要真正的给钱,只要后续发货了,债务就能消除。所以我们真正要看的就是短期借款、应付票据这几项需要真正给钱的。效率类的指标效率类的指标目前处于新手阶段的我主要关注ROE,也就是净资产收益率,这也是我在介绍巴菲特投资的相关书籍中学到的,芒格也说长期来看投资公司的收益约等于公司的净资产收益率。它的公式为:净利润/净资产;衡量企业用股东的钱赚钱的效率。我在之前的一些书籍之中,推荐看ROE在10%以上的公司,那么我也采用这个标准来作为我筛选公司的指标。总结最后我给出我目前采用的筛选公司的具体指标:最好被纳入沪深300等宽基指数之中(我不认为我有能力找到被各路股评人和专家都看走眼的公司,所以最好的办法就是在大家公认的优等生中找)连续3年净资产收益率不小于10%连续3年公司扣非净利润为正、净利润为正营业现金流 / 净利润 > 1连续3年公司自由现金流为正流动资产与流动负债的比值大于70%累计分红派现金额大于累计融资金额连续分红5年以上,且后3年分红金额不能大幅减少股息支付率大于30%小于70%我比较看重公司的分红,对我这样的新手来说,我不认为我能找到被大家普遍看衰的低估公司。我也不认为我能预判市场能准确找到未来盈利能力大幅增加的公司。我个人的定位是找到那些财务稳健能持续分红的好公司,如果能叠加营业收入和每股分红都缓慢增长的企业就不错了。
2026年04月19日
43 阅读
0 评论
0 点赞
2026-04-11
长江电力(600900) 初步分析
本篇是我尝试初步分析公司的第二篇,本周我打算尝试分析长江电力。基本信息公司名称: 中国长江电力股份有限公司所处行业:电力——水利发电核心业务:水利发电特点:背靠长江,发电几乎不需要成本,唯一的成本可能就是电机的维修与更新商业模式靠什么挣钱:背靠长江,靠长江水发电,将电力卖给国家电网,靠卖电挣钱护城河是什么: 独有的巨型水电站资源壁垒,长期购电协议 + 优先上网长期购电协议 + 优先上网,极低的边际发电成本未来5到10年分红会消失吗:不会,长江水不会枯竭,发电量常年稳定分红历史连续多少年分红不间断: 分红连续23年不间断近5年分红是否稳定:分红稳定并逐年增长最近12个月股息率大概是多少:股息率大概为3.51%左右分红率 (分红/净利润):71%盈利稳定性5 年净利润: 近几年下滑5-10 年营业额: 营业额一直大幅增加特点: 经营相对稳定有一定增长,但是成本攀升导致利润不增反降现金流经营活动现金流是否稳健:经营活动的现金流同比忽高忽低,借款攀升财务安全资产负债率: 60.79% 对于水电行业来说,属于正常水平有息负债高吗: 52%短期应付债务对财务有影响吗: 短期债务600多亿,而经营现金流只有500多亿,叠加账上现金的60多亿还债够呛是否有大的风险:负债一直增加,叠加营业成本增加。而营收主要靠发电,未来不改变,经营压力会增大赚钱能力ROE:13.01%估值与安全边界股息率: 当前处于偏高水平PB: 处于正常偏低的水平PE: 低估核心风险电价固定的情况下,营收增长完全靠发电量增长,主要靠长江水量,如果运营成本不降低,叠加未来长江枯水期,运营压力会增大结论商业模式: 背靠长江,靠天吃饭,特许经营,商业模式优秀财务状况: 财务费用高,拖累利润,但偿债风险可控分红情况:分红长期稳定估值:处于正常水平当前股息率小于4%,不具备投资价值。曾今发生过管理层希望违背当初的分红承诺,被投资人骂之后又改了回来的事情,我认为这是缺乏诚信,侵犯股东权益的恶劣事件。叠加25年长江电力拿着股东的钱投资葛洲坝航运扩能工程,很难想象它能在投资之后有足够的钱分红。因此我不打算将它纳入我的股票池。
2026年04月11日
34 阅读
0 评论
0 点赞
2025-03-16
Emacs 折腾日记(十七)——文本属性
我们在上一篇中介绍了如何对文件中的文本进行操作,本篇主要来介绍关于文本的属性。是的,文本也有属性。这里的文本属性有点类似于Word中的文字属性,文本中对应的字符只是文本属性的一种,它还包括文本大小、字体、颜色等等内容。Emacs中的文本也是类似的。于符号的属性类似,文本的属性也是由键值对构成。名 字和值都可以是一个 lisp 对象,但是通常名字都是一个符号,这样可以用这个 符号来查找相应的属性值。复制文本通常都会复制相应的字符的文本属性,但是 也可以用相应的函数只复制文本字符串,比如 substring-no-properties、 insert-buffer-substring-no-properties、buffer-substring-no-properties。产生一个带属性的字符串可以用 propertize 函数。(propertize "abc" 'face 'bold) ;; ⇒ #("abc" 0 3 (face bold))这里我们使用 face 来设置它的字体为粗体。或者我们可以使用C-x C-f 任意的打开或者创建一个文本文件,在文件中输入(insert (propertize "abc" 'face 'bold))我们可以看到它会在当前光标后面插入一个粗体的 abc 字符串。需要注意的是,我们在*scratch* buffer 中是无法看到这个效果的。因为*scratch* buffer 中开启了font-lock-mode。正如它的名字表示的那样,它锁定了字体,它里面的字体属性都是实时计算出来的。在插入文本之后它的属性很快就被修改了。因为*scratch* buffer 本质上还是一个elisp的编程环境,它里面有关于elisp的语法高亮、自动对齐等特性。它会自动的修改输入的文本属性。我们可以使用 (font-lock-mode - 1) 来关闭这个mode,然后执行上述代码就可以看到具体的效果了。虽然文本属性的名字可以是任意的,但是一些名字是有特殊含义的。属性名含义category值必须是一个符号,这个符号的属性将作为这个字符的属性face控制文本的字体和颜色font-lock-face和 face 相似,可以作为 font-lock-mode 中静态文本的 facemouse-face当鼠标停在文本上时的文本 facefontified记录是否使用 font lock 标记了 facedisplay改变文本的显示方式,比如高、低、长短、宽窄,或者用图片代替help-echo鼠标停在文本上时显示的文字keymap光标或者鼠标在文本上时使用的按键映射local-map和 keymap 类似,通常只使用 keymapsyntax-table字符的语法表read-only不能修改文本,通过 stickness 来选择可插入的位置invisible不显示在屏幕上intangible把文本作为一个整体,光标不能进入field一个特殊标记,有相应的函数可以操作带这个标记的文本cursor(不知道具体用途)pointer修改鼠标停在文本上时的图像line-spacing新的一行的距离line-height本行的高度modification-hooks修改这个字符时调用的函数insert-in-front-hooks与 modification-hooks 相似,在字符前插入调用的函数insert-behind-hooks与 modification-hooks 相似,在字符后插入调用的函数point-entered当光标进入时调用的函数point-left当光标离开时调用的函数composition将多个字符显示为一个字形这些东西我觉得也不需要记住,在需要的时候查查文档就好了。但是我参考的教程把它列出来了,那么我也在这里列出来把。由于字符串和缓冲区都可以有文本属性(如果没有特别指定文本对象的属性,那么默认使用缓冲区定义的文本属性),所以下面的函数通常不提供特定参数就是检 查当前缓冲区的文本属性,如果提供文本对象,则是操作对应的文本属性。查看文本属性查看文本对象在某处的文本属性可以用 get-text-property 函数。(setq foo (propertize "abc" 'face 'bold)) ;; ⇒ #("abc" 0 3 (face bold)) (get-text-property 0 'face foo) ;; ⇒ bold这里有两个问题需要注意一下,首先我们使用 propertize 为abc设置了文本属性face的值为bold,也就是将字符串设置为粗体。但是其中的0 和 3 代表什么呢?这里的0和3代表的是采用这个属性的字符在字符串中的范围,上面的代码中,整个abc字符串都采用整个属性,所以它的范围是[0, 3) 这个区间。要验证这一点我们可以使用下列代码(setq foo (concat "abc" (propertize "cde" 'face 'bold))) ;; ⇒ #("abccde" 3 6 (face bold)) (insert foo) 我们插入foo发现,它会插入 "abccde" 这么几个字符串,但是只有 "cde" 三个是加粗的根据这个提示,很明显的,get-text-property 中输入的0代表的就是“abc”字符串第0个,也就是字符a的属性。get-char-property 和 get-text-property 相似,但是它是先查找 overlay 的 文本属性。overlay 是缓冲区文字在屏幕上的显示方式,它属于某个缓冲区,具 有起点和终点,也具有文本属性,可以修改缓冲区对应区域上文本的显示方式。get-text-property 是查找某个属性的值,用 text-properties-at 可以得到某 个位置上文本的所有属性。修改文本属性put-text-property 可以给文本对象添加一个属性。它也是需要传入一个范围值,例如我们在前面foo的基础上使用以下代码(put-text-property 0 3 'face 'italic foo)我们再针对 foo 执行插入操作,此时会发现 abc 这个子串变成斜体了。和 put-text-property 类似,add-text-properties 可以给文本对象添加一系列的属性。和 add-text-properties 不同,可以用 set-text-properties 直接设置文本属性列表。你可以用 (set-text-properties start end nil) 来除去 某个区间上的文本属性。也可以用 remove-text-properties 和 remove-list-of-text-properties 来除去某个区域的指定文本属性。这两个函数的属性列表参数只有名字起作用,值是被忽略的。以下的例子还是建立在之前的 foo 变量之上,此时它的值为 #("baccde" 0 3 (face italic) 3 6 (face bold))。也就是前三个字符是斜体,后三个是加粗(set-text-properties 0 1 nil foo) ;; 取消了 a 字符的文本属性 foo ;; ⇒ #("abccde" 1 3 (face italic) 3 6 (face bold)) (remove-text-properties 2 4 '(face nil) foo) foo ;; ⇒ #("abccde" 1 2 (face italic) 4 6 (face bold)) (remove-list-of-text-properties 4 6 '(face nil) foo) foo ;; ⇒ #("abccde" 1 2 (face italic))查找文本属性文本属性通常都是连成一个区域的,所以查找文本属性的函数是查找属性变化的 位置。这些函数一般都不作移动,只是返回查找到的位置。使用这些函数时最好 使用 LIMIT 参数,这样可以提高效率,因为有时一个属性直到缓冲区末尾也没 有变化,在这些文本中可能就是多余的。next-property-change 查找从当前位置起任意一个文本属性发生改变的位置。 next-single-property-change 查找指定的一个文本属性改变的位置。 next-char-property-change 把 overlay 的文本属性考虑在内查找属性发生改 变的位置。next-single-property-change 类似的查找指定的一个考虑 overlay 后文本属性改变的位置。这四个函数都对应有 previous- 开头的函数,用于查找当前位置之前文本属性改变的位置(setq foo (concat "abc" (propertize "edf" 'face 'bold) (propertize "hij" 'pointer 'hand))) ;; ⇒ #("abcdefhji" 3 6 (face italic) 6 9 (face bold)) (next-property-change 1 foo) ;; ⇒ 3 (next-single-property-change 1 'pointer foo) ;; ⇒ 6text-property-any 查找区域内第一个指定属性值为给定值的字符位置。 text-property-not-all 和它相反,查找区域内第一个指定属性值不是给定值的 字符位置。(text-property-any 0 9 'face 'bold foo) ;; ⇒ 3 (text-property-not-all 3 9 'face 'bold foo) ;; ⇒ 6
2025年03月16日
26 阅读
0 评论
0 点赞
2025-03-07
Emacs 折腾日记(十五)——窗口
在上一节提到,当前buffer不一定是当前显示在屏幕上的那个buffer,想要修改显示的buffer,可以使用窗口相关的api。这节来介绍一些窗口的操作。窗口是屏幕上用于显示一个缓冲区 的部分。和它要区分开来的一个概念是 frame。frame 是 Emacs 能够使用屏幕的 部分。可以用窗口的观点来看 frame 和窗口,一个 frame 里可以容纳多个(至 少一个)窗口,而 Emacs 可以有多个 frame。不知道各位读者是否学习过MFC或者QT,这里的窗口就是MFC中的View,而frame则是整个界面框架,包括菜单栏工具栏、标题栏、状态栏等等部分。而窗口仅仅是最中间显示buffer的那一部分。分割窗口刚启动时,emacs 都是只有一个 frame 一个窗口。多个窗口都是用分割窗口的函 数生成的。分割窗口的内建函数是split-window。这个函数的参数如下:(split-window &optional window size horizontal)这个函数的功能是把当前或者指定窗口进行分割,默认分割方式是水平分割,可 以将参数中的 horizontal 设置为 non-nil 的值,变成垂直分割。如果不指定 大小,则分割后两个窗口的大小是一样的。分割后的两个窗口里的缓冲区是同 一个缓冲区。使用这个函数后,光标仍然在原窗口,而返回的新窗口对象:(split-window) ;; ==> #<window 7 on *scratch*>根据前面对于 optional 后参数的介绍,要填入 horizontal 的值实现竖直切分,需要填充前面的几个参数,如果不给则默认是nil。实际上上面的代码传入的可选参数都是nil,那么我们可以进行如下调用实现竖直分割窗口:(split-window nil nil 1) ;; ==> #<window 10 on *scratch*>我们也可以使用 selected-window 来获取当前选中的窗口,当前选中的窗口就是光标所在的窗口(split-window (selected-window) nil 1) ;; ==> #<window 11 on *scratch*>在进行实验的时候发现,分割的时候是在当前窗口的基础之上分割的,它是类似于这样的一个过程,它只在Win1所在的窗口区域进行划分,除非改变当前窗口。 +---------------+ +---------------+ | | | | | | win1 | | win1 | win2 | | | --> | | | | | | | | | | | | | +---------------+ +---------------+ | v +---------------+ +---------------+ | 4 | 5 | | | | | | | | win2 | | win1 | win2 | |--------| | <-- |-------| | | win3 | | | win3 | | | | | | | | +---------------+ +---------------+可以看成是这样一种结构(win1) -> (win1 win2) -> ((win1 win3) win2) -> (((win4 win5) win3) win2)删除窗口如果要让一个窗口不显示在屏幕上,要使用 delete-window 函数。如果没有指定 参数,删除的窗口是当前选中的窗口,如果指定了参数,删除的是这个参数对应 的窗口。删除的窗口多出来的空间会自动加到它的邻接的窗口中。如果要删除除 了当前窗口之外的窗口,可以用 delete-other-windows 函数。当一个窗口不可见之后,这个窗口对象也就消失了(setq foo (selected-window)) (delete-window foo) (windowp foo) ;; ==> t (window-live-p foo) ;; ==> nil (delete-other-windows foo) ;; ==> error, 因为先删除foo所对应的窗口,现在已经无法找到这个窗口了,所以这里删除它以外的会报错窗口配置窗口配置(window configuration) 包含了 frame 中所有窗口的位置信息:窗口 大小,显示的缓冲区,缓冲区中光标的位置和 mark,还有 fringe,滚动条等等。 用 current-window-configuration 得到当前窗口配置,用 set-window-configuration 来还原。(setq foo (selected-window)) (split-window foo nil t) (split-window) (setq wc (current-window-configuration)) (delete-other-windows foo) (set-window-configuration wc)我们一行一行的执行上述代码,会发现调用 delete-other-windows 删除之前的窗口之后再次调用 set-window-configuration 会恢复上次保存的结果。看到这里各位读者是否有这么一个想法:利用这两个函数实现一个自动保存和恢复窗口结构的功能呢?但是经过测试,current-window-configuration 得到的对象并不能持久化的保存的到文件中,即使写到文件中,读取的时候也会报错。下面是我的测试代码(setq workspace-file-path "~/.session") ;; 保存窗口的配置 (defun my/save-current-workspace () (with-temp-file workspace-file-path (print (current-window-configuration) (current-buffer)))) ;; 加载窗口的配置 (defun my/load-current-workspace () (when (file-exists-p workspace-file-path) (with-temp-buffer (insert-file-contents workspace-file-path) (set-window-configuration (read (current-buffer))))))在执行保存之后,我们查看文件得到的是一个类似于 #<window-configuration> 的字符串,并没有别的内容,在调用 set-window-configuration的时候会报错。选择窗口前面提到过可以使用 selected-window 来获取当前光标所在的窗口。我们可以使用 select-window 来选择某个窗口作为当前窗口。使用 other-window 来选择另外的窗口。该函数是一个在不同窗口之间快速跳转的一个函数,它按照窗口创建的时间的逆序进行排序,根据传入的整数参数来决定跳转到第几个窗口。(progn (setq foo (selected-window)) (message "Original window: %S" foo) (other-window 1) (message "Current window: %S" (selected-window)) (select-window foo) (message "Back to original window: %S" foo))这里有两个特殊的宏 save-selected-window 和 with-selected-window。它的作用是在执行语句之后,选择的窗口回到之前选择的窗口。with-selected-window 和 save-selected-window 几乎相同, 只不过 save-selected-window 选择了其它窗口。这两个宏不会保存窗口的位置 信息,如果执行语句结束后,保存的窗口已经消失,则会选择最后一个选择的窗口(save-selected-window (select-window (next-window)) (goto-char (point-min)))上述代码会选择另一个窗口并将光标移动到缓冲的开始位置。当前 frame 里所有的窗口可以用 window-list 函数得到。可以用 next-window 来得到在 window-list 里排在某个 window 之后的窗口。对应的用 previous-window 得到排在某个 window 之前的窗口walk-windows 可以遍历窗口,相当于 (mapc proc (window-list))。 get-window-with-predicate 用于查找符合某个条件的窗口窗口大小信息窗口是一个长方形区域,所以窗口的大小信息包括它的高度和宽度。用来度量窗 口大小的单位都是以字符数来表示,所以窗口高度为 45 指的是这个窗口可以容 纳 45 行字符,宽度为 140 是指窗口一行可以显示 140 个字符mode line 和 header line 都包含在窗口的高度里,所以有 window-height 和 window-body-height 两个函数,后者返回把 mode-line 和 header line 排除后 的高度(window-body-height) ;; ==> 53 (window-height) ;; ==> 54滚动条和 fringe 不包括在窗口的亮度里,window-width 返回窗口的宽度。所以 window-body-width 和 window-width 返回的结果一样(window-body-width) ;; ==> 234 (window-width) ;; ==> 234也可以用 window-edges 返回各个顶点的坐标信息。window-edges 返回的区域包含了 滚动条、fringe、mode line、header line 在内,如果单纯的想要返回文本所在区域可以使用 window-inside-edges(window-edges);; ==> (0 0 238 54) (window-inside-edges) ;; ==> (1 0 236 54) 如果需要的话也可以得到用像素表示的窗口位置信息,这里用到的函数是 window-pixel-edges 和 window-inside-pixel-edges(window-pixel-edges) ;; ==> (0 0 1908 922) (window-inside-pixel-edges) ;; ==> (8 0 1884 905)到目前为止,我们有了手段可以遍历窗口以及获取窗口的坐标,那么利用这些数据就可以做到记录和恢复之前的窗口布局了。我最开始的思路是采用 walk-windows 来遍历窗口,并且使用 window-pixel-edges 来记录每个窗口的区域。但是这么做有一些问题无法解决:首先还原的时候创建窗口只能采用 split-window,而 split-window 是基于之前的窗口来创建的,walk-windows 无法反映出这种层级关系。另外就是emacs 中没有函数来设置窗口左上角的坐标,我们只能通过函数来改变窗口的宽和高,窗口的位置在使用 split-window 创建的时候已经决定了。所以我们需要一种能表示层级关系的结构来存储窗口的信息。这个时候就要引入 window-tree 函数了。这个函数可以返回当前 frame 窗口布局的树状结构。为了说明它的返回值,我们先来举一个例子。首先打开emacs,此时看到只有一个窗口,暂时叫它窗口A在窗口上垂直分割一个窗口,新生成的窗口叫做窗口B,此时左侧的窗口是A,右侧的是B在B窗口上水平分割一个窗口,生成一个新的C窗口此时应该有3个窗口,它们的布局如下:+---------------+ | | | | A | B | | |-------| | | C | | | | +---------------+如果用树来表示这个布局,可以组成这么一颗树 frame / \ left right (win A) / \ / \ top bottom win B win C对于叶子节点来说,window-tree 返回的数据形式是 (DIR EDGES CHILD1 CHILD2 ...) 各部分代表的含义如下:DIR,表示分割类型,t表示竖直分割,nil表示水平分割EDGES, 表示窗口区域的坐标,格式为 (LEFT TOP RIGHT BOTTOM),以字符为单位CHILDREN, 子节点列表,可以是分支节点或叶子节点而叶子节点是一个窗口对象。上面的窗口布局,使用 window-tree 得到的结果如下( (nil (0 0 84 35) #<window 3 on *scratch*> (t (42 0 84 35) #<window 7 on *scratch*> #<window 9 on *scratch*>)) #<window 4 on *Minibuf-0*>)去除掉minibuffer部分,着重分析一下文本区域的分割(nil (0 0 84 35) #<window 3 on *scratch*> (t (42 0 84 35) #<window 7 on *scratch*> #<window 9 on *scratch*>))首先水平分割,占区域大小为 (0 0 84 35)。此时上面一个部分是 win3。下半部分右进行了分割。下半部分采用竖直方式进行分割,占区域为 (42 0 84 35)。这个部分有两个子窗口win7 和 win9。感觉分割的顺序与我们的直觉相悖。但是仔细想想好像又能产生之前那种结果 (42 0) +---------------+ | | | | win3 | win7 | | |-------| | | win9 | | | | +---------------+ (84 35)我们可以写下如下代码来进行这个结构的解析(defun my-current-window-configuration () ;; pai chu minibuffer de shu ju (my-window-tree-to-list (car (window-tree)))) (defun my-window-tree-to-list (tree) (if (windowp tree) 'win (let ((dir (car tree)) (children (cddr tree))) (list (if dir 'vertical 'horizontal) (if dir (my-window-height (car children)) (my-window-width (car children))) (my-window-tree-to-list (car children)) (if (> (length children) 2) (my-window-tree-to-list (cons dir (cons nil (cdr children)))) (my-window-tree-to-list (cadr children))))))) (defun my-window-height (win) (if (windowp win) (window-height win) (let ((edge (cadr win))) (- (nth 3 edge) (nth 1 edge))))) (defun my-window-width (win) (if (windowp win) (window-width win) (let (edge (cadr win)) (- (nth 2 edge) (car edge)))))根据这个结构编写一个还原的功能(defun my-list-to-window-tree (conf) (when (listp conf) (let (newwin) (setq newwin (split-window nil (cadr conf) (eq (car conf) 'horizontal))) (my-list-to-window-tree (nth 2 conf)) (select-window newwin) (my-list-to-window-tree (nth 3 conf))))) (defun my-set-window-configuration (winconf) (delete-other-windows) (my-list-to-window-tree winconf))可以使用如下代码进行调用(setq foo (my-current-window-configuration)) ;; do something (my-set-window-configuration foo)窗口对应的缓冲区窗口对应的缓冲区可以用 window-buffer 函数得到:(window-buffer) ;; ==> #<buffer *scratch*>缓冲区对应的窗口也可以用 get-buffer-window 得到。如果有多个窗口显示同一 个缓冲区,那这个函数只能返回其中的一个,由window-list 决定。如果要得到 所有的窗口,可以用 get-buffer-window-list(get-buffer-window (get-buffer "*scratch*")) (get-buffer-window-list (get-buffer "*scratch*"))让某个窗口显示某个缓冲区可以用 set-window-buffer 函数。 让一个缓冲区可见可以用 display-buffer。默认的行为是当缓冲区已经显示在某个窗口中时,如果不是当前选中窗口,则返回那个窗口,如果是当前选中窗口, 且如果传递的 not-this-window 参数为 non-nil 时,会新建一个窗口,显示缓 冲区。如果没有任何窗口显示这个缓冲区,则新建一个窗口显示缓冲区,并返回 这个窗口。display-buffer 是一个比较高级的命令,用户可以通过一些变量来改 变这个命令的行为。比如控制显示的 pop-up-windows, display-buffer-reuse-frames,pop-up-frames,控制新建窗口高度的 split-height-threshold,even-window-heights,控制显示的 frame 的 special-display-buffer-names,special-display-regexps, special-display-function,控制是否应该显示在当前选中窗口 same-window-buffer-names,same-window-regexps 等等。这里的函数实在是太多了,我想暂时不用都记住,现在又有各种大模型,到时候有需求直接使用问就行。或者记住这一个函数,后面要扩展自己去查文档
2025年03月07日
21 阅读
0 评论
0 点赞
2025-02-21
Emacs折腾日记(十四)——buffer操作
教程 中的下一节应该是正则表达式。但是我觉得目前来说正则表达式对我来说不是重点,而且正则表达式我还是比较了解,没必要专门去学习,在使用的时候看看相应的章节就好。况且现在有AI这个利器,在处理正则表达式应该问题不大。所以这里就略过这节,直接进入后面的学习截止到前面的一些文章,我觉得应该已经涉及到了emacs lisp中的语法要点,现在去看一些emacs配置中的代码不太会一头雾水了。离攒自己的配置又进了一步。期间我想过是不是可以跳过教程后面的内容直接进入配置的过程呢?仔细考虑了一下,我觉得还是有必要跟着教程深入了解一下操作emacs对象的一些API。就像我之前学习C/C++编程一样,如果只学语法部分,最多也就能写写基于链表等数据结构的黑框框的信息管理系统。如果想要写点带界面的或者带网络功能的或者稍微复杂点的程序就离不开操作系统,网络编程,数据库等等知识。emacs的学习可能也是这样,现在也只能写点算术运算或者say-hello 这样的玩具。想要跟emacs结合起来,真正流畅的操作emacs,还需要学一些emacs自身的知识。缓冲区名称在学习vim的时候已经很详细的了解过什么是缓冲区,以及缓冲区与文件有什么区别。在这里我想就没有必要再谈论了,如果有读者不太清楚这方面的内容,欢迎阅读我博客中关于vim缓冲区的部分。emacs中缓冲区的概念与vim基本没什么区别。唯一的区别可能是emacs中一些内置的缓冲区与vim的不太一样。emacs 里的所有缓冲区都有一个不重复的名字。所以和缓冲区相关的函数通常都是可以接受一个缓冲区对象或一个字符串作为缓冲区名查找对应的缓冲区。有一个习惯是名字以空格开头的缓冲区是临时的,用户不需要关心的缓冲区。所以现在一般显示缓冲区列表的命令都不会显示这样的变量,除非这个缓冲区关联一个文件。要得到缓冲区的名字,可以用 buffer-name 函数,它的参数是可选的,如果不指定参数,则返回当前缓冲区的名字,否则返回指定缓冲区的名字。更改一个缓冲区的名字用 rename-buffer,这是一个命令,所以你可以用 M-x 调用来修改当前缓冲区的名字。如果你指定的名字与现有的缓冲区冲突,则会产生一个错误,除非你使用第二个可选参数以产生一个不相同的名字,通常是在名字后加上 <序号> 的方式使名字变得不同。你也可以用 generate-new-buffer-name 来产生一个唯一的缓冲区名。它需要传入一个参数,表示buffer的名称,如果当前buffer有名称与指定名称冲突,它会在你提供的名称后面加一些后缀,否则就采用传入的名称;; scratch buffer 中 (buffer-name) ;; ==> *scratch* ;; 此时 *scratch* 已经被重命名成了 scratch (rename-buffer (generate-new-buffer-name "scratch")) ;; ==> scratch当前缓冲区可以使用 current-buffer 来获取当前缓冲区,需要注意的是当前缓冲区不一定是显示在当前屏幕上的那个缓冲区。这个跟工作目录有点像,当前目录并不一定就是程序所在的目录或者当前打开的文件所在的目录。我们可以使用 set-buffer 来设置当前缓冲区,但是前面我们说过当前缓冲区并不一定是显示在屏幕上的那个缓冲区,即使修改当前缓冲区,也不会改变当前窗口上显示的缓冲区(set-buffer "*Messages*") ;; ==> #<buffer *Messages*>,但是屏幕上显示的缓冲区没有变化如果要切换当前屏幕显示的缓冲区需要配置窗口相关的函数,例如我们可以使用 switch-to-buffer(switch-to-buffer "*Messages*")前面提到 set-buffer 可以改变当前缓冲区,但是我们调用 buffer-name 获取当前缓冲区的名称时得到还是 scratch buffer(set-buffer "*Messages*") ;; ==> #<buffer *Messages*> (buffer-name) ;; ==> #<buffer *scratch*> (buffer-name) ;; ==> "*scratch*"这是因为我们如果采用 C-x C-e 来分别执行,这就相当于在命令行执行命令一样,每次语句结束之后,emacs会重新刷新上下文环境。而 buffer-name获取的是当前上下文环境中的当前缓冲区名称。上下文环境随着上一条语句的结束而更新,这就导致了当前缓冲区变化。这个过程可以描述为如下的过程[主进程环境] │ ├── [逐行执行L1] → 创建临时子环境 → 执行set-buffer → 销毁子环境 └── [逐行执行L2] → 创建新子环境 → 读取buffer-name → 返回主环境值 所以如果要得到正确的结果,就是一次性执行完这两条语句,按照我当前的知识储备有三种办法:第一个办法就是使用 eval-buffer,从messages buffer 中获取输出信息第二个办法,使用 progn 将两条语句包含起来第三个办法就是将它包装成一个函数或者宏来执行(progn (set-buffer "*Messages*") (buffer-name)) ;; ==> "*Messages*"但是我们不能仅仅依靠这种包裹代码的方式来实现切换buffer的效果。因为这个命令很可以会被另一个程序员来调用。你也不能直接用 set-buffer 设置成原来的缓冲区,因为set-buffer不能处理错误或退出情况。正确的作法是使用 save-current-buffer、with-current-buffer 和 save-excursion 等方法save-current-buffer 能保存当前缓冲区,执行其中的表达式,最后恢复为原来的缓冲区。如果原来的缓冲区被关闭了,则使用最后使用的那个当前缓冲区作为语句返回后的当前缓冲区。lisp 中很多以 with 开头的宏,这些宏通常是在不改变当前状态下,临时用另一个变量代替现有变量执行语句。比如 with-current-buffer 使用另一个缓冲区作为当前缓冲区,语句执行结束后恢复成执行之前的那个缓冲区save-excursion 与 save-current-buffer 不同之处在于,它不仅保存当前缓冲区,还保存了当前的位置和 mark。在 scratch 缓冲区中运行下面两个语句就能看出它们的差别了(save-current-buffer (set-buffer "*scratch*") (goto-char (point-min)) (save-excursion (set-buffer "*scratch*") (goto-char (point-min))上面两段代码,都是先保存当前缓冲区,然后使用 set-buffer 保证当前缓冲区是 scratch buffer,接着调用goto-char移动鼠标光标到buffer最开始的位置。随着代码块的结束,会自动切换回对应的buffer。但是因为 save-excursion 会额外保存当前位置和 mark,所以我们发现第一段代码光标位置跑到缓冲区最开始的位置,而第二段代码光标位置不变在对比一下它们与 with-current-buffer 的区别,with-current-buffer 调用时已经帮我们使用 set-buffer设置好了当前缓冲区,而且也会保存当前缓冲区,在结束之后也会还原当前缓冲区。但是它使用的是 save-current-buffer。我们可以使用 C-h C-f 来查看并找到它的源代码(defmacro with-current-buffer (buffer-or-name &rest body) (declare (indent 1) (debug t)) `(save-current-buffer (set-buffer ,buffer-or-name) ,@body))我们发现它其实就是用 save-current-buffer 做了一次封装。如果我们对上面的测试代码稍加修改使用 with-current-buffer 实现,例如(with-current-buffer "*Messages*" (goto-char (point-min)))执行之后我们发现,它的光标位置也改变了。创建和关闭缓冲区产生一个缓冲区必须用给这个缓冲区一个名字,所以两个能产生新缓冲区的函数都是以一个字符串为参数:get-buffer-create 和 generate-new-buffer。这两个函数的差别在于前者如果给定名字的缓冲区已经存在,则返回这个缓冲区对象,否则新建一个缓冲区,名字为参数字符串,而后者在给定名字的缓冲区存在时,会使用加上后缀 (N 是一个整数,从2开始) 的名字创建新的缓冲区。(get-buffer-create "temp") (with-current-buffer "temp" (insert "this is temp buffer")) (switch-to-buffer "temp")上面的代码,我们先创建一个新的temp buffer,并且切换到这个buffer,然后在这个buffer中调用insert函数,插入一段话。最后可以让窗口显示这个buffer来验证结果关闭一个缓冲区可以用 kill-buffer。当关闭缓冲区时,如果要用户确认是否要关闭缓冲区,可以加到 kill-buffer-query-functions 里。如果要做一些善后处理,可以用 kill-buffer-hook。通常一个接受缓冲区作为参数的函数都需要参数所指定的缓冲区是存在的。如果要确认一个缓冲区是否依然还存在可以使用 buffer-live-p。要对所有缓冲区进行某个操作,可以用 buffer-list获得所有缓冲区的列表。如果你只是想使用一个临时的缓冲区,而不想先建一个缓冲区,使用结束后又需要关闭这个缓冲区,可以用 with-temp-buffer 这个宏。从这个宏的名字可以看出,它所做的事情是先新建一个临时缓冲区,并把这个缓冲区作为当前缓冲区,使用结束后,关闭这个缓冲区,并恢复之前的缓冲区为当前缓冲区。在缓冲区内移动在学会移动函数之前,先要理解两个概念:位置(position)和标记(mark)。位置是指某个字符在缓冲区内的下标,它从1开始。更准确的说位置是在两个字符之间,所以有在位置之前的字符和在位置之后的字符之说。但是通常我们说在某个位置的字符都是指在这个位置之后的字符。这点很符合我们的直觉,一般在编写代码或者文档的时候当前的光标就是在文本之间移动。标记和位置的区别在于位置会随文本插入和删除而改变位置。一个标记包含了缓冲区和位置两个信息。在插入和删除缓冲区里的文本时,所有的标记都会检查一遍,并重新设置位置。这对于含有大量标记的缓冲区处理是很花时间的,所以当你确认某个标记不用的话应该释放这个标记。创建一个标记使用函数 make-marker。这样产生的标记不会指向任何地方。你需要用 set-marker 命令来设置标记的位置和缓冲区。(setq foo (make-marker)) ; ==> #<marker in no buffer> (set-marker foo (point)) ; ==> #<marker at 195 in *scratch*>point 函数其实返回一个整数,表示当前光标在哪个位置,既然这里只用传入位置就可以正确的将foo这个标签绑定到对应的缓冲区,这里set-marker 应该是以当前缓冲区作为标签的缓冲区,我们可以使用下面的代码来验证(with-current-buffer "*Messages*" (set-marker foo (point))) ;; ==> #<marker at 477 in *Messages*>也可以用 point-marker 直接得到 point 处的标记。或者用 copy-marker 复制一个标记或者直接用位置生成一个标记(point-marker) ;; ==> #<marker at 211 in *scratch*> (copy-marker 20) ;; ==> #<marker at 20 in *scratch*> (copy-marker foo) ;; ==> #<marker at 195 in *scratch*>如果要得一个标记的内容,可以用 marker-position,marker-buffer(marker-position foo) ;; ==> 195 (marker-buffer foo) ;; ==> #<buffer *scratch*>位置就是一个整数,而标记在一般情况下都是以整数的形式使用,所以很多接受整数运算的函数也可以接受标记为参数。比如加减乘。(goto-char (+ (marker-position foo) 10)例如上面的代码我们移动光标到第205个字符的位置。和缓冲区相关的变量,有的可以用变量得到,比如缓冲区关联的文件名,有的只能用函数来得到,比如 point。point 是一个特殊的缓冲区位置,许多命令在这个位置进行文本插入。每个缓冲区都有一个 point 值,它总是比函数 point-min 大,比另一个函数 point-max 返回值小。注意,point-min 的返回值不一定是 1,point-max 的返回值也不定是比缓冲区大小函数 buffer-size 的返回值大 1 的数,因为 emacs 可以把一个缓冲区缩小(narrow)到一个区域,这时 point-min 和 point-max 返回值就是这个区域的起点和终点位置。所以要得到 point 的范围,只能用这两个函数,而不能用 1 和 buffer-size 函数。按单个字符位置来移动的函数主要使用 goto-char 和 forward-char、backward-char。前者是按缓冲区的绝对位置移动,而后者是按 point 的偏移位置移动比如(goto-char (point-min)) ; 跳到缓冲区开始位置 (forward-char 10) ; 向前移动 10 个字符 (forward-char -10) ; 向后移动 10 个字符 (backward-char 10) ; 向后移动 10 个字符 (backward-char -10) ; 向前移动 10 个字符按词移动使用 forward-word 和 backward-word。至于什么是词,这就要看语法表格的定义了。按行移动使用 forward-line。没有 backward-line。forward-line 每次移动都是移动到行首的。所以,如果要移动到当前行的行首,使用 (forward-line 0)。如果不想移动就得到行首和行尾的位置,可以用 line-beginning-position 和 line-end-position。得到当前行的行号可以用 line-number-at-pos。需要注意的是这个行号是从当前状态下的行号,如果使用 narrow-to-region 或者用 widen 之后都有可能改变行号。由于 point 只能在 point-min 和 point-max 之间,所以 point 位置测试有时是很重要的,特别是在循环条件测试里。常用的测试函数是 bobp(beginning of buffer predicate)和 eobp(end of buffer predicate)。对于行位置测试使用 bolp(beginning of line predicate)和 eolp(end of line predicate)缓冲区的内容要得到整个缓冲区的文本,可以用 buffer-string 函数。如果只要一个区间的文本,使用 buffer-substring 函数。point 附近的字符可以用 char-after 和 char-before 得到。point 处的词可以用 current-word 得到,其它类型的文本,比如符号,数字,S 表达式等等,可以用 thing-at-point 函数得到。ting-at-point 可以获取光标处的很多类型的内容,它需要传入一个符号作为类型,例如 'word、'symbol、'url。不同的类型包含有不同的文本属性,第二个参数表示是否去除文本属性。如果当前位置有内容,则返回内容,否则返回nil(defun show-current-word () (interactive) (let ((word (thing-at-point 'word t))) ;; 'word 类型,t 表示去除文本属性 (if word (message "当前单词: %s" word) (message "光标位置没有单词"))))修改缓冲区的内容要修改缓冲区的内容,最常见的就是插入、删除、查找、替换了。下面就分别介绍这几种操作。插入文本最常用的命令是 insert。它可以插入一个或者多个字符串到当前缓冲区的 point 后。也可以用 insert-char 插入单个字符。插入另一个缓冲区的一个区域使用 insert-buffer-substring。删除一个或多个字符使用 delete-char 或 delete-backward-char。删除一个区间使用 delete-region。如果既要删除一个区间又要得到这部分的内容使用 delete-and-extract-region,它返回包含被删除部分的字符串。最常用的查找函数是 re-search-forward 和 re-search-backward。这两个函数参数如下(re-search-forward REGEXP &optional BOUND NOERROR COUNT) (re-search-backward REGEXP &optional BOUND NOERROR COUNT)其中 BOUND 指定查找的范围,默认是 point-max(对于 re-search-forward)或 point-min(对于 re-search-backward),NOERROR 是当查找失败后是否要产生一个错误,一般来说在 elisp 里都是自己进行错误处理,所以这个一般设置为 t,这样在查找成功后返回区配的位置,失败后会返回 nil。COUNT 是指定查找匹配的次数。替换一般都是在查找之后进行,也是使用 replace-match 函数。和字符串的替换不同的是不需要指定替换的对象了。
2025年02月21日
48 阅读
0 评论
0 点赞
2025-02-19
Emacs折腾日记(十三)——函数、宏以及命令
之前在开篇介绍简单的elisp时候就提到过函数,后面的一些示例中也用到了一些函数,但是都是一些基本的概念,这篇将深入了解函数的一些特性。首先要判断一个符号是否是函数,可以使用 functionp 来判断。(defun foo() 1) (foo) (functionp 'foo) ;; ==> t (setq var 1) (functionp 'var) ;; ==> nil不光函数,以下几种functionp也返回t函数。这里的函数特指用 lisp 写的函数。原子函数(primitive)。用 C 写的函数,比如 car、append。lambda 表达式特殊表达式宏(macro)。宏是用 lisp 写的一种结构,它可以把一种 lisp 表达式转换成等价的另一个表达式。命令。命令能用 command-execute 调用。函数也可以是命令。参数列表的语法过去我们的所有函数都是定参的函数,也就是说是确定了参数个数的函数。但是实际使用中会大量使用不定参函数,也就是参数不确定的函数。在C/C++ 以及 Python中会大量使用。它的一个使用场景就是某些时候不传就采用默认值,否则就采用用户定义的值。另一个场景就是像printf这样事先无法确定到底要输出多少内容。elisp中的函数完整定义如下(defun func (REQUIRED-VARS... [&optional OPTIONAL-VARS...] [&rest REST-VAR]))前面是确定的参数列表,也就是说前面的参数在调用函数时必须传入,而&optional 之后是可选参数,如果要传入可选参数这个 &optional 关键字是必须写上的。这里的可选参数也是需要在定义时一个个的指定出来,但是&rest 之后定义的只用一个变量来使用,在传入的时候可以传入任意个参数。例如下面的例子(defun foo (var1 var2 &optional op1 op2 &rest rest) (list var1 var2 op1 op2 rest)) (foo 1 2) ;; ==> (1 2 nil nil nil) (foo 1 2 3) ;; ==> (1 2 3 nil nil) (foo 1 2 3 4) ;; ==> (1 2 3 4 nil) (foo 1 2 3 4 5) ;; ==> (1 2 3 4 (5)) (foo 1 2 3 4 5 6) ;; ==> (1 2 3 4 (5 6))从这个例子我们可以得出以下几个结论:当可选参数没有提供时,在函数体里,对应的参数值都是 nil。我们可以通过判断是否为nil来判断用户是否传了参&rest 要捕获后面所有传入的参数,所以它必须在参数列表的最后面,它的值是一个list当 &rest 与 &optional 共存时,优先匹配 &optional 参数,最后如果有剩余的参数则分配给 &rest教程原文中是有关于文档字符串的描述的。但是我想现在我作为一个菜鸟,将来要组织自己的配置也主要依靠拷贝粘贴别人现有的东西再组合,没有多少机会参与那种高大上的开源项目,自己将来弄的配置估计也没什么人用,而且我也会详细记录自己攒配置的过程,所以这里就不需要给函数写过于详细的文档说明。这里我就跳过这块了。如果有读者对这块感兴趣可以看原文。函数调用在编写程序的时候会有这种需求,一个框架负责处理大块的内容,比如数据解析、转发等等,它会预留一些接口来让用户在此基础之上处理自己的业务逻辑,比较典型的就是http server,或者gui程序框架。在C/C++ 中一般会预留一些函数指针类型的参数进行回调或者提供接口供使用方重载实现自己的逻辑。在elisp中也有这样的操作,但是它就没有虚函数、虚基类或者函数指针的概念。在elisp中通过符号调用一个函数使用的方法是 funcall 和 apply。它们都是通过符号来调用函数的,唯一的区别在于如何处理传入参数。我们通过一个例子来看看它们有什么不同。我们还是用上面定义的foo函数来测试(funcall 'foo 1 2 3 4 5 6) ;; ==> (1 2 3 4 (5 6)) (apply 'foo 1 2 3 4 5 6) ;; ==> error (apply 'foo 1 2 3 4 '(5 6)) ;; ==> (1 2 3 4 (5 6)) (apply 'foo 1 2) ;; ==> error (apply 'foo '(1 2)) ;; ==> (1 2 nil nil nil) (apply 'foo 1 2 3 4) ;; ==> error (apply 'foo 1 2 '(3 4)) ;; ==> (1 2 3 4 nil) (apply 'foo 1 2 3 4 5) ;; ==> error (apply 'foo 1 2 3 4 '(5)) ;; ==> (1 2 3 4 (5))从上面的结果可以看出,funcall 直接按照对应函数定义的参数列表进行传参即可。而apply在传参的时候最后一个参数必须是list,并且在嗲用时会自动将list参数给展开并传入各个参数。从上面的区别可以看出,如果在调用函数的时候,参数已经通过list进行了组织的话,那么使用apply更为合适,否则使用funcall。宏宏是lisp家族中一个非常重要,也非常灵活的内容,可以说宏是lisp的灵魂。之前在看到一些lisp相关的教程时都说,宏实现了利用代码生成代码,并且因为宏的存在导致lisp中扩展出了大量的方言。可以说没有宏,lisp就不是lisp了,或者说lisp就没这么灵活了。但是C/C++中也有宏的概念,C/C++中的宏是在预处理阶段进行简单的文本替换,然后编译替换之后的结果。虽然利用宏,C/C++中可以实现很多非常复杂的功能,但是它远远没有lisp的宏灵活。要详细了解宏的相关内容,我们先回忆一下之前介绍的elisp的知识。首先elisp或者lisp的代码本身就是一颗语法树。它被写作一个list。也就是说list既可以作为代码执行,也可以作为数据,例如 (setq x 1) 它是一段代码。而 '(setq x 1) 它是一个列表,列表中有3个元素,分别是 setq 、x、1 这么三个符号和数字。再者elisp特有的符号系统,例如 x 表示一个变量,可以对它进行求值,'x 代表一个符号,根据前面所学的,我们可以通过符号找到符号中记录的值、函数、属性等等。基于这两个内容,我们可以通过操作list来实现生成一段代码。例如下列的例子(defun my-inc (var) (list 'setq var (list '1+ var))) (setq x 0) (eval (my-inc 'x)) ;; ==> 1上面的其实就是返回了一个list, (setq var (1+ var))。后面我们通过 eval 来执行这个返回的list。需要注意的时,函数调用时会首先将变量进行求值,然后将值作为参数传入,但是这里我们希望并不希望传入一个具体的值,而是希望他能操作我们传入的变量值,并改变它,要做到这点需要传入一个符号。这里有点像C++ 中的引用传递定义宏其实跟定义函数非常相似。我们只需要将关键字由 defun 改为 defmacro。(defmacro my-inc(var) (list 'setq var (list '1+ var))) (setq x 0) (my-inc x) ;; ==> 1我们发现宏与函数的一个不同点,函数中代码在函数被调用时执行,并且参数是在调用时进行求值并传入。而宏调用时需要展开它返回的表达式(或者这里直接就是一个list)。然后将参数作为符号传入。宏最后需要返回一段可执行的list数据,如果没有返回,会影响展开执行,最终可能会报错,例如下面的例子(defmacro my-inc (var) (setq var (1+ var))) (setq x 0) (my-inc x) ;; ==> error这里的问题在于这个宏定义的代码是一个直接执行的代码,并不是一个list,所以在调用它的时候会直接执行,但是又需要将参数作为符号绑定,所以它在被调用的时候会执行(setq 'x (1+ 'x)) 这段代码,而这里的x是一个符号,无法直接对符号进行赋值,所以它会报x的类型错误。这里已经显示出了,elisp中的宏与C/C++中宏的不同。首先C/C++中的宏只是简单的字符串替换,可以将它理解为它生成了新的C/C++源码的代码,它在预处理阶段来执行代码的替换。而elisp中并没有简单的进行替换,根据之前介绍lisp表达式的解析,其实宏返回的是一颗抽象语法树。在扩展宏的时候不断的进行抽象语法树的修改和重建,最后在执行的时候将传入的参数作为符号放入到这颗树中的对应节点。我们可以使用 macroexpand 来查看宏展开的样子。(defmacro bad-inc (var) (setq var (1+ var))) (macroexpand '(bad-inc 0)) ;; ==> 1我们发现之前错误的实现并没有生成可执行的代码,而是直接返回一个常数。因为宏中的代码首先在展开的时候就已经执行了。相当于返回了 setq var (1+ 0) 的值,也就是1。(defmacro my-inc (var) (list 'setq var (list '1+ var))) (macroexpand '(my-inc x)) ;; ==> (setq x (1+ x))使用 macroexpand 可以使宏的编写变得容易一些。但是如果不能进行 debug 是很不方便的。在宏定义里可以引入 declare 表达式,它可以增加一些信息。目前只支持两类声明:debug 和 indent。debug 可选择的类型很多,具体参考 info elisp - Edebug 一章,一般情况下用 t 就足够了。indent 的类型比较简单,它可以使用这样几种类型:nil 也就是一般的方式缩进defun 类似 def 的结构,把第二行作为主体,对主体里的表达式使用同样的缩进整数 表示从第 n 个表达式后作为主体。比如 if 设置为 2,而 when 设置为 1符号 这个是最坏情况,你要写一个函数自己处理缩进。从前面的例子就可以看到,如果在定义宏的时候使用list cons 等来构建list是非常麻烦的,一旦要构造非常复杂的程序,可能直接就歇菜了。为了方便,elisp中提供了一些符号来简化操作。 ` 读作backquote,表示被它包裹的表达式都是quote,可以理解为它里面的直接构建了一个list如果希望它里面的某个位置不作为quote的一部分,而是直接作为列表的元素,可以使用 , , 也就是它会对后面的内容进行求值如果要让一个列表作为整个列表的一部分(slice),可以用 ",@",它会将后面的内容作为列表参数依次添加到当前列表中。我想起来了之前接触过的quote,也就是 ' 。它表示后面的内容不进行求值,作为符号,虽然它也可以构造一个list,但是二者还是有些不同,例如'(list x (+ 1 2)) ;; ==> (list x (+ 1 2)) `(list x (+ 1 2)) ;; ==> (list x (+ 1 2)) '(list x ,(+ 1 2)) ;; ==> (list x (\, (+ 1 2))) `(list x ,(+ 1 2)) ;; ==> (list x 3) (setq var '(2 3)) '(list x ,@var) ;; ==> (list x (\,@ var)) `(list x ,@var) ;; ==> (list x 2 3)我们使用上面的方法稍微弄一个复杂一点的宏(defmacro max(a b) `(if (> ,a ,b) ,a ,b)) (max 4 5) (max (1+ 2) (+ 3 6)) '(macroexpand '(max (1+ 2) (+ 3 6))) ;; ==> (if (> (1+ 2) (+ 3 6)) (1+ 2) (+ 3 6))这里是一个经典的C/C++ 中的max宏。虽然实现不严谨,有一些副作用,但是可以从上面看到一些用法。首先使用 ` 表示返回一个列表,以供调用的时候进行展开。再者对于传入的a和b需要使用, 来表示需要求解它们的值,实现参数的绑定,否则将会得到一个错误,例如(defmacro max(a b) `(if (> a b) a b)) (macroexpand '(max 4 5))) ;; ==> (if (> a b) a + b)如果将上述的 , 全部替换成 ,@ 就不太合适了,因为 ,@ 是将列表中的值取出来组成新的列表,并不会想 , 那样进行求值。例如(defmacro max(a b) `(if (> ,@a ,@b) ,@a ,@b)) (macroexpand '(max (1+ 2) (+ 3 6))) ;; ==> (if (> 1+ 2 + 3 6) 1+ 2 + 3 6)命令emacs 运行时就是处于一个命令循环中,不断从用户那得到按键序列,然后调用对应命令来执行。emacs 中的命令可以说就是一个函数,它是一个特殊的函数,是里面包含了 interactive 表达式的函数。这个表达式指明了这个命令的参数。比如下面这个命令(defun say-hello (name) (interactive "swhat's your name:") (message "hello, %s" name))当解释器加载了该函数之后就可以使用 M-x 来调用这个函数。我们根据提示输入一个名字,emacs会在minibuffer中输出一段话。我们发现,在interactive 表达式后面跟的字符串前面多了一个 s 字符。我们可以通过这个多加的字符来控制命令参数的类型和行为,例如使用 s 表示字符串参数,n 表示数字参数,f 代表文件,r 代表区域。interactive 的字符十分复杂,而且繁多。用的时候看 interactive 函数的文档还是很有必要的。但是不是所有时候都参数类型都能使用代码字符,而且一个好的命令,应该尽可能的让提供默认参数以让用户少花时间在输入参数上,这时,就有可能要自己定制参数。首先学习和代码字符等价的几个函数。s 对应的函数是 read-string,n 代表的是 read-file,f代表的是 read-file-name。其实大部分代码字符都是有这样对应的函数或替换的方法。我们可以使用这些方法来替代前面的代码字符,假如传入的是一个表达式,那么对表达式进行计算之后返回的列表元素就是命令的参数,例如我们用 read-string 来代替之前例子中的s。(defun say-hello (name) (interactive (list (read-string "what's your name: "))) (message "hello, %s" name))教程 中还列举了一些常见的字符代表的函数,这里我就不列出来了。各位读者有兴趣的话也可以去看看。
2025年02月19日
70 阅读
0 评论
0 点赞
1
2
...
9