IPP网络打印协议解析:从HTTP事务到属性协商与排障

发布时间:2026/11/6 6:44:26

IPP网络打印协议解析:从HTTP事务到属性协商与排障 简介面向网络协议学习者和打印服务开发者的 IPP 网络打印协议源码包聚焦 HTTP/1.1 之上的打印作业提交、打印机状态与属性查询、作业控制等交互流程适合用于理解跨平台远程打印的标准化报文结构与客户端、服务端通信机制。压缩包共 32 个文件约 35KB以 8 个 C 源文件和 5 个头文件为主体辅以 Makefile 构建脚本、readme 说明及 svn 元数据文件体量轻便便于梳理协议实现骨架。源码涵盖请求解析、状态机跟踪、数据编码、错误处理以及 HTTPS 安全传输等关键环节可作为网络编程与协议分析的学习样例开发者也能据此观察打印服务的基本组织方式为自定义打印客户端、服务端或系统集成提供参考。目前已有 3876 人学习适合具备一定 C 语言与 TCP/IP 基础、希望深入 IPP 实现细节的读者研读。1. 打印机不是“被驱动”的IPP 把一次打印变成了一次 HTTP 事务IPPInternet Printing Protocol网络打印协议最反直觉的一点是它根本不关心打印机怎么把墨点喷到纸上。它关心的是一台打印机如何作为一台 HTTP 服务器靠一份属性清单把自己的能力说清楚再接收一个可查询、可取消、有状态的“作业资源”。传统打印是“驱动把文档翻译成设备私有字节流再往一个只管收数据的端口灌进去”而 IPP 把这件事拆成了两层文档格式的翻译交给 document-format 协商传输和状态交给 HTTP 与属性模型。这解释了一个特别常见的困惑——同一台打印机为什么在 A 机器上装完驱动就能用在 B 机器上却连“缺纸”都报不出来。因为前者走的是驱动加原始端口后者走的是 IPP两者的信息量根本不在一个量级。搞懂这套属性协商的时序和编码几乎所有“打不出来”的问题都能在几分钟内定位到具体是哪一步出的问题。2. IPP 为什么长在 HTTP 上一次作业到底发几个请求IPP 的传输层不是自己造的而是直接复用了 HTTP/1.1。这个决定带来的直接后果是一次打印不再是一条命令而是一串有明确先后顺序的事务。理解这串事务比记住任何一个属性名都重要。2.1 从“私有字节流 端口”到“资源 方法”老式打印的路径很短驱动把页面渲染成设备能懂的页面描述语言客户端把它塞进一个 TCP 连接目标端口通常是 9100 之类的原始端口或者走 515 端口的行式打印协议。这条路径的致命缺陷不是慢而是没有回执。字节流灌完之后客户端无法确认对方是真的接收了、还是打印机还在预热、还是纸张卡在中间通道里。状态只能靠人跑到机器面前看面板。IPP 换了一种建模方式打印机是一个可以通过 URI 定位的资源作业是它的子资源。客户端向打印机 URI 发 POST请求体里带三样东西——要执行的操作、一组描述这次操作意图的属性、以及可选的文档数据。响应体里带回来的是状态码和一组描述结果的属性。这样一来“打印机有没有纸”“支不支持双面”“作业现在到哪一步了”全部变成了可以机器读取的字段而不是驱动安装包里那份写死的配置文件。这也是免驱方案能成立的根本原因。驱动里最有价值的部分其实不是渲染代码而是那份“这台机器支持什么”的能力描述。IPP 把这份描述从客户端搬到了服务端客户端只要能读懂xxx-supported系列属性就能自己拼出可用选项。注意能力查询本身也是一次完整的 IPP 请求同样要 POST不是 GET。请求体里必须带attributes-charset这类必选操作属性所以你不能用浏览器地址栏去试探。2.2 三种提交模型Print-Job、Create-Job Send-Document、Print-URI规范里给了三条路径实际工程中常用的是前两条。第一条是一体化提交操作码0x0002Print-Job。请求体里同时包含操作属性组、作业属性组和文档数据文档数据一直延续到请求体结束。服务端读完最后一个字节就认为文档传完了作业自动进入待处理状态。这种方式最简单缺点是原子性太强属性错了、文档格式错了整次请求就废了而且一个作业只能有一份文档。第二条是分离式提交先用0x0005Create-Job创建一个空作业拿到job-uri再用若干个0x0006Send-Document往这个作业里追加文档最后一个请求把last-document设成true表示封口。这种方式支持一个作业多份文档而且每份文档可以带自己的document-format混合提交 PDF 和位图就成了可能。第三条是0x0003Print-URI只给一个 URI让打印机自己去取文件。这个操作在规范里存在但在实际部署中支持率很低因为涉及服务端主动发起网络请求安全边界和网络可达性都很麻烦。一次典型的分离式作业事务序列大致是这样步骤操作码方向请求体携带关键属性10x000BGet-Printer-Attributes客户端 → 打印机无文档printer-uri、requested-attributes20x0004Validate-Job客户端 → 打印机无文档job-name、document-format、sides30x0005Create-Job客户端 → 打印机无文档job-name、requesting-user-name40x0006Send-Document客户端 → 打印机文档 1job-uri、last-documentfalse50x0006Send-Document客户端 → 打印机文档 2job-uri、last-documenttrue60x0009Get-Job-Attributes客户端 → 打印机无文档job-uri、requested-attributes第 2 步的 Validate-Job 值得单独说。它只做属性校验不创建作业返回的就是“这组属性我能不能接受”。很多客户端跳过这一步直接 Create-Job结果就是作业创建成功、文档也传完了最后卡在job-state6processing-stopped不动排查成本立刻翻倍。把校验前移是这套协议给出的后悔药。第 6 步的轮询也不是可选项。作业提交成功和作业打印完成是两件完全不同的事前者只说明服务端接受了资源后者要看job-state走到9completed。2.3 631 端口、TLS 与“为什么必须是 POST”标准端口是 631加密版本走ipps方案本质是 HTTP over TLS。选 631 不是技术必然是注册端口的分配结果但它在工程上有个好处不需要额外开放高位端口防火墙策略里加一条规则就能管住。必须用 POST 的原因有三个。其一请求体是二进制结构长度在构造时不一定已知需要靠Content-Length或分块传输来界定边界这是 POST 的语义范畴。其二作业创建不是幂等操作重发一次就会打出两份而 PUT 的语义恰恰鼓励重试。其三请求体里必须携带必选的操作属性一个空 body 的 GET 根本表达不了“我要查询哪些属性、用什么字符集”这层意思。顺带提一句分块传输。当文档体积较大、客户端又不想先算好总长度时会启用分块编码。这个选择在直连场景下没问题一旦中间隔了某些会重写报文体的设备IPP 的二进制属性就有被破坏的风险——这是后半部分排查章节里一个很典型的故障源头。3. 二进制编码属性是怎么被塞进请求体里的IPP 的请求体不是文本是一段有严格布局的字节序列。这段布局的设计目标很明确让解析器可以单向流式读取不需要回退、不需要找分隔符、不需要预先知道总长度。3.1 八字节定长头与组标签任何一次请求或响应的前 8 个字节都是固定的2 字节版本号主版本、次版本各 1 字节、2 字节操作码或状态码、4 字节 request-id。request-id 由客户端生成响应必须原样带回用来把异步乱序的响应和请求对上号。这个字段在并发提交多个作业时是唯一的关联依据丢了它客户端就无法判断收到的响应属于哪一次提交。8 字节之后是属性组序列。每个组以一个单字节的组标签开始0x01operation-attributes操作属性组描述“这次请求要干什么”0x02job-attributes作业属性组描述“这份作业怎么打”0x04printer-attributes打印机属性组只出现在响应里0x05unsupported-attributes不支持属性组服务端用来回话“你给的这些我不认”0x03end-of-attributes结束标记之后如果还有字节那就是文档数据组的顺序是有语义的。操作属性组必须第一个出现因为里面的attributes-charset和attributes-natural-language决定了后续所有文本类属性怎么解码。如果解析器先读到作业属性再去读字符集那就已经晚了。3.2 value-tag、name-length、value-length 的三段式组内每个属性的编码格式是固定的四段1 字节 value-tag、2 字节 name-length、name 字符串、2 字节 value-length、value 字节。value-tag 决定了 value 该怎么解释这是整个编码的核心设计。value-tag类型值的编码方式0x21integer4 字节大端有符号整数0x22boolean1 字节非零为真0x23enum4 字节大端整数取值范围由规范枚举0x33rangeOfInteger两个 4 字节整数表示上下界0x34/0x37begCollection / endCollection集合的开始与结束标记0x41textWithoutLanguageUTF-8 文本0x42nameWithoutLanguageUTF-8 名称0x44keyword受限取值的 UTF-8 关键字0x45uriURI 字符串0x47charset字符集名必须是小写0x48naturalLanguage语言标签0x49mimeMediaTypeMIME 类型字符串两个长度字段都是 2 字节大端无符号数这意味着单个属性名和单个属性值的长度上限都是 65535 字节。绝大多数属性远远用不到这个量级但文档名称这类由用户输入的内容确实可能超限超了就只能截断或者干脆拒绝——这是协议层面的硬约束不是实现偷懒。type 和 tag 分离是个有意思的取舍。同一个“双面打印”的语义在请求里可能用keyword表示two-sided-long-edge在响应里可能被放进一个集合结构里。解析器必须按 tag 分支处理不能假设某个属性名的类型是固定的。3.3 一个可以对着抓包结果跑的解析器下面这段解析器覆盖了最常用的 tag足以把一次 Get-Printer-Attributes 的响应完整展开。它的结构是典型的单遍扫描读一个 tag根据 tag 决定后面几个字节怎么解释然后指针前移。import struct # 属性组起始标签到名称的映射 GROUP_TAGS { 0x01: operation-attributes, 0x02: job-attributes, 0x04: printer-attributes, 0x05: unsupported-attributes, } END_OF_ATTRIBUTES 0x03 def decode_value(tag: int, raw: bytes): # 整数和枚举都是 4 字节大端 if tag in (0x21, 0x23): return struct.unpack(i, raw)[0] # 布尔只占 1 字节非零即真 if tag 0x22: return raw ! b\x00 # 区间是两个 4 字节整数用元组表示上下界 if tag 0x33: return struct.unpack(ii, raw) # 所有文本类属性统一按 UTF-8 解容错处理非法字节 if tag in (0x41, 0x42, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49): return raw.decode(utf-8, replace) # 其余octetString、集合标记等原样返回交给上层处理 return raw def parse_ipp(body: bytes) - dict: # 前 8 字节主版本、次版本、操作码或状态码、request-id major, minor, code, request_id struct.unpack(BBHI, body[:8]) pos, groups, current 8, {}, None while pos len(body): tag body[pos] if tag END_OF_ATTRIBUTES: break if tag in GROUP_TAGS: current GROUP_TAGS[tag] groups[current] {} pos 1 continue # 单个属性value-tag(1) name-length(2) name value-length(2) value name_len struct.unpack(H, body[pos 1:pos 3])[0] name body[pos 3:pos 3 name_len].decode(utf-8, replace) vpos pos 3 name_len value_len struct.unpack(H, body[vpos:vpos 2])[0] raw body[vpos 2:vpos 2 value_len] value decode_value(tag, raw) # name-length 为 0 表示这是上一个属性的追加值属于同一个 1setOf if name_len 0: last_key list(groups[current].keys())[-1] if not isinstance(groups[current][last_key], list): groups[current][last_key] [groups[current][last_key]] groups[current][last_key].append(value) else: groups[current][name] value pos vpos 2 value_len return { version: f{major}.{minor}, code: code, request-id: request_id, groups: groups, }几个关键点值得单独说明。第一struct.unpack(BBHI, ...)里的表示大端这是网络字节序也是 IPP 规定的唯一字节序不存在小端变体。第二value_len是 2 字节所以raw的最大长度是 65535遇到超长值只能截断解析器里不能假设一定能读到完整内容。第三追加值的判定完全靠name_len 0这个设计让多值属性的编码不需要额外的计数或结束标记代价是解析器必须记住“上一个属性名是什么”不能用简单的字典赋值。code字段的双重身份也容易让人出错。解析请求时它是操作码解析响应时它是状态码两者共用同一个位置。同一个解析函数复用到两个方向上必须由调用方根据上下文决定怎么解释这个值。3.4 1setOf 与 name-length0 的编码歧义1setOf是 IPP 里表示“一个以上值”的类型大量出现在xxx-supported系列属性上。它的编码方式是第一个值正常写 name后续值把 name-length 置为 0value-length 照常写。解析器读到 name-length 为 0就知道这是上面那个属性的又一个值。问题出在两个地方。其一追加值和“名称为空字符串的属性”在字节层面没法区分。规范的做法是约定属性名不能为空所以 name-length 为 0 就只可能是追加值。但如果自己写构造器时不小心传了个空名字生成出来的字节流会被对端理解成追加值排查时非常隐蔽。其二集合类型begCollection到endCollection内部也可能出现 memberAttrName这里的 name-length 处理规则和普通属性不一样混在一起解析会直接错位。我一般会在解析器外面再包一层规范化把所有单值属性统一转成列表让上层代码不用关心“这个属性到底是单值还是多值”。属性的基数信息其实在服务端能力里是明确的客户端不该靠猜。4. 属性协商打印机能力是怎么被问出来的能力协商是 IPP 真正区别于老式打印的地方。它不是一个可选的增强而是整个作业提交流程的前置条件。搞清楚这套协商模型就能解释为什么同一个参数在这台机器上生效、在那台机器上被静默忽略。4.1 能力矩阵supported、default、ready 三件套每一个可配置的打印参数在打印机属性里通常对应三个属性名构成一个三元组sides-supported取值范围是one-sided、two-sided-long-edge、two-sided-short-edge中若干个表示这台设备支持哪些双面方式sides-default如果不指定sides服务端会用哪个值sides-ready当前这一刻实际可用的是哪些比如双面单元故障时它就只剩one-sided-supported是静态能力声明的是“理论上能做什么”-ready是动态状态声明的是“现在能做什么”。这两个必须分开因为设备的长边装订单元可能临时不可用但能力描述不该因此改变。客户端如果只看-supported就假定一定能用遇到-ready收窄的情况就会失败。-default的作用常被低估。当客户端没有显式指定某个作业属性时落到的就是默认值。默认值在不同设备上差异很大print-color-mode-default在一台黑白机上只能是monochrome在彩色机上可能是color也可能是auto。依赖默认值就等于放弃了对输出结果的控制权。4.2 ipp-attribute-fidelity失败还是降级这是整个协商模型里最关键的一个布尔属性也是最容易被客户端忽略的一个。当ipp-attribute-fidelity为true时客户端在说我列出的每一个作业属性都必须被严格满足有一个做不到就整个作业失败不要给我打折扣。当它为false时客户端在说尽力而为做不到的你可以替换成你能做的或者在unsupported-attributes组里告诉我你跳过了哪些。服务端的回应也因此分三种。全部满足返回0x0000successful-ok。部分被替换或忽略且 fidelity 为 false返回0x0001successful-ok-ignored-or-substituted-attributes同时在响应里带上unsupported-attributes组列出被替换的具体属性名。如果 fidelity 为 true 且存在无法满足的属性返回0x040Bclient-error-attributes-or-values-not-supported。大部分客户端图省事把 fidelity 设成false结果就是用户选了双面、选了高质量打出来还是单面草稿而客户端一声不吭。这不是打印机的错是客户端主动放弃了知情权。生产级的客户端应该设true或者至少在收到0x0001时把unsupported-attributes里的内容展示给用户。下面是构造一个带 fidelity 的最小作业请求的写法import struct # 操作码常量 OP_PRINT_JOB 0x0002 OP_GET_PRINTER_ATTRIBUTES 0x000B def attr(tag: int, name: str, value: bytes) - bytes: 按 IPP 的四段式编码一个属性 n name.encode(utf-8) return bytes([tag]) struct.pack(H, len(n)) n \ struct.pack(H, len(value)) value def build_print_job(printer_uri: str, request_id: int, doc: bytes, sides: str two-sided-long-edge) - bytes: # 版本 2.0操作码 Print-Jobrequest-id 用于响应关联 header struct.pack(BBHI, 2, 0, OP_PRINT_JOB, request_id) body b\x01 # 操作属性组开始 body attr(0x47, attributes-charset, butf-8) # 规范强制 utf-8 body attr(0x48, attributes-natural-language, bzh) body attr(0x45, printer-uri, printer_uri.encode()) body attr(0x42, requesting-user-name, bdemo-user) body attr(0x49, document-format, bapplication/pdf) body b\x02 # 作业属性组开始 body attr(0x42, job-name, 测试文档.encode(utf-8)) body attr(0x44, sides, sides.encode()) # keyword 类型 body attr(0x22, ipp-attribute-fidelity, b\x01) # true禁止静默降级 body b\x03 # 属性结束之后是文档 return header body doc这段代码里attr()是通用编码器四个参数分别对应 value-tag、属性名、值的原始字节。build_print_job()的行为分三段先写 8 字节头再按组写属性和组标签最后拼上文档数据。ipp-attribute-fidelity用0x22的 boolean tag 编码值只有 1 个字节写b\x01就是真。把sides放到作业属性组而不是操作属性组也是必须的——服务端是按组来区分属性语义的放错组会被当成未知属性直接丢进unsupported-attributes。4.3 job-state 状态机与 printer-state 的联动作业状态是一个枚举值取值 3 到 93pending、4pending-held、5processing、6processing-stopped、7canceled、8aborted、9completed。看起来很直白但真正有用的信息在于它和打印机状态的联动关系。printer-state只有三个值3idle、4processing、5stopped。当打印机状态变成 stopped所有处于 processing 的作业会被推到6processing-stopped。这是一个非常关键的区别作业没有失败也没有被取消它只是被挂起了。原因可能缺纸、可能卡纸、可能某个人在面板上按了暂停。等打印机恢复成4作业会自动回到5。没有这层联动客户端只能看到一个不动的5然后无限等待。有了这层联动客户端在轮询job-state的同时也读一下printer-state和printer-state-reasons立刻就能知道是介质问题还是机械故障。这种“作业状态跟着设备状态走”的设计把设备层的物理事件透传到了协议层是老式打印协议完全不具备的能力。4.4 document-format 协商与八位字节流的陷阱document-format决定服务端怎么解释后面那段文档数据。常见取值有application/pdf、image/jpeg、以及各种位图光栅格式。当客户端写application/octet-stream时语义是“我不知道这是什么格式你自己嗅探”。嗅探听起来很智能实际是个坑。服务端只能靠文件头魔数判断判断失败就没有第二次机会作业直接进8aborted。更麻烦的是某些实现会把嗅探失败包装成“格式不支持”返回错误的位置非常靠后客户端很难判断到底是自己的 PDF 有问题还是格式协商出了问题。稳妥的做法是先查document-format-supported从里面挑一个双方都认的明确类型别把决定权交出去。如果必须用八位字节流至少要在 Validate-Job 阶段先试一次把格式问题挡在作业创建之前。5. 排查IPP 作业失败的五个现场与定位顺序IPP 的排障难点在于错误信息分散在三个层次HTTP 层、IPP 状态码层、属性层。绝大多数误判都源于只看了第一层就下结论。下面这五个现场是我在不同项目里反复遇到过的顺序也大致对应从外到内的排查路径。5.1 现象HTTP 返回 200但打印机毫无反应现象很迷惑人抓包看 HTTP 状态码是 200响应体也有内容可纸就是不出来客户端也报成功。原因在于 IPP 把自己的状态码放在了响应体的第 3、4 字节HTTP 的 200 只代表“这次 HTTP 事务完成了”完全不代表“这次打印操作成功了”。服务端完全可以在 HTTP 200 的响应里返回0x0406client-error-not-found表示你请求的作业不存在。解决方式是先解出响应头的code字段再去判断。用前面那段解析器parse_ipp(resp_body)[code]就是真实状态。我在任何涉及打印的客户端里都会把这个字段单独打日志从不依赖 HTTP 状态码。5.2 现象请求被拒返回0x040A或 HTTP 415现象是作业根本创建不起来响应里明确说文档格式不支持。原因通常有两个。一是客户端声明了application/octet-stream但传的内容服务端嗅探不出来比如文件头被截断、或者传的是个压缩包。二是客户端声明的格式根本不在document-format-supported列表里比如某些设备只认光栅位图你却直接丢了个 PDF 过去。解决办法是先做一次 Get-Printer-Attributes把document-format-supported取出来做交集选一个靠谱的类型再提交。如果交集为空那就得在客户端侧做格式转换别指望服务端能兜底。5.3 现象作业长期停在job-state5processing永不结束现象是状态一直是 processing客户端轮询几十次都不变最后超时。原因几乎都是打印机侧被挂起了而客户端没读printer-state。缺纸、卡纸、面板暂停、某个耗材到寿命都会让printer-state变成5stopped进而把作业推到6processing-stopped。如果客户端只看job-state会误以为是 processing 卡死。解决办法是轮询时同时请求job-state、job-state-reasons、printer-state、printer-state-reasons这四个属性。job-state-reasons里会给出media-empty、media-jam这类关键字直接指向物理原因。这一步做完原本的玄学问题就变成了可读的错误码。5.4 现象中文作业名到服务端变成乱码或问号现象是打印出来的作业列表里作业名全是方块或问号。原因有两个层面。第一层是attributes-charset必须是小写的utf-8这是规范强制的写成UTF-8或者gbk会被直接拒绝。第二层是很多人把语言标签塞进了 charset写成zh-CN这等于声称自己用了一个叫“zh-CN”的字符集服务端当然解不了。真正的语言标签应该放在attributes-natural-language里。解决办法是把这两个属性严格区分charset 永远是utf-8natural-language 才是zh或者zh-cn。文本值本身一律 UTF-8 编码不要做二次转码。5.5 现象双面、装订、色彩模式设置被静默忽略现象最隐蔽作业成功创建也成功打印就是参数没生效输出和默认设置一模一样。原因通常是三类。一是属性放错了组把作业属性放进了操作属性组服务端不认识就跳过了。二是ipp-attribute-fidelity设成了false服务端把不支持的值替换成默认值还回了0x0001客户端没读这个状态码就当成完全成功。三是用了缩写或大小写不对的关键字比如two-sided少了后缀而 keyword 类型是精确匹配的。解决办法是打开 fidelity并在收到0x0001时解析响应里的unsupported-attributes组把被替换的属性名和值原样展示或记录。这一个动作能解决掉大半“设置了但没用”的投诉。6. 设计取舍为什么 IPP 宁可“笨”也不用 JSON把 IPP 的编码摊开看它确实不够现代固定偏移、魔数 tag、2 字节长度上限、追加值靠长度为零来暗示。如果今天重新设计很多人第一反应会是换成 JSON 或者某种 schema 驱动的文本格式。但把几个维度摆在一起这个选择的合理性就出来了。维度IPP 二进制文本格式JSON / XML解析方式长度前缀单遍流式指针一路前移需要词法分析或递归下降可能回溯类型信息value-tag 显式标注不依赖 schema靠约定或外部 schema数字与字符串易混体积属性名只出现一次整数定长 4 字节字段名重复数字转 ASCII 有膨胀上限单值 65535 字节硬边界无固定上限但解析器内存压力不可控演进靠-supported属性自描述能力需要版本号或 schema 注册中心调试肉眼不可读必须有解析器直接可读最关键的一条是“单遍流式”。打印机的固件资源极其有限解析器必须能在常量内存里跑完整个请求不能为了找一个闭合标签把整段 body 缓存下来。长度前缀恰好满足这个约束读到长度就知道后面多少字节处理完就丢内存占用是常数。文本格式在同样场景下要么限制最大报文长度要么引入流式解析器两条路都比长度前缀复杂。第二条是类型的确定性。sides到底是字符串还是枚举copies到底是整数还是字符串在 JSON 里要靠约定在 IPP 里由 tag 直接给定。这省掉了整个 schema 校验层对固件来说是很实在的收益。代价也很明确调试成本高。你不能靠curl加肉眼看请求必须写解析器。这就是为什么这类协议的排障总是显得神秘——不是协议本身有多复杂是中间少了一层可读性。我的习惯是每接一台新设备先用 Get-Printer-Attributes 把原始响应存一份到本地再用解析器展开成可读的属性表。这份快照既是能力清单也是后续所有“参数为什么不生效”的对照基准。从那以后我每次接入新的打印设备都强制自己先走一遍“取能力快照、解包、对着规范核属性”的流程再动手写提交逻辑翻车的次数明显少了。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/11/6 6:44:26

沈阳企业官网 AI 爬虫抓取验证:日志 4 个字段怎么核

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/6 7:39:28

网页嵌入地图找不到按钮?高德地图iframe嵌入实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/6 7:39:28

追踪亚马逊雨林的年际变化:FOREST//LOSS DETECTIVE 实战指南

追踪亚马逊雨林的年际变化:FOREST//LOSS DETECTIVE 实战指南 当卫星影像穿越云层,清晰记录下每一片森林的消失,我们便拥有了保护地球之肺的有力武器。本篇博客将带你用 Earth Engine 和 Hansen 全球森林变化数据集,在巴西亚马逊的朗多尼亚州(Rondnia)绘制出一张张森林损失…

2026/11/6 7:34:28

Formality:Bug记录

相关阅读 Formalityhttps://blog.csdn.net/weixin_45791458/category_12841971.html?spm1001.2014.3001.5482 本文记录博主在使用Synopsys的形式验证工具Formality中遇到的一个Bug。 Bug复现 情况一 // 例1 module dff (input clk, input d_in, output d_out …

2026/11/6 7:34:28

OpenCV 高级 001-图像修复

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/6 7:34:28

02:奇数单增序列

/*** 题目名称&#xff1a;奇数单增序列 <p>* 题目来源&#xff1a;http://noi.openjudge.cn/ch0110/02/ <p>* 程序功能&#xff1a;按升序输出指定整数序列中的奇数** author 潘磊&#xff0c;just_panleijust.edu.cn* version 1.0*/import java.util.List; impor…

2026/11/6 7:34:28

高阶数据结构(C++)

一、并查集 1.并查集原理 在一些应用问题中&#xff0c;需要将n个不同的元素划分成一些不相交的集合。开始时&#xff0c;每个元素自成一个 单元素集合&#xff0c;然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询某一 个元素归属于那个集合的运算。适…

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代&#xff0c;基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而&#xff0c;与配合度高的人类不同&#xff0c;宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态&#xff0c;更无法控制光照环境。实际应用中&#xff0c;我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/6 1:03:39

MCU晶振电路设计全解析:从负载电容计算到PCB布局调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代&#xff0c;基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而&#xff0c;与配合度高的人类不同&#xff0c;宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态&#xff0c;更无法控制光照环境。实际应用中&#xff0c;我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑