301重定向那条链,三个地方各写了一份规则
本文目录
- 从四个门进同一个首页,要走几步?
- 四个入口会不会落到不同的终点
- 一条链上混着301和302,是谁写的?
- 那几跳到底是谁答的?
- 为什么这件事必然被写在三个地方?
- HSTS这第二份副本,说的是同一件事吗?
- 裸域和www,为什么会有两份不一样的HSTS?
- 那个91天的数字,是谁替你填的?
- 把HSTS写在http响应上,浏览器会读吗?
- 第三份副本存在浏览器里,谁在维护它?
- 这些多出来的跳,SEO上到底掉什么?
- 第一笔:抓取预算
- 第二笔:链条越长,信号越糊
- 第三笔:用临时码做永久归一
- 第四笔:HSTS那份声明的实际收益是安全,不是排名
- 我的尺子错在哪几处
- 自己站上怎么查、按什么顺序改
- 常见问题解答
- 首页跳两次,会不会影响排名?
- http到https那一跳能不能靠HSTS省掉?
- max-age该填多少?
- 裸域和www的HSTS必须一样吗?
- 怎么知道自己的域名在不在浏览器的预置清单里?
- preload进去了还能退出来吗?
- 这些数据能代表中文站吗?
- 权威参考资料
摘要:把131个海外品牌站的首页,分别从http裸域、http带www、https裸域、https带www四个门各进了一次。121个站能走到底,其中从http裸域进来要跳两次的59个、跳三次的17个、跳四次的还有2个。真正值得看的不是跳了几次,是26个站在同一条链上混用了两种以上的跳转码——301和302并排出现,一个说永久一个说临时;还有19个站,能从响应头直接认出相邻两跳是由不同的层作答的。再往下,同一件事还有第二份和第三份副本:HSTS响应头109个站发了,可裸域和www两台主机给出的不是同一份的有12个;而浏览器内置的那份预置清单里,8个站在头里写着preload、清单里却查不到,其中一个的状态是被拒。
这件事是从一个很小的疑问开始的。有天保哥在给一个客户看抓取日志,发现Googlebot请求首页的次数不少,但真正拿到200的比例低得有点古怪。顺手用curl把首页的四种写法各敲了一遍,才看清楚:从最朴素的那个写法进去,要连跳三次才落地。
更有意思的是,那三跳的状态码不一样。第一跳301,第二跳301,第三跳302。同一个动作——把你送到唯一那个首页——被写成了三条规则,而且第三条用的还是临时跳转。
一条规则写错不奇怪。三条规则写在三个不同的地方,只有一条被改过,这才是常态。这轮就是冲着这个去的:把131个站的首页当成一个测量对象,看同一件事在几个地方各存了一份,以及这几份彼此对不对得上。
从四个门进同一个首页,要走几步?
先说样本。131个站里,121个能从至少一个入口走到200;剩下10个对我这个客户端一律403或418(aboutyou、boohoo、farfetch、fjallraven、madewell、nespresso、pandora、thefarmersdog、underarmour、warbyparker),拿不到就是拿不到,不硬凑。
四个入口分别是:http加裸域、http加www、https加裸域、https加www。每一条都不跟随重定向,一跳一跳地记状态码和响应头。
| 入口写法 | 0跳直达 | 跳1次 | 跳2次 | 跳3次 | 跳4次 |
|---|---|---|---|---|---|
| http://裸域/ | 0 | 41 | 59 | 17 | 2 |
| http://www./ | 1 | 64 | 45 | 8 | 2 |
| https://裸域/ | 34 | 64 | 19 | 3 | 0 |
| https://www./ | 60 | 51 | 8 | 2 | 0 |
规范终点落在www上的81个,落在裸域上的40个。这个分布本身没什么好争的,两种选法在排名上没有差别,纠结它属于浪费时间,具体理由www和非www哪个更好这个老问题里已经算过账。
值得停一下的是最后一列。跳三次、跳四次的那批不是小站:theordinary从http带www进来要连跳四次才到家,suitsupply和cybex-online同样是四跳,byredo是301、302、308三种码连着来。这类链条不是设计出来的,是长出来的。
四个入口会不会落到不同的终点
这是我开工时最想抓的一条:如果几层规则各写各的,四个入口应该有机会落到不同的地址上。
结果是0例外。121个站,四个基础入口最终都落在同一个200地址上,一个反例都没有。这条假设被数据直接否掉了,我把它写在这里而不是删掉,是因为它给后面所有结论定了一个底:这些站在“终点是谁”这件事上是有共识的,出问题的从来不是终点,是通往终点的路径。尺子能量出反例,只是这次反例的数量恰好是零,这跟尺子没量对是两回事。
一条链上混着301和302,是谁写的?
HTTP语义的现行规范把这几个码分得很清楚:301是永久,302是临时。永久意味着“以后别再来问了,直接去新地址”,临时意味着“这次先这么走,下次还得再问我一遍”。把域名归一这种十年不变的事写成302,等于每一次抓取都要多花一个来回。
我统计的是:同一条链上,出现了两种或两种以上的3xx状态码。
121个站里有26个中招,占21.5%。按入口算是68条链。组合分布很集中:
| 状态码组合 | 链条数 | 这两个码分别在说什么 |
|---|---|---|
| 301+302 | 54 | 一条说永久搬走了,一条说这次临时借道 |
| 301+307 | 4 | 永久搬走,和“临时且不许改方法” |
| 301+302+308 | 3 | 三种语义挤在一条链上 |
| 302+308 | 3 | 临时和“永久且不许改方法” |
| 其余组合 | 4 | 301+307+308、307+308、302+307各若干 |
arcteryx那条特别典型:从http带www进去,依次是301、308、307。三跳三个码。308和307是301和302的“不许把POST改成GET”版本,一条链上同时出现,基本可以断定不是同一个人在同一个地方写的。
我本来打算把“归一路径上用了临时码”单独统计一遍,结果发现这批站和上面那26个完全重合,一个不多一个不少。反过来说就是:没有任何一条链是把301和308这两个永久码混着用的,凡是混用,混进来的那一节必定是临时跳转。
这个重合不是巧合,它把机制说得比我原来想说的还清楚:后加的那一层,倾向于用临时码。临时码在心理上是“我先这么应付一下,回头再改”,可回头这件事从来不发生,于是这句“先这么走”就在链条里待了下来。301、302、404和410各自的语义边界是十几年没变过的东西,但它变不变,和有没有人回头去改那行配置,是两码事。
那几跳到底是谁答的?
光看状态码只能说“像是两个人写的”。要坐实,得看每一跳是谁答的。
办法很土但有效:把每一跳响应里的server、via、cf-ray、x-vercel-id、x-amz-cf-id、x-azure-ref这类能认出厂商的头都记下来,然后看同一条链上相邻两跳的标识变没变。
19个站的链条上,相邻两跳来自不同的层。点几个名字:
- braun.com:从http裸域进来,第一跳由Azure的应用网关答,第二跳由IIS答,最后落在Azure的CDN上。三跳,三层。
- canyon.com:第一跳的server写着某域名商的托管服务,第二跳写着同一家的转发服务,第三跳才是Cloudflare。域名商那边留着的转发规则,和CDN上配的规则,谁也不知道谁。
- bonprix.com:Apache答两跳,然后交给CloudFront。
- dji.com:CloudFront先答一个301,再由另一层答302。
- charleskeith.com:一个叫urllo的转发服务先答一跳,之后两跳都在Cloudflare里绕。
这些痕迹做不了假。一个站要是所有跳转都在同一个nginx里做完,响应头会长得一模一样。头不一样,就说明这条链是拼出来的——每一段是不同时期、不同的人、在不同的控制台里加的。
为什么这件事必然被写在三个地方?
说到这里可以把机制摊开了。一个电商站上线到今天,“把访客送到唯一那个首页”这条规则,天然有至少三个可以写它的位置:
- 域名商或DNS那一层。买域名时顺手开的转发、别名,或者早年为了做跳转买的URL转发服务。这一层的特点是:开的时候是运营或者老板开的,之后谁都想不起来它还在。
- CDN或边缘那一层。Cloudflare的页面规则、CloudFront的函数、Akamai的属性配置。这一层改起来最快,所以出事时第一个被改的往往是它。
- 源站或应用那一层。nginx的server块、Apache的.htaccess、框架自己的中间件。这一层最“正统”,也最少被动,一份从旧配置里导进来的重写规则能在里面躺好几年。
三层都能把请求扔向同一个地址。问题是没有任何机制强制它们保持一致:你在CDN上加了一条规则解决问题,源站那条旧规则不会因此消失,它只是排到了后面,从“第一跳”变成了“第二跳”。链条就是这么一节一节长出来的,很像老房子里的电线:新拉一路不等于旧的那路被拆了,它只是被压在了墙里面。
这也是为什么改配置的时候看不出来。Nginx每次reload都通过,可Googlebot每8次抓取仍有1次撞在301上说的是同一件事的另一面:单层的语法检查永远是绿的,因为每一层单独看都没错。多层之间谁压过谁,也不一定按你以为的来——响应头和meta打架时赢的从来不是更严的那条,跳转这边的优先级同样是先到先得,不是更严格的先得。
顺带说一句边界。首页到底有几个写法能直接打开(多一道斜杠、加个index.html、把路径改成大写),这轮我也顺手量了,但那是另一个题目,同一个页面十一种URL写法都返回200已经把它拆得很细,这里不重复。本文只盯跳转链和跟着它的那几份声明。
HSTS这第二份副本,说的是同一件事吗?
http到https那一跳,除了301之外还有第二种说法,叫HSTS。它是一个响应头,意思是“从今天起的这么长时间里,浏览器你别再用http来找我了,自己改成https”。
换句话说,301规则和HSTS头在说同一件事:这个站只走https。只不过一个是每次现场拦,一个是提前打招呼、让浏览器自己在本地拦。
121个站里,规范终点上带HSTS的有109个。听着不错,拆开看就没那么好了:
| max-age档位 | 站数 | 说明 |
|---|---|---|
| 不到30天 | 1 | 基本等于没开 |
| 30天到180天 | 53 | 最大的一档 |
| 180天到1年 | 7 | |
| 整1年(31536000) | 39 | 规范推荐的下限 |
| 超过1年 | 9 | 两年居多 |
另外两个附加项更能说明问题:写了includeSubDomains的只有23个,写了preload标记的只有10个。也就是说,绝大多数站的这份声明只覆盖它自己这一台主机。
裸域和www,为什么会有两份不一样的HSTS?
这是本轮最直接命中主题的一处。同一个域名,裸域和www是两台主机,各自发各自的响应头。理论上,同一件事应该说得一模一样。
这一节的口径和上一节不同:上一节看的是规范终点那一台,这一节把裸域和www分开各看一次。两边都发且完全一致的99个,两边都发但不一样的9个,只有一边发的3个,两边都不发的10个。那12个对不上的:
| 站点 | 裸域那份 | www那份 |
|---|---|---|
| bollandbranch | max-age=7889238 | max-age=31536000 |
| burrow | max-age=31536000 | max-age=7889238 |
| chubbiesshorts | max-age=7889238 | max-age=31536000 |
| gymshark | max-age=7889238 | max-age=31536000; includeSubDomains; preload |
| harrys | max-age=7889238 | max-age=31536000 |
| italic | max-age=7889238 | max-age=31536000 |
| nomadgoods | max-age=31536000 | max-age=7889238 |
| suitsupply | max-age=63072000 | max-age=86400 ; includeSubDomains |
| prose | max-age=31536000, max-age=31536000 | max-age=31536000 |
| bonprix | max-age=604800 | 不发 |
| peakdesign | 不发 | max-age=31536000 |
| zwilling | max-age=63072000; includeSubDomains | 不发 |
suitsupply那一对差得最狠:一边两年,一边一天,中间隔着730倍。而prose那一行更值得看——它的裸域响应里,HSTS头出现了两次。两层各自加了一遍,谁也没发现另一层已经加过了。值一样纯属运气好,要是不一样,浏览器会按第一个出现的算,后面那个白写。这个坑和响应头自相矛盾那一轮里量到的形态是同一类。
还有一个细节:这12个对不上的里面,有6个的差异形态一模一样——一边是7889238,一边是31536000。这个规律太整齐,整齐到不像是巧合。
那个91天的数字,是谁替你填的?
7889238秒是多久?91.3天。不是整月,不是整季度,不是整年,是一个谁也不会手打出来的数。
它在121个站里出现了58次。一个值出现58次,只有两种解释:要么58家公司的运维碰巧都喜欢这个数,要么它根本不是这58家填的。
要坐实第二种,光看这55个站不够——那只是在找支持自己的证据。按自基线的做法,得把没发这个值的站也一起量一遍,看这个值到底跟着什么走。于是我把能走到200的118个站的首页HTML全拉了一遍,只认页面里的资源域名这种硬指纹,不猜。这一步的样本比上面少3个,是因为有3个站这轮HTML没拿回来,下面的百分比都按118这个分母算。
结果分成两组:发7889238的55个站,指纹全部指向Shopify,占100%;不发这个值的63个站里,认不出平台的26个、Shopify 12个、Salesforce 10个、Next.js 9个、Magento 3个、BigCommerce 2个、WordPress 1个。
方向非常清楚:发这个值的,无一例外都在同一个平台上;这个值就是平台的出厂值,没有任何一个站主碰过它。
反过来那一栏更有意思。同一个平台上,有12个站没用出厂值——说明这个值是能改的。可这12个站改出来的结果是:
- 改成整1年(31536000)的6个:anker、eufy、govee、peakdesign、soundcore、vuoriclothing
- 改成两年(63072000)的3个:buckmason、kotn、ruggable
- 改成180天(15552000)的2个:mejuri、shopify自己
- 干脆不发的1个:joolz
这就是那句话最好的注脚:没人管的时候,55个站说的是同一句话;一旦有人管,12个站管出了4种不同的说法。统一从来不是维护出来的,是没人动出来的。这和白名单在没人复查时慢慢漂移是一个机制的两面。
把HSTS写在http响应上,浏览器会读吗?
不会。HSTS的规范原文写得很明白:这个头只有出现在https响应上才作数,出现在http响应上必须被忽略。道理也简单——http连接本来就可能被中间人改,如果允许它宣布“以后都走https”,那反过来也就允许中间人宣布别的。
但有8个站在http的301响应里也发了HSTS:arcteryx、assos、lookfantastic、philips、rab.equipment、rituals、sostrenegrene、vaude。
这行头不会造成任何危害,它只是被安静地丢掉。危害在于它会让人误判:你在浏览器的网络面板里看到这行头,会以为HSTS已经生效了。它是一句说给听不见的人听的话。
这类“写了但没人读”的形态,在响应头里那248个死字段那一轮里已经量过一次,只不过那次是字段本身没人认,这次是位置不对。
第三份副本存在浏览器里,谁在维护它?
HSTS有个先天缺口:浏览器第一次访问这个站之前,根本没收到过那个头,所以第一次请求还是会走http。补这个缺口的办法叫preload——把域名提交到一份清单里,这份清单直接编译进浏览器,浏览器出厂时就知道这个站只走https。
这就是第三份副本。它和前两份的区别在于:前两份在你的服务器上,改了立刻生效;第三份被直接编译进浏览器,进去慢,出来更慢。
我拿域名逐个问了这份清单的状态接口,131个站的答案相当冷清:已经在清单里的3个,进去过又被移出的3个,申请过被拒的1个,剩下124个从来没在这份清单上出现过。
然后是这轮最锋利的一组对照。头里明明白白写着preload标记、清单里却查不到的,有8个站:
| 站点 | 它自己发的HSTS头 | 清单里的真实状态 |
|---|---|---|
| bellroy | max-age=15552000; preload | 从未收录 |
| brompton | max-age=31536000; includeSubDomains; preload | 从未收录 |
| gymshark | max-age=31536000; includeSubDomains; preload | 从未收录 |
| lookfantastic | max-age=31536000; includeSubDomains; preload | 从未收录 |
| quince | max-age=31536000; includeSubDomains; preload | 从未收录 |
| rituals | max-age=63072000; includeSubDomains; preload | 从未收录 |
| byredo | max-age=31536000; includeSubDomains; preload | 已被移出 |
| shopify | max-age=15552000; includeSubDomains; preload | 被拒 |
preload那个标记,本身没有任何功能。它不是开关,是一句声明:“我同意被收进清单”。真正决定你在不在清单里的,是清单那边的审核。这8个站的头里写着“我在”,清单说“你不在”。
最后两行值得单独讲。byredo的状态是已被移出——它进去过,后来因为某次不满足条件被拿了出来,可站上那行preload标记留到了今天。而shopify自己那一行,是这轮最完整的一个闭环:清单的硬条件之一是max-age至少一年,它写的是15552000,180天,只有一半。带着一个不满足条件的值去申请,结果当然是被拒;被拒之后,那个preload标记还在头里挂着。
一家给几十万商家提供建站服务的公司,自己的域名在这份清单上的状态是rejected,而它替客户填的那个默认max-age是91天——离清单要求的门槛更远。这不是嘲笑谁,这恰恰说明问题不在能力,在于没有任何一个环节负责把这三份副本拉回同一个值上。
还有一个反方向的例子:aboutyou在清单里的状态是preloaded,而它对我这个客户端一律返回403,那个403响应里一个HSTS头都没有。清单说它承诺只走https,现场拿不到任何承诺。这个观察有边界——403是给我这种客户端的,真实浏览器看到的可能不一样,所以我只把它当成一个提醒,不当结论。
这些多出来的跳,SEO上到底掉什么?
把上面的东西落到能算账的地方。判断标准不用自己发明,搜索侧对各类跳转的处理方式是公开写着的。
第一笔:抓取预算
一次多余的跳转,对爬虫就是一次多余的往返。首页是被抓得最频繁的地址,一天几十上百次很常见。多跳一次,一年就是几万次白跑。这笔账在大站上是实打实的,小站上感受不明显,但也没有任何理由留着。条件请求那一轮算过同类的账,形态不同,逻辑一样。
第二笔:链条越长,信号越糊
关于301是否损耗权重,那个流传多年的说法早就该退休了,15%损耗的来历我单独写过。但“不损耗”不等于“随便跳”。链条长了会带来两个实际后果:一是超过一定跳数搜索引擎会放弃跟随;二是链条中间任何一跳出问题,整条路径断掉,而你从终点做体检永远看不见。
第三笔:用临时码做永久归一
这是26个站的实际状态。302告诉爬虫“原地址还有效,下次还来问”,于是原地址不会被合并掉,两个地址长期并存在索引里,谁被选成规范页由算法决定,不由你决定。canonical这件事你说了不算那一轮已经量过一次自我声明和实际判定的落差,跳转码是同一个故事的上游。
第四笔:HSTS那份声明的实际收益是安全,不是排名
得说清楚:HSTS不是排名因素。它值得做,是因为它把“第一次访问走明文”这个窗口关掉了,也顺带把http到https那一跳从每次请求省成一次。做它是为了安全和速度,不是为了排名,别在方案里把它包装成SEO项目。
我的尺子错在哪几处
四次,其中两次改变了结论。
| 错在哪 | 差点写出的错结论 | 怎么发现的 |
|---|---|---|
| 并发太高,34个站返回429 | 把“我被限流了”读成“这个站没数据”,样本直接缺掉四分之一 | 429集中出现在同一批站上,而这批站单独慢速重跑就正常。限流不是站点的属性,是我自己的属性 |
| 拿响应头认建站平台 | 想用x-shopid证明那55个站是同一个平台,全样本命中0,差点写成“认不出平台” | 回头看真实响应头,那批站全部走同一家CDN,平台标识被挡在后面。改用页面里的资源域名才量得出来 |
| 把“没抓到”当成“没有指纹” | 归因台第一版有18个站抓取失败,被算进了“没有平台指纹”那一栏,100%变成了67% | 失败的那18个恰好是我手工验过确实在那个平台上的,数字和手感对不上 |
| 只统计发了那个出厂值的站 | “这个值跟着平台走”会变成一句没有对照组的话 | 补量了不发这个值的63个站,才知道同平台里还有12个改过 |
第二和第三条是同一个毛病的两种形态:用一把从没验证过能不能量出反例的尺子,去量一个自己已经有预期的对象。第一条则是老问题了,连着几轮都栽在同一处。
自己站上怎么查、按什么顺序改
不需要任何工具,curl就够。
for u in "http://例子.com/" "http://www.例子.com/" \
"https://例子.com/" "https://www.例子.com/"; do
echo "== $u"
curl -sSI -o /dev/null -w '%{http_code} -> %{redirect_url}\n' "$u"
done要看完整链条和每一跳是谁答的,把-I换成带-D -的GET,逐跳读server、via、cf-ray这几个头。注意用HEAD查有可能查不全,有些站对HEAD和GET给的头不一样。
改的顺序,按投入产出排:
- 先把链条压到最多一跳。目标是任何一个入口最多跳一次就到终点。做法是在最外层(通常是CDN)一次性判完协议和主机名,直接给出最终地址,而不是让它一层层往里传。
- 把归一路径上的临时码全换成301。如果链路上有POST请求需要保留方法,用308,别用302。
- 去把源站和域名商那两层的老规则找出来。这一步最花时间,也最容易被跳过。判据是:把CDN规则临时关掉,看请求会落到哪儿——落到哪儿,哪儿就还留着一条你忘了的规则。这一步别在业务高峰做。手上有多个域名要合并的,顺序上先把域名整合那套决策走完再来清链条,否则清完还得再清一遍。
- 裸域和www两台主机的HSTS对齐。两边发同一个值,或者明确只让其中一台对外。各类服务器上这个头怎么配、出问题怎么回滚,HSTS的开启与回滚流程那篇写得比这里细。
- 决定要不要preload,然后让三份副本说同一句话。不打算提交清单,就把头里那个preload标记删掉——留着它只会让下一个接手的人以为已经做过了。打算提交,就先把max-age提到一年以上、补上includeSubDomains,再去提交,并且记住这件事是单向门:进清单容易,退出来要等好几个浏览器版本。
最后一句提醒,来自那8个站:一行声明写在自己家里,不等于外面那份记录跟着变了。凡是需要第三方点头的声明,都要在自己这边留一个能查回去的动作,否则它就会像那8个preload标记一样,一直挂在那里说着一句不再成立的话。这和证书有效期一路砍到47天之后续期只能自动化是同一个道理:凡是有有效期又不由你单方面决定的东西,都得有人盯。
这一轮和前两轮其实是一条线上的。政策页那行最后更新日期量的是人写给人看的声明没人再动,Cookie到期日那一轮量的是同一个到期时刻有三种续期方式,这一轮量的则是同一件事被三个地方各写了一份。共同的那句话是:写下来那天都是对的,之后没人负责让它们一起变。
常见问题解答
首页跳两次,会不会影响排名?
直接影响排名的证据没有。实际影响是抓取效率和链路可靠性:多一跳就多一次往返,链路中间任何一节坏掉,从终点做体检都发现不了。真要排优先级,它排在内容和索引问题后面,但属于一次性改完就长期受益的那类,性价比很高。
http到https那一跳能不能靠HSTS省掉?
只能省掉回头客那一次。浏览器第一次访问你的站之前没收到过这个头,第一跳还是走http。所以那条301规则不能删,HSTS是加在它旁边的,不是替代它。只有进了浏览器的预置清单,第一次访问才会被直接改写。
max-age该填多少?
不打算提交预置清单,一年(31536000)是个稳妥值。刚开始上,可以先填一个小值观察几天,确认全站包括所有子域都能走https之后再提上去。要提交清单,一年是硬门槛,还得同时带上includeSubDomains和preload两个标记,三者缺一不可。
裸域和www的HSTS必须一样吗?
规范没有强制,但没有理由不一样。它们是同一个品牌的同一个站,给浏览器两个不同的期限只会让行为难以预测。如果其中一台只用来跳转,最省事的做法是让它发和主站完全相同的那一份。
怎么知道自己的域名在不在浏览器的预置清单里?
清单方提供了公开的查询页,输入域名就能看到状态:从未收录、待收录、已收录、待移除、已移除。别用“我头里写了preload”当判据,那只是一句申请意向,不是收录结果。
preload进去了还能退出来吗?
能,但很慢。提交移除申请之后,还要等浏览器发布新版本、用户升级到那个版本,旧版本浏览器在缓存过期前仍然只走https。所以提交前要确认所有子域,包括那些内部用的、老的、快忘了的,全都支持https。这是一扇很难往回走的门。
这些数据能代表中文站吗?
不能直接代表。样本全是海外品牌的独立站,CDN和建站平台的构成跟国内很不一样。但“同一件事被几层各写一份、只有一层被改过”这个机制跟地域无关,做法照搬即可,数字别照搬。
权威参考资料
本文标题:《301重定向那条链,三个地方各写了一份规则》
本文链接:https://zhangwenbao.com/redirect-chain-three-layers-hsts-preload-audit.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0