ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

OpenRTB 2.5中文注解:计算广告对接实战指南

OpenRTB 2.5中文注解:计算广告对接实战指南 简介OpenRTB 2.5 中文翻译文档面向程序化广告开发者、DSP/SSP 技术人员及计算广告方向学习者解决英文协议文档阅读门槛问题。该协议采用 JSON 数据格式定义了 Ad Exchange 与 SSP、DSP 之间的实时竞价通信流程中文版可帮助国内从业者系统理解 Bid Request、Bid Response、Impression、User、Device、Site/App、Bidder Seat、Price Floors、Ad Slot 及 Ext 扩展等核心模块从请求构建到响应策略均有清晰说明同时覆盖 2.5 版本对 GDPR 隐私处理、视频音频富媒体支持、精准地理位置定位等新增能力为实际业务接入提供参考。资源为单一 PDF 文件压缩包仅 1.36MB便于离线查阅、全文检索或打印作为案头参考。文档翻译风格直观保留英文术语便于中英对照可直接用于团队学习与内部培训。目前已有 707 人学习下载适合初学者快速建立协议全景认知也适合广告算法工程师在实际项目中对照使用提升程序化交易的理解和落地能力。1. 每个做计算广告的人都该有一份OpenRTB 2.5中文注解做程序化广告的同行应该都有过这种经历接到需求要对接某个DSP或ADX对方甩过来一份OpenRTB 2.5英文原版PDF打开一看几百页字段多到怀疑人生。我先啃了一个月才理清BidRequest和BidResponse的结构后来做了份注释版直接在协议原文上标注字段含义和对接时踩过的坑效率直接翻倍。这套方法今天完整拆给你从协议对象结构到字段边界、从数据流向到排查思路一次讲透。适合刚入行的算法工程师、服务端开发也适合需要和媒体方掰扯请求参数的增长同学。2. 协议对象拆解从BidRequest到BidResponse的数据流向2.1 一次竞价请求里到底有些什么OpenRTB 2.5整个协议可以浓缩成一次HTTP请求和一次HTTP响应买方DSP从卖方ADX/SSP收到BidRequest解析后返回BidResponse。看起来简单但BidRequest内部的对象嵌套层级深很容易看晕。我拆的时候是按「请求是谁发的、用户是谁、流量在哪个页面、允许哪些出价方式」这个思路去分的。最顶层是BidRequest对象核心子对象包括Impression数组、Site或App对象、Device对象、User对象。Impression数组表示一次请求里可能同时存在多个广告位每个广告位有独立的ID、宽高、位深、格式类型。Site和App二选一用来描述流量环境——是网页流量还是应用内流量两边字段差异很大。Device记录设备信息包括UA、IP、设备ID、运营商、屏幕尺寸。User带用户ID和用户特征。一个常见的误解是把BidRequest当成一个扁平JSON去解析写出来的解析器又脆又难扩展。正确做法是先把每个对象定义成独立的class或struct再按协议里的嵌套关系组装。比如Impression里还有一个Pmp对象Pmp里还有Deal数组每一层单独建类型后面加字段时才不至于把代码改崩。2.2 请求侧的五个关键对象与字段映射翻译过程中我把BidRequest拆成五个必须掌握的根级对象分别是Impression、Site/App、Device、User、Regs。这五个对象基本决定了你能否正确解析一次请求。Impression是核心里面的Banner、Video、Native三个子对象分别对应横幅、视频、原生三种广告形态各自字段差异很大。横幅广告重点关注w、h、pos、battr视频广告重点看mimes、minduration、maxduration、protocols原生广告看Native对象的请求结构。Site和App的区别在于App多了一个storeurl字段和bundle字段用来标识应用商店地址和包名。Device对象的关键不是字段多而是字段的优先级——协议明确规定当Device携带了ip和ua时应优先使用Device里的值而不是从请求头部去解析。很多DSP在这个地方翻车一边用设备IP做地域定向一边又从请求头重新解析IP两边对不上导致定向失效。User对象里有个buyeruid字段这是媒体方给DSP的用户ID通常用于跨请求识别同一个用户。但注意buyeruid不等于cookie也不等于设备ID它是媒体方自己维护的映射关系。还有data数组里面装的是用户分段数据但这些数据往往来自第三方数据供应商解析时要做类型校验因为有些媒体方不按规范填segment的name字段。2.3 响应侧BidResponse的对象结构BidResponse比BidRequest简单得多核心就是Bid数组和seatbid数组。每个SeatBid代表一个广告账户或卖家席位SeatBid里包含一个或多个Bid对象。Bid对象里的字段直接决定这次竞价是否有效id、impid、price是三个必填字段price单位是CPM的千分之一即微单位很多新手在这里被坑——直接把数据库里的CPM价格填进去价格放大了1000倍广告根本不会赢。Bid对象里的adomain是广告主域名用于广告审核和内容过滤iurl是广告素材的预览地址cid是广告计划IDcrid是创意ID。这几个字段在日志分析和结算核对时非常重要特别是crid很多DSP内部把crid当创意唯一标识但协议里crid只是创意ID不保证全局唯一严谨的做法是crid加上dsp自身的账户ID拼成全局唯一键。还有一个字段是adm——实际广告内容。对于横幅和原生广告adm通常是HTML代码片段或JSON数据对于视频广告adm是VAST XML。协议规定adm和nurl二选一即可但如果两边都给了DSP应优先使用adm。这里的坑在于adm里的宏替换OpenRTB定义了十几个宏比如AUCTION_PRICE、AUCTION_ID这些宏需要DSP在返回前自己替换成真实值否则前端渲染时会展示成一段奇怪的字符串。2.4 协议里没有明说的两个约定OpenRTB 2.5文档里有一部分内容藏在规范里没展开但实际对接时绕不开。第一个是BidRequest里的tmax字段这个字段表示卖方期望买方在多长时间内返回响应单位是毫秒。协议没有规定默认值但行业普遍把tmax设置在120ms到200ms之间。如果你的DSP响应速度慢tmax被媒体方设置的很小你还没算完出价请求就超时了广告根本不会参与竞价。所以务必对自己的接口做压测保证P99在100ms以内。第二个是wseat和seat字段的配合。BidRequest里的wseat表示只接受指定卖家席位的竞价BidRequest里的seat数组表示买方允许使用的席位。DSP端要做的是在发起竞价前先检查自己的seat是否在允许列表里不在直接放弃避免白白消耗算力。媒体端如果发现请求里带了wseat但DSP返回的seatbid里的seat不在列表内会直接丢弃这条响应。这两个字段虽然不起眼但直接影响竞价通过率。3. 翻译文档的实战价值字段级注解与边界条件3.1 为什么英文原版读起来慢OpenRTB 2.5的原版规范是一份技术性很强的英文文档对非母语者不友好。它的主要问题不是词汇难而是表述高度压缩一个字段的描述往往只有一两句话缺少使用场景和边界条件。比如Impression对象里的displaymanager字段规范只说明这是广告SDK的渲染标识但完全没有提这个字段在不同广告形态下的含义差异——在App场景里它可能是SDK的版本号在Web场景里它可能是某个广告联盟的代号。再比如BidRequest里的test字段规范只写「1表示测试请求」但没有说明测试请求在计费、日志、数据上报层面都应该被忽略。如果你直接把测试请求当成正常流量接入线上链路不仅会造成数据污染还会在结算时产生纠纷。这些内容原版文档不会告诉你只有实际踩过坑的人才清楚。中文翻译的价值在于把字段描述从「能读出意思」升级为「能直接指导开发」。我在翻译过程中对每个字段做了四类标注字段含义、数据类型、必填与否、边界条件。这个做法让阅读效率提升很多开发同学拿到文档后能直接照着写代码而不是在英文原版里反复翻找字段定义。3.2 字段级翻译的标注体系从翻译到注解我把整个文档整理成了一套注解体系核心是四层标注基础定义、协议约束、行业经验、实现建议。基础定义指的是字段的官方英文解释的标准翻译这部分严格忠实于原版避免歧义。协议约束是指字段的可选值、取值范围、枚举类型这些都是开发时直接要用的。行业经验层标注的是在真实业务中该字段的典型用法和常见误用。实现建议层直接给出代码层面的处理方案。以Device对象的ifa字段为例基础定义是「广告主标识符」。协议约束是在iOS上通常为IDFA在Android上为GAID在Windows Phone上为Advertising ID。行业经验是部分媒体在用户拒绝跟踪时会传0或空值DSP端要区分处理不能直接当成无效请求丢弃。实现建议是ifa字段需要和服务端日志里的设备ID做关联但如果用户重置了广告标识符旧日志里的关联关系会断裂需要定时重算。这套注解体系的实操价值在于减少了沟通成本。之前开发同学遇到不懂的字段会反复来问每次都要翻原版文档去查现在直接把翻译文档扔给他自己就能定位到问题。更重要的是翻译后的字段命名保持和英文原版完全一致即JSON里的key避免出现「文档里写的是中文意思代码里用的是英文key」两头对不上的尴尬。3.3 翻译中的常见翻车点和质量控制翻译技术文档最怕的是表面流畅但术语不规范我第一版翻译也犯了这个错。其中最典型的争议是「impression」这个词有人在文档里译成「印象」有人译成「展示」还有人译成「曝光」。在计算广告领域「展示」和「曝光」都可接受但全文必须统一。我最终统一为「展示」并且在第一次出现时括号注明英文原词后续全部走统一译法。第二个翻车点是bidfloor这个字段直译是「出价底价」但在实际业务里它表达的是「最低可接受出价」。译成「出价底价」容易让人以为是一种出价方式译成「最低出价」又会和bid的lower bound混淆。最终译法定为「竞价底价」并在注解里补充说明这是卖方的期望价格下限DSP的出价必须高于这个值才可能赢得竞价。质量控制的完整流程分三步先按章节翻译并加注再由另一位懂协议的工程师做技术审校最后对照英文原版逐条核对所有key和枚举值。翻译里最危险的事情是自己编造术语比如把「seatbid」译成「座位竞价」正确译法是「席位出价」。为避免这个风险所有关键术语的第一处翻译都会加英文原文既保证了可读性又保留了技术准确性。4. 从翻译文档到可运行的代码核心模块的实现映射4.1 定义数据模型类型、嵌套与枚举拿到翻译文档后第一步要做的是把数据模型落地成代码。这里以Java为例说明数据类的定义方式。需要严格对照BidRequest的结构逐层定义类型每个字段名保持和JSON key完全一致。public class BidRequest { private String id; private ListImpression imp; private Site site; private App app; private Device device; private User user; private int tmax; private ListString seat; private int wseat; private int test; private Regs regs; // getter/setter省略 }逻辑说明BidRequest的顶层字段严格对应协议的JSON属性名其中id是必填项imp是数组site和app在规范上被定义为「二选一」但实际流量里偶尔会出现两个同时存在的情况。稳妥做法是两个字段都建做兼容性判断时以「imp的格式类型」为主要分支依据。参数说明tmax建议在解析时设置默认值协议没有写默认值实际对接时若媒体方没传DSP侧应主动设置一个合理值比如120ms避免后续超时控制逻辑拿不到值。test字段类型是int而不是boolean因为协议定义的枚举里0和1只是常规值未来可能扩展。4.2 Impression子对象的层级结构Impression是BidRequest里最复杂的对象内部嵌套Banner、Video、Native、Pmp四个可选对象。定义时要注意的是这些子对象全部是可选的但imp数组本身必须有至少一个元素。public class Impression { private String id; private int bidfloor; // 竞价底价单位是CPM的千分之一 private String bidfloorcur; // 底价货币单位默认USD private int instl; // 是否插屏1是0否 private String tagid; // 广告位ID或标签 private Banner banner; private Video video; private Native nativeObject; private Pmp pmp; }参数说明bidfloor的单位是微单位即CPM的千分之一货币单位默认是USD。如果媒体方传的是EURDSP在出价前必须做汇率换算否则可能出现「表面出价高于底价实际按汇率折算后低于底价」的丢单情况。tagid是媒体方定义的广告位标识这个字段在日志分析里是核心维度务必原样保留不要做清洗和截断。instl字段可以辅助判断广告形态——是普通横幅还是插屏但和banner对象的pos字段要配合着看单独依赖instl判断会有误差。4.3 BidResponse的构造与宏替换响应侧的代码比请求侧简洁核心是构造Bid对象并处理宏替换。宏替换是新手最容易漏的环节但少了它会直接影响竞价成功率和后期归因。// Node.js版本的Bid构造示例 const bid { id: bid_ Date.now(), impid: impId, // 必须等于BidRequest里的imp.id price: calculatedPriceInMicros, // 单位是千分之一CPM adid: creativeId, crid: creativeVersionId, adomain: [example.com], adm: creativeContent, // HTML/VAST/JSON nurl: https://dsp.example.com/win/notify?bid${AUCTION_PRICE} }; // 在返回前替换adm和nurl中的宏 const macroMap { ${AUCTION_PRICE}: bid.price, ${AUCTION_ID}: bidRequest.id, ${AUCTION_IMP_ID}: impId }; for (let key in macroMap) { bid.nurl bid.nurl.replace(new RegExp(key.replace(/[.*?^${}()|[\]\\]/g, \\$), g), macroMap[key]); }逻辑说明宏替换有两个方向——DSP在返回响应前需要把adm和nurl里的宏替换成实际值如果DSP选择在nurl通知URL里保留AUCTION_PRICE宏表示将价格回传延迟到竞价成功后的通知请求中处理。两种做法在行业里都有但要注意一致性如果adm里已经嵌入了价格nurl里就不要再用AUCTION_PRICE宏否则会在价格更新时产生两次不同的价格数据。参数说明adomain必须用数组而不是单个字符串这是和旧版协议最大的兼容性差异之一。impid字段必须精确匹配BidRequest中某个imp的id匹配不上媒体方会直接丢包。4.4 日志字段的落库设计解析完BidRequest和构造完BidResponse之后还需要考虑日志的落库结构。竞价日志是后期排查问题、做数据分析的基础字段设计得不合理后期会非常痛苦。一个简洁的竞价日志表至少需要包含以下字段request_idBidRequest的id、imp_id广告位实例id、dsp_id我方标识、seat_id我方席位、creative_id创意id、price出价微单位、bid_floor竞价底价、resultwin/lose/error、latency_ms处理耗时、timestamp。其中request_id和imp_id是联合唯一键用于标识一次竞价里的一个广告位。result字段建议用枚举值而不是字符串方便后续做聚合统计。日志字段的命名要和协议字段保持一致特别是price和bidfloor单位都是微单位。很多团队在日志里各自用自己的单位体系有的用元有的用分对账时非常痛苦。最稳妥的做法是统一按协议单位存储即微单位所有展示层需要元的地方在查询时做除法。这样可以保证日志数据与ADX侧的结算数据可以直接对齐不需要做二次换算。5. 避坑指南翻译与对接中的常见问题5.1 现象用直译字段名写代码导致JSON解析失败原因OpenRTB 2.5的JSON字段名是固定的必须和协议原文完全一致。翻译文档如果只翻译含义而不保留key名开发同学按中文含义取名字比如把imp写成了impressions解析出来的对象永远是空。解决翻译文档里的每个字段都必须同时保留原始key名和中文含义并且以原始key名为准做映射。我在文档排版时统一用「imp展示数组」这种格式先key后含义保证开发同学不会产生歧义。代码里的数据类字段名一律用协议key注释里写中文含义。5.2 现象BidRequest里的badv和bcat没做校验广告内容被媒体拒登原因BidRequest里的badv是广告主黑名单域名bcat是拒绝的IAB内容分类。DSP如果没有读取这两个字段做过滤就会出现「ADX明确说了不接某类广告DSP还是把这类广告送上去」的情况媒体方直接拒登或标记低质量流量。解决在竞价入口处加一个前置过滤器先把badv和bcat解析出来和待投放的广告计划做匹配。匹配规则是广告计划里的广告主域名命中badv直接跳过广告计划的内容分类命中bcat直接跳过。这个逻辑必须在出价计算之前执行不要等出价算完了再判断否则浪费算力还会拉高延迟。5.3 现象Device对象的ip字段和请求头里的ip不一致地域定向失效原因BidRequest的Device对象里带了ip请求头部也带了X-Forwarded-For或类似的头。两者的值在部分流量里不一致原因可能是媒体方做了IP清洗或代理转发。DSP如果只信请求头里的IP就会和协议里要求的一致性原则冲突。解决按协议建议以BidRequest中Device对象的ip字段为准请求头IP仅作为补充维度。代码里判断逻辑是如果Device下存在IPv4或IPv6字段则直接使用否则从请求头解析。注意IPv6字段的存在性很多媒体方的IPv6流量的IP只出现在IPv6字段里传统的IPv4解析逻辑会漏掉。5.4 现象tmax设置过长DSP响应慢导致整体超时原因tmax是媒体方定义的期望响应时长DSP如果自身处理链路过长在流量高峰期会超过tmax。媒体方通常不会等超时而是直接丢弃迟到的响应。症状是DSP在压测里赢率正常但线上赢率骤降。解决对DSP的竞价接口做分阶段耗时统计包括请求解析、定向过滤、出价计算、响应序列化四个阶段。优化顺序是优先做定向过滤的前置化把不需要参与出价的流量尽早拦截其次做创意内容的缓存避免每次请求都重新拉取素材最后做响应序列化的简化减少不必要的字段组装。5.5 现象日志里记录的竞价价格和学习时的出价价格对不上原因竞价日志里记的price如果是在宏替换前计算的原始值而最终返回媒体方的price经过了加价或减价逻辑两边就无法对应。在模型训练时用的标签如果用的是日志里未做最终调整的价格模型学出来的出价策略会和线上执行策略不一致。解决日志里必须同时记录原始出价和最终出价两个字段。原始出价用于模型训练最终出价用于结算对账。两个字段的差额就是策略层做的价格调整。缺失这个区分后期模型迭代时你根本说不清线上真实成交价到底是哪个。6. 从翻译文档反推竞价端点实现从truncate解析到出价策略验证读完翻译文档之后一个高价值的进阶动作是把协议描述反向实现为一个可测试的竞价端点。这里分享我的做法不依赖任何商业SDK直接用Node.js写一个最小的竞价服务把翻译文档当需求文档用。const express require(express); const app express(); app.use(express.json()); app.post(/bid, (req, res) { const br req.body; const imp br.imp br.imp[0]; if (!imp || !imp.id) return res.status(204).end(); const bid { id: bid- Math.random(), impid: imp.id, price: calculateBid(imp), crid: creative-001, adomain: [example.com], adm: html.../html }; res.json({ id: br.id, seatbid: [{ seat: dsp-seat-1, bid: [bid] }] }); }); function calculateBid(imp) { const floor imp.bidfloor || 0; const estimate estimateCtr(imp); // 理想情况下这里是CTR模型输出 return Math.max(floor 10, Math.round(estimate * 1000)); }逻辑说明这个端点的结构严格按BidResponse的约束来做id取请求id保持上下文一致seatbid里可以带多个seat。calculateBid函数里的floor加10是简单地确保出价高于底价的最小逻辑实际业务里estimateCtr一般是CTR模型或CVR模型的预估输出price所以用CPM微单位便于和媒体方结算。参数说明response里的id不需要重新生成直接复用request的id有助于日志串联全局追踪crid用字符串类型没有问题但注意如果同一个创意有多个版本crid需要区分版本。验证方法可以这样设计先用协议文档里给的示例请求做一次端到端冒烟测试确认字段名、类型和枚举值全部匹配然后构造一批异常请求包括缺少imp数组、imp缺少id、device无IP、site和app同时为空验证服务的容错能力最后用模拟流量做一次48小时不间断压测记录响应耗时和错误率。当所有场景都跑通后这个端点就可以作为正式对接的骨架工程。从那以后我每次对接新的ADX都会强制自己走一遍这个流程先读翻译文档理清对象层级和字段边界再写数据类和解析器然后起最小端点做验证最后再做日志字段设计。前两步看起来慢但省掉了后面联调时大量「这个字段原来是这样」的返工。希望帮到你少走点我走弯过的路。本文还有配套的精品资源点击获取
返回列表