news 2026/9/5 10:54:35

基于OSM数据构建城市知识图谱:从空间数据到智能应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于OSM数据构建城市知识图谱:从空间数据到智能应用

简介:本资源是一份面向高校人工智能与地理信息交叉方向课程大作业的OSM城市知识图谱构建实践方案,聚焦开放街景地图(OSM)数据的知识抽取、结构化建模与图谱可视化全流程。资源包共27个文件,涵盖6个XML格式的原始OSM解析中间数据、5个JSON文件用于实体与关系定义、4个CSV及2个XLSX表格存储POI与AOI属性信息,辅以4个Python脚本(build.py、tool.py等)实现数据清洗、标签映射与图谱生成,2个Jupyter Notebook(test.ipynb等)提供可运行的调试与演示环境,另有README.md和配置文件支撑快速复现。压缩包大小为26.25MB,目录结构分层清晰,含data/、code/、.idea/等模块,便于理解知识图谱构建中数据流与代码逻辑的对应关系。目前已有333人学习下载,适合具备Python基础与NLP初步认知的学习者开展知识图谱实战训练,直接获取从原始地理数据到Neo4j或NetworkX可加载图谱的完整技术路径与可执行代码。

1. 项目概述:从开放地图数据到城市智能“大脑”

最近在做一个挺有意思的项目,核心就是利用OSM(OpenStreetMap)数据来构建一个城市级别的知识图谱。你可能听说过知识图谱,它本质上是一种用图结构来组织和表示知识的技术,节点代表实体(比如“人民广场”、“地铁1号线”、“星巴克”),边代表实体间的关系(比如“位于”、“包含”、“相邻于”)。而OSM,这个被誉为“地图界的维基百科”,为我们提供了海量、免费且结构相对丰富的全球地理空间数据。把这两者结合起来,我们就能尝试为一座城市构建一个数字化的、可被机器理解和推理的“知识大脑”。

这个“大脑”能做什么?想象一下,你是一个城市规划师,想分析某个区域商业设施与公共交通站点的匹配度;或者你是一个物流算法工程师,需要理解复杂的道路通行规则(比如单行道、限高)来优化路径;又或者,你只是想开发一个更懂本地生活的智能助手,它能告诉你“从A地到B地,步行经过的几个街区里有哪些评价不错的咖啡馆和书店”。这些场景的背后,都需要对城市空间、设施及其关联关系有深度的结构化理解。传统的GIS(地理信息系统)数据可能更侧重于几何形状和基础属性,而知识图谱则擅长揭示和利用实体间丰富的语义关系,这正是其价值所在。

这个项目适合谁呢?如果你是对空间数据挖掘、知识图谱构建或智慧城市应用感兴趣的开发者、数据分析师或学生,那么接下来的内容会是一个不错的实操参考。整个过程会涉及数据获取、清洗、转换、建模到最终存储和应用的完整链条,我会尽量把每个环节的“为什么”和“怎么做”讲清楚,并分享一些我踩过的坑和总结的技巧。

2. 核心思路与技术选型:为什么是“OSM + 知识图谱”?

2.1 OSM数据的优势与挑战

选择OSM作为数据源,不是因为它完美无缺,而是因为它在开放性、覆盖度和数据维度上提供了一个绝佳的起点。

核心优势:

  1. 开放与免费:这是最根本的吸引力。你可以合法地下载全球任意区域的数据,用于商业或非商业项目,没有使用限制和费用壁垒。
  2. 丰富的语义标签(Tags):OSM的数据模型基于“节点(Node)”、“路径(Way)”和“关系(Relation)”,每个元素都可以通过无数的key=value标签来描述其属性。例如,一个way可以标记为highway=residential(居民区道路)、name=南京路,同时还可以有maxspeed=30lanes=2等属性。这种灵活的标签系统蕴含了巨大的语义信息。
  3. 社区驱动的持续更新:基于众包模式,OSM数据的更新速度在某些区域,尤其是城市,可能非常快,能及时反映新开的店铺、新建的道路等变化。

主要挑战:

  1. 数据质量不均:不同地区、不同贡献者导致的数据完整性和准确性差异很大。繁华都市的数据可能非常细致,而偏远地区则可能只有主干道。
  2. 标签体系松散:虽然丰富,但key=value的用法并非完全标准化。同一个含义可能有多种标签表示(如cuisine=chinesecuisine=chinese_food),需要大量的清洗和归一化工作。
  3. 几何与语义的耦合:在OSM原始数据(如.osm.pbf格式)中,地理几何信息(点、线、面)和语义属性是紧密绑定的。构建知识图谱时,我们需要将它们解耦:几何信息可能存入专门的时空数据库(如PostGIS)用于空间查询,而语义关系和属性则存入图数据库。

2.2 知识图谱的建模设计

我们的目标是将OSM的原始数据转换成一个城市知识图谱。这里的关键是设计一个贴合城市领域的概念模型(本体)。我参考了OSM的标签分类和一些通用的城市信息模型,设计了以下核心实体类型和关系:

核心实体类型:

  • 区域(District):如行政区、商圈、住宅区。对应OSM中boundary=administrativeplace=suburb等的关系或区域。
  • 道路(Road):包括各级公路、街道、小巷。对应highway=*的路径(Way)。
  • 交叉口(Junction):道路的相交点,可作为路径规划的关键节点。通常由道路路径的端点或特定节点衍生。
  • 兴趣点(POI):如餐厅、商场、学校、公园、地铁站。对应各种amenity=*, shop=*, leisure=*等的节点或区域。
  • 交通设施(Transport):如公交站、地铁站、停车场。是POI的一个子类,但关系更聚焦。
  • 建筑物(Building):对应building=*的路径或关系。

核心关系类型:

  • 位于(locatedIn):POI/建筑物 位于 某个区域或道路旁。
  • 属于(partOf):一个区域属于另一个更大的区域(如街道属于行政区)。
  • 连接(connectsTo):道路与道路在交叉口相连,或道路连接两个区域。
  • 包含(contains):一个区域(如公园)内部包含多个设施(如厕所、湖泊)。
  • 相邻(adjacentTo):两个区域或建筑物彼此相邻。
  • 拥有(has):一条道路拥有某些属性,如hasMaxSpeedhasLanes

注意:这个模型是一个起点,在实际项目中需要根据具体应用场景进行扩展或精简。例如,做物流配送可能需要细化道路通行限制(限高、限重、货车禁行)关系;做商业分析则需要强化POI品类品牌关系链。

2.3 技术栈选型与理由

一个完整的构建流水线需要多个工具协同工作。以下是我的选型及理由:

  1. 数据获取与预处理:osm2pgsql + PostGIS

    • osm2pgsql:这是将OSM的.osm.pbf格式数据导入PostgreSQL/PostGIS数据库的事实标准工具。它高效、稳定,并且提供了灵活的样式文件(.style)让你自定义哪些OSM标签需要导入到数据库表中。
    • PostGIS:作为空间数据库,它负责存储和处理原始的几何数据(点、线、面)。后续我们可以从这些几何数据中提取拓扑关系(如相邻、连接),这是构建图谱中空间关系的基础。选择它是因为其强大的空间计算能力和与PostgreSQL生态的无缝集成。
  2. 知识图谱存储:Neo4j

    • 为什么是图数据库?因为我们的核心是“实体-关系-实体”这种网状结构。关系数据库(如PostgreSQL)的多表JOIN在深度关系查询上性能堪忧,而图数据库是为这种场景而生的。
    • 为什么选Neo4j?它是目前最流行的原生图数据库,拥有成熟的Cypher查询语言(非常直观,类似“找到从A餐厅步行10分钟内能到达的所有地铁站”这样的查询写起来很自然),丰富的生态和文档。虽然JanusGraph等开源方案也可行,但Neo4j在易用性和开发效率上更胜一筹,适合快速原型和中等数据量级(城市级别)的应用。
  3. 数据处理与转换:Python (Pandas, OSMnx, py2neo)

    • Python是整个数据流水线的“粘合剂”。
    • Pandas:用于从PostGIS中提取出的表格数据进行清洗、转换和关联分析。
    • OSMnx:一个非常强大的Python库,专门用于从OSM下载、建模、分析和可视化街道网络。它可以直接将OSM数据构建成NetworkX图(复杂网络分析库),这对于提取道路拓扑(连接关系)极其方便,省去了大量自己处理几何相交计算的麻烦。
    • py2neo:Neo4j的Python驱动,用于将我们处理好的结构化数据批量导入Neo4j,创建节点和关系。
  4. 可视化与探索:Neo4j Browser / Gephi

    • Neo4j Browser:内嵌的Web工具,可以直接用Cypher查询并可视化结果图,是开发和调试的利器。
    • Gephi:如果需要更复杂的网络分析(如社区发现、中心度计算)或出版级可视化,Gephi是一个专业的开源选择。

3. 实操构建全流程解析

3.1 第一步:数据获取与入库

首先,我们需要获取目标城市的OSM数据。以“上海市”为例。

1. 下载数据:你可以从 Geofabrik 等网站下载按国家/地区分片的.osm.pbf文件。对于中国,可以下载china-latest.osm.pbf,然后用osmconvertosmfilter工具裁剪出上海区域(需要上海的边界框坐标)。更简单的方法是使用Overpass API,它支持通过查询语言直接下载特定区域的数据。不过对于构建全市图谱,下载完整分片文件再裁剪通常更稳定。

2. 搭建PostGIS环境:在服务器或本地安装PostgreSQL,并启用PostGIS扩展。创建一个专门的数据集,例如osm_shanghai

# 示例:创建数据库和扩展 createdb osm_shanghai psql -d osm_shanghai -c "CREATE EXTENSION postgis; CREATE EXTENSION hstore;"

注意hstore扩展很重要,因为osm2pgsql默认会将OSM的标签以hstore键值对格式存储在一个字段中,便于灵活查询。

3. 使用osm2pgsql导入数据:准备一个自定义的.style文件,决定哪些OSM标签成为数据库表的独立字段。对于知识图谱,我们关心的是语义,所以可以只导入几何和标签,不导入渲染相关的字段。一个简化的my.style文件可能包含:

node,way name text linear node,way highway text linear node,way amenity text linear node,way shop text linear node,way building text linear node,way railway text linear ...

然后运行导入命令:

osm2pgsql -c -d osm_shanghai -U postgres --hstore --style my.style shanghai.osm.pbf

-c表示创建新表(planet_osm_point,planet_osm_line,planet_osm_polygon等)。导入完成后,你就在PostGIS中拥有了上海市的结构化空间数据。

3.2 第二步:从空间数据到拓扑关系提取

这是构建图谱关系的关键一步。我们需要从几何数据中推导出实体间的空间关系。

1. 提取道路网络与连接关系:这是OSMnx大显身手的地方。我们可以直接用OSMnx下载上海的道路网络,但它底层也是调用Overpass API,对于大城市可能超时。更可靠的方法是使用我们已经导入PostGIS的数据。

首先,从PostGIS中导出主要道路(highwayin ('motorway', 'trunk', 'primary', 'secondary', 'tertiary', 'residential'))的线数据及其属性。然后,使用OSMnx的graph_from_gdfs函数或NetworkX库,基于线的端点(起点和终点)的坐标匹配来构建拓扑图。如果两条道路的端点坐标在容差范围内相同,则认为它们相连。

import geopandas as gpd import networkx as nx from shapely.geometry import Point, LineString import psycopg2 # 连接PostGIS,读取道路线数据 conn = psycopg2.connect("dbname=osm_shanghai user=postgres") sql = "SELECT osm_id, name, highway, maxspeed, geometry FROM planet_osm_line WHERE highway IS NOT NULL;" roads_gdf = gpd.read_postgis(sql, conn, geom_col='geometry') conn.close() # 构建一个图 G = nx.MultiDiGraph() # 使用有向多重图,因为道路可能有方向 for idx, road in roads_gdf.iterrows(): # 将LineString转换为点序列 coords = list(road['geometry'].coords) start_node = coords[0] end_node = coords[-1] # 确保节点存在,用坐标作为节点ID(可哈希) G.add_node(start_node, pos=start_node, type='junction') G.add_node(end_node, pos=end_node, type='junction') # 添加边(道路) G.add_edge(start_node, end_node, osm_id=road['osm_id'], name=road['name'], highway_type=road['highway'], maxspeed=road['maxspeed'], geometry=road['geometry']) # 如果是双向道路(非oneway),添加反向边 if road.get('oneway') != 'yes': G.add_edge(end_node, start_node, osm_id=road['osm_id'], name=road['name'], highway_type=road['highway'], maxspeed=road['maxspeed'], geometry=LineString(list(reversed(coords)))) # 反向几何 # 现在G中就包含了道路的拓扑连接关系

这个过程生成了道路网络的拓扑图G,其中的边就是Road实体,节点是Junction实体,而G中的邻接关系就是connectsTo关系。

2. 提取POI与区域的“位于”关系:对于POI(点数据),我们需要判断它位于哪个区域(面数据)内或哪条道路附近。

  • 位于区域内:使用PostGIS的空间谓词ST_WithinST_Intersects。例如,将planet_osm_point(POI)与planet_osm_polygon(区域,如boundary='administrative')进行空间连接。
    -- 找出所有餐厅位于哪个行政区 SELECT poi.osm_id as poi_id, poi.name, a.name as district_name FROM planet_osm_point poi JOIN planet_osm_polygon a ON ST_Within(poi.geometry, a.geometry) WHERE poi.amenity = 'restaurant' AND a.boundary = 'administrative' AND a.admin_level = '8'; -- 假设8级是区县
  • 位于道路旁:可以使用ST_Distance查找一定阈值内(如50米)最近的道路。更精确的做法是使用ST_LineLocatePointST_LineInterpolatePoint找到POI在道路线上的投影点,并计算距离。

3. 提取区域间的包含与相邻关系:

  • 包含关系:同样使用ST_Within。例如,街道多边形(place=suburb)被行政区多边形(boundary=administrative)包含。
  • 相邻关系:使用ST_Touches判断两个区域多边形是否边界接触。

3.3 第三步:知识图谱的构建与导入

在提取了实体和关系数据后,我们需要将它们导入Neo4j。

1. 设计Neo4j图模型:根据之前的建模设计,在Neo4j中创建对应的节点标签(Label)和关系类型(Relationship Type)。

  • 节点标签:District,Road,Junction,POI,Building,Transport等。
  • 关系类型:LOCATED_IN,PART_OF,CONNECTS_TO,ADJACENT_TO,HAS等。

2. 使用py2neo批量导入:将上一步处理好的数据(通常是CSV文件或Pandas DataFrame)通过py2neo的批量操作接口导入Neo4j。切忌使用单条Cypher语句循环插入,性能极差。

from py2neo import Graph, Node, Relationship, NodeMatcher from tqdm import tqdm # 连接Neo4j graph = Graph("bolt://localhost:7687", auth=("neo4j", "password")) tx = graph.begin() # 批量创建节点(示例:创建行政区节点) districts_df = ... # 从PostGIS查询得到的行政区DataFrame node_dict = {} # 用于存储节点对象,方便后续创建关系 for idx, row in tqdm(districts_df.iterrows(), total=len(districts_df)): node = Node("District", osm_id=row['osm_id'], name=row['name'], admin_level=row['admin_level']) tx.create(node) node_dict[row['osm_id']] = node # 批量创建关系(示例:创建POI位于行政区的关系) poi_in_district_df = ... # 从空间连接查询得到的结果 for idx, row in tqdm(poi_in_district_df.iterrows()): poi_node = node_dict.get(row['poi_osm_id']) # 假设POI节点已创建并存入字典 district_node = node_dict.get(row['district_osm_id']) if poi_node and district_node: rel = Relationship(poi_node, "LOCATED_IN", district_node) tx.create(rel) graph.commit(tx)

对于大规模数据,更推荐使用Neo4j官方提供的**neo4j-admin database import工具或APOC库**的批量导入过程,它们支持直接从CSV文件高速导入,性能比驱动接口高几个数量级。

3. 属性与索引优化:

  • 为经常用于查询的属性(如name,osm_id,amenity)创建索引或唯一约束,可以极大提升查询速度。
    CREATE INDEX poi_amenity_index FOR (p:POI) ON (p.amenity); CREATE CONSTRAINT road_osm_id_unique FOR (r:Road) REQUIRE r.osm_id IS UNIQUE;
  • 将一些复杂的几何信息(如道路的详细LineString)作为属性存储可能会使节点过于臃肿。一种常见的做法是只在Neo4j中存储一个空间参考(如中心点坐标WKT字符串或GeoHash),详细的几何体仍然留在PostGIS中,通过osm_id进行关联查询。这被称为“混合存储”架构。

4. 核心应用场景与查询示例

构建好的城市知识图谱能支持哪些查询?以下是一些典型的Cypher查询示例:

场景1:商圈分析 - “找出静安区所有步行5分钟内可达地铁站的咖啡馆”这需要结合空间距离计算(通常需要在导入时预计算或通过扩展实现)。假设我们有一个Transport节点(地铁站)和POI节点(咖啡馆),并且它们都有location属性(存储为WKT点)。我们可以使用Neo4j的空间插件或预计算的距离关系。

// 假设已创建了空间索引,并且存在`DISTANCE`关系 MATCH (district:District {name: '静安区'})<-[:LOCATED_IN]-(poi:POI {amenity: 'cafe'}) MATCH (poi)-[:NEARBY {distance: 300}]->(station:Transport {station_type: 'subway'}) // 300米内视为步行5分钟 RETURN poi.name, station.name, poi.brand ORDER BY poi.name;

场景2:路径规划辅助 - “找出从A大厦到B公园,货车可以通行的所有路径”这需要利用道路的属性和限制。我们在Road节点上有maxweight(限重)、maxheight(限高)等属性。

MATCH (start:Building {name: 'A大厦'}) MATCH (end:POI {leisure: 'park', name: 'B公园'}) MATCH path = (start)-[:LOCATED_IN|NEARBY*..5]->(road1:Road) // 连接到路网 -[:CONNECTS_TO*..20]->(road2:Road) // 在路网中寻路 -[:LOCATED_IN|NEARBY*..5]->(end) WHERE ALL(r IN nodes(path) WHERE r:Road AND (r.maxheight IS NULL OR r.maxheight > 4.0) AND (r.maxweight IS NULL OR r.maxweight > 10.0)) RETURN path LIMIT 10;

这个查询会找到所有路径,并确保路径上的每一条道路都满足货车通行条件(假设限高>4米,限重>10吨)。这比单纯的最短路径查询更符合实际业务逻辑。

场景3:社区发现 - “识别城市中功能高度混合(居住、商业、休闲密集)的区域”这属于图分析范畴。我们可以使用Neo4j的图数据科学库(GDS)。

// 1. 投影一个子图,包含区域节点和它们之间基于POI相似性的关系 CALL gds.graph.project( 'district_similarity', { District: {properties: ['name']} }, { SIMILAR: { type: 'SIMILAR_TO', // 这是一个需要预先计算好的关系,基于区域间共享的POI类型和数量 orientation: 'UNDIRECTED', properties: 'score' } } ); // 2. 运行Louvain社区发现算法 CALL gds.louvain.stream('district_similarity') YIELD nodeId, communityId RETURN gds.util.asNode(nodeId).name AS district, communityId ORDER BY communityId, district;

通过分析,我们可能会发现几个社区,其中一个社区可能包含了大学城(周边餐饮、书店、运动设施密集),另一个可能是纯居住区等。

5. 常见问题、挑战与优化策略

在实际操作中,你肯定会遇到不少坑。这里记录一些典型问题和我的解决思路。

1. 数据规模与性能问题

  • 问题:一个大型城市(如上海)的OSM数据导入Neo4j后,节点和关系可能达到千万级。复杂查询可能变慢。
  • 对策
    • 数据剪裁:根据应用场景,只导入相关数据。例如,只导入主要道路和特定类型的POI。
    • 混合存储:如前所述,将详细的几何信息留在PostGIS,Neo4j只存核心语义和拓扑。复杂空间查询交给PostGIS。
    • 索引优化:确保所有常用的查询条件都已建立索引。复合索引有时比单字段索引更有效。
    • 分步计算与物化视图:对于一些耗时的衍生关系(如“步行5分钟内可达”),可以预先计算好并作为关系存储,用空间换时间。

2. OSM标签不一致与数据清洗

  • 问题cuisine=chinesecuisine=chinese_foodcuisine=sichuan并存,给分类分析带来困难。
  • 对策
    • 建立映射表:创建一个从OSM原始标签到标准分类的映射字典。例如,将所有中餐相关的标签映射到Cuisine:Chinese
    • 使用外部知识库:可以结合Wikidata等外部知识库来归一化实体类型。例如,通过OSM元素的wikidata标签关联到Wikidata ID,从而获得更规范的分类。
    • 概率性清洗:对于大量文本字段(如name),可能需要使用自然语言处理技术进行去重、纠错和归一化。

3. 空间关系计算的精度与效率矛盾

  • 问题:精确计算数百万个POI与道路/区域的空间关系(如最近道路)非常耗时。
  • 对策
    • 空间网格化:将整个区域划分为规则网格(如H3或S2网格),先计算POI和道路所属的网格,只在同一或相邻网格内进行精确计算,大幅减少计算量。
    • 使用专业空间库:在Python层使用ShapelyR-tree索引(通过geopandas.sindex)来加速空间查询,而不是将所有计算都推到数据库。
    • 近似计算:对于某些应用,使用点的经纬度进行简单的距离计算或GeoHash匹配可能就足够了,无需精确的几何计算。

4. 图谱模型的动态更新

  • 问题:OSM数据在不断变化,如何让知识图谱保持同步?
  • 对策
    • 增量更新策略:定期(如每周)获取OSM的差分文件(.osc),解析出新增、修改、删除的元素。然后设计一个增量更新的流水线,只更新图谱中受影响的部分。这比全量重建要复杂,但对生产系统至关重要。
    • 版本化管理:可以考虑为节点和关系添加时间戳属性,记录其生效时间,从而支持历史状态查询或时序分析。

5. 复杂查询的编写与调试

  • 问题:Cypher查询可能变得非常复杂,尤其是涉及多跳关系和多重条件时。
  • 对策
    • 分步调试:在Neo4j Browser中,将复杂查询拆分成多个简单的MATCHRETURN语句,逐步验证中间结果。
    • 使用PROFILEEXPLAIN:分析查询执行计划,找出性能瓶颈(如全节点扫描),并通过添加索引或重构查询来优化。
    • APOC过程库:善用APOC库提供的丰富过程,如路径展开、集合操作、数据转换等,它们能简化很多复杂逻辑。

构建一个实用的城市知识图谱绝非一蹴而就,它更像是一个持续迭代和优化的数据工程。从粗糙的原始数据到精细、可推理的知识网络,每一步都需要对业务需求、数据特性和技术工具的深入理解。我的经验是,从一个小的、明确的应用场景(比如“分析公司周边午餐选择”)开始,构建一个最小可行产品(MVP),然后再逐步扩展数据范围和模型复杂度,这样更容易把控方向和取得阶段性成果。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 10:51:47

AI图像生成技术实践:角色场景创作与Stable Diffusion部署指南

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

作者头像 李华
网站建设 2026/9/5 10:49:13

Python+OCR+GPU加速实现毫秒级倒计时精准识别

简介&#xff1a;本资源是一套面向Python进阶学习者与游戏自动化实践者的《三角洲行动》限时皮肤抢购专用工具&#xff0c;聚焦解决人工抢购中倒计时识别不准、响应延迟高、多分辨率适配难等核心痛点。程序基于Python 3.9构建&#xff0c;深度融合GPU加速图像处理&#xff08;P…

作者头像 李华
网站建设 2026/9/5 10:48:49

GPU加速OCR实时倒计时识别技术实践

简介&#xff1a;本资源是一套面向Python进阶学习者与游戏自动化实践者的《三角洲行动》限时皮肤抢购专用工具&#xff0c;聚焦解决人工抢购中倒计时识别不准、响应延迟高、多分辨率适配难等核心痛点。程序基于Python 3.9构建&#xff0c;深度融合GPU加速图像处理&#xff08;P…

作者头像 李华
网站建设 2026/9/5 10:42:10

SolidWorks 2020工程级施肥播种机三维模型:参数化设计与应用指南

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

作者头像 李华