开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

326k star🔥原来国内的独立开发者都在做这些事情

发表于 2024-04-26

☀️ 前言

  • 大家好,我小卢,你是否每天都在重复做着 CRUD 的工作,你是否每天下班后就瘫在家里玩起游戏、 刷起视频,那你应该了解一下国内的独立开发者们都在做些什么。
  • 有的人在做百词斩助手

  • 有的人在做AI 视频生成

  • 有的人在做实时金价网站

  • 这些都是在这个开源项目 chinese-independent-developer 找到的。
  • github地址:github.com/1c7/chinese…
  • 我在编辑这篇文章的时候这个仓库已经达到了足足32.6k star,目前还在稳定提升。

🚄 项目介绍

  • chinese-independent-developer 聚合所有中国独立开发者的项目,每个项目都有具体时间和开发状态,都标记的清清楚楚,有哪些还在 开发,有哪些已经 停止维护标记的清清楚楚。

  • 这个列表分为两个面板:程序员版和主面板。这两个版本,一个面向程序员,一个面向普通用户,各自承载着不同的功能和使命。

主面板

  • 主面板是给普通用户用的,打开即用,无需命令行或编程基础,你可以找到网站、App、桌面端应用等,它们都是打开即用的。这个版面的存在,让非程序员用户也能轻松享受到独立开发者的创意成果。
  • 这里有大量国内程序员制作的产品,比如AI 动漫生成器、AI 3D 表情生成器、AI 宝可梦生成器, 实时金价 OCR工具,网页转图片,AI视频生成器等等有趣的产品,如果你对独立开发感兴趣那肯定得了解一下。

程序员面板

  • 这个版面呢主要是方便技术人员去查看开源过的github地址,用户需要懂得使用命令行,了解如何进行 npm install 等基本操作操作。这里展示的是开源博客、命令行工具等,是专为技术人员准备的列表,如果你对哪个技术感兴趣直接点进去就可以看到源码。
  • 这个面板的初衷是:Issue 和 PR 里偶尔有人提交一些不错的东西,但打开一看,不是普通用户能用的东西,但是作者觉得这些项目也需要曝光度,所以单独开这一个列表。
  • 比如 TTS 文字转语音,短视频去水印等等功能在这里都可以找到

👋 写在最后

  • chinese-independent-developer 不仅仅是一个项目列表,它是一个创意的汇聚地,每个独立开发者的一些小巧思和大胆的想法都存在这里。
  • 如果你还在为不知道做什么而烦恼时,来这里看看,或许对你有帮助。
  • 如果您觉得这篇文章有帮助到您的的话不妨🍉🍉关注+点赞+收藏+评论+转发🍉🍉支持一下哟~~😛您的支持就是我更新的最大动力。
  • 如果想跟我一起讨论和学习更多的前端知识可以加入我的前端交流学习群,大家一起畅谈天下~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

ReactNative 开发之旅 项目配置与版本管理实践

发表于 2024-04-26

同名公众号:小武码码码

在曾经的 ReactNative 多个开发项目中,经常需要同时处理多个项目,这些项目使用的 ReactNative 版本各不相同。随着项目数量的增多,我发现手动切换和管理不同版本的 ReactNative 变得越来越困难。环境配置冲突、版本不兼容等问题时常发生,严重影响了开发效率。

为了解决这些问题,我开始探索如何更高效地管理 ReactNative 开发环境和项目依赖。下面我将详细分享我的实践经验,希望对大家有所帮助。

  1. ReactNative 开发环境配置

1.1 全局安装 ReactNative CLI

首先,我们需要安装 ReactNative 的命令行工具,即 ReactNative CLI。建议使用 npx 来运行,避免全局安装带来的版本冲突问题。

1
csharp复制代码npx react-native init AwesomeProject

1.2 配置 Android 开发环境

ReactNative 开发 Android 应用需要配置 Java、Android SDK 等环境。具体步骤如下:

  1. 安装 JDK8 或更高版本,并配置 JAVA_HOME 环境变量。
  2. 安装 Android Studio,并在 SDK Manager 中安装对应版本的 Android SDK。
  3. 配置 ANDROID_HOME 环境变量,指向 Android SDK 目录。
  4. 将 Android SDK 的 platform-tools 目录添加到 PATH 环境变量中。

1.3 配置 iOS 开发环境

如果你使用 macOS 开发 iOS 应用,则需要安装 Xcode 和 CocoaPods:

  1. 从 App Store 安装最新版本的 Xcode。
  2. 使用以下命令安装 CocoaPods:
1
复制代码sudo gem install cocoapods

1.4 常见问题与解决方案

在配置开发环境时,我遇到了一些常见问题,例如:

  1. Android SDK 版本不兼容: 检查 ReactNative 项目的 android/build.gradle 文件,确保 compileSdkVersion 和 targetSdkVersion 与已安装的 SDK 版本一致。
  2. iOS 依赖安装失败: 检查网络连接,或尝试使用 VPN。必要时可以手动下载相关依赖包并放置到正确的目录中。
  1. ReactNative 多版本管理

随着项目数量增多,不同项目使用不同版本的 ReactNative 变得非常普遍。为了方便切换和管理不同版本,我使用了 rnvm 这个工具。

2.1 安装 rnvm

使用 npm 全局安装 rnvm:

1
复制代码npm install -g rnvm

2.2 管理 ReactNative 版本

使用 rnvm 可以方便地安装和切换不同版本的 ReactNative。常用命令如下:

  1. 列出所有可用的 ReactNative 版本:
1
复制代码rnvm list
  1. 安装指定版本的 ReactNative:
1
复制代码rnvm install 0.66.4
  1. 在不同版本之间切换:
1
perl复制代码rnvm use 0.66.4

2.3 项目级的版本控制

除了全局管理 ReactNative 版本外,我们还可以在项目级别指定所需的版本。具体做法是在项目根目录下创建一个 .rnvmrc 文件,并写入所需的 ReactNative 版本号:

1
复制代码0.66.4

这样,当我们进入该项目目录时,rnvm 会自动切换到指定的 ReactNative 版本。

  1. 实践收益

通过上述的环境配置和版本管理实践,我的 ReactNative 开发效率得到了显著提升:

  1. 统一的开发环境配置流程,避免了环境问题导致的开发阻塞。
  2. 使用 rnvm 管理多个 ReactNative 版本,切换版本更加方便,不同项目之间的依赖冲突也得到了解决。
  3. 项目级的版本控制,确保团队成员使用相同的 ReactNative 版本,减少了协作中的问题。
  1. 总结

ReactNative 开发中,合理的环境配置和灵活的版本管理是提高效率的关键。通过掌握 ReactNative CLI、rnvm 等工具的使用,并建立规范的配置流程,我们可以更从容地应对多项目、多版本的开发场景。

希望我的实践经验能给大家带来一些启发。如果你有其他更好的实践方式,欢迎留言交流!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

解密Prompt系列28 LLM Agent之金融领域摸索

发表于 2024-04-26

本章介绍金融领域大模型智能体,并梳理金融LLM的相关资源。金融领域的大模型智能体当前集中在个股交易决策这个相对简单的场景,不需要考虑多资产组合的复杂场景。交易决策被简化成市场上各个信息,包括技术面,消息面,基本面等等在不同市场情况下,对资产价格变动正负面影响的综合判断。

而使用大模型智能体最显著的优势,在于对海量信息的高效处理,存储,以及对相关历史信息的联想。不要和Agent比知识广度和工作效率这一点已经是普遍共识。 下面我们看下这两篇论文使用了金融市场的哪些信息,分别是如何处理,思考并形成交易决策的。

FinMEM

  • FINMEM: A PERFORMANCE-ENHANCED LLM TRADING AGENT WITH LAYERED MEMORY AND CHARACTER DESIGN
  • github.com/pipiku915/F…

FinMeM是使用文本模态信息,通过差异化召回不同时效性,重要性,相关性的不同金融市场信息,通过微调让模型学习个股交易决策型Agent

  1. Profiling

FinMem的Profile是交易Agent的全局指令类似system-prompt ,包含两个部分

  • 金融市场专家知识:包括个股的基本信息例如行业,公司信息,历史股价走势等等
  • 3种不同的风险偏好:保守,中性和激进,通过不同prompt,影响模型在不同信息下的交易决策
    以及论文提出可以动态转换风险偏好,例如开始亏损的时候可以转换成保守策略,哈哈虽然感觉这难道不是散户亏钱的常规逻辑,赚钱了激进,亏损了就保守~

image

  1. Memory

image

虽然是叫记忆模块,但其实包含了信息收集入库,召回总结和反思的全部流程

  • Layered Long Term Memory:不同时效性的内容存储
  • Working Memory:多路内容召回,排序,总结和反思
    • Summarization
    • Observation
    • Reflection

2.1 Layered Long Term Memory

上图中的Bottom部分就是Memroy真正的存储部分。这里论文只使用了金融市场里三种数据源

  • 短时效(shallow Layer):市场新闻
  • 中时效(Intermediate Layer):公司季报
  • 长时效性(Deep Layer):公司年报

在真实场景中其实划分远远不只这些,时效性从长到短还有例如研报,宏观数据,行业数据,路演会议,机构调研,公告,政策,快讯,各种市场面技术指标等等,复杂程度要高的多。因此个人感觉直接按照数据源来定义时效性可能更合适,毕竟不同数据源之间的时效性几乎都是不同的。

在召回以上不同时效性的内容时的打分排序策略借鉴了斯坦福小镇里面对于智能体相关记忆的召回策略(不熟悉的同学看这里LLM Agent之只有智能体的世界)。核心就是只基于相关性来进行记忆召回在时效性敏感的领域中是不可以的。因此召回内容的排序会基于多个因子进行打分,这里其实传统搜广推借助显式的反馈,排序做的更加复杂。这里因为相对缺乏显式的直接反馈,所以只用了相关性,新鲜度,重要性这三个打分维度进行加总。

image

其中新鲜度的计算会依赖于上面的时效性分层,不同分层的金融数据,会有不同的新鲜度计算公式。核心就是时效性更长,该信息对金融市场资产价格的影响持续时间更长,信息的时间敏感度更小,因此在计算新鲜度的时候它的指数衰减更慢。例如快讯可能当日就会时效,而年报效应可能持续好几个月。论文分别用了2周,季度和一年作为指数衰减的系数。

相关性这里用了text-embedding-ada-002计算cosine距离。而重要性论文同样做了不同系数的时间衰减,但系数vlv_lvl按不同分布随机采样的操作属实是没看懂,但是哈哈这些都是细节不重要,咱重点看框架,看框架~

2.2 Working Memory

**有了分层的信息存储,下一步就是在每一天进行交易决策的时候,进行一系列的信息召回、总结、分析的流程了。**这里我们使用论文中的例子在2023-01-24日交易TSLA,inquery=”Can you make an investment decision on TSLA on 1/24/2023”,之后的流程如下

  • summary:信息摘要和情绪分类

摘要会先基于inquery使用上面的打分排序逻辑,去存储中召回相关信息,并基于以下Prompt,对每条信息进行总结。输出是新闻总结和新闻对资产的[正面,负面,中性]的概率,概率加和为1,这里可以直接取logprobs进行归一化。单个人感觉直接使用概率最高的标签,同时丢弃熵值较高的新闻可能噪声更低一些。

image

  • Observation:市场观测和动量指标

这里论文只使用了个股的动量因子作为市场观测,也就是连续N天的累计涨跌幅。训练样本会给出未来一日动量,目的是让模型学习什么样的消息面情绪会带来未来的价格变动,而测试样本是历史3天的动量,目标是让模型预测未来的价格变动。个人感觉这里训练和测试其实应该对齐,也就是训练样本也提供历史3天的动量因子。这样在后面的反思中也会使用技术指标。

image

  • Reflection:反思

反思分成两个部分:

  • 及时思考:基于上面的summray和observation,给出交易建议(Buy,sell,hold),交易原因,和以上summary中具体哪条信息(ID)影响了模型决策。
  • 延伸思考:基于最近M天的及时思考,决策后的收益,和股价走势,但我似乎没找到这部分的具体prompt…..延伸反思的结果会存到Deep layer用于后面的交易决策

image

3. Decision

最终的交易决策会基于当前大模型的profile,Top-K的信息召回,历史累计收益,和延伸思考最终给出交易决策(Buy,SELL,HOLD)。而所谓的训练阶段,其实是依赖真实的资产价格变动,得到更准确的延伸思考(对交易决策的反思),在测试阶段可以使用训练阶段存储的历史思考结果。这部分感觉FinAgent的逻辑更清晰些,看迷糊的朋友可以看后面FinAgent的流程。

image

FinAgent

  • A Multimodal Foundation Agent for Financial Trading:
    Tool-Augmented, Diversified, and Generalist
  • 只有论文无开源代码

FinAgent是加入了图片模态信息的个股交易决策型Agent,主要包括以下几个模块

  • Market Intelligence:市场信息收集和情绪识别汇总模块
  • Reflection:价格异动归因和历史交易决策归因模块
  • Decision Making:买卖交易决策模块
  • Memeory:用于存储以上模块的历史信息

image.png

FinAgent一定程度上参考了FinMeM,思路有些相似,不过Prompt输入输出的结构更加清晰和贴近真实市场,几个核心的差异点如下

  • FinAgent无需微调,直接使用GPT4和GPT4V
  • 数据差异
    • 加入了图片模态的信息,包括K线图和历史交易图
    • 加入了更多的技术面指标相关交易策略
  • 在时效性处理上,FinAgent使用大模型来对每个信息源的时效性进行判断
  • 在召回排序上,FinAgent没有使用更复杂的打分排序,而是使用query改写,召回当前数据相关的历史数据,更遵从金融市场上历史会重演的基本逻辑。
  1. Market Intelligence

MI模块是特定金融主体的信息收集,分析总结和情绪识别模块(类似FinMeM的summary,但加入了和历史信息的联动)。它分成了当日市场信息(Latest MI),和历史市场信息(Past MI)两个部分。前者反映了最新的资产异动,后者利用金融市场中历史会不断重演的特性。例如上次苹果产品发布,对APPL股价带来5%的提振,那如果今日舆情显示苹果又有新产品发布,那我们可能会预期有相似的正向影响。

首先是当日市场信息,主要做以下几件事情

  1. 获取资产当日(T)的资产价格,新闻舆情(通过FMPAPI接口)信息,拼接后作为大模型的上文,填充到下面latest_market_intelligence
  2. Analysis:基于系统指令和任务描述,对以上所有信息进行COT分析,论文通过prompt给出了分析方式,简化成每条信息对资产价格的正面/负面/中性影响,以及短/中/长的影响时效性。
  3. Summary:对以上Analysis的分析结果进行总结,只保留核心投资观点,并对上面相似的正负面和时效性进行聚合。
  4. Query生成:基于以上的总结内容,生成短/中/长不同时效性的检索query(关键词),用于搜索该资产的历史信息。

这里论文使用了XML格式的prompt来承载各类信息,整个Lastest MI的Prompt如下,其中iframe会填充具体的系统指令,任务描述,如何分析资产正负面影响,如何进行分析总结和query生成,以及输出格式。iframe的具体内容实在是太长了,大家直接看论文附录G

image

其次是历史市场信息部分,主要做以下几件事情

  1. 基于上面的Query,去搜索该资产历史(<T)不同时间窗口的各类信息,包括但不限于:不同周期的资产价格变化,新闻舆情,研究报告等等
  2. 和上面当日市场信息相同,对信息进行分析和总结,不过不需要生成query了。

论文没有提供具体的数据,只给了大致的分析结果如下

image

  1. Reflection

收集完信息,就进入了反思模块,也分成了两个阶段low-level和high-level。前者基于上面的Market Intelligence提供的舆情正负面影响和股价变动,对短/中/长期股价异动进行归因(类似FinMeM的及时分析)。后者基于Market Intelligence,历史和当前的low-level的反思,对交易决策进行归因(类似FinMeM的延伸思考)。论文只提供了简化后的效果如下

image

下面我们具体说下Low-Level和High-Level的输入输出

2.1 Low-Level Reflection

low-level的反思prompt的构成如下

1. 输入

  • 以上Market Intelligence的总结,包括历史和当前舆情对资产正负面的影响
  • 多模态K线图,以下是用来帮助模型理解K线图的Prompt指令

image

* 短/中/长期的价格走势变动描述,描述模板如下:

image

2. 输出

  • reason:分别对短/中/长期的资产价格变动进行归因,原因可以是Momentum等技术面原因,也可以是Market Intelligence中舆情带来的消息面原因
  • summary:对以上的分析进行总结,作为后面High-Level的输入
  • query:和上面Market Intelligence相同,生成召回Query,用于召回Memory中的相关历史Low-Level Reflection。

因为涉及到了K线图的多模态理解,这里论文使用了GPT-4V来完成以上prompt指令。具体Prompt指令,详见附录G。

2.2 High-Level Reflection

High Level的反思prompt构建如下

1. 输入

  • Market Intelligence: 同Low Level
  • Low Level Relection:包括最新的Low-Level反思,和使用以上Low-Level query召回的历史Low-Level对于该资产价格变动的反思
  • 多模态TradingChart,以下是用来帮助模型理解图片的prompt指令:交易图包括价格趋势图,和历史买入和卖出的时间点。

image

2. 输出

  • reasoing:基于MI,Low-Level,价格变动,分析历史每一笔交易决策是否正确(带来收益),以及在各个时间点,影响交易决策因素有哪些,权重如何
  • improvement: 如果存在错误的交易决策,应该如何改进,并给出新的买卖时间点,例如 (2023-01-03: HOLD to BUY)
  • summary: 对以上分析进行总结
  • query:同样生成query用于召回历史high-level
  1. Decision-making

最后的决策模块,基于以上三个模块的输入和额外的技术指标,分析师观点等补充信息,进行交易决策。Prompt构建如下

1. 输入

  • Market Intelligence
  • Low Level Reflection
  • High Level Reflection: 包括最新的交易反思和历史的交易反思
  • Augmented Tools:这里论文使用工具补充获取了以下信息
    • Expert guidance:论文似乎未说明专家信息来源,只说是类似文章的数据源。猜测可能是买方买方观点,例如XX股买入推荐一类
    • strategy:传统技术面交易策略,类似啥MACD穿越,KDJ金叉,这里论文通过工具调用获取证券的MACH,KDJ和均值回归等技术指标,指标相关描述如下

image

* prompt: 告诉模型如何利用以上的各个输入信息,来给出交易决策。
image

2. 输出

  • analysis:step-by-step的分析以上各个信息输入的综合影响
  • reasoning:针对以上分析给出买卖操作的原因
  • action:基于分析原因给出交易行为,BUY,SELL,HOLD

image

4. 效果

最后说下效果评估,这里论文使用个股交易的累计收益率作为评价指标,对比基于技术指标的规则交易,基于RL的方案,以及上面的FinMem,在年化收益率,夏普比率上均有显著提升,在最大回撤上基本持平。

image

同时论文进行了消融实验,对比只使用M(MI市场信息),只使用T(Tools买卖方观点+技术面),和加入反思,加入交易决策的效果。数据就比较有意思了,只使用M和T的效果竟然差不多,核心提升来自反思模块。不过这里结论和市场有很大关系,哈哈美国市场的实验结论没有直接迁移到A股的可能~

image

金融领域大模型资源梳理

金融大模型应用

  • Reportify: 金融领域公司公告,新闻,电话会的问答和摘要总结
  • Alpha派: kimi加持会议纪要 + 投研问答 +各类金融资讯综合的一站式平台
  • 况客FOF智能投顾:基金大模型应用,基金投顾,支持nl2sql类的数据查询,和基金信息对比查询等
  • ScopeChat:虚拟币应用,对话式组件交互和问答
  • AInvest:个股投资类检索增强问答,ChatBI金融数据分析做的有点厉害
  • HithinkGPT:同花顺发布金融大模型问财,覆盖查询,分析,对比,解读,预测等多个问题领域
  • FinChat.io:超全的个股数据,个股投资助手
  • TigerGPT: 老虎证券,GPT4做个股分析,财报分析,投资知识问答
  • ChatFund:韭圈儿发布的第一个基金大模型,看起来是做了多任务指令微调,和APP已有的数据功能进行了全方位的打通,从选基,到持仓分析等等
  • 无涯Infinity :星环科技发布的金融大模型
  • 曹植:达观发布金融大模型融合data2text等金融任务,赋能报告写作
  • 妙想: 东方财富自研金融大模型开放试用,但似乎申请一直未通过
  • 恒生LightGPT:金融领域继续预训练+插件化设计
  • bondGPT: GPT4在细分债券市场的应用开放申请中
  • IndexGPT:JPMorgan在研的生成式投资顾问
  • Alpha: ChatGPT加持的金融app,支持个股信息查询,资产分析诊断,财报汇总etc
  • Composer:量化策略和AI的结合,聊天式+拖拽式投资组合构建和回测
  • Finalle.ai: 实时金融数据流接入大模型

金融Agent论文

  • WeaverBird: Empowering Financial Decision-Making with Large Language Model, Knowledge Base, and Search Engine
  • FinGPT: Open-Source Financial Large Language Models
  • FinMem: A Performance-Enhanced LLM Trading Agent with Layered Memory and Character Design
  • AlphaFin:使用检索增强股票链框架对财务分析进行基准测试
  • A Multimodal Foundation Agent for Financial Trading: Tool-Augmented, Diversified, and Generalist
  • Can Large Language Models Beat Wall Street? Unveiling the Potential of AI in stock Selection

金融SFT论文

  • BloombergGPT: A Large Language Model for Finance
  • XuanYuan 2.0: A Large Chinese Financial Chat Model with Hundreds of Billions Parameters
  • FinVis-GPT: A Multimodal Large Language Model for Financial Chart Analysis
  • CFBenchmark: Chinese Financial Assistant Benchmark for Large Language Model
  • CFGPT: Chinese Financial Assistant with Large Language Model
  • InvestLM: A Large Language Model for Investment using Financial Domain Instruction Tuning
  • BBT-Fin: Comprehensive Construction of Chinese Financial Domain Pre-trained Language Model, Corpus and Benchmark
  • PIXIU: A Large Language Model, Instruction Data and Evaluation Benchmark for Finance
  • sota.jiqizhixin.com/project/dee…

想看更全的大模型相关论文梳理·微调及预训练数据和框架·AIGC应用,移步Github >> DecryPrompt

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

深入解析与应用 Nodejs 中的 QS 库

发表于 2024-04-26

构建现代 web 应用时,处理 HTTP 请求的查询字符串是一个常见且重要的任务。Node.js 中的 QS 库提供了一套功能丰富的 API,用于解析和构造 URL 中的查询字符串。在这篇文章中,我们将探讨如何使用 QS 库高效地管理查询字符串,并通过具体代码示例展现其强大的功能。

🛠️ 快速开始

首先,你需要在你的项目中安装 qs。通过以下命令可以轻松完成安装:

1
bash复制代码npm install qs

然后在你的项目文件中引入 qs 模块:

1
javascript复制代码const qs = require('qs');

📖 解析查询字符串

使用 qs.parse 方法可以轻松地将查询字符串转换为 JavaScript 对象。

1
2
3
4
5
6
7
8
9
10
11
javascript复制代码// 解析简单的查询字符串
const queryObj = qs.parse('key1=value1&key2=value2');
console.log(queryObj); // 输出: { key1: 'value1', key2: 'value2' }

// 解析包含数组的查询字符串
const queryArray = qs.parse('a[]=b&a[]=c');
console.log(queryArray); // 输出: { a: ['b', 'c'] }

// 解析包含嵌套对象的查询字符串
const queryNestedObj = qs.parse('user[name]=Tom&user[age]=25');
console.log(queryNestedObj); // 输出: { user: { name: 'Tom', age: '25' } }

🛠️ 构建查询字符串

qs.stringify 方法让我们能将对象序列化成查询字符串的形式。

1
2
3
4
5
6
7
8
9
10
11
javascript复制代码// 构建简单对象的查询字符串
const queryString1 = qs.stringify({ page: 1, limit: 10 });
console.log(queryString1); // 输出: 'page=1&limit=10'

// 构建包含数组的查询字符串
const queryString2 = qs.stringify({ tags: ['nodejs', 'qs'] });
console.log(queryString2); // 输出: 'tags[0]=nodejs&tags[1]=qs'

// 通过设置 arrayFormat 选项,改变数组在查询字符串中的格式
const queryStringWithBrackets = qs.stringify({ tags: ['nodejs', 'qs'] }, { arrayFormat: 'brackets' });
console.log(queryStringWithBrackets); // 输出: 'tags[]=nodejs&tags[]=qs'

🔍 高级参数解析

qs 提供了丰富的选项,以支持更复杂的解析场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
javascript复制代码// 解析获取忽略 URL 前缀 '?'
const queryWithPrefix = qs.parse('?a=1&b=2', { ignoreQueryPrefix: true });
console.log(queryWithPrefix); // 输出: { a: '1', b: '2' }

// 支持指定分隔符
const queryWithSemicolon = qs.parse('a=1;b=2', { delimiter: ';' });
console.log(queryWithSemicolon); // 输出: { a: '1', b: '2' }

// 支持复杂嵌套对象的深度自定义
const deepNested = qs.parse('a[b][c]=true', { depth: 0 });
console.log(deepNested); // 输出: { a: { '[b][c]': 'true' } }

// 支持ISO-8859-1字符集解析
const isoEncoded = qs.parse('a=%A7', { charset: 'iso-8859-1' });
console.log(isoEncoded); // 输出: { a: '§' }

🚀 高级字符串构建

创建自定义查询字符串,以适应各种后端 API 的不同需求。

1
2
3
4
5
6
7
8
9
10
javascript复制代码// 以不编码的形式构建查询字符串
const unencodedStr = qs.stringify({ a: { b: 'c' } }, { encode: false });
console.log(unencodedStr); // 输出: 'a[b]=c'

// 支持自定义序列化日期对象
const dateObj = new Date('2020-01-01');
const customDate = qs.stringify({ date: dateObj }, {
serializeDate: function (d) { return d.toISOString().split('T')[0]; }
});
console.log(customDate); // 输出: 'date=2020-01-01'

安全提示

在处理查询字符串时,始终考虑到安全性,以避免潜在的注入攻击。 qs 库默认对某些可能会覆盖原型属性的输入进行了限制。你可以通过 plainObjects 或 allowPrototypes 选项来进行自定义处理。

仓库地址:github.com/ljharb/qs

通过使用 qs库,我们可以有效地管理复杂的查询字符串,无论是为了发送 HTTP 请求还是处理接收到的请求参数。现在你已经准备好在你的 Node.js 应用中使用 qs 来提高查询字符串的处理效率了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MyBatis源码之MyBatis中SQL语句执行过程 My

发表于 2024-04-26

MyBatis源码之MyBatis中SQL语句执行过程

SQL执行入口

我们在使用MyBatis编程时有两种方式:

方式一代码如下:

1
2
java复制代码SqlSession sqlSession = sqlSessionFactory.openSession();
List<Student> studentList = sqlSession.selectList("com.sjdwz.dao.StudentMapper.findAll");

方式二代码如下:

1
2
3
java复制代码SqlSession sqlSession = sqlSessionFactory.openSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
List<Student> studentList = studentMapper.findAll();

方式一入口分析

方式一调用的是SqlSession接口的selectList方法:

截图

执行的是DefaultSqlSession中的实现

截图

经过多次重载后,调用的是此方法:

截图

MappedStatement封装的是我们的SQL语句;

方法内部执行的是executor的query方法。

方法二入口分析

方法二调用的是SqlSession接口的getMapper(Class type)方法:

截图

我们实际调用的是DefaultSqlSession实现类中的方法:

截图

截图

此方法内部又调用了

1
java复制代码 configuration.<T>getMapper(type, this);

截图

最后调用了:

截图

1
java复制代码return mapperProxyFactory.newInstance(sqlSession);

通过工厂创建了接口的代理对象。

Mapper接口动态代理

上面讲到会通过

1
java复制代码 mapperProxyFactory.newInstance(sqlSession);

来创建动态代理类;

那创建动态代理类会执行哪些方法呢?

截图

可以看到有MapperProxy这个类,实际上都会执行MapperProxy类中的invoke方法。

invoke方法

invoke方法会先判断方法是不是Object的方法,如果是,就直接调用;否则会执行cachedInvoker()方法

cachedInvoker()的作用是获取缓存中MapperMethodInvoker,如果没有则创建一个,而MapperMethodInvoker内部封装了MethodHandler。

cachedInvoker

当cacheInvoker返回了PalinMethodInvoker实例之后,紧接着调用了这个实例的PlainMethodInvoker#invoke方法

PlainMethodInvoker

然后就调用了execute()方法

execute方法

里面调用了sqlSession的方法。

方法的调用关系如下:

1
2
3
4
5
6
7
8
9
10
11
java复制代码MapperProxy#invoke()//代理对象执行的方法,代理后所有Mapper的方法调用时,都会调用这个invoke方法
MapperProxy#cachedInvoker()//代理对象里的Method也是对象,为了避免频繁new对象,在这里给Method对象加缓存
methodCache#computeIfAbsent()//从缓存中取Method对象,取不到创建之后加入缓存
//PainMethodInvoker是MapperProxy的内部类
PlainMethodInvoker#invoke()//执行方法,所有代理对象的方法都会执行同一个。代理对象本质是方法的拦截器!
MapperMethod#execute()//执行方法,最终还是在调用SqlSession接口
SqlSession#insert()
SqlSession#update()
SqlSession#delete()
SqlSession#selectOne()
SqlSession#selectList()

SQL执行流程

查询SQL执行流程

主要步骤:

  1. selectOne/selectList
  2. SQL获取
  3. 参数设置
  4. SQL执行
  5. 封装结果集

调用关系如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码DefaultSqlSession#selectOne()//执行单记录
DefaultSqlSession#selectList()//查询列表
CachingExecutor#query()
MappedStatement#getBoundSql()//获取SQL语句
CachingExecutor#query()//二级缓存查询
CachingExecutor.delegate = SimplyExecutor//装饰设计模式:Caching的对SimpleExecutor查询加二级缓存装饰
SimplyExecutor#query()
BaseExecutor#query()//子类执行查询直接调用父类方法,一级缓存
BaseExecutor#queryFromDatabase()//缓存没有则去查询数据库,一级缓存
SimplyExecutor#doQuery()//simple执行查询
SimplyExecutor#prepareStatement()//准备查询语句
RoutingStatementHandler#parameterize()//路由delegate=PreparedStatementHandler
PreparedStatementHandler#parameterize()//设置查询参数
DefaultParameterHandler#setParameters()//设置查询参数
RoutingStatementHandler#query()//路由delegate=PreparedStatementHandler
PreparedStatementHandler#query()//执行SQL,封装结果集
PreparedStatement#execute()//执行SQL查询操作
DefaultResultSetHandler#handleResultSets()//封装返回值,将查询结果封装成Object对象

增删改SQL执行流程

主要步骤

  1. insert|update|delete方法分析
  2. SQL获取
  3. 参数设置
  4. SQL执行
  5. 封装结果集

MyBatis中增删改的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码//DefaultSqlSession
@Override
public int insert(...) {
return update(statement, parameter);
}
@Override
public int update(String statement) {
return update(statement, null);
}
@Override
public int delete(...) {
return update(....);
}

我们发现,增删改最后执行的都是update,这是因为insert、update、delete实际上都是对数据库中数据的改变。

执行流程为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码DefaultSqlSession#update()
CachingExecutor#update()//执行更新
flushCacheIfRequired()//执行增删改前,清除对应二级缓存
CachingExecutor.delegate = SimplyExecutor//装饰模式
SimplyExecutor#update()//调用父类模板方法
BaseExecutor#update()//执行更新
BaseExecutor#clearLocalCache()//清除一级缓存,LocalCache
BaseExecutor#doUpdate()//调用子类方法
SimplyExecutor#doUpdate()//simple执行更新
SimplyExecutor#prepareStatement()//准备查询语句
RoutingStatementHandler#parameterize()//delegate=PreparedStatementHandler
PreparedStatementHandler#parameterize()//设置查询参数
DefaultParameterHandler#setParameters()//设置查询参数
RoutingStatementHandler#update()//delegate=PreparedStatementHandler
PreparedStatementHandler#update()
PreparedStatement#execute()//执行SQL,完成增删改查操作
reparedStatement#getUpdateCount()//获取影响行数

图示

最后我们画出执行的流程图如下:

执行流程图

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

FFmpeg开发——基础篇(二) 前言 环境准备 ffmpe

发表于 2024-04-26

前言

书接上回,我们比较详细的介绍了ffmpeg开发过程中会接触到的主要结构体,当然,其实还有AVFilter模块,但是对于初学者而言,忽略掉过滤器部分也无伤大雅,并不影响对于ffmpeg开发流程的主体的学习,而且AVFilter也不算是特别常用,在音视频开发中也有其他方式可以实现AVFilter的效果,因此暂时可以先忽略。

本文我们用一段相对完整,但是不算复杂的ffmpeg程序来实现我们上文提到的那些知识。

环境准备

在进行ffmpeg开发之前,一般建议大家自行获得ffmpeg源码,手动编译获得相应的动态库(dll/so),然后再正式进行c/c++开发工作。

ffmpeg可以在windows,linux系统上开发,一般推荐linux上来开发(本人用的linux环境,但也有windows环境),因为windows其实也是模拟了一些linux的环境的。

windows环境安装与编译

windows环境下主要参考这篇文章ffmpeg库编译安装及入门指南。

注意以下几点:

  • 博文中作者的建议安装选项大家都尽可能安装上。
  • ffmpeg源码尽可能下载最新版本。
  • 编译ffmpeg库的build-ffmpeg.sh脚本替换如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
shell复制代码#!/bin/sh
basepath=$(cd `dirname $0`;pwd)
echo ${basepath}

cd ${basepath}/ffmpeg-5.1.2-src
pwd

export PKG_CONFIG_PATH=${PKG_CONFIG_PATH}:/d/repos/ffmpeg/x264_install/lib/pkgconfig
echo ${PKG_CONFIG_PATH}

./configure --prefix=${basepath}/ffmpeg_5.2.1_install \
--enable-debug --disable-asm --disable-stripping --disable-optimizations \
--enable-gpl --enable-libx264 --disable-static --enable-shared \
--extra-cflags=-l${basepath}/x264_install/include --extra-ldflags=-L${basepath}/x264_install/lib

make -j8
make install

主要是添加一些可debug配置,为后面调试做准备

linux环境安装与编译

在linux中就不需要安装MSYS2了,而缺的编译工具什么的按照提示使用linux的软件包管理管理工具(比如apt等)安装即可。

然后下载最新源码,libx264源码,编译过程仍然可以使用或者 ffmpeg库编译安装及入门指南中提供的编译脚本。

注意build-ffmpeg.sh脚本同样需要替换一下脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
shell复制代码#!/bin/sh
basepath=$(cd `dirname $0`;pwd)
echo ${basepath}

cd ${basepath}/ffmpeg-5.1.2-src
pwd

export PKG_CONFIG_PATH=${PKG_CONFIG_PATH}:/d/repos/ffmpeg/x264_install/lib/pkgconfig
echo ${PKG_CONFIG_PATH}

./configure \
--enable-debug --disable-asm --disable-stripping --disable-optimizations \
--enable-gpl --enable-libx264 --disable-static --enable-shared \
--extra-cflags=-l${basepath}/x264_install/include --extra-ldflags=-L${basepath}/x264_install/lib

make -j8
make install

去掉了–prefix=xxx配置,把ffmpeg生成物推送到系统默认的环境变量的路径中,免得还需要自行配置,后面就可以直接调用和使用依赖库了。如果想自定义产物生成目录也可直接参考windows的脚本。

代码编辑器

代码编辑器可以使用 Visual Studio Code,Clion,或者其他趁手的都行。

生成产物与开发使用

编译成功之后,不仅有ffmpeg依赖库(lib文件夹)和头文件(include文件夹),还有ffmpeg,ffprobe,ffplayer这样的可执行程序,可以直接在命令行中进行调用。

在后面的开发过程中,我们至少会用到头文件和依赖库。

对于windows环境而言,为了简单起见,每新建一个工程,可以把ffmpeg生成的头文件都添加进来,然后按需调(虽然有些不环保)。

image.png

windows环境在编译时需要指定链接库,还是可以参考 ffmpeg库编译安装及入门指南;linux如果编译产物在系统默认目录中的话则不需要。

ffmpeg开发

环境安装完毕之后,正式进入正题。

我们要开发的程序的功能是,读取一个视频文件,解码音频和视频部分,并且把解码后的视频中的一帧或者几帧图保存成ppm格式。

这里主要包含到ffmpeg的解封装,解码,色彩空间转换的过程,以及对解码数据的认识。

至于ppm,它是一个未压缩的RGB图片的格式(jpg就是压缩后的图片格式),文件在操作系统中可以正常打开查看,这不是本文的重点。

函数入口

接下来我们直接看代码

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
cpp复制代码#include <cstdio>
#include "common.h"
#include "iostream"
// 因为ffmpeg中的库都是C编写的,使用cpp开发,引用C库需要extern "C"配置,适配C/cpp函数名编译的不同规则
extern "C"{
#include "libavformat/avformat.h"
#include "libavformat/avio.h"
#include "libavcodec/avcodec.h"
#include "libswscale/swscale.h"
#include "libavutil//imgutils.h"

}
using namespace std;

AVFormatContext *av_fmt_ctx_input = nullptr;

int video_stm_index = -1;
int audio_stm_index = -1;
int ret = 0;

// 提前定义好结构体,便于解码音频和视频时的变量的统一管理
typedef struct StreamContext{
//解码音频的解码器上下文
AVCodecContext *audioAVCodecCtx = nullptr;
//解码视频的解码器上下文
AVCodecContext *videoAVCodecCtx= nullptr;
//表示视频的数据流
AVStream *videoStream= nullptr;
//表示音频的数据流
AVStream *audioStream= nullptr;
//色彩空间转换后的AVFrame
AVFrame *rgbFrame = nullptr;
};
// 根据定义好的结构体声明一个变量
struct StreamContext streamContext;
// 色彩空间转换模块的上下文
SwsContext *swsContext = nullptr;

/********其他函数***********/
//.....
// 后文补充
//......
/********其他函数***********/


// 入口函数
int main(int argc,char *args[]) {
// 同目录下存放任意一个MP4文件,便于直接读取
const char *input_file = "bunny.mp4";
// avformat_open_input,解封装,并读取文件头信息,创建av_fmt_ctx_input结构体对象
if ((ret = avformat_open_input(&av_fmt_ctx_input,input_file, nullptr, nullptr))<0){
print_log("avformat_open_input", ret); // 错误处理,print_log是自定义的一个函数,用于打印一些错误信息
return -1;
}
// 主要针对某些没有文件头的视频文件情况,会尝试从文件主体中去读取一些文件的信息
ret = avformat_find_stream_info(av_fmt_ctx_input, nullptr);
if(ret<0){
print_log("avformat_find_stream_info", ret);
return ret;
}
// 打印一下av_fmt_ctx_input目前持有的信息,(如果不想要也可以去掉)
av_dump_format(av_fmt_ctx_input,-1,input_file,0);
// 1,分别对视频和音频的解码进行初始化的准备
// 就是获取对应的流,以及初始化对应的解码器
if (initVideo() < 0 || initAudio() < 0){
return -1;
}
// 初始化这个用来转换的AVFrame,
// 需要手动设置frame->data,frame->linesize这两个空间 在前一篇文章中说到过
ret = initRGBFrame();
if (ret<0){
print_log("initRGBFrame",-1);
return ret;
}
// 创建AVPakcet结构体的对象,前一篇文章说过它是存放编码数据的结构体
AVPacket *av_packet = av_packet_alloc();

// av_read_frame 读取视频文件的中的数据流 到av_packet中,
// 此时av_packet中就存放了一块编码过的数据
while (av_read_frame(av_fmt_ctx_input,av_packet)>=0){
// av_packet->stream_index表示这个packet数据来自AVFormatContext中的streams数组的哪个下标
// 通过判断来区分packet里面装的是音频数据还是视频数据,需要分开解码
if (av_packet->stream_index == video_stm_index){ // video_stm_index就是我们找到的视频流所在的数组下标
ret = decodeData(av_packet,streamContext.videoAVCodecCtx,1);

}else if (av_packet->stream_index == audio_stm_index){
// decode audio

}

if (ret<0){

break;
}

}
// 集中释放AVCodecContext,AVPacket,AVFormatContext等资源
avcodec_free_context(&(streamContext.audioAVCodecCtx));
avcodec_free_context(&(streamContext.videoAVCodecCtx));
av_packet_free(&av_packet);
avformat_close_input(&av_fmt_ctx_input);

return 0;
}

上面是程序的变量和入口函数,也就是整个程序的主框架了。

从上面的注释可以比较通畅的了解程序的执行过程。从中也能找到前一篇文章中提到的许多代码片段,这里其实算是做了一个整合。

音视频配置初始化

接下来我们看看initVideo和initAudio,其实两者基本是一致的,理论上可以合并成一个函数。

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
ini复制代码
int initVideo(){
//av_find_best_stream 用于从av_fmt_ctx_input中找到类型为AVMEDIA_TYPE_VIDEO的流的数组下标
// 当然由于我们此时已经直到AVFormatContext->nb_streams 流数组的长度,所以可以手动遍历。
// av_find_best_stream函数就是手动遍历查找的。
video_stm_index = av_find_best_stream(av_fmt_ctx_input,AVMEDIA_TYPE_VIDEO,-1,-1, nullptr,0);
if (video_stm_index == -1 ){ // 如果-1,表示没有找到我们想要的数组下标,返回错误
print_log("video_index_error",video_stm_index);
return -1;
}
cout<< "video stream index: "<<video_stm_index<<endl; //打印信息
//拿到了视频流
streamContext.videoStream = av_fmt_ctx_input->streams[video_stm_index];

// 接着开始准备进行解码器的初始化
// 上一篇文章我们说过,视频流中有解码该流的数据的解码器id
// 此时我们通过解码器id,找到对应的解码器的详细信息(AVCodec),或者也可以直接把它理解为解码器
// avcodec_find_decoder是找对应的解码器,avcodec_find_encoder是找对应的编码器,别弄错了
auto codec = avcodec_find_decoder(streamContext.videoStream->codecpar->codec_id);
// 然后通过这个codec,创建该解码器的上下文,
// 但是此时上下文里还没有视频流的有效信息
auto av_codec_ctx = avcodec_alloc_context3(codec);
// 于是我们把视频流的有效信息赋值到解码器上下文中
ret = avcodec_parameters_to_context(av_codec_ctx,streamContext.videoStream->codecpar);
if (ret<0){
print_log("video avcodec_parameters_to_context",ret);
return ret;
}
// 对解码器进行初始化,准备开始解码
ret = avcodec_open2(av_codec_ctx,codec, nullptr);
if (ret<0){
print_log("video avcodec_open2",ret);
return ret;
}
streamContext.videoAVCodecCtx = av_codec_ctx;
return 0;
}


int initAudio(){
audio_stm_index = av_find_best_stream(av_fmt_ctx_input,AVMEDIA_TYPE_AUDIO,-1,-1, nullptr,0);
if (audio_stm_index == -1){
print_log("audio_index_error",audio_stm_index);
return -1;
}
cout<< "audio stream index "<<audio_stm_index<<endl;
streamContext.audioStream = av_fmt_ctx_input->streams[audio_stm_index];

auto codec = avcodec_find_decoder(streamContext.audioStream->codecpar->codec_id);
auto av_codec_ctx = avcodec_alloc_context3(codec);
ret = avcodec_parameters_to_context(av_codec_ctx,streamContext.audioStream->codecpar);
if (ret<0){
print_log("audio avcodec_parameters_to_context",ret);
return ret;
}
ret = avcodec_open2(av_codec_ctx,codec, nullptr);
if (ret<0){
print_log("audio avcodec_open2",ret);
return ret;
}
streamContext.audioAVCodecCtx = av_codec_ctx;

return 0;
}

根据上面的代码和注释,也能发现,关于AVStream,AVCodec,AVCodecContext的使用基本都符合前一篇文章中对于对应结构体的基本使用说明。当然这个过程中是有许多详细的参数是可以设置的,也可以把他们变得复杂一点,但是目前这不是重点。

手动配置AVFrame->data

接下来我们看看initRGBFrame的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
arduino复制代码int initRGBFrame(){
//先创建一个AVFrame结构体
streamContext.rgbFrame = av_frame_alloc();

auto width = streamContext.videoAVCodecCtx->width;
auto height = streamContext.videoAVCodecCtx->height;

// 通过像素格式,图片宽高,来计算当前所需的缓冲空间大小,最后一个字段是对齐字数
auto bufferSize = av_image_get_buffer_size(AV_PIX_FMT_RGB24,width,height,1);
uint8_t * buffer = (uint8_t *)av_malloc(bufferSize);
// AV_PIX_FMT_RGB24 packed RGB 8:8:8, 24bpp, BGRBGR...
// 在data[8]数组中保存在data[0]中
//根据缓冲大小,像素格式,宽高来填充 rgbFrame->data和rgbFrame->linesize
av_image_fill_arrays(streamContext.rgbFrame->data,streamContext.rgbFrame->linesize,buffer,
AV_PIX_FMT_RGB24,width,height,1);
// 创建视频帧转换的上下文,libswscale可以提供颜色转换,图片尺寸放缩等能力
swsContext = sws_getContext(width,height,streamContext.videoAVCodecCtx->pix_fmt,width,height,
AV_PIX_FMT_RGB24,0, nullptr, nullptr, nullptr);
if (swsContext == nullptr){
return -1;
}
return 0;
}

手动创建并填充AVFrame的过程,需要首先创建AVFrame的结构体,然后申请填充 rgbFrame->data和rgbFrame->linesize这两个字段,前一篇文章中说到过,不是编解码过程中使用AVFrame需要我们手动申请这块内存。具体可以看FFmpeg开发——基础篇————AVFrame。

现在我们准备先把视频解码成YUV帧,然后把YUV帧通过libswscale转换成RGB帧。解码过程中使用AVFrame是不需要我们手动申请或填充data等字段的,但是scale转换过程自然就需要了。

解码与转换

做完了上述的准备之后,可以正式开始进行解码操作了:从数据流中读取数据到AVPacket中,然后把AVPakcet中的数据发送给解码器,接着从解码器中读取数据到AVFrame中,就获得了一个解码后的帧。

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
scss复制代码int decodeData(AVPacket  *av_packet,AVCodecContext *av_codec_ctx,int is_video) {
// 发送数据到解码器
ret = avcodec_send_packet(av_codec_ctx,av_packet);
if (ret<0){
print_log("video avcodec_send_packet",ret);
return ret;
}
// 创建一个AVFrame用来承接解码后的数据(此时不用在手动填充data等字段了)
AVFrame *av_frame = av_frame_alloc();
while (true){
// 从解码器中读取解码后的数据到AVFrame中
ret = avcodec_receive_frame(av_codec_ctx,av_frame);
if (ret == AVERROR_EOF){ // 到文件结束
ret = 0;
break;
} else if (ret == AVERROR(EAGAIN)){ // avpacket的数据不够形成一帧数据,需要继续往解码器发送avpacket
ret = 0;
break;
}else if(ret<0){ // 其他错误
print_log("video decode error",ret);

break;
}else{
// ret>=0 表示正常,此时会得到的av_frame基本上都是YUV420P的色彩格式,
if(is_video>0){ // 处理视频数据
// sws_scale函数可以对AVFrame进行转换(颜色空间转换,图片宽高放缩等)
// (YUV420P) to (packed RGB 8:8:8)
ret = sws_scale(swsContext, ( uint8_t const* const*)av_frame->data, av_frame->linesize, 0, av_frame->height,
streamContext.rgbFrame->data, streamContext.rgbFrame->linesize);
if (ret<0){
print_log("sws_scale_frame",ret);
break;
}
// 此时rgbFrame内就保存了RGB格式的数据,接下来我们只要把数据写入到文件即可
saveRGBImage(0);
ret = -1;
break; // 只解码一帧就退出
}else{

}


}
}
av_frame_free(&av_frame);

return ret;
}

这里主要涉及到两个点,解码过程和转换过程。

解码过程的API调用比较简单,也可以看AVFrame之编解码使用方式。

转换过程本质上是YUV2RGB的算法以及数据存储方式,关于前者其实在移动开发中关于视频的一些基本概念——YUV与RGB的转换介绍了相关转换原理;而数据存储方式则在文章FFmpeg开发——基础篇(一)之 AVFrame的data与linesize中有介绍到Planar和packed两种存储放在在AVFrame->data中的表现形式。了解不同的存储方式在ffmpeg中的表现形式我们才能正确的保存数据。

保存文件

然后我们最后看看数据保存过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码void saveRGBImage(int index){
char fileName[32];

sprintf(fileName,"frame_%d.ppm",index); // 定义一下文件名frame_0.ppm
FILE *file = fopen(fileName,"wb"); // 打开文件
if (file == nullptr){
return;
}
int width = streamContext.videoAVCodecCtx->width;
int height = streamContext.videoAVCodecCtx->height;
int line_size = streamContext.rgbFrame->linesize[0];
// 写入ppm文件的文件头,P6
fprintf(file, "P6\n%d %d\n255\n", width, height);
for (int i = 0; i < height; ++i) {
//相当于一行一行的写入数据,(也可以计算数据总数,一次性写入)
// line_size是一行的长度,从第0行开始,每次写入一行长度的数据
fwrite(streamContext.rgbFrame->data[0]+i*line_size,1,line_size,file);
}
fclose(file);
}

ppm格式的详细信息见PPM文件格式详解

log打印的函数

1
2
3
4
5
6
7
8
9
c复制代码char* print_log(const char *tag,int ret){
const int max_buf = 1024;
char buf_log[2048] = "";
// av_strerror函数能够根据当前错误码给我们返回一些错误信息
// 虽然非常粗糙,但是聊胜于无。
av_strerror(ret,buf_log,max_buf);
cout<< tag << " error: %d %s" << ret << buf_log << endl;
return "";
}

总结

把上述代码合并之后,就是这个程序的完整代码。

我们可以从一个视频文件中读取数据,解码,然后获取其中第一帧YUV帧,转换为RGB帧,最后把RGB帧保存为一张未压缩的图片文件。

虽然我们对音频的解码做了初始化准备配置,本来想做些其他功能,后来感觉有点多余,demo中处理视频就行了,它和video的解码过程是一致的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用 Python接入 OpenAI API,梳理清楚对话角

发表于 2024-04-26

前言

回顾上篇文章写到如何实现最简单的OpenAI对话请求示例:

使用 Python接入 OpenAI API,实现简单的对话生成,介绍其中相应参数含义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码from openai import OpenAI
from dotenv import load_dotenv, find_dotenv

_ = load_dotenv(find_dotenv())
client = OpenAI()

response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Who won the world series in 2020?"},
{"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
{"role": "user", "content": "Where was it played?"}
]
)
print(response) # 打印的是一个对象
print(response.choices[0].message.content) # 打印的是一个具体的回复的内容

最后返回了一个对象,其实在model=”gpt-3.5-turbo”,增加一行response_format={ “type”: “json_object” }, 将返回值设置为json格式就会输出如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
json复制代码{
"choices": [
{
"finish_reason": "stop",
"index": 0,
"message": {
"role": "assistant",
"content": "The World Series in 2020 was played at Globe Life Field in Arlington, Texas."
}
}
],
"created": 1714051759,
"id": "chatcmpl-6p9XYPYSTTRi0xEviKjjilqrWU2Ve",
"model": "gpt-3.5-turbo-0125",
"object": "chat.completion",
"usage": {
"prompt_tokens": 18,
"completion_tokens": 54,
"total_tokens": 72
}
}

这样返回的内容格式可以更容易给开发者处理。

对话角色

在上面的例子中messages有几个角色分别为:system,user,assistant,对应的解释为:

  • system:用于设置 AI 的行为、背景等,比如设定其为人工智能专家等。
  • assistant:通常是模型的回复,可用于提供上下文。
  • user:模型的使用者,也即聊天内容的发起者,同时可用于提供上下文。

为了让API记住对话的上下文,就要每次在对话的时候都要 携带历史对话的assistant,user的记录,给个示例就明白:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码from openai import OpenAI
from dotenv import load_dotenv, find_dotenv

_ = load_dotenv(find_dotenv())
client = OpenAI()

response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "你是一个ChatGPT助手,你帮助用户回答问题"},
{"role": "user", "content": "说10遍王子请睡觉"},
    {"role": "assistant", "content": "王子请睡觉。王子请睡觉。王子请睡觉。王子请睡觉。王子请睡觉。王子请睡觉。王子请睡觉。王子请睡觉。王子请睡觉。王子请睡觉。"},
{"role": "user", "content": "你上面回答了几次王子请睡觉"}
]
)
print(response.choices[0].message.content) # 我回答了10次“王子请睡觉”。

这就记住了上次的对话,当然携带的内容过多占用的token就会越多,目前ChatGPT3.5最大上下文应该是16K,16k对应16000个token,测试了一下复制了很多assistant和user的对话历史在代码中,token在超过16k的时候报错情况如下:

1
css复制代码openai.BadRequestError: Error code: 400 - {'error': {'message': "This model's maximum context length is 16385 tokens. However, your messages resulted in 17184 tokens. Please reduce the length of the messages.", 'type': 'invalid_request_error', 'param': 'messages', 'code': 'context_length_exceeded'}}

所以在开发多轮对话API的时候要考虑token的占用预估的问题。OpenAI的文档有对token计算的说明,我们以后再研究。

函数调用

函数调用(Function Call)是 OpenAI API 的一项强大功能,它允许开发者向模型提供自定义函数,从而扩展模型的功能并使其能够处理更复杂的任务。借助函数调用,开发者可以:

  • 引入外部数据: 模型可以访问并处理来自外部 API 或数据库的数据,例如天气预报、股票行情等。
  • 实现复杂逻辑: 模型可以执行更复杂的逻辑操作,例如计算、排序、过滤等。
  • 增强交互性: 模型可以与用户进行更具交互性的对话,例如根据用户输入提供个性化建议或完成特定任务。

今天就引入外部数据做一个BTC价格查询的函数调用,详细注释在代码里面:

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
ini复制代码from openai import OpenAI
from dotenv import load_dotenv, find_dotenv
import requests,json


_ = load_dotenv(find_dotenv())
client = OpenAI()

def get_coin_prince(coin):
# Execute query here
url = 'https://data-api.binance.vision/api/v3/ticker/24hr?symbol='+coin.upper()+"USDT" # binance直接请求获得结果数据
response = requests.get(url)
response = response.json()
try:
return response['lastPrice']+'USDT'
except:
return "The coin is not exist"

# 函数描述
tools = [
{
"type": "function",
"function":{
"name": "get_coin_prince",
"description": "Get a coin Price from Binance, The price unit is in US dollars",
"parameters": {
"type": "object",
"properties": {
"coin": {
"coin": "string",
"description": "Abbreviations for Virtual Currency",
}
},
"required": ["coin"],
},
}
}
]

messages=[
{"role": "user", "content": "BTC的价格是多少?"},
],
messages = []
messages.append({"role": "system", "content": "你是一个虚拟币查询机器人"})
messages.append({"role": "user", "content": "btc的价格是多少?"})
response = client.chat.completions.create(
model="gpt-3.5-turbo-0613",
messages=messages,
tools=tools,
  tool_choice="auto", 
)

print(response) # 打印的是一个对象

response_message = response.choices[0].message # 消息对象

tool_calls = response_message.tool_calls # 识别是否触发函数调用
if tool_calls: # 判断是否为函数调用
available_functions = {
"get_coin_prince": get_coin_prince,
} # 本例中只有一个函数,但可以有多个
messages.append(response_message)
for tool_call in tool_calls: # 遍历函数调用
function_name = tool_call.function.name
function_to_call = available_functions[function_name] # 找到本地目标函数
function_args = json.loads(tool_call.function.arguments) # 获取函数参数的具体值
function_response = function_to_call(
coin=function_args.get("coin"),
) # 真实调用本地函数
messages.append(
{
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": function_response, #本地调用函数内容返回值放进对话 ,接下来给ChatGPT组装内容
}
) # 新一轮的对话的函数调用细节(函数ID,规则,名称,参数)

# 开始第二次对话
second_response = client.chat.completions.create(
model="gpt-3.5-turbo-0125",
messages=messages,
) # 从模型中获取一个新的响应
print(messages) # 调试组装过的对话内容
print(second_response)
res = second_response.choices[0].message.content

print(res)  # BTC的价格为64477.51 USDT。

从原理上面来梳理,就是声明函数,给对话增加额外参数提示可以自动调用函数,识别到有函数调用,就本地调用函数再组装进行第二次对话获得最终结果,第一次接触还是比较难以理解的,顺利弄完了很有成就感,注意以上代码需要有相对应的网络环境。

最后

今天算是真的搞懂了大模型的本地函数调用,找了好多资料都没有我这样的研究过程示例,学到了很开心,今天就介绍到这里,ChatGPT的API基础的都介绍完毕了,后面可能会来解密一下OpenAI API中的详细参数和Token计算,敬请期待~

如果觉得内容不错,欢迎点个赞,你的支持是我更新的动力。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Huggingface最强视觉模型Idefics2开源,80

发表于 2024-04-26

前言

Hugging Face近日宣布开源了一款名为Idefics2的全新多模态模型,该模型不仅在参数规模上大幅超越前作,还在多个经典视觉-语言基准测试中展现出卓越表现,完全有资格与LLava-Next-34B、MM1-30B-chat等更大规模模型一争高下。

Idefics2是Idefics1的升级版,共有80亿参数,许可协议为Apache 2.0,光学字符识别(OCR)能力也得到了大幅增强。这无疑为广大开源社区从事多模态研究提供了一个强大的基础模型。值得一提的是,Idefics2已经在Hugging Face的Transformers中集成,便于开发者进行后续的细分任务微调。

  • Huggingface模型下载: huggingface.co/HuggingFace…
  • AI快站模型免费加速下载:aifasthub.com/models/Hugg…

技术创新

从技术细节来看,Idefics2在多个方面实现了创新:

  • 图像处理:Idefics2摒弃了传统的固定尺寸图像裁剪方式,而是保持图像的原生分辨率和纵横比,通过子图像切分等策略来适应输入要求。这不仅可以更好地保留视觉信息,也提高了处理效率。
  • OCR 性能:通过针对性的训练数据,Idefics2的光学字符识别能力得到了大幅提升,可以准确转录图像或文档中的文字内容,从而更好地理解图表和文档等结构化信息。
  • 模型架构:相比上代,Idefics2在融合视觉特征到语言主干网络的方式上进行了优化,采用了Perceiver池化和MLP模态映射的方式,进一步简化了跨模态信息的交互。

这些创新使得Idefics2在性能上远超Idefics1 (80亿参数),在多个基准测试中取得了领先地位,部分指标甚至超过了商业闭源模型Gemini Pro和Claude 3 Haiku。

全方位的数据支持

Idefics2的训练数据来自于多个公开可用的数据集,包括网页文档(如维基百科)、图文配对(如Public Multimodal Dataset、LAION-COCO)以及OCR数据(如PDFA、IDL和Rendered-text)等。此外,研究团队还自行整理了一个名为”The Cauldron”的多模态指令微调数据集,共包括50个手工精选的数据源,格式化为多轮对话。

通过这些丰富的数据支持,Idefics2不仅在视觉问答、文本生成等基准测试中取得领先成绩,在图表分析、文档信息提取以及基于视觉的算术运算等方面也展现出了出色的能力。

应用前景广阔

总的来说,Idefics2的开源发布为多模态人工智能的发展注入了新的活力。作为一款性能优异、技术先进的通用多模态模型,它不仅可以在视觉问答、内容生成等经典任务上发挥优势,还能够胜任图表分析、文档处理等更复杂的应用场景。

值得一提的是,得益于Hugging Face Transformers的集成,Idefics2可以便捷地被微调用于各种多模态下游任务,为广大开发者提供了一个强大的基础设施。相信在开源社区的共同努力下,Idefics2必将为多模态人工智能的未来发展贡献更多力量。

模型下载

Huggingface模型下载

huggingface.co/HuggingFace…

AI快站模型免费加速下载

aifasthub.com/models/Hugg…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Tapable的神秘之处-源码解析(1) Tapable的神

发表于 2024-04-26

Tapable的神秘之处-源码解析(1)

前言:研究webpack过程中,发现tapable在其中占据了很重要的成分,所以就看看它的奥妙之处

Tapable是一个在webpack中被广泛使用的核心模块,它提供了一组灵活的钩子函数,可以在不同的生命周期中插入自定义的逻辑。通过使用Tapable,我们可以轻松地实现各种功能,从简单的插件拓展到复杂的编译过程优化。在这篇文章中,我们将深入探索Tapable的神秘之处,了解它的底层实现。

从官方文档可以看到tapable提供以下方法

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesLoopHook,
AsyncSeriesWaterfallHook
} = require("tapable");

为了弄清楚这些方法都是有什么秘密可言,让我们开始从tapable仓库开始探究吧。

仓库地址:github.com/webpack/tap…

源码目录结构

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
markdown复制代码tapable
├─ .babelrc
├─ .editorconfig
├─ .eslintrc
├─ .gitattributes
├─ .gitignore
├─ .prettierrc.js
├─ .travis.yml
├─ README.md
├─ lib
│ ├─ AsyncParallelBailHook.js
│ ├─ AsyncParallelHook.js
│ ├─ AsyncSeriesBailHook.js
│ ├─ AsyncSeriesHook.js
│ ├─ AsyncSeriesLoopHook.js
│ ├─ AsyncSeriesWaterfallHook.js
│ ├─ Hook.js
│ ├─ HookCodeFactory.js
│ ├─ HookMap.js
│ ├─ index.js
│ ├─ MultiHook.js
│ ├─ SyncBailHook.js
│ ├─ SyncHook.js
│ ├─ SyncLoopHook.js
│ ├─ SyncWaterfallHook.js
│ ├─ util-browser.js
│ └─ __tests__
│     ├─ AsyncParallelHooks.js
│     ├─ AsyncSeriesHooks.js
│     ├─ Hook.js
│     ├─ HookCodeFactory.js
│     ├─ HookStackOverflow.js
│     ├─ HookTester.js
│     ├─ MultiHook.js
│     ├─ SyncBailHook.js
│     ├─ SyncHook.js
│     ├─ SyncHooks.js
│     ├─ SyncWaterfallHook.js
│     └─ __snapshots__
│       ├─ AsyncParallelHooks.js.snap
│       ├─ AsyncSeriesHooks.js.snap
│       ├─ HookCodeFactory.js.snap
│       └─ SyncHooks.js.snap
├─ LICENSE
├─ package.json
├─ tapable.d.ts
└─ yarn.lock

可以看出tapable的源码方法都是放置在lib文件夹中,lib/index.js就是tapable入口文件,并且除了10个对应的Hook文件,还有其他几个方法文件。

今天我们先来看看 Hook.js这个文件到底做了那些有意义的事

核心实现方法lib/Hook.js

代码里定义个一个Hook类的构造函数 接受两个可选参数:args(参数数组)和name(Hook的名称)。在构造函数中,初始化了一些实例属性,包括_args、name、taps、interceptors等,并设置了默认的调用方法(call、callAsync、promise)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码初始化配置部分
constructor(args = [], name = undefined) {
   this._args = args; // 参数配置数组
   this.name = name; // Hook的名称
   this.taps = []; // 用于存储注册到 Hook 实例上的 tap 对象。
   this.interceptors = []; // 用于存储注册到 Hook 实例上的拦截器对象
   // 将全局常量 CALL_DELEGATE 赋值给实例属性 _call、call。表示某个调用的委托函数或逻辑。
   this._call = CALL_DELEGATE;
   this.call = CALL_DELEGATE;
   // 将全局常量 CALL_ASYNC_DELEGATE 赋值给实例属性_callAsync、callAsync。表示某个【异步】调用的委托函数或逻辑。
   this._callAsync = CALL_ASYNC_DELEGATE;
   this.callAsync = CALL_ASYNC_DELEGATE;
   // 将全局常量 PROMISE_DELEGATE 赋值给实例属性_promise、promise。表示某个【Promise】调用的委托函数或逻辑。
   this._promise = PROMISE_DELEGATE;
   this.promise = PROMISE_DELEGATE;
   // 暂时没看到具体用途,可能在后续的方法中确定。
   this._x = undefined;
   // compile抽象方法的实现,强制派生类/子类必须被重写以提供具体的实现。
   this.compile = this.compile;
   
   this.tap = this.tap;
   this.tapAsync = this.tapAsync;
   this.tapPromise = this.tapPromise;
}

这段代码定义了一个注册机制,支持不同类型的注册方式(同步、异步、基于Promise的异步),通过对外提供的 tap, tapAsync, 和 tapPromise 方法来实现。它主要用于事件或插件系统中,允许开发者根据不同需求注册回调函数。

_tap方法

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
js复制代码_tap(type, options, fn) {
   if (typeof options === "string") {
       options = {
           name: options.trim()
      };
  } else if (typeof options !== "object" || options === null) {
       throw new Error("Invalid tap options");
  }
   if (typeof options.name !== "string" || options.name === "") {
       throw new Error("Missing name for tap");
  }
   if (typeof options.context !== "undefined") {
       deprecateContext();
  }
   options = Object.assign({ type, fn }, options);
   options = this._runRegisterInterceptors(options);
   this._insert(options);
}
​
tap(options, fn) {
   this._tap("sync", options, fn);
}
​
tapAsync(options, fn) {
   this._tap("async", options, fn);
}
​
tapPromise(options, fn) {
   this._tap("promise", options, fn);
}
  • _tap(type, options, fn) 方法是这个注册机制的内核。
  • 参数 type 指明了注册的类型,可以是 "sync", "async", 或 "promise"。
  • 参数 options 可以是一个字符串或一个对象。如果是字符串,将被转换成一个对象,对象中有一个 name 属性。如果是一个对象,它必须至少包含一个名为 name 的字符串属性。options 还可以包含一个可选的 context 属性,若存在,则调用 deprecateContext() 函数(可能用于标记该属性为过时)。
  • 参数 fn 是一个将被注册的函数。
  • 方法内部,首先检查 options 参数的有效性,然后将 type 和 fn 加入到 options 对象中,最后通过 _runRegisterInterceptors 方法处理 options,并通过 _insert 方法将处理后的 options 插入到注册列表中。

tap, tapAsync, tapPromise 方法:

这三个方法是 _tap 方法的封装,分别对应不同的 type 参数:

  • tap(options, fn) 用于注册同步类型的回调。
  • tapAsync(options, fn) 用于注册异步类型的回调。
  • tapPromise(options, fn) 用于注册基于Promise的异步回调。

使用场景:

这种模式广泛用于插件系统和事件处理系统,例如Webpack的插件系统就是一个典型的例子。通过这种方式,开发者能够以灵活的方式扩展或修改应用程序的行为,同时保持代码的整洁和组织。每种类型的回调提供了不同的处理方式,以适应不同的执行环境和需求:

  • 使用 tap 注册的回调将同步执行,适用于执行时间短、不涉及IO操作的场景。
  • 使用 tapAsync 注册的回调将异步执行,但需要回调函数来通知执行完成,适用于涉及IO操作等可能需要等待的场景。
  • 使用 tapPromise 注册的回调期望返回一个Promise,适用于现代异步处理,特别是在使用async/await语法时。

_runRegisterInterceptors方法

1
2
3
4
5
6
7
8
9
10
11
js复制代码_runRegisterInterceptors(options) {
   for (const interceptor of this.interceptors) {
       if (interceptor.register) {
           const newOptions = interceptor.register(options);
           if (newOptions !== undefined) {
               options = newOptions;
          }
      }
  }
   return options;
}

这是一个内部方法,用于在注册钩子之前对options对象进行拦截器注册配置。

  • _runRegisterInterceptors(options) 用于在注册钩子之前对options对象进行拦截器注册。。
  • 参数 options注册的钩子对象。
  • 方法内部,对于this.interceptors数组中的每个拦截器,检查interceptor.register是否存在。如果存在,调用interceptor.register方法,并传入options对象。如果interceptor.register方法返回一个新的选项对象newOptions,更新options为newOptions。最后返回更新后的options对象。

_runRegisterInterceptors提供了一种扩展和定制回调函数的机制,通过钩子注册和拦截器,可以灵活地控制和改变回调函数的行为

withOptions方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码withOptions(options) {
   // 用于将给定的选项与原始选项进行合并
   const mergeOptions = opt =>
   Object.assign({}, options, typeof opt === "string" ? { name: opt } : opt);
​
   return {
       name: this.name,
       tap: (opt, fn) => this.tap(mergeOptions(opt), fn),
       tapAsync: (opt, fn) => this.tapAsync(mergeOptions(opt), fn),
       tapPromise: (opt, fn) => this.tapPromise(mergeOptions(opt), fn),
       intercept: interceptor => this.intercept(interceptor),
       isUsed: () => this.isUsed(),
       withOptions: opt => this.withOptions(mergeOptions(opt))
  };
}

是tapable用于创建一个新的含有特定选项的对象的公共方法

isUsed方法

1
2
3
js复制代码isUsed() {
   return this.taps.length > 0 || this.interceptors.length > 0;
}

用于检查钩子是否被使用。

intercept方法

1
2
3
4
5
6
7
8
9
10
11
js复制代码intercept(interceptor) {
   // 重置钩子对象的编译状态
   this._resetCompilation();
   // 对interceptor进行复制,并推到interceptors数组里
   this.interceptors.push(Object.assign({}, interceptor));
   if (interceptor.register) {
       for (let i = 0; i < this.taps.length; i++) {
           this.taps[i] = interceptor.register(this.taps[i]);
      }
  }
}

用于注册一个拦截器,并同时将拦截器对象添加到钩子对象的 taps 数组中。

_resetCompilation方法

1
2
3
4
5
js复制代码_resetCompilation() {
   this.call = this._call;
   this.callAsync = this._callAsync;
   this.promise = this._promise;
}

用于重置钩子对象的编译状态。

_insert方法

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
js复制代码_insert(item) {
this._resetCompilation();
let before;
if (typeof item.before === "string") {
before = new Set([item.before]);
} else if (Array.isArray(item.before)) {
before = new Set(item.before);
}
let stage = 0;
if (typeof item.stage === "number") {
stage = item.stage;
}
let i = this.taps.length;
while (i > 0) {
i--;
const x = this.taps[i];
this.taps[i + 1] = x;
const xStage = x.stage || 0;
if (before) {
if (before.has(x.name)) {
before.delete(x.name);
continue;
}
if (before.size > 0) {
continue;
}
}
if (xStage > stage) {
continue;
}
i++;
break;
}
this.taps[i] = item;
}

解析 item.before 和 item.stage

  • 如果 item.before 是字符串类型,将其作为一个元素放入一个新的集合 before 中。
  • 如果 item.before 是数组类型,将其作为多个元素放入一个新的集合 before 中。
  • 如果 item.stage 是数字类型,将其赋值给 stage。

从钩子对象的回调函数列表的末尾开始遍历,直到找到合适的插入位置

该方法的目的是根据给定的条件将新的回调函数插入到钩子对象的回调函数列表中。通过调整回调函数的顺序,可以控制它们的执行顺序和优先级。

结尾

这个 Hook 类为 tapable 提供了以下功能和特性:

  1. 注册和执行回调函数:Hook 类提供了 tap、tapAsync 和 tapPromise 方法,用于注册回调函数并在适当的时机执行这些回调函数。
  2. 参数配置:可以在 Hook 类的构造函数中传入参数配置数组,用于在执行回调函数时传递参数。
  3. 拦截器:Hook 类支持注册拦截器对象,并在需要时调用拦截器的 register 方法对已注册的回调函数进行修改或替换。
  4. 调用委托函数:Hook 类定义了 _call、call、_callAsync、callAsync、_promise 和 promise 等委托函数,用于委托具体的调用行为。
  5. 钩子编译:Hook 类的 compile 方法可以被派生类重写,用于提供具体的编译逻辑,生成最终的调用函数。
  6. 钩子选项:Hook 类提供了 withOptions 方法,用于将给定的选项与原始选项进行合并,并返回一个新的对象,该对象具有与原始 Hook 实例相同的方法,但使用合并后的选项。
  7. 基本信息:Hook 类包含一些基本的信息,如名称(name)和注册的回调函数(taps)。
  8. 可用性检查:Hook 类提供了 isUsed 方法,用于判断当前 Hook 实例是否被使用,即是否注册了回调函数或拦截器对象。

通过这些功能和特性,Hook 类为 tapable 提供了一种机制,用于在不同的时机执行注册的回调函数,并支持对回调函数进行拦截和修改。这为插件系统、事件系统和钩子机制提供了基础框架和工具。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

C++操作xlsx初体验:OpenXLSX(建议使用)、li

发表于 2024-04-25

引言

C++操作xlsx着实麻烦,所以为此精心创作水一篇文章也是很OK的吧。

本文GitHub传送门。作者:hans774882968以及hans774882968以及hans774882968

本文52pojie:www.52pojie.cn/thread-1917…

本文juejin:juejin.cn/post/736168…

本文CSDN:blog.csdn.net/hans7748829…

本文打算体验的库:

  • libxl。要氪金,否则使用的功能会受限。这里我就不氪了。
  • OpenXLSX。

体验结果:建议使用OpenXLSX。

环境

  1. Windows10 64位
  2. g++.exe (x86_64-win32-seh-rev1, Built by MinGW-Builds project) 13.1.0
  3. VSCode+CMake插件+CMake Tools插件。本项目是用CMake Tools插件创建的,过程不赘述。

如何下载高版本g++

咕果一下,找到一个现成的。

VSCode修改CMake Tools插件使用的g++版本

左侧导航栏点击CMake的图标,左上角的PROJECT STATUS处有一个树形组件,其中有Configure节点,节点下有一项GCC 13.1.0 x86_64-w64-mingw32,hover会出现铅笔,点击即可修改。若没有看到你想选的编译器,可以先选择[Scan for kits]让插件先帮你找下。

执行哪个二进制文件

同上找到树形组件,其中有Debug和Launch节点,hover会出现铅笔,点击即可修改你想执行的二进制文件。

libxl

libxl官方文档并没有给出VSCode+CMake Tools如何配置,但可以参考eclipsecpp的配置文档。添加的配置如下:

1
2
3
4
cmake复制代码# 路径必须用左斜杠分隔
include_directories(D:/libxl-4.3.0.14/include_cpp)
target_link_libraries(libxl_read_xlsx_demo D:/libxl-4.3.0.14/lib64/libxl.lib)
target_link_libraries(libxl_write_xlsx_demo D:/libxl-4.3.0.14/lib64/libxl.lib)

另外需要注意,一定记得把bin64/libxl.dll复制到exe文件所在的文件夹下,否则能编译成功,但运行时会静默失败。

示例代码看官方文档就会写了,这里就不贴出来了,只列出一些注意点。

一、Book* book = xlCreateXMLBook();用于读写07及以后版本的xlsx。

二、cmake生成的二进制文件在build文件夹下,而我的示例xlsx在根目录下,所以考虑先getcwd再拼接出所求路径:

1
2
3
4
5
6
7
8
9
10
cpp复制代码#include <bits/stdc++.h>
#include <direct.h>
// 省略其他头文件
using namespace std::filesystem;

string get_inp_xlsx_path() {
char* cwd_buffer = getcwd(NULL, 0);
path p = canonical(path(cwd_buffer) / ".." / "inp" / "example.xlsx");
return p.string();
}

三、调用canonical前需要保证路径的文件/文件夹存在。为此,我们引入了utils.h utils.cpp,并修改cmake:

1
cmake复制代码add_executable(libxl_write_xlsx_demo libxl_write_xlsx_demo.cpp utils.cpp utils.h)

OpenXLSX

参考其GitHub的README,我选用了最简单的方式(和参考链接1一样的方式):先复制源码到项目根目录,然后配置cmake:

1
2
3
4
5
6
7
cmake复制代码add_executable(opxl_read_xlsx_demo opxl_read_xlsx_demo.cpp)
add_executable(opxl_write_xlsx_demo opxl_write_xlsx_demo.cpp utils.cpp utils.h)

add_subdirectory(OpenXLSX)

target_link_libraries(opxl_read_xlsx_demo OpenXLSX::OpenXLSX)
target_link_libraries(opxl_write_xlsx_demo OpenXLSX::OpenXLSX)

最后导入:

1
2
cpp复制代码#include <OpenXLSX.hpp>
using namespace OpenXLSX;

于是就遇到了报错:uint32_t等未定义,需要导入cstdint。我搜索报错信息,只找到了这个issue。于是我就按照报错提示,给若干.hpp手动添加了#include <cstdint>。幸运的是,添加后就能编译成功了。

示例代码看官方Demo就会写了,这里就不贴出来了,只列出一些注意点。

一、官方Demo似乎没有给读取Excel的API,这里说一下:

1
2
3
4
5
6
7
cpp复制代码void read_example_xlsx() {
string xlsx_path = get_inp_xlsx_path();
XLDocument doc;
doc.open(xlsx_path);
auto wks = doc.workbook().worksheet("Sheet1");
read_data(wks);
}

二、和libxl不同,OpenXLSX写入的公式不会在打开Excel时自己计算好结果。libxl官方说这不是bug,是feature:

Nota that OpenXLSX does not calculate the results of a formula. If you add a formula to a spreadsheet using OpenXLSX, you have to open the spreadsheet in the Excel application in order to see the results of the calculation.

三、同理,日期写入后打开Excel文档,也是一个浮点数,需要手动调格式才能看到日期。

参考资料

  1. 配置OpenXLSX:blog.csdn.net/weixin_4456…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…161718…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%