☰
XPath完整笔记:从语法轴函数到移动端元素定位实战
2026/10/27 16:38:48 网站建设 项目流程

做爬虫和自动化测试这些年,我电脑里存得最多的不是脚本,而是一堆 XPath 表达式。原因很简单:CSS 选择器在页面结构一变就废,正则表达式在嵌套标签面前基本靠运气,只有 XPath 能一路从兄弟节点摸到祖先,再从祖先拐到隔壁分支,把“我要的那个元素”从几千个 div 里精准揪出来。这篇内容就是我这些年攒下来的一份 XPath 完整笔记,从最基础的节点路径一路写到轴、函数、条件组合和性能取舍,最后拿一个本地生活服务类 APP(下称“示例 APP”)的界面元素匹配做落地演练——移动端的节点树和网页完全是两套东西,写法上有很多反直觉的地方,我会把踩过的坑一个个摊开讲。不管你是刚学爬虫、正在写 UI 自动化脚本,还是做数据采集的工程师,只要手上要跟 HTML 或 XML 打交道,这篇都能直接拿去用。

1. 我为什么还在用 XPath:场景判断与方案取舍

1.1 XPath 到底在解决什么问题

先把定位说清楚。XPath 是一门在 XML/HTML 文档里“寻址”的语言,它把整个文档看成节点树,然后你用一条路径表达式去描述“从哪儿出发、走哪条路、到哪个节点停”。它的输出可以是单个节点、一组节点,也可以是字符串、数字、布尔值。这一点很关键,很多人以为 XPath 只能“选元素”,其实它本身还能做计算,比如count(//li)直接返回列表项数量,string-length(normalize-space(//h1))能返回标题长度,这些在做页面结构校验的时候特别好用。

我在实际项目里用 XPath 主要干四件事:一是数据采集时定位目标内容块;二是 UI 自动化里定位按钮、输入框、列表项;三是做页面结构断言,比如“列表至少渲染出 20 条才算加载成功”;四是在数据清洗阶段对 XML/接口返回报文做筛选。这四类场景对表达式的诉求差别很大,抓数据要求“容错”,自动化要求“唯一且稳定”,断言要求“能计数”,清洗要求“能按属性精确过滤”,所以没有一个万能写法,得分开设计。

理解这一点之后你就不会陷入“背语法”的误区。XPath 的语法其实很少,真正难的是针对具体页面判断该用哪种路径描述方式,以及如何让这条路径在页面小改版之后依然活着。这部分经验只能靠一次次定位失败攒出来,我后面会把方法拆成可复用的套路。

1.2 XPath、CSS 选择器、正则:三种方案的真实对比

新手最常见的困惑是:明明 CSS 选择器写起来更短,为什么还要学 XPath?我一般让人先看一张表,把三者的能力边界摆出来,选型时直接对号入座。

能力维度XPathCSS 选择器正则表达式
按文本内容定位原生支持text()='登录'不支持支持但脆弱
向上找父节点/祖先支持parent::、ancestor::不支持不支持
定位兄弟节点支持following-sibling::仅相邻兄弟+、~不支持
按位置序号取值支持[2]、[last()]、[position()>3]支持:nth-child()不支持
逻辑运算组合and、or、not()部分支持支持
字符串函数contains/starts-with/normalize-space极少强大但需自行拼接
执行性能中等,//全树扫描较慢较快,浏览器原生优化好取决于模式复杂度
移动端原生控件支持支持(依赖引擎实现)基本不支持不适用

结论很清楚:只要涉及“按文字找元素”“往上找父级”“找同级兄弟”,CSS 直接出局,这不是写法问题而是能力缺失。移动端更极端,原生控件树的定位基本只有 XPath、ID、accessibility 标识几条路可走,CSS 选择器在那里几乎不存在。

正则的问题不在能力,在稳定性。我见过太多人用re.findall(r'<div class="price">(.*?)</div>', html)抓价格,页面一加个换行、一改成<span>嵌套就全线崩。正则适合处理“已经拿到的一段纯文本”,不适合处理“结构化的节点树”。正确的分工是:XPath 负责从树里把节点取出来,正则负责对取出来的文本做二次清洗。顺序千万别反。

提示:如果目标只是“给某个 class 的元素加样式”或“取一层子元素”,用 CSS 更省事;一旦判断里出现文本、父级、兄弟任一要素,直接上 XPath,别浪费时间绕。

1.3 各引擎对 XPath 的支持差异,别踩版本坑

这一条新手最容易忽略,但踩了会非常痛苦:不同环境里跑的 XPath 版本不一样,语法不是完全通用的。

浏览器(Chrome、Firefox 的document.evaluate)和 Selenium 走的是XPath 1.0。Python 的lxml也是1.0。这意味着matches()、lower-case()、ends-with()、replace()这些 2.0 才有的函数统统不可用,写了就报Invalid expression。我当初写了个ends-with(@href,'.pdf'),本地测试一直报错,查了半天才发现是版本问题。

移动端的引擎差异更大。Android 侧基于 UIAutomator 的定位,XPath 是由驱动层自己解析的,能力是 1.0 的子集,而且对轴的完整支持有限;跨平台框架里,同一个表达式在 Android 和 iOS 上因为控件类名体系不同(一边是android.widget.TextView,一边是XCUIElementTypeStaticText),几乎不可能复用。这类差异后面第 4 章会展开讲。

实践中我的处理办法是:把 XPath 表达式当成“带方言的代码”来管理,建立一份兼容性底线清单——只用 1.0 的核心函数,不用matches、不用ends-with,需要“以某串结尾”就用substring(@href, string-length(@href)-3)='.pdf'这种等价写法绕过。这条经验值一次踩坑的时间。

2. 基础语法:把节点路径这件事说透

2.1 七种节点类型与文档树模型

学 XPath 之前必须先接受一个观念:文档不是字符串,是树。HTML 里每一个标签、每一个属性、每一段文字,甚至注释,都是树上的一个节点。XPath 定义了七种节点类型:元素节点、属性节点、文本节点、命名空间节点、处理指令节点、注释节点、文档节点(根节点)。日常打交道最多的是前三种。

这里有个高频误解:<div class="price">¥29.9</div>里,div是元素节点,class是属性节点,¥29.9是div的子文本节点。所以//div[@class='price']/text()能拿到价格,而//div[@class='price']拿到的是整个元素。很多人写//div[@class='price']然后打印出一堆标签,就是因为节点类型没对上。

还有一个坑:文本节点包含空白。格式化过的 HTML 里,<span> 价格 </span>的文本节点是价格带前后空格,直接和'价格'比较会失败。这个问题在移动端尤其严重,因为控件树里的 text 属性经常带不可见空白。解决方案后面会讲,标准做法是normalize-space()包一层。

至于“根节点”,/表示文档根,//是/descendant-or-self::node()/的缩写,意思是“从当前节点往下任意深度找”。这两个符号的区别是很多人写错路径的根源:绝对路径以/开头,脆得一碰就断;相对路径以//或.开头,容错高得多。我默认只用//开头。

2.2 路径表达式与四大轴的关系

路径表达式里的每一段,本质上是“轴 + 节点测试 + 谓词”的缩写。比如child::div[1]完整写法是“子轴 + div 元素 + 位置 1”,缩写成div[1]。日常写的//div[@id='app']完整形式是descendant-or-self::node()/child::div[attribute::id='app']。

XPath 一共有 13 条轴,但常用的就四五条:

轴名含义典型用途
child直接子节点(默认轴)常规层级下降
descendant/descendant-or-self所有后代//的展开形式,全局搜索
parent直接父节点从内容回到容器
ancestor所有祖先从文本回溯到某层容器取属性
following-sibling之后的所有同级从标签跳到相邻的值
preceding-sibling之前的所有同级反向取值
attribute属性节点@class的完整形式

一开始我不建议背全 13 条,就记下面四条缩写:.是 self,..是 parent,@是 attribute,//是 descendant-or-self。这四条覆盖了 90% 的日常写法。等遇到“取完价格还要回到整个卡片去拿标题”这种需求时,再去查ancestor::,那时候你自然会记住它。

2.3 谓词:方括号里的门道比想象中多

谓词是 XPath 的灵魂,也是新手翻车最多的地方。先看几个最容易错的点。

第一,索引从 1 开始,不是 0。//li[1]是第一个 li,不是第二个。这个和绝大多数编程语言相反,写 Python 的人第一次几乎必错。

第二,索引的优先级是“同一父节点下的位置”。//ul/li[1]取的是每个 ul 下的第一个 li,而不是“所有 li 里的第一个”。如果你想要“文档里所有 li 的第一个”,必须加括号:(//ul/li)[1]。这个括号区别我强调三遍都不为过,因为它直接影响你会不会取到 500 条重复数据的首条。

第三,谓词可以写逻辑和函数。//div[@class='item' and position()<=5]取前五条;//div[not(@data-hidden)]取没有 hidden 属性的;//div[contains(@class,'active')]用于 class 是多值的情况。

说到 class 多值,这里有个必须讲的坑。<div class="item item-active extra">这个元素,写//div[@class='item']是选不到的,因为属性值精确匹配要求完全相等。正确写法是//div[contains(concat(' ', normalize-space(@class), ' '), ' item ')]——前后补空格再匹配,避免item误命中item-active。这个写法看起来啰嗦,但它是判断“某个类名是否真的存在于 class 列表中”的标准解法,我在正式的采集脚本里一律用它,而不是裸用contains。

2.4 通配符、多路径与运算符

基础最后一块是三类实用语法。通配符*匹配任意元素,@*匹配任意属性,node()匹配任意类型节点。//div/*就是 div 的所有子元素。这个在“结构固定但标签名不固定”的场景很好用,比如前端框架换组件库时标签从span变成em,用*能扛过去一段时间,但我不建议长期依赖,因为它会让表达式失去语义。

多路径选择用|,//h1 | //h2同时取两种标题,返回结果是并集且按文档顺序排列。做内容提取时把正文里可能的标题层级一次性捞出来很实用。

运算符方面记住这些:+ - * div mod做算术(注意除号是div不是/),= != < <= > >=做比较,and or做逻辑,|做并集。比较运算里有个细节:在 1.0 里对节点集用=是“存在任意一个相等即真”,//div[@class='a' or @class='b']和//div[@class='a' | @class='b']的语义完全不同,前者是过滤,后者是合并结果集再过滤,容易写出效果诡异的表达式。

看一段可以拿来练手的结构,后面章节的例子都基于它:

<div class="goods-list"> <div class="goods-item">// 1. 取所有商品标题文本 $x("//div[contains(concat(' ',normalize-space(@class),' '),' goods-item ')]//h3[@class='title']/text()") // 2. 取第一个商品的价格(注意括号) $x("(//span[@class='price'])[1]/text()") // 3. 取"月售"里数字大于 1000 的商品标题(1.0 无正则,只能先取后过滤) $x("//div[@data-id][number(substring-after(span[@class='sales'],'月售 '))>1000]/h3/text()")

第三条里的number()加substring-after()是个很实用的组合:遇到“月售 1200”这种带中文前缀的数字,先用substring-after截掉前缀,再用number()转数值,就能在表达式层做数值比较。这个技巧在数据采集里省掉了很多 Python 侧的清洗代码。

3. 高级用法:从“能选中”到“选得稳”

3.1 轴的真实用法:回溯父级与跨级取兄弟

基础够用之后,真正的分水岭是会不会用轴。轴解决的是“元素之间没有直接父子关系,但语义上有关联”的问题。

最典型的需求:列表里每个卡片有一个“状态标签”,我需要根据标签文字判断整个卡片该不该抓。这时候从标签出发,用ancestor::回到卡片容器拿>// 找到文字为"已下架"的标签,向上回到最近的商品卡片,取它的><dl class="detail"> <dt>配送费</dt><dd>¥3</dd> <dt>起送价</dt><dd>¥20</dd> </dl>

要拿到“配送费”对应的值,就用following-sibling:::

$x("//dt[normalize-space()='配送费']/following-sibling::dd[1]/text()")

这里的[1]是必须的,因为following-sibling::dd会返回后面所有dd,不取下标的第一个会得到一个集合。这个坑我踩过,写成了following-sibling::dd/text(),结果把“配送费”和“起送价”两个值都拿回来了,还纳闷为什么数据对不上。

反过来“值在标签前面”的情况用preceding-sibling::。这两个轴配合使用,基本能解决所有“表格状结构无 table 标签”的页面。

第三种是“取完当前元素还想看后面 N 个同级”,用following-sibling::*[position()<=3]取后三个。做分页区域解析的时候很顺手。

注意:following::和preceding::这两个轴(不带 sibling)扫描范围是整棵树的后续/前置节点,性能极差且结果常出人意料。我基本只用 sibling 版本,除非明确需要跨层级。

3.2 函数家族:文本清洗与条件判断的主力

函数是 XPath 保留项目里最能提升效率的部分。按用途我把它分成三组。

字符串组,做文本清洗的主力:

函数作用我常用的场景
normalize-space()去首尾空白并把连续空白压成一个空格几乎所有文本比较前都包一层
contains(a,b)是否包含子串class 多值、URL 部分匹配
starts-with(a,b)是否以某串开头按前缀区分同类元素
substring-before/after(a,b)按分隔符截取“月售 1200”取数字
string-length()长度过滤掉过短的占位文本
translate(a,b,c)字符级替换模拟大小写无关比较
concat()拼接拼出完整 URL

1.0 没有lower-case(),如果要忽略大小写比较,标准写法是用translate把大写字母全部映射成小写:

$x("//div[contains(translate(@class,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'card')]")

这行丑陋但有效,我把它存成代码片段用了很多年。真要写的时候,把@class换成目标属性即可。

数值组:count()数节点、sum()求和、number()强制转数值、floor/ceiling/round取整。count()在断言里非常好用,比如判断“搜索结果至少 10 条才继续处理”。

逻辑与位置组:not(expr)取反,position()当前序号,last()最后一个,boolean()转布尔。[last()]取最后一条很常用,[position() mod 2 = 1]取奇数位置在需要交错处理时有用。

有个细节值得单独说:text()和.的区别。//div/text()返回 div 的直接文本子节点,不包含子元素里的文字;//div//text()返回所有后代文本;而string(//div)或//div/.在语义上会做“字符串化”,把整个元素所有后代文本拼起来。取一整段富文本内容时,normalize-space(string(.))是最好用的写法,一行拿到干净文本。

3.3 构造“最短唯一路径”的四步法

高级用法最终要落在一个目标上:写出一条足够短、足够唯一、足够稳的表达式。我总结了一套四步流程,基本能应付 95% 的页面。

第一步,找锚点。优先找“语义稳定”的东西:固定文案(如“确认提交”)、业务属性(>// 写法一:绝对路径,一改版就死 $x("/html/body/div[2]/div/div[3]/button") // 写法二:依赖自动生成 class,构建一变就死 $x("//button[@class='css-k8x2ba']") // 写法三:文案锚点 + 祖先定容器 + 相对下探 $x("//div[contains(@class,'order-panel')]//button[normalize-space()='提交订单']")

第三种明显最稳。文案偶尔会变,但“订单面板里的提交按钮”这个语义关系是产品层面的,不会随前端重构消失。这就是那套四步法的核心思想:让定位表达式表达业务语义,而不是表达 DOM 结构。

3.4 性能取舍://到底有多贵

很多人不知道//是有成本的。//展开是descendant-or-self::node()后接节点测试,意味着要从当前节点遍历整棵子树。在几千个节点的页面上,几十条这样的表达式跑下来,耗时能到几百毫秒——在爬虫里不算什么,在 UI 自动化里就是肉眼可见的卡顿。

优化思路有三个。第一,缩小起点。能写成//div[@id='list']//span就别写成//span,前者只在 list 子树里扫。第二,优先过滤属性而不是文本。属性比较是字符串直接比对,文本比较要遍历所有文本节点,后者更慢。第三,谓词尽量前置,//div[@class='x'][@data-id='1']比//div[@class='x' and @data-id='1']在执行顺序上没本质差别,但把能快速排除的条件写在前面有助于减少候选集,这是实践中的经验值。

在移动端这条经验要乘以十。移动端 XPath 的引擎实现通常是“把整个控件树序列化出来再在应用侧匹配”,有的驱动版本一次 XPath 查询要几十到几百毫秒,一个页面几十条 XPath 会让整个测试套慢到离谱。所以移动端我的原则是:能不用 XPath 就不用,必须用的时候只写短路径 + 唯一属性。

4. 落地实操:移动端界面元素匹配的完整演练

4.1 移动端没有 HTML,XPath 为什么还能用

先把一个常见困惑解开:网页里 XPath 是对 HTML 文档树寻址,APP 里没有 HTML,凭什么还能用?原因是移动端自动化框架会把当前屏幕的控件层级序列化成一份类似 XML 的结构(Android 侧基于无障碍节点信息,iOS 侧基于可访问性树),这份结构和 HTML 在形态上高度相似——同样有根节点、有层级、有属性(text、resource-id、class、content-desc、bounds)。XPath 引擎匹配的就是这份序列化结果,所以语法和网页基本一致,但属性名和类名体系完全不同。

这个背景直接决定了两件事。第一,网页上练出来的@class='btn'在 APP 里完全不适用,安卓的 class 是全限定名,比如android.widget.Button、androidx.recyclerview.widget.RecyclerView。第二,属性优先级要重新排:移动端最稳定的是resource-id(开发人员手写的标识),其次是text,再次是content-desc,最不可靠的是class和bounds(坐标会随屏幕尺寸变)。

提示:APP 的控件树需要用相应的自动化工具抓取后才能写 XPath。写之前先抓一份当前页面的层级结构保存成文件,对着文件写,比一边跑一边猜效率高得多。

4.2 解剖一个列表页:从源码到表达式

下面是我在某本地生活服务类 APP(示例 APP)里抓到的一段简化控件结构,为了避免暴露具体业务,我把类名和属性做了通用化处理,但结构形态和真实情况一致:

<hierarchy> <node class="android.widget.FrameLayout" resource-id="com.example.app:id/root"> <node class="android.widget.LinearLayout" resource-id="com.example.app:id/search_bar"> <node class="android.widget.EditText" text="搜索商家、商品" resource-id="com.example.app:id/search_input" /> </node> <node class="androidx.recyclerview.widget.RecyclerView" resource-id="com.example.app:id/feed_list"> <node class="android.widget.FrameLayout" resource-id="com.example.app:id/feed_item"> <node class="android.widget.TextView" text="手工牛肉面" /> <node class="android.widget.TextView" text="¥28.0" /> <node class="android.widget.TextView" text="月售 1200" /> </node> <node class="android.widget.FrameLayout" resource-id="com.example.app:id/feed_item"> <node class="android.widget.TextView" text="番茄鸡蛋盖饭" /> <node class="android.widget.TextView" text="¥18.5" /> <node class="android.widget.TextView" text="月售 860" /> </node> </node> <node class="android.widget.Button" text="提交订单" resource-id="com.example.app:id/submit_btn" /> </node> </hierarchy>

对着这段结构,我先给结论:这个页面里最稳的两条表达方式是resource-id和text,其他都属于凑合。下面逐个演示。

点击搜索框(用 id):

driver.find_element(AppiumBy.XPATH, "//*[@resource-id='com.example.app:id/search_input']")

注意这里用*而不是具体类名,因为类名可能因为组件库升级从EditText换成androidx.appcompat.widget.AppCompatEditText,而 id 不会变。

点击列表里第 2 个卡片的标题(用位置 + 条件):

driver.find_element( AppiumBy.XPATH, "(//*[@resource-id='com.example.app:id/feed_item'])[2]//android.widget.TextView[1]" )

这里的括号很重要,原因和网页一样:不加括号[2]会被理解成“每个 feed_item 下的第二个子节点”,加了括号才是“所有 feed_item 里的第二个”。

点击“提交订单”按钮(用文案,兼容多语言时慎用):

driver.find_element(AppiumBy.XPATH, "//*[normalize-space(@text)='提交订单']")

normalize-space必须加。移动端控件的 text 属性经常莫名其妙带前后空格或者全角空格,我在三个不同版本的驱动上都遇到过,不加就偶发失败,而且失败率不到 10%,最难排查。

按文案找到卡片再取卡片 id(反向定位):

driver.find_element( AppiumBy.XPATH, "//*[@text='手工牛肉面']/ancestor::*[@resource-id='com.example.app:id/feed_item'][1]" )

这是我在列表页做“按名称找条目”的标准写法,比用位置索引稳得多,因为列表顺序会变,名称不会。

4.3 六个可复用的匹配套路与对照表

把上面的写法抽象一下,实践中我固定用这六套模板,遇到新页面直接往里套:

目标推荐写法不推荐写法原因
点固定按钮//*[@resource-id='xxx']//android.widget.Button[3]id 不随布局调整变化
点带文案的入口//*[normalize-space(@text)='文案']//*[contains(@text,'文案')]contains 容易命中多个
取列表第 N 项(//*[@resource-id='item'])[N]//*[@resource-id='item'][N]后者取的是每父级下第 N 个
按名称找卡片//*[@text='X']/ancestor::*[@resource-id='item'][1]//*[@resource-id='item'][2]顺序会变,语义不会
取卡片内字段//*[@resource-id='item'][1]//android.widget.TextView[2]全页面//android.widget.TextView[2]限定在容器内避免误命中
无 id 无文案//*[contains(@content-desc,'X')]依赖bounds坐标坐标随分辨率变

表格里第一条和最后一条是一对:能用 id 绝不用别的方式,能退到 content-desc 绝不退到坐标。中间几条的核心思想是“先锚定容器,再在容器内找相对位置”,这样即使页面整体加了一层或换了个容器,表达式也不用改。

真实操作时我还会做一件事:把表达式里的*换成更具体的类名之前,先用*跑通,跑通了再决定是否收紧。因为*的匹配范围广,能先验证“路径逻辑是否成立”;等确认无误,再收紧到具体类名获得性能收益。反过来的顺序会让你在“路径错”和“类名错”两件事里反复横跳。

4.4 参数化与可维护性:把 XPath 从代码里抽出来

写到最后,真正决定项目能不能维护的不是表达式本身,而是表达式放在哪。我早期的脚本里 XPath 直接写死在每一行定位代码中,页面一改,我得全文搜索find_element逐个改。后来统一改成“定位表 + 模板函数”的结构,情况好很多:

LOCATORS = { "search_input": "//*[@resource-id='{pkg}:id/search_input']", "feed_item": "//*[@resource-id='{pkg}:id/feed_item']", "submit_btn": "//*[normalize-space(@text)='提交订单']", } def item_by_index(i, pkg="com.example.app"): return f"({LOCATORS['feed_item'].format(pkg=pkg)})[{i}]" def item_by_name(name, pkg="com.example.app"): return f"//*[@text='{name}']/ancestor::*[@resource-id='{pkg}:id/feed_item'][1]"

这套结构有三个好处。包名抽成变量,换渠道包时只改一处;位置索引和名称索引分成两个函数,调用方语义清晰;单个定位变了只改字典里一行。我实测过一个 200 多条定位的脚本,改成这种结构后一次改版维护时间从两三个小时压到二十分钟以内。

再补一个细节:text是用户可见文案,做多语言或活动文案改动时会变。如果你的应用有多语言需求,别用 text 定位,回到resource-id或content-desc。这个选择要在写表达式的第一分钟就定下来,改起来成本很高。

5. 常见问题与排查实录

5.1 十类高频错误的成因与解法

这一节按我实际被问到的频率排序,每一条都是真实踩过的。

第一类:表达式返回空列表,但页面上明明有。九成是三个原因之一:文本里带空白导致精确匹配失败;iframe或WebView里的内容不在当前文档树;页面还没渲染完。排查顺序就是先normalize-space,再切换上下文,最后加显式等待。

第二类:Invalid selector或Invalid expression报错。通常是三种情况:用了 2.0 函数(matches、ends-with)在 1.0 引擎上跑;引号嵌套错乱,比如//div[@title="他说"好""];括号不配对。引号问题最隐蔽,建议外层统一双引号、内层单引号,或者用concat拼。

第三类:取到了,但取的是第一个而不是想要的那个。检查[N]前有没有括号。这是最高频的一个错误,没有之一。

第四类:contains(@class,'x')命中了不该命中的元素。contains是子串匹配,x会命中xx、x-active。换成contains(concat(' ',normalize-space(@class),' '),' x ')。

第五类:表达式在浏览器控制台能跑,在 Selenium 里失败。检查是不是用了$x()里的写法直接搬到 Python 字符串里,忘了转义引号;或者浏览器里页面已经渲染完,而脚本执行时机太早。

第六类:移动端偶发定位不到,重试又能过。动画、转场、列表懒加载都会造成这个问题。别用固定 sleep,用“等待元素存在且可见”的显式等待,条件本身也用 XPath 表达。

第七类:同一句 XPath 在 Android 能用,iOS 全部失效。控件类名体系不同,android.widget.TextView在 iOS 上根本不存在。跨平台脚本必须把类名从表达式里拿掉,只用属性和层级。

第八类:坐标相关写法在不同分辨率下全乱。bounds属性是像素值,换设备就变。除非做图像级操作,否则永远别用坐标定位。

第九类:表达式太长了,改起来痛苦。这是设计问题不是语法问题,回到 3.3 的四步法,用容器切分,把一条长表达式拆成“容器表达式 + 局部表达式”。

第十类:明明唯一,第二天跑就命中多个了。典型是列表里出现了同名条目,或者动态插入了一条广告位。解决办法是给表达式加上业务属性的约束,比如带上>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询