这篇文章比较长,介绍全面。恐怕很多人读不完,先来一个Too Long Don’t Read的总结:
从Http头中获取“真实客户端IP地址”时,请使用X-Forwarded-For
列表中最右边的IP。
XFF
中最左边的IP通常被认为是“最接近客户端”和“最真实”的,但它很容易伪造被欺骗。不要将它用于任何与安全相关的事情。
选择最右边的XFF
IP时,请确保使用该标头的最后一个真实地址。
使用由反向代理设置的特殊“真实客户端IP”头(如X-Real-IP
, True-Client-IP
等)可能很好,但这取决于 a)反向代理实际如何设置它;b)如果它已经存在/欺骗,反向代理是否设置它;c)如果有反向代理,如何配置反向代理。
任何非反向代理专门设置的标头都不可信。比如,如果您不检查X-Real-IP
标头直接在 Nginx 后面追加,那你可能将读取欺骗值。
许多限速器都使用可欺骗的IP实现,这容易受到绕过限速器导致内存溢出攻击。
如果你在代码或基础设施的任何地方使用所谓的“Real IP”,你现在就需要去检查你是如何提取它的。
下面将详细解释这些内容,因此请继续阅读。
使用X-Forwarded-For
或其他HTTP标头获取所谓的“Real IP”,目前地使用状态非常糟糕。这些HTTP标头设计不正确、不一致,结果导致被不恰当地使用。这导致各种项目中的安全漏洞,并且肯定会在将来导致更多的问题。
在研究了一段时间的限速器之后,我开始关心 IPv6 处理。我写了一篇文章,详细介绍了IPv6限速如何导致速率限制器逃逸和内存溢出。然后,我转而担心限速器在负载均衡(或任何反向代理)后面时,如何确定要限速的IP。正如你所看到的,情况很糟糕。
但这不仅是关于限速器。如果你曾经接触过查看X-Forwarded-For
标头的代码,或者如果你使用别人的代码去获取所谓的“RealIP”,那么你绝对需要小心谨慎。这篇文章将帮助你理解为什么。
Web 服务对其客户端的 IP 地址感兴趣的原因有很多:地理统计、地理定位、审计、限速、防止滥用、会话历史记录等。
当客户端直接连接到服务器时,服务器可以看到客户端的 IP 地址。如果客户端通过一个或多个代理(任何类型的代理:正向、反向、负载均衡器、API 网关、TLS 卸载、IP 访问控制等)进行连接,则服务器只能直接看到客户端连接使用的最终代理的 IP 地址。
为了将原始 IP 地址传递到服务器,有几个常用的HTTP标头:
X-Forwarded-For
是逗号分隔的 IP 列表,每个经过的代理都会将访问者追加到该IP列表。按照这个想法,第一个IP(由第一个代理添加)是真正的客户端IP。每个后续 IP 都是路径上的另一个代理。最后一个代理的 IP 不存在(因为代理不添加自己的 IP,并且因为它直接连接到服务器,因此其 IP 无论如何都可以直接使用)。后面将经常讨论这个问题,所以它将缩写为“XFF”。
Forwarded
是最官方的标头但似乎使用最少。我们将在下面更详细地介绍它,但它实际上只是 XFF 的一个更高级版本,它具有我们将要讨论的相同问题。
还有特殊的单 IP 标头,如 X-Real-IP(Nginx)、CF-Connecting-IP(Cloudflare) 或 True-Client-IP(Cloudflare 和 Akamai)。我们将在下面详细讨论这些,但它们不是本文的主要重点。
在讨论如何正确使用 XFF 之前,我们将讨论使用X-Forwarded-For
可能出错的多种形式。
首先,也是最重要的一点,您必须始终意识到,由不受您控制的任何代理添加(或似乎已经添加)的任何 XFF IP 都是完全不可靠的。任何代理都可以以任何它想要的方式添加、删除或修改标头。客户端也可以最初将标头设置为它想要的任何内容,以使欺骗球滚动。例如,如果您向 AWS 负载均衡器发出此请求:
1 | curl -X POST https://my.load.balanced.domain/login -H "X-Forwarded-For: 1.2.3.4, 11.22.33.44" |
负载均衡器后面的服务器将获得以下信息:
1 | X-Forwarded-For: 1.2.3.4, 11.22.33.44, <actual client IP> |
还有这个:
1 | curl -X POST https://my.load.balanced.domain/login -H "X-Forwarded-For: oh, hi,,127.0.0.1,,,," |
会给你这个:
1 | X-Forwarded-For: oh, hi,,127.0.0.1,,,,, <actual client IP> |
正如你所看到的,目前都只是通过,这个标头前面的信息不会被改变也不会被验证。最终的实际 IP 只是附加到已经存在的内容后。
(除了curl
和自定义客户端之外,还有类似于ModHeader的Chrome插件可让您在浏览器请求中设置 XFF 标头。但是,如何设置标头对我们来说并不重要,重要的是攻击者可以利用这一点。
Multiple message-header fields with the same field-name MAY be present in a message if and only if the entire field-value for that header field is defined as a comma-separated list [i.e., #(values)]. It MUST be possible to combine the multiple header fields into one “field-name: field-value” pair, without changing the semantics of the message, by appending each subsequent field-value to the first, each separated by a comma. The order in which header fields with the same field-name are received is therefore significant to the interpretation of the combined field value, and thus a proxy MUST NOT change the order of these field values when a message is forwarded.
这适用于 XFF,因为它是一个逗号分隔的列表。这可能使获取最右边(甚至最左边)的 IP 容易出错。
例如,Go语言有三种获取标头值的方法:
http.Header.Get(headerName)
以字符串形式返回第一个标头值。http.Header.Values(headerName)
返回一个字符串切片(数组),其中包含headerName
标头的所有实例的值。(在查找之前headerName
会被规范化)http.Header
是个map[string][]string
,可以直接访问。(map的key是规范化的标头名称。这类似于使用Values
。所以这是攻击:
req.Header.Get("X-Forwarded-For")
并获取第一个标头。你把它分开,拿最右边的。与 Go 不同,Twisted 获取单个标头值的方法返回最后一个值。(为什么没有标准的、通用的、公认的行为?这避免了上述攻击,但它可能会导致一个不同的(不太可能的)问题:如果你使用最右边的算法(如下所述),你需要从右边向后寻找第一个不受信任的IP。但是,如果您的一个反向代理添加了新的标头而不是附加(根据 RFC,这是一件有效的事情)怎么办?现在,您想要的 IP 在最后一个标头中无处可寻——它充满了受信任的反向代理 IP,而真正的 IP 位于 XFF 标头的先前实例中。
这里可能存在一种微妙的、假设的攻击:
请注意,当我使用 AWS ALB 后面的服务器进行测试时,我发现 ALB 已经连接了 XFF 标头。所以这很好。我不知道其他反向代理是否也这样做,但我敢打赌没有真正的一致性。
最好的办法是自己合并所有 XFF 标头。
(值得询问和检查的是,确保反向代理附加到正确的标头,因为附加到错误的标头会破坏采取最正确的代理的可信度。我只检查了 AWS ALB 和 Cloudflare,他们做对了。如果有人发现做错了什么,请告诉我。
即使在完全非恶意的情况下,任何 XFF IP(尤其是最左边的 IP)也可能是私有/内部 IP 地址。如果客户端首先连接到内部代理,它可能会将客户端的私有 IP 添加到 XFF 标头中。这个地址永远不会对你有用。
因为X-Forwarded-For
不是官方标准,所以没有正式的规范。大多数示例显示 IP 地址以逗号空格(,
) 分隔,但空格并不是严格要求的。(例如,HTTP/1.1 RFC 说像 XFF 这样的标头只是“逗号分隔”。我查看的大多数代码仅按逗号拆分,然后修剪值,但我发现至少有一个代码会查找逗号空间。
在测试时,在我看来,AWS ALB 在添加 IP 时使用逗号空间,但 Cloudflare 只使用逗号。
这应该不言而喻,但是如果您收到的是 HTTP-not-S 请求,那么任何人都可以在它们到达您之前修改标头。值得一提的是,闯入者无法搞砸“最右边”的方法(如下所述),因为他们无法搞砸从互联网到您的反向代理或服务器的最终连接的 IP。
所以只要加密你的流量,好吗?
一些反向代理会删除任何意外或不需要的标头,但有些(如AWS ALB)不会。因此,攻击者可以设置X-Client-IP、True-Client-IP标头,例如并直接连接到您的服务器。如果您的反向代理没有专门为您设置它们,您无需被愚弄使用它们。
不幸的是,尝试让自己了解 XFF 也很困难。
MDN Web Docs 通常是此类内容的黄金标准,但关于 XFF 的页面根本没有提到这些风险; 它说“最右边的 IP 地址是最新代理的 IP 地址,最左边的 IP 地址是原始客户端的 IP 地址”,没有任何警告。维基百科条目要好得多:“由于很容易伪造X-Forwarded-For
字段,因此应谨慎使用给定的信息。最右边的 IP 地址始终是连接到最后一个代理的 IP 地址,这意味着它是最可靠的信息来源。
【2022-03-09:为 MDN 文档创建了一个issue。 2022-03-19:我重写了页面,对其进行了公关,更改现已生效。您可以在此处查看原始Forwarded
页面的 PDF。现在要修复页面…】
其他来源也同样可变。有些人对标头被欺骗的可能性或私人地址的存在(1、2、3、4、5)一无所知。其他人在提及风险方面做得很好(6,7,8),但有时您必须深入阅读才能获得警告。
让我们做一些基线陈述:
如果你要做一些与安全有关的事情,你需要使用你信任的IP–最右边的IP。这里最明显的例子是限速。如果您为此使用最左边的 IP,攻击者可以在每个请求中欺骗不同的 XFF 前缀值,并完全避免受到限制。
此外,他们可能会通过强制您存储太多的单个条目来耗尽您的服务器内存——每个虚假 IP 一个条目。似乎很难相信将 IP 地址存储在内存中会导致耗尽 - 尤其是当它们存储在生存时间有限的缓存中时,但请记住:
对于所有攻击者和配置来说,它仍然不可行,但不应不加考虑地将其驳回。
或者,攻击者可以强制您对其他用户的 IP 地址进行速率限制/阻止。他们可以提供真实的 IP 地址,但不能提供他们的 IP 地址,您最终会被愚弄以限制其速率。(如果你使用“真实”的IP进行滥用报告,你最终可能会抱怨错误的人。
使用最右边的 IP 进行速率限制的缺点是,您可能会阻止一个代理 IP,该 IP 实际上不是滥用的来源,而只是被一堆不同的客户端使用,如果您只是使用最左边的 IP,您就会意识到这一点。是的,好吧。这似乎不太可能,而且它仍然比允许攻击者轻而易举地绕过您的速率限制器并使您的服务器崩溃要容易得多。
如果你正在做一些与安全无关的事情……认真考虑您的用例。假设您只想对您的统计数据进行 IP 地理位置查找。可能最左边的 IP 就是你想要的。您的绝大多数用户不会进行任何标头欺骗,并且随机互联网代理的地理位置对您没有好处,因此您可能会在最接近用户的 IP 上获得最佳结果。
另一方面,您可能需要考虑您期望拥有多少使用 Internet 代理的用户。可能足够少,如果你地理定位错误的东西,它不会损害你的统计数据。攻击者有没有办法通过故意歪曲你的地理统计数据来伤害你?可能不是,但花点时间认真考虑一下。
因此,在编写“GetRealClientIP(request)”函数时要小心。确保它有一个关于如何使用它的大警告注释。或者编写两个函数:GetUntrustworthyRealClientIP(request)
和GetTrustworthyButLessRealClientIP(request)
。这些都是可怕的名字。也许只是传递一面旗帜。无论如何,关键是要防止函数的调用者对结果的性质产生任何混淆。
使用该函数的结果时也要小心。编写代码很容易,让最左边的 IP 进行一些地理查找,然后决定您还需要进行速率限制……所以你不妨使用相同的“realClientIP”变量!哎呀。这可能是使错误的代码看起来错误的好时机。
请记住,最终代理 IP(或客户端的地址(如果直接连接)不在 XFF 标头中。为此,您需要查看您的请求连接信息。(在 Go 中的http.Request.RemoteAddr
,许多 CGI 服务器的REMOTE_ADDR
环境变量等)
阅读本文时,请记住,最终的代理 IP 不在 XFF 列表中,而是RemoteAddr
。另请注意,RemoteAddr
它可能具有ip:port
形式,具体取决于您的平台(就像在 Go 中一样)——当然可以确保只使用 IP 部分。
列出所有X-Forwarded-For
标头中的所有IP。RemoteAddr
也是有用的。
默认使用最右边的方法。仅在必要时使用最左边的,并确保谨慎使用。
如果您的服务器直接连接到 Internet,则可能有 XFF 标头,也可能没有(取决于客户端是否使用代理)。如果存在 XFF 标头,请选择最左侧的 IP 地址,该地址是有效的非专用 IPv4 或 IPv6 地址。如果没有 XFF 标头,请使用RemoteAddr
。
如果您的服务器位于一个或多个反向代理后面,请选择最左边的 XFF IP 地址,该地址是有效的非私有 IPv4 或 IPv6 地址。(如果没有 XFF 标头,则需要立即修复网络配置问题。
永远不要忘记安全隐患!
如果您的服务器直接连接到互联网,则 XFF 标头不可信。使用RemoteAddr
。
如果您的服务器位于一个或多个反向代理后面,并且无法从 Internet 直接访问,则需要知道这些反向代理的 IP 地址或请求将通过的 IP 数量。我们将这些称为“受信任的代理 IP”和“受信任的代理计数”。(最好使用“受信任的代理 IP”,原因如“网络体系结构更改”部分所述。
受信任的代理 IP 或受信任的代理计数将告诉您在找到不属于某个反向代理的第一个 IP 之前,您需要检查距离 XFF 标头的右侧多远。此 IP 是由您的第一个受信任的代理添加的,因此是您唯一可以信任的 IP。使用它。
(请注意,我在这里说的不是“有效的非私有IP”。这样做很诱人,只是为了更加安全,如果你这样做,我不会责怪你,但如果你不能相信你自己的反向代理来添加适当的IP,那么你就会遇到更大的问题。
同样,如果您支持一个或多个反向代理并且没有 XFF 标头,您需要立即弄清楚人们如何直接连接到您的服务器。
如果您的所有反向代理都与您的服务器位于同一私有 IP 空间中,我认为可以使用最右边的非私有 IP,而不是使用“受信任的代理 IP”或“受信任的代理计数”。这相当于将所有专用 IP 范围添加到“受信任的代理 IP”列表中。
这不起作用的一个例子是,如果您位于外部反向代理服务(如 Cloudflare)后面——它不在您的私有地址空间中。
让我们看看真实世界的例子!
警告:我在这里有点得意忘形。我只打算看看几个我熟悉的项目,但危险使用最左边的命中率太高了,所以我一直在寻找。(即使做得好,也有一些有趣和有教育意义的方面。
(如果这里没有提到某个工具或服务,那是因为我没有看过它,或者找不到足够的信息。我包括了所有的成功和失败。
让我们从一些好消息开始。
Cloudflare 将CF-Connecting-IP
标头添加到通过它的所有请求中;它添加True-Client-IP
为需要向后兼容性的企业用户的同义词。这些标头的值是单个 IP 地址。我能找到的对这些标头的最完整描述听起来像是它们只是使用最左边的 XFF IP,但这个例子不够完整,我自己也尝试了一下。令人高兴的是,看起来他们实际上使用了最正确的方法。
Nginx 提供了一个默认未启用的模块,用于添加 X-Real-IP 标头。这也是一个单一的IP。正确且完全配置时6,它还使用不在“受信任”列表中的最右边的 IP。所以,最右边的IP。也不错。
同样,当配置为 查看X-Forwarded-For
时,Apache 的mod_remoteip会选择最右边的不受信任的 IP 进行设置REMOTE_ADDR
。
Akamai 做了非常错误的事情,但至少对此发出了警告。以下是有关它如何处理X-Forwarded-For
和True-Client-IP
(原始强调)的文档:
X-Forwarded-For header is the default header proxies use to report the end user IP that is requesting the content. However, this header is often overwritten by other proxies and is also overwritten by Akamai parent servers and thus are not very reliable.
The True-Client-IP header sent by Akamai does not get overwritten by proxy or Akamai servers and will contain the IP of the client when sending the request to the origin.
True-Client-IP is a self provisioned feature enabled in the Property Manager.
Note that if the True-Client-IP header is already present in the request from the client it will not be overwritten or sent twice. It is not a security feature.
The connecting IP is appended to X-Forwarded-For header by proxy server and thus it can contain multiple IPs in the list with comma as separator. True-Client-IP contains only one IP. If the the end user uses proxy server to connect to Akamai edge server, True-Client-IP is the first IP from in X-Forwarded-For header. If the end user connects to Akamai edge server directly, True-Client-IP is the connecting public IP seen by Akamai.
相关位是“True-Client-IP
是X-Forwarded-For
标头中的第一个 IP”和“如果 True-Client-IP 标头已经存在于来自客户端的请求中,则不会被覆盖”。因此,True-Client-IP
要么是最左边的 XFF IP,要么是保留客户端欺骗的原始值。只是最糟糕的事情。
但是,也有一句话“这不是安全功能”。嗯,这当然是真的。这个警告可以吗?没有大量 Akamai 用户出于安全相关目的使用True-Client-IP
的可能性有多大?
(我不确定如何解释上面的内容,当它说 XFF 标头“被 Akamai 父服务器覆盖”时。当它说“覆盖”时,它是否意味着“附加到”?还是 Akamai 实际上吹走了现有的标头值?这将违背XFF的精神。
Fastly 添加具有单个 IP 值的 Fastly-Client-IP 标头。我认为它使用了最正确的 XFF IP:
从本质上讲,Fastly-Client-IP
是向 Fastly 发出请求的非 Fastly 事物。
但是:
该值在 Fastly 网络的边缘不受修改保护,因此如果客户端自己设置此标头,我们将使用它。如果你想防止这种情况[你需要做一些额外的配置]。
因此,默认情况下,它是微不足道的欺骗性的。同样,似乎很有可能有很多人将其默认行为用于与安全相关的目的,并使自己容易受到攻击。Fastly-Client-IP
]]>
网站在当今数字时代已成为许多组织获取信息的关键来源。然而,从多个网站的网页中提取和组织半结构化数据存在挑战,尤其是在希望保持广泛适用性的同时实现高度自动化时。在追求自动化的过程中,自然而然的发展是将网页数据提取的方法从仅能处理单个网站扩展到通常在同一领域内处理多个网站。尽管这些网站共享相同的域,但数据的结构可能差异巨大。一个关键问题是在保持足够准确性的同时,这样的系统能够通用地涵盖大量网站。该论文检查了在多个瑞典保险公司网站上进行的自动化网络数据提取的效率。先前的工作表明,使用包含多个领域网页的已知英语数据集可以取得良好的结果。选择了最先进的模型MarkupLM,并使用监督学习使用两个预训练模型(一个瑞典模型和一个英语模型)在标记的汽车保险客户网络数据的训练集上进行零样本学习。结果显示,这样的模型可以通过利用预训练模型,在源语言为瑞典的情况下,以相对较小的数据集在领域范围内取得良好的准确性。
数字时代使互联网成为主要信息来源。互联网上的数据丰富且复杂度增加,同时对更复杂服务的需求也在不断增加。尽管有大量数据可供探索,一个关键挑战是在满足数据质量和有效性要求的前提下,尽可能高效而准确地提取和结构化信息。数据的结构范围从非结构化数据(如文本)到半结构化数据(如超文本标记语言(HTML))再到结构化数据,后者可以采用表格或数据库生成的HTML形式 [1, 2]。
尽管人类可以手动提取这些数据,但自动化这一过程是非常可取的,即最小化人工劳动、错误和干预。存在一些可自动提取信息的网络数据提取方法,但它们的使用高度依赖于泛化和鲁棒性要求。
另一种选择是网站提供 Web 应用程序编程接口(API),使用诸如 RESTful API 或 GraphQL API 等技术。然而,在本论文中不会探讨这种替代方案。
另一种选择是网页的行业标准格式。通过模板对网站进行一些标准化,如[3]所述,但本论文不会关注这种替代方案。
自动化网络数据提取的一个主要问题是系统的灵活性和通用性。根据Sergio Flesca等人的说法,许多系统依赖于包装器,“一组适用于从网站提取信息的提取规则” [4],这些规则与其训练时紧密耦合的网站的底层文档对象模型(DOM)[5]树结构相关。这使得系统对结构的变化非常敏感,除非进行包装器维护 [6],同时在未在训练集中的网站上提取正确数据方面效果不佳。任何这类系统的一个极具吸引力的特征是从先前未见过的网站提取数据(即,它应具有泛化能力),并且在满足使用提取数据的应用程序的具体要求的同时保持足够的准确性。尝试在生成和维护这样的系统期间最小化涉及的手动人工劳动会进一步增加问题的复杂性。
问题的一个有趣的限定是将自动化网络数据提取系统的泛化能力缩小到一组具有一些相似之处的网站。其中一种方法是创建一个特定领域的系统,旨在从同一垂直(即领域)内的多个网站中提取相同类型的对象(例如,图书)。这使系统能够充分利用这些网站在信息和结构上潜在共享的相似之处。
这个问题在很大程度上依赖于所需数据的复杂性(例如,结构水平和目标属性数量),以及目标领域网站表示(即,HTML布局)的相似性。另一个方面是网站的语言,这是一个依赖自然语言处理(NLP)从文本中提取语义意义的系统(即,模型)中的因素。在训练数据有限时,预训练表示通常对提高性能至关重要。虽然英语有大量高质量的预训练模型,但瑞典语的数量并不如此之多。问题的一个有趣方面是预训练表示对网页数据提取模型性能的影响。
一个带有监督学习的网络数据提取模型能够从未见过的瑞典保险网站中提取信息的效果如何?
该论文旨在探索自动化从同一垂直内的多个网站中提取网络数据的可能性。具体而言,将使用瑞典保险网站的用户网页,其中包含其保险计划的摘要。这将有望为使用当前先进技术(SOTA)模型从瑞典保险网站提取数据的可能性和效率提供一些见解。
该论文旨在确定一个适用的网络数据提取模型,然后在瑞典汽车保险网站上对其进行修改和评估。子目标包括:
项目中采用的研究方法将是设计科学 [7],并使用实验方法进行评估。设计科学是一种范式,其中通过设计的工件产生知识和解决方案。
该论文将采用 MarkupLM 模型(参见第2.4.5节),并进行必要的修改以使其与瑞典语兼容。该模型(即,工件)将通过实验评估,以确定它在测试数据集中从未见过的保险网站中提取数据的效果如何。准确性将使用三个指标进行测量:精确度、召回率和 F-分数(这些指标在第2.3节中描述)。
该论文探讨并评估单一模型的变种,而非多个不同模型。所使用的数据将仅为瑞典语且为HTML格式。对于数据集的基准真实性,将不进行手动标注。相反,将使用公司(即,Insurely)开发的手工提取机制生成基准真实性。
第二章介绍了有关自动化网络数据提取的相关背景信息。第三章介绍了解决问题所使用的方法和方法论。第四章描述了对先进技术(SOTA)模型的修改。第五章呈现了对模型进行评估的结果。第六章讨论了所获得的结果,最后第七章提出了论文的结论并提出未来的工作。
这一章概述了与网页数据提取领域(第2.1节)和深度学习(第2.2节)相关的技术,这些技术可能在网页数据提取系统中使用。第2.3节描述了用于评估网页数据提取系统的一些性能指标。不同的网页数据提取方法和三个先进技术(SOTA)模型作为相关工作被介绍(第2.4节)。
网页数据提取是指从网页中提取信息的过程。软件系统通过在内容更改时自动和重复地从网页中提取数据来执行网页数据提取 [8]。每个网页将如第2.1.1节所述表示,页面的特定部分将如第2.1.2节所述被处理。
文档对象模型(DOM)是一个API,使得文档(如HTML和可扩展标记语言(XML)文档)能够被表示为逻辑树(如图2.1所示),由节点组成,每个节点包含对象。通过将文档表示为DOM,然后操作DOM,可以以编程方式更改网页(例如,结构、样式或内容)[5]。
XML路径语言(XPath)以路径符号提供了一种灵活的方法来寻址XML或HTML对象的部分。XML路径语言(XPath)表达式可用于在HTML文件的DOM树中导航,而无需依赖DOM核心特性,例如Document和Node接口,这些接口提供了getElementById()和ChildNodes等方法和属性 [9]。图2.2显示了应用于同一HTML对象的两个XPath表达式的示例。
JavaScript对象表示法(JSON)是一种轻量级的与语言无关的数据格式 [10]。JSON具有易于阅读和编写的文本格式,如图2.3所示。它基于两种结构:一组键/值对和一个有序列表。键/值对的集合称为对象,其中键/值对在左括号和右括号之间列出,键/值之间用冒号分隔。有序列表可以包含多个对象。
1 | [ |
深度学习是机器学习的一个子集,其中建模并训练神经网络,试图模拟人脑在学习过程中的行为 [11]。在深度学习中,需要较少的数据预处理,可以使用非结构化数据,如文本和图像。使用深度学习的一个显著优势是自动特征提取,机器决定哪些特征是相关的,而无需依赖人类专家。使深度学习网络“深”的主要因素包括层中神经元的数量、这些层之间连接的复杂方式以及训练网络所需的大量计算能力 [12]。
以下小节介绍了几个深度学习概念,这些概念对理解模型架构很重要,具体包括卷积神经网络(CNNs)(第2.2.1节)、循环神经网络(RNNs)(第2.2.2节)和变压器(第2.2.3节),以及迁移学习的概念(第2.2.4节)。
卷积神经网络(CNNs)是深度网络的主要架构之一,其目标是通过利用卷积进行特征检测,学习数据中的高阶特征。这通过对两组信息应用数学运算来实现 [12]。CNNs主要用于机器视觉(例如,图像分类),但也适用于文本分析。在建模数据(如图像)时,CNNs具有较高的计算效率,否则在全连接网络中可能导致连接数量激增。
主要的三个层组包括:输入层、特征提取层和分类层,如图2.4所示。架构各层之间的主要区别在于特征提取层,它包含两种类型的层:卷积层和池化层。
卷积层在数据中寻找特征,通过对输入应用滤波器将这些特征组合成高阶特征。图2.5中显示了一个这样的滤波器,其核(即,滤波器)向量的权重为[1/3, 1/3, 1/3]。在一层中可以应用多个不同的滤波器。在应用滤波器后,激活函数用于决定哪些神经元应该被激活并传播其值。两种这样的激活函数是修正线性单元(ReLU)和高斯误差线性单元(GELU)。线性函数ReLU对于所有非负输入都输出相应的输入,否则输出零,即max(0, x)。而GELU [13] 是一个更复杂的非线性函数,可以看作是ReLU的一个更平滑的版本。
池化层在卷积层之后使用,以减小(即,下采样)数据表示的空间大小。这有助于减少网络记忆训练数据的自由度(即,过拟合),而是迫使其进行学习泛化;因此,在未见过的数据上表现更好。最大池化是其中的一种常见变体,它选择滤波器区域中的最大值。
RNN与其他类型的神经网络有所不同,因为它们具有对数据的时间维度(即,时间依赖性)进行建模的能力。RNN在每个输入(即,时间步)之间保留状态,它使用这些状态对数据进行建模,然后将状态传递到下一个时间步。这种时间反馈使模型能够捕捉上下文,特别是对于需要基于当前和先前输入生成/推断序列的敏感数据,如语言、音频和文本 [12]。
长短时记忆(LSTM)[14]是最常见的RNN架构之一。其主要优势在于它能够在时间步之间保持内存不变。这种特性使其能够克服梯度消失问题,即模型由于模型(即,权重)的更新变得非常小,导致模型停止学习,无法进一步捕获任何输入。
变压器是由Ashish Vaswani等人在他们的论文《Attention is all you need》[15]中提出的最新架构之一。它是一种完全基于注意机制而非循环或卷积的架构。与具有顺序性质的循环相比,这种结构在训练期间具有更大的并行性,其中新的隐藏状态是作为过去状态的函数而生成的。
变压器架构基于一个编码器-解码器结构,包括编码器和解码器堆栈,每个堆栈由六个相同的层组成。编码器堆栈负责将输入的符号序列映射为连续表示。解码器堆栈生成一个符号序列,其中每次生成一个符号,并在下一生成步骤中用作额外的输入。
迁移学习是通过从相关领域传递信息来改进某一领域中的学习者的一种方式。在神经网络中,由于需要更大的数据集来训练网络以避免过拟合 [16],迁移学习可以发挥重要作用,特别是在训练集有限的情况下。与从头开始训练一个模型不同,可以利用已经使用与目标域相关的更大数据集进行训练的模型,用于任务如文本情感分析和图像分类 [17]。迁移学习可以在包含两个阶段的学习框架中形式化:预训练和微调 [18]。
预训练阶段包括捕捉一个或多个任务的知识。这可以通过大规模未标记的语料库来学习良好的表示,然后在其他任务中使用这个表示。预训练的一些优势包括学习通用语言表示、更好的模型初始化以及在小数据集上防止过拟合的正则化效果 [16]。微调阶段使用预训练模型,并进一步使用代表特定问题的较小数据集进行所谓的下游(即,目标)任务的训练。
该模型的三个评估指标将是:精确度、召回率和F分数。在关注分类性能的机器学习应用中,这些是关键指标。
精确度衡量正类别的预测值,同时避免将负类别错误地分类为正类别 [19]。具体而言,正确定义的预测中实际正确的比例:
召回率衡量正类别的预测值,同时避免将正类别错误地分类为负类别 [19]。具体而言,正确定义的预测与所有实际正类别的比例:
F分数,即F1分数,是精确度和召回率之间的调和平均值。基于F-beta分数,其中精确度和召回率根据beta值具有不同的权重 [19]。当beta为1时,精确度和召回率具有相等的权重(即,相等的重要性)。
在优先考虑精确度或召回率的情况下,高度依赖于具体情境。由于它们通常对彼此产生相反的影响 [20],最大化其中一个可能会降低另一个。在医学诊断中,假负例可能比假正例更昂贵(即,致命),因此在这种情况下可能更重要,应相应地加以权重。
存在一些可以构建在其基础上的相关工作。具体而言,有关网络数据提取文献的调查(第2.4.1节),语言模型Bidirectional Encoder Representations from Transformers(BERT)(第2.4.3节)以及SOTA模型MarkupLM(第2.4.5节),本论文使用它们作为基础。
Emilio Ferrara等人进行了一项调查,全面概述了网络数据提取领域的文献,并为网络数据提取应用提供了分类框架 [21]。他们确定了两种主要的算法方法:树匹配和机器学习算法。
树匹配算法利用Web页面的半结构化特性,以HTML的形式表示为带有标签的有序根树,即DOM树。
这些类型的算法使用XPath语法处理DOM树中的特定元素。它们依赖于XPath表达式,以找到两个文档之间相似树的所谓树编辑距离匹配。类似于字符串编辑距离问题,两个有序树可以通过尽可能少的操作(即,节点删除、插入或替换)来相互转换以匹配。简单的树匹配算法 [22]是树编辑距离匹配问题的高效且易于实现的解决方案 [23]。
机器学习算法是一种适用于具有不同结构的多个网站的领域特定提取的良好方法。这些算法依赖于手动标记的网站,以获取领域专业知识,一些最早使用机器学习的系统包括WIEN [24]、Rapier [25]和WHISK [26]。
WIEN专注于归纳学习技术,以自动生成包装器。生成的规则可能类似于“忽略所有字符,直到找到第一个’.’并提取餐厅名称,该字符串以第一个’:’结束。然后,再次忽略所有字符,直到找到’(‘并提取以’)’结束的字符串。” [27]。类似这样的规则会在存在多个对象的情况下重复,直到无法与其他对象匹配。
Rapier使用有限的句法和语义信息学习规则,而无需在文档之前进行解析或后处理。规则分为三种模式:前填充器、填充器和后填充器。其中前填充器和后填充器充当左右分隔符,而填充器模式描述目标信息结构。
WHISK生成可以处理各种结构的文档(从自由文本到HTML)的规则。这些规则是一种特殊类型的正则表达式(即,尝试与输入文本匹配的模式),由两个组件组成。第一个组件负责确定短语必须处于其中以使其相关的正确上下文,而另一个指定要提取的短语的哪些部分。
Shuo Zhang等人进行了一项调查 [28],研究了有关网页表提取的文献。其目的是确定和描述几个网页表提取任务及其相互依赖关系。他们确定了六个主要类别,用于对文献进行分类。这些类别包括:表提取、表解释、表搜索、问题回答、知识库增强和表增强。
他们定义了一个表由以下元素组成:页面标题、标题、列、单元格、行、列和实体。提出了一种表分类方案,通过两个维度内容和布局来区分表。
表提取是在网页上检测和提取表格,然后以一致的格式存储的过程。在网上提取表格的第一步是过滤掉“不好的”表格(例如,用于布局或格式目的的表格)。这通常通过关系表分类来完成,以识别包含关系数据的表格。在这里,可以使用具有布局或内容类型特征的机器学习分类器。布局特征可以是行数、列数或平均单元格字符串长度。而内容类型特征可以是表体中非字符串数据的百分比、带
有数字字符的单元格的比例,或包含 <span>
标签的单元格的比例。类似的方法也可以用于表头检测和表类型分类,前者检测表是否包含标题行或列,而后者根据预定义的分类法对表进行分类。
表解释旨在发现网页上表格的语义,以便对表格中的数据进行智能处理。使用分类法来了解表列的含义以及它们是否与其他列相关。主要的任务有列类型识别、实体链接和关系提取。
列类型识别涉及确定列类型并定位核心列(即,主体列),通常是最左边的列。实体链接是指检测实体(例如,人物、组织和地点),这对于揭示语义至关重要。关系提取旨在将一对列与其内容之间的关系关联起来。
表搜索通过关键字查询返回带有排名列表的表,其中查询可以是一个表或多个关键字。主要有基于关键字和基于表的两种搜索类型。基于关键字的搜索返回给定关键字查询的表的排名列表。
问题回答试图使用表格中的结构化数据回答自然语言处理问题。使用表格回答问题的主要挑战是将非结构化查询与表格中的结构化信息匹配。将查询解析为形式化表示的任务称为语义解析,其中生成逻辑表达式,可在知识库上执行。
知识库增强使用表格数据来探索、扩展或构建知识库。知识探索可以在具有属性搜索查询或实体关系查询的表格上进行。通过使用知识库进行注释,然后从表格中提取信息,可以扩展现有的知识库。如果表格包含丰富的信息,它可以转化为新的知识库。
表增强通过添加附加数据扩展现有表格。它可以分为三个任务:行扩展、列扩展和数据完成。行扩展通常应用于水平关系表。相反,列扩展通常通过查找相似的表格,然后评估这些表格中的列标题和值来添加额外的列。数据完成可以应用于整个列,通过匹配来自其他表格的类似列,或在单个单元格上使用机器学习算法,例如k最近邻或线性回归。
Jacob Devlin等人提出了一种名为BERT的新语言表示模型 [29]。预训练语言模型已被证明可以在句子和标记级别的任务上提高几种自然语言处理问题。然而,以前的技术限制了预训练模型的体系结构选择,使其能够联合条件化左侧和右侧(即双向)上下文,这对于标记级别的任务如问答至关重要。BERT通过利用Transformer体系结构(第2.2.3节)和两个预训练目标实现了双向预训练。
该体系结构是一个多层双向Transformer编码器,并具有需要最小更改用于最终下游体系结构的属性。输入表示可以处理单个和多个句子作为输入序列,并以三种方式嵌入:令牌、段和位置嵌入。这三种嵌入求和以表示输入嵌入,如图2.6所示。一个令牌可以是三种情况之一:特殊的序列开始令牌([CLS]),一个单词或一个分隔令牌([SEP])以区分句子。特定于序列中的令牌所属的段嵌入(例如,句子A或B)。位置嵌入编码了序列中令牌的位置。
两个目标,遮蔽语言建模(MLM)和下一句预测(NSP),在预训练期间被使用。MLM通过随机遮蔽输入标记的一部分,然后训练模型预测被遮蔽标记,使模型学习双向表示。NSP使模型学习两个句子之间的关系。选择两个句子A和B,其中句子B一半的时间被随机替换,要求模型预测句子B是否跟随句子A。
BERT使用两个数据集进行预训练:BooksCorpus [30](800M字)和English Wikipedia(2500M字)。BERT是第一个基于微调的表示模型,在多个标记和句子级任务上取得了SOTA结果,如通用语言理解评估(GLUE)[31]、斯坦福问答数据集(SQuAD)[32]和带有对抗生成的情境(SWAG)[33]。
Yichao Zhou等人探索了在相同垂直领域内从多个网站提取数据的可能性 [34]。他们提出的模型∗,称为SimpDOM,在使用Few-Shot Learning(FSL)准确提取未见网站的数据时取得了SOTA结果。
SimpDOM模型的主要思想是专注于HTML页面的DOM树表示,并为每个变量节点构建丰富的表示。该方法避免了昂贵的网页呈现过程,利用DOM树中节点属性值的语义。
该架构由DOM树简化模块、离散特征模块和文本编码器组成。DOM树简化模块提取具有不同值的所有节点的上下文(因为在数据点之间具有相同值的节点不感兴趣)。上下文是其友好节点(即附近节点)的特征。离散特征模块通过添加额外的离散特征(例如XPath、叶节点类型和相对节点位置)来增强节点表示。文本编码器是CNN-LSTM的组合,对字符和单词级特征进行编码。
使用Structured Web Data Extraction(SWDE)数据集对SimpDOM进行评估。该数据集最初由郝强等人创建 [35],包含来自80个不同网站的124,000个标记页面,分为八个垂直领域(例如汽车、图书和电影),每个领域包含3到5个感兴趣的属性(例如标题和作者)。在每个垂直领域中,使用10个网站中的5个作为种子站点(即训练集中的站点),SimpDOM实现了93.75的平均F1分数。
SimpDOM的作者选择使用一个基于Global Vectors for Word Representation(GloVe)[36]架构训练的,包含60亿标记的著名预训练词嵌入来初始化他们的模型。
Junlong Li等人研究了创建一个模型∗,能够解决多个文档理解任务,适用于视觉丰富的标记文档,如HTML和XML文件 [37]。任务包括文档理解、类型分类和视觉问答。通过利用DOM树,可以对文档的不同元素之间建模位置关系,而不是使用显式的2D表示,这对文档渲染的设备高度依赖。通过使用DOM树建模位置关系而不使用渲染的2D可视化,简化了预训练,同时仍然利用了文档布局。
BERT [29]体系结构被用作编码器,其中嵌入层扩展了额外的输入XPath嵌入。然后,该模型通过三个主要目标进行预训练:遮蔽标记语言建模(MMLM),节点关系预测(NRP)和标题页匹配(TPM)。MMLM是MLM的扩展,通过使用文本和标记作为输入,遮
邓翔等人提出了一种能够解决类似文档理解任务的模型,与MarkupLM一样,利用了DOM树表示法,就像以前的工作所做的一样 [39]。该模型基于BERT(与MarkupLM相同),其参数是从预训练的BERT模型(对非结构化文本进行预训练)中初始化的,然后进一步训练以捕获HTML文档的结构和布局信息。该模型以两个目标进行预训练:遮蔽节点预测(MNP)和遮蔽标记预测(MTP)。MTP类似于BERT中执行的MLM目标(以及MarkupLM中的修改变体MMLM)。MNP通过不仅遮蔽输入标记而且遮蔽整个节点来进一步概括模型,以迫使模型学习树级上下文化,并对布局具有整体视图。
该模型的主要方法是将文档编码为一组子树,其中嵌入了位置信息,并采用了自监督预训练。首先通过去除与网页结构和语义无关的所有DOM节点(例如,<script>
和 <style>
元素),然后将树分割成子树来构建一组子树。分割是通过在整个DOM树上应用具有固定步长(即单位)的滑动窗口来完成的。滑动是这样进行的,以便在同一直接周围的节点被捕获在同一子树中。
在培训过程中使用的数据量与SimpDOM和MarkupLM中使用的数量有所不同。DOM-LM仅使用了10%的种子站点(2和5)的数据,而不是所有的数据,这与SimpDOM和MarkupLM不同。结果可见于表2.2。
本章在第2.1节介绍了相关的关键网络技术DOM和XPath,第2.2节介绍了深度学习架构,如RNN和Transformer,第2.3节介绍了度量标准,第2.4节介绍了相关工作,如BERT和MarkupLM。
在本章中,介绍了研究方法。研究过程在第3.1节中描述,数据收集过程在第3.2节中描述,最后,在第3.3节中描述了实验设计和评估框架。
该论文遵循设计科学研究过程,该过程可以分为六个活动,如Ken Peffers等人在[40]中提出的。这些活动包括:问题识别和动机、解决方案目标、设计和开发、演示、评估和沟通。根据这个过程,研究人员并不被期望按顺序进行这些活动,而是取决于所选择的方法(例如,问题中心或目标中心)。
问题识别和动机的活动涉及具体说明研究问题并证明解决方案的价值。这可以通过获取有关问题状态和解决方案重要性的知识来实现。
解决方案目标指的是解决方案的定量或定性目标。这些目标通常涉及所期望的解决方案,该解决方案应更好或解决未解决的问题。在这个阶段,需要了解当前解决方案。
设计和开发涉及创建一种工件解决方案(例如,构造、模型、方法)。在创建解决方案之前,需要理解理论,决定工件的功能和架构。
演示工件在解决问题时的有效性。这可以通过实验、模拟、证明或案例研究来完成。
评估是衡量工件支持问题解决的效果。通过使用相关的度量和分析技术,可以确定工件的有效性,并作为迭代回活动3(设计和开发)的基础,以尝试在可行的情况下改进工件。
沟通是最后的活动,在其中整个过程都被记录在研究论文中。这包括问题及其重要性、工件及其效用、研究的设计以及与社区的相关性。
问题是在主机公司识别的,解决方案得到了证明。随后进行文献研究以更好地了解问题的状态和当前的解决方案。然后确定了建立在当前解决方案基础上以在新环境中解决问题的目标。工件的评估类似于相关工作中的模型。最后,在这篇论文中记录了整个过程。
汽车保险数据来自几家瑞典保险公司,其中主机公司目前使用和维护手工制作的包装器。数据将采用HTML的形式,并附有JSON文件,表示由包装器提取的值的基本事实。敏感用户信息在用于模型之前被混淆。仅使用具有不为空的JSON对应文件的HTML文件。如果JSON中没有提取的值,则假定包装器失败或用户在给定网站上没有保险。
实验设计遵循相关工作中使用的设计[34, 35, 37],在k个种子站点上对模型进行微调,然后在其余的n−k个站点上进行评估(即零-shot学习)。评估指标是页面级F1分数,最终F1分数是每个k的所有排列的平均值。模型使用一个瑞典和一个英文预训练模型初始化,然后进行实验性评估和比较。
模型在深度学习的Amazon Machine Image(AMI)[41]上进行训练,实例类型为G4dn [42],具有以下规格:1个Nvidia Tesla T4图形处理单元(GPU),8个Intel Cascade Lake虚拟中央处理单元(CPU),32GB内存。
本章描述了获取数据集和修改针对性网站的开源模型的步骤。数据预处理步骤在第4.1节中介绍,MarkupLM模型的实施和修改细节在第4.2节中给出。
在使用模型训练数据之前,数据需要进行处理。从公司收到的数据包括三种类型的文件:HTML、JSON和文本(日志)文件。HTML文件包含用户的保险数据,并且仅限于包含单一保险的页面(省略包含多个保险的HTML文件)。JSON文件包含目标属性的提取数据,公司手动开发的包装器执行提取。最初的计划是使用JSON数据作为数据集的基本事实,但由于某些值是使用正则表达式进行转换的,因此与HTML中的文本不是精确匹配,这是不可能的。日志文件包含包装器整个执行流程的日志消息。幸运的是,提取的值在转换之前被转储到日志中,因此可以使用日志中的数据作为基本事实。
模型要提取的属性数量被限制为三家选定公司的较大部分数据中出现的五个属性:Trygg-Hansa、If和Moderna。数据集中仅使用包含所有属性的数据点。这五个属性是:保险覆盖类型、保险单号、年度保费金额、续保日期和车辆注册号。每家公司的数据点数量如表4.1所示,每个属性的统计信息如表4.2所示。
通过解析包含以JSON格式提取和未经处理的值的日志,生成了包含HTML和基本事实文件的数据集。基本事实JSON中的对象通过保险类型(即汽车保险)进行过滤。如果网页上有其他类型的保险或超过一种汽车保险,则会省略数据点。之所以这样做,是因为三家公司中有两家公司在单个网页上显示客户的所有保险,而在DOM树中没有区分。这意味着所有保险都以相同的HTML列表对象的方式列出,而且在其中没有任何特定顺序,这样模型就很难学习上下文如何区分一个保险对象和另一个保险对象。
在数据集可以传输到AMI并在模型中使用之前,必须模糊化可以与特定个人关联的所有敏感数据。属性保单持有人、地址、保险单号和车辆注册号都被替换为随机生成的值。使用网站www. fejk.se生成虚假姓名、个人身份号码和地址,而使用Python脚本生成保险和车辆注册号。
所使用的模型基于开源模型MarkupLM [37],该模型在第2.4.5节中有描述。此模型使用Python编写,使用Pytorch机器学习框架 [43] 和提供API以轻松下载和训练SOTA预训练模型的Transformers库 [44]。模型的执行步骤如图4.1所示。
第一步将HTML文件打包成适当的Python数据对象,然后将它们序列化为单个文件,这是使用pickle库 [45] 完成的。第二步创建了每个HTML文件与相应基本事实之间的映射,并将其作为pickle文件分别存储在每个网站上。最后一步使用所有种子站点的排列训练和评估模型,其中在训练之前使用预训练的模型初始化。模型在多个周期(即在训练集中循环)中进行训练,最后在未被种子化的每个网站上进行评估。
大部分的修改发生在数据准备和评估步骤。数据准备步骤,即HTML与基本事实之间的映射,需要修改以处理瑞典字符,并处理网站的边缘情况,其中基本事实的值并未单独位于正确的节点中,而必须使用正则表达式进行匹配。评估步骤通过更详细的记录和在每个周期后进行模型评估,引入了早停机制,以在训练损失不再降低时终止训练。
在对保险数据集进行微调之前,MarkupLM模型是使用预训练模型初始化的。论文中使用的两个预训练模型是MarkupLMLARGE和来自瑞典国家图书馆的瑞典BERT模型。
MarkupLM论文的作者们 [37] 也开源了两个预训练模型,MarkupLMBASE和MarkupLMLARGE。他们首先在Common Crawl(CC)数据集∗的 2400 万个英语网页上对MarkupLM模型进行了预训练,该数据集使用了原始论文作者发布的Robustly Optimized BERT pretraining Approach (RoBERTa)模型进行初始化 [46]。然后在SWDE数据集上对该模型进行了微调。MarkupLMLARGE在SWDE上的性能见表2.1。
瑞典国家图书馆(瑞典文:Kungliga biblioteket)于2020年发布了三个基于BERT和A Lite BERT (ALBERT) [47] 的预训练瑞典语言模型。这些模型是在一个18,341 MB的瑞典文语料库上进行训练的,该语料库由报纸、政府报告、法定电子存档、社交媒体评论和维基百科的文本组成。他们的预训练BERT模型名为KB-BERT,用于在微调之前初始化模型。初始化是通过使用Transformers库实现的,加载托管在AI社区站点Hugging Face [48] 上的预训练模型,该站点还负责Transformers库。表4.3显示了两个预训练模型的一些关键模型配置参数,这些参数遵循原始BERT论文的设置(除了词汇表大小)。
这一章介绍了模型评估的结果。以精确度、召回率和F1分数为指标,展示了两个模型的页面级结果,并详细分析了属性级结果。
两个模型的总体结果显示在表5.1中。使用KB-BERT初始化的模型在训练过程中使用一个种子站点时,最佳F1分数为41.4,当使用两个种子站点时为47.3。使用MarkupLMLARGE初始化的模型在使用一个种子站点进行训练时,最佳F1分数为80.2,使用两个种子站点时为88.9。在评估过程中变化的最佳设置分别在表5.2和表5.3中显示,对应着一个和两个种子站点。
使用KB-BERT初始化的模型每个属性的F1分数显示在表5.4中。该模型无法学习保单号的表示,同时在处理年度保费属性时也存在一些问题。
使用MarkupLMLARGE初始化的模型每个属性的F1分数显示在表5.5中。该模型对大多数属性学到了一个表示,但在处理保险类型属性时并不那么成功。
这一章将讨论第5章中提出的结果,同时也将包括对实现目标(第1.4节)和研究问题(第1.2节)的探讨。
结果表明,尽管MarkupLMLARGE模型并不“理解”瑞典语,而是学会了HTML的一般结构,但它在预训练模型的强大性上表现出色。这一事实事后看来并不令人惊讶,因为选择的五个属性中有四个不特定于瑞典语。
使用MarkupLMLARGE初始化的模型在三个属性上表现非常好:保险单号、续保日期和车辆注册号,在使用一个和两个种子站点时,F1分数均超过94%。保险单号具有特定格式(例如123 456 789、AB00123456.2.3和123456-78),每家公司有时甚至有几种格式。续保日期始终以YYYY-MM-DD的格式呈现,而车辆注册号有两种格式之一,即ABC123或ABC12D。年度保费这个属性的得分相对较低,这很可能是因为它在不同公司之间的格式不同,有时包含瑞典语词汇(例如“年”这个瑞典词)。然而,即使对于只包含瑞典语词汇的保险类型属性,该模型在某种程度上也成功地学到了,特别是在将种子站点数量从一个增加到两个时。
与MarkupLMLARGE初始化的模型相比,使用KB-BERT初始化的模型表现相对较差。使用一个和两个种子站点时,得分最高的属性是覆盖类型和续保日期,F1分数均超过71%。覆盖类型通常是“halvförsäkring”、“helförsäkring”、“trafikförsäkring”或其变体中的一个词,这是模型在这方面优于MarkupLMLARGE初始化的模型的唯一属性。令人惊讶的是,该模型无法学习保险单号的表示,对于使用一个和两个种子站点时的F1分数均低于4%。这可能是由于不同公司之间的格式不一致,再加上一些格式包含标点符号,这可能使学习变得更加困难,因为BERT将标点符号视为输入序列中“句子”的分隔符。
另一个方面是,瑞典BERT使用了较小的BERTBASE的参数,而MarkupLMLARGE使用了较大的BERTLARGE,它们在隐藏层的数量、隐藏层的大小以及每个隐藏层的注意头的数量上存在差异(如表4.3所示)。然而,在原始的BERT论文[29]中,BERTLARGE在GLUE基准上的得分约高出BERTBASE约3%,而在使用5个种子站点时,MarkupLMLARGE在SWDE数据集上的得分比MarkupLMBASE高出约1.5%。这应该表明,性能的这种大差异不能仅通过网络大小的差异来解释。
在表5.2和表5.3中显示的设置是为了寻找最佳精度而进行的调整的设置。在表现最佳的运行之间,最常见的批量大小为2。这似乎是合理的,因为数据集很小,数据集中最大的公司有100个数据点,而最小的公司只有23个。用于上下文的前面节点的最佳数量是4。训练时间的巨大差异可以通过两个预训练模型之间的参数差异(主要是层数和层大小)来解释。作为参考,BERT论文的作者使用4个云张量处理单元(TPU)[49]对BERTBASE进行了训练,分别对BERTLARGE进行了16个云TPU的训练,分别进行了4天的训练。
在网络数据提取领域存在多种度量标准,然而,本论文专注于三个准确性指标:召回率、精确度和F分数。尽管这些指标并非完美[50],但它们在机器学习评估中被广泛使用。由于大多数相关工作都在使用这些指标,尽管它们在处理负面例子时表现不佳,但它们被选择了。F分数的beta值被选择为1(即F1分数),因为没有(通过主机公司)明确要求将精确度优先于召回率,反之亦然。
在第1.4节中提到的四个子目标是:数据集获取、模型识别、模型修改和模型评估。根据结果,可以说这四个子目标都已经实现。
收集到的数据比预期的要小,无论是从公司和网站的数量上还是从每家公司和网站的数据点数量上。然而,尽管数据集相对较小,MarkupLMLARGE模型取得了相对较好的分数,展示了预训练模型的强大性能。
模型的识别是成功的,并导致了SOTA模型MarkupLM的产生,尽管最初选择的是SimpDOM(第2.4.4节),这在第7章中作为一个局限性进一步讨论。模型修改按照最初的计划使用了一个预训练的瑞典BERT
模型,尽管其性能不及MarkupLM作者发布的预训练模型。最后,该模型使用了两个不同的预训练模型,在使用一个和两个种子站点时进行了评估。
最初提出的研究问题是:
一个带有监督学习的网络数据提取模型在从未见过的瑞典保险网站中提取信息的能力如何?
最初的假设是这样的系统必须对瑞典语有一个良好的表示才能有效,因此需要使用在瑞典语上进行预训练的语言模型进行探索。然而,结果表明,这种对语言的依赖性表示不一定是必要的,至少当大多数属性不是语言特定的或者在其上下文中不被语言特定的文本包围时。此外,即使对于语言特定的属性,例如保险类型,也是在既预训练于英语语言又预训练于网页的模型表现得更好,而后者仅在瑞典语上进行了预训练。
对研究问题的回答是这样的模型实际上在瑞典保险网站上表现良好,达到了与在SWDE数据集上的SOTA模型相似的结果(如表2.1所示)。
最初的假设认为在瑞典网站上,以英语数据为基础的模型性能较差。然而,结果表明,如果属性不包含特殊的瑞典字符,这样的模型表现良好。这种模型的学习强调HTML文档结构,特别是当值嵌入在结构化格式(如表格)中时。虽然没有达到100%的准确性,但在手动创建的包装器因站点故障或HTML更改而失败的情景下,该模型可能会很有价值。
显著的限制包括有限的可用数据量,导致较少的候选公司可供论文使用。数据的倾斜和耗时的预处理影响了模型的探索。尝试复制 SimpDOM 的努力没有成功,妨碍了与 MarkupLM 的潜在比较。
尚未完成的工作
下一个明显需要做的事
从经济角度来看,所探讨的模型减少了在相同行业内设置数据提取的手动工作,并减少了对覆盖的网站进行的维护。与模型培训相关的计算成本可以通过使用开源预训练模型来缓解。伦理考虑涉及根据政府政策和法规处理客户数据。敏感数据的混淆是一种方法,但自动确定哪些数据是敏感并需要混淆是本工作范围之外的问题。
引用:
在讨论《苏联解体的根本原因》时,提到了几个问题。其中有一个是:
那场运动其悲怆性在于试图挑战: A.人性 B.财产私有制。注意不是生产资料私有制,是财产私有制。
多数人选的是人性。早半年前,我也会选人性。但后来想想家,并不究竟。这种所谓的人性很大程度上是财产私有制铸就的。人性本身没有善恶,它随环境的改变而改变,好的环境(或者说好的制度)造就好的人性,坏的环境造就坏的人性。大家仔细琢磨琢磨这句话,是不是?
疫情期间,内地各省医护人员驰援武汉,其中固然有行政指令,但很多都是志愿出征。相比之下,香港疫情期间则是七千医护人员大罢工,韩国则有过十三万医护人员大罢工。你总不能说因为他们天生自私,而内地人民天生就无私吧。当然是受制度影响。人性和制度之间,一定程度上是有驯化关系的。大家看到的损公肥私,人不为己天诛地灭,本质上都是财产私有制外化为人性之后的表现。
之前聊过两次避税的问题,我发现很多人都把逃税问题道德化了。明星、网红挣了那么多钱,怎么还逃税呢?说实话,易地而处,换了你我,赚了那么多,也会想方设法少交税的,这是财产私有制所决定的。只要有可能,每个人都会试图最大化私人占有。你之所以不避税,是因为你无税可避。这不是道德问题,当你把矛头直指明星的时候,问题的关键就被忽略掉了。为什么税收洼地的bug会有这么多呢?再有,许多人有个天真的逻辑,认为人有钱了就应该慷慨一些,你都那么有钱了为什么不多交点税呢?注意,钱少的时候叫零花钱,钱多的时候叫什么?钱多了就要拿来投资,获取利润或者孳息,这个时候它的名字叫资本。
资本以增值为核心,对成本费用最是锱铢必较。所以,当一个人非常有钱的时候,就要为资本考量利弊得失了。当你只有100块钱的时候,投资回报率从1%增长到2%也才涨了1块钱;当你有100亿的时候,1%的投资增长将为你带来1个亿的投资回报。有钱人在生活花费上可能会非常慷慨,但是在投资和生意场上则一定上精打细算,分斤拨两。你肯定见过这样的老板,红包一甩就是好几千,送礼也毫不吝啬,但是一转头管理公司的时候,对费用卡扣就非常严格,你部门那小打印机有年头了,总卡纸不好用,想买个新的,报上去都不给批。
再次回顾一下王朝周期律吧。王朝中后期土地兼并,而占有大量土地的士绅不纳税,底层又交不上税,财政困难,接着就引向了两个结果。要么是没钱养不起兵,被外族击溃,要么是就是向底层加税,官逼民反。换到近现代依旧适用,只是士绅换成了资本家,土地兼并放大为财富兼并。
我之前说周期律运转的源动力是不公平,其实不对。周期律的起点是什么,是兼并。那我们再问一句,为什么要兼并?财产私有制嘛。财产私有制之下,人们会不遗余力地扩大私人占有,所以才会造就古代版的官僚资本主义。官僚大地主联合集团,有了兼并也才会引发后续的连锁反应。所以,不公平是结果,周期律运转的源动力是财产私有制。正因如此我才会在苏联解体那期最后的小结里说,财产私有制是带有自毁倾向的,是所有帝国走向熵增的源头。
那为什么王朝周期律可以周而复始呢?因为历经起义或革命之后,兼并在相当程度上被打破,王朝初期重新实行均田,或者类似均田的政策,近现代则叫土改。
我们讲历代王朝经济恢复,经常会提到王朝初期的帝王轻徭薄赋、无为而治、与民休息…其实这些都是表象,内在原因是兼并被打破,经济、社会重新焕发活力。革命的本质是什么?革命的本质是打破兼并,是激烈的产权关系调整,是将合法利益非法化。再说直白一点,是对财产私有制的一次具备正当性的集体侵犯。为防有人挑刺,我强调一下,这里的“侵犯”是站在和平年代视角回望之下的叙事。所以大家一定要明白,为什么学历史的时候书上会说,“私有财产神圣不可侵犯”是资产阶级宪法原则。因为当你将财产私有制神圣化的时候,底层的起义、革命的正当性就被彻底否定了。我说这些并不是在批判或者否定财产私有制,大家去价值化的看,这是客观存在的历史、经济规律,不以人的意志为转移。
财产私有制是具有双面性的。一方面,它是目前为止人类社会一切伦理的基础,是个人奋斗的出发点,可以想一想,你的大部分喜怒忧思惊恐悲都和财产私有制有直接或间接的关系。但另一方面,它也是一切政治经济问题的总根源。所以为什么说“悲怆”,表面上看他是在斗官僚资本主义,限制资产阶级法权,实质上他在做什么?他是在挑战财产私有制作为源动力的王朝周期律。这是不可能成功的。时代和一辈人还为此付出了巨大的代价。不可为而为之,是为“悲怆”。
我解释按劳分配的时候,举了一个金某、慕某做鞋的例子。说金某手速快,慕某手速慢,结果金某鞋子做的就多,差量累计,金某财富超过慕某,甚至还能开工厂、财富传承后代。在这个过程中,赤中生白,所以叫资产阶级权利。
有朋友提出问题了,说我讲的不是按劳分配,是按要素分配。这个例子确实不对,但又要分两个层次来说。
第一层,我并没说这个故事中每个环节都是按劳分配,我只强调两个人的起点是按劳分配,由这个起点开始,由于天赋差异逐渐形成财富分化,金家拥有了更多的生产要素——资本、土地、技术,而慕家始终只有劳动力要素。按要素分配导致两家的经济基础进一步分化,也就是说是按劳分配促成了后来的按要素分配。这个过程的叙事问题不大。
第二层,金某、慕某各自做鞋,两个人明显属于个体户。个体户劳动所得是否属于按劳分配?这里就有问题了。
大家初中时代就学过,有三种劳动所得:按劳分配所得、按劳动要素分配所得、按个体劳动成果所得。同样做鞋子赚钱,你在国企,那就是按劳分配所得;你在私企,那就是按劳动要素分配所得;你是个体户,那就是按个体劳动成果分配所得。为啥会有这样的分法,仅仅是按照公有制、私有制、个体经济来做的区分吗?往深了不太好讲,只能简单解释下。一家私企,老板投资,工人干活,老板投入的是资本要素,工人投入的是劳动力要素,分钱的时候,按生产要素分配,老板拿的那部分就是资本要素所得,工人拿的那部分就是劳动要素所得。切换到国企,工人拿完工资之后,剩下的归公司或上交国家,而国家是代劳动者管理生产资料,也就是说公司、国家的那部分归根到底也是劳动者的,只不过留作扩大再生产罢了,这个时候理论上只有劳动者在分账,所以是按劳分配。如果是个体户,交完给国家的税费,剩下的都是自己的,没有众多劳动者分账这一步,所以叫按个体劳动成果分配。但从宽泛的意义上来看,个体劳动成果分配是按劳分配的一种极端形式。所以我那个例子可以做个修正,说金某和慕某都是某国企鞋厂员工。咱们现在一般就是这么作区分的,但是,前述的区分方式很笼统。大家要知道一件事,打从建国以来,中国语境里的“按劳分配”就和马克思所说的“按劳分配”不是一回事儿。咱们只是借用了概念,内涵有非常大的区别。我们说的“按劳分配”其实是“按劳动力贡献分配”。具体有点小复杂,我就不展开说了。
还有,我想再强调一下,苏联解体那期,我不是在否定按劳分配。如果把那期看完,应该能明白我的意思。但是很遗憾,苏联解体那期分为两个版本,一个是拆分版、一个是完整版。数据显示拆分版完播率50%,完整版只有28%,明白了吧,大家没有必要在评论区吵,多数人只看了几分钟就来下结论了,网络就是这样的。
直接税改革事关税基调整,事关财政,而充裕的财政于国运、于亿万人民之未来至关重要。帝国的灭亡往往直接肇因于财政的崩溃,而直接税改革是巩固财政,打破王朝周期律的关键。
首先要明白财政的重要性。说来很有意思,聊到个人,聊到公司,我们都能明白钱有多重要。可是一说到国家,好多人就忽然闹不明白了,堕陷到文化传统、意识形态之类的赛道里争辩,却忽略了财政的决定性作用。
不论资社,这个世界很现实,只要做事就得花钱,基本建设、科教文卫、社会保障、企业扶持、国防安全…干啥都要花钱。各国政府就是这个世界上最大的烧钱主体。只要有钱,啥事儿都好办,摆平了就风平浪静,没钱了,妖魔鬼怪就一股脑儿冒出来。
大家读历史一定有一个感觉,每到王朝末期,天灾就集体上线,旱灾、蝗灾、洪涝、地震、瘟疫…似乎天人感应一般,水利设施也都荒废破坏了,这所有的一切恶化了老百姓的生存条件,成为王朝灭亡的加速器。结果就是农民起义风起云涌,中原大地王朝兴替。
实际上,管他是发大水还是闹蝗灾,国家只要能够拨款赈灾,这事儿就解决了,解决了就没有后续了,大家也就不记得有这回事儿了,没解决,亡国了,大家也就跟着记住了这些事儿了。本质上是幸存者偏差。
那么为什么没解决呢?因为历代王朝到了末年,除了官僚主义之外,都不约而同的罹患同一种癌症——财政危机。
没钱消灾、没钱维护基础设施、没钱支付军费…啥都干不了。贫穷王朝百事哀,结果要么是分裂割据,要么是外敌捅进家门,王朝可不久灭亡了吗!无论是唐宋元明清,还是罗马拜占庭奥斯曼苏联,细查历史你会发现,他们衰弱的原因或许各有不同,但最终都直接或间接死于财政危机。
其实很好理解,一家公司只要还能搞到钱,就有可能度过危机起死回生,一旦资金链断了,基本就玩完了。而帝国就像是一家巨型公司,所以美国就在悬崖边儿溜达呢,如果不能解决财政问题,搞不好就要步各大帝国的后尘。
刚才还提到清朝,好多人有个误解,以为清朝是财政崩溃的例外,说大清寿终正寝的时候,账上还趴着三亿两银子,是康乾盛世的十倍。这个就开玩笑了,那个数字其实是1911年的财政收入,是收入而不是盈余,收入再多也得看够不够花。对于大清来讲,不够,远远不够,光是外债就压力山大,所以清末财政赤字严重,常年如此,要不然干嘛找列强借款啊,要不然也不至于要把民间集资的铁路权收归国有,白嫖人家筹集的资金,结果闹出了湖广四川轰轰烈烈的“保路运动”,清廷紧急抽调湖北新军镇压,武汉空虚,阴差阳错之下,武昌就起义了,大清就没了。
就近来说,去年疫情,也能够看出财政的重要性,中国所以疫情控制得好,一方面是大家看到国家重视、上下齐心,一竿子插到底的组织能力…另一方面,是大家没太注意的,就是防疫投入舍得砸钱。患者治疗、物资采购、医护补助、火神山雷神山方舱医院、疫苗研发,哪一方面都花销不菲。2020年疫情防控,各级财政支出超过了4000亿人民币。对比下印度就知道,钱都花在军费上,边境闹的挺欢,等到防疫没钱了,倒是印共马执政的喀拉拉邦,一直在医疗教育上舍得投入,加上认真的执行方一政策,疫情期间相对而言表现就还不错。另外,为啥印度近些年一直在大搞私有化,变卖国有资产,还是财政缺钱,私有化的众多领域之中就包括电力,印度最近缺电这事儿,和电力私有化就有关系。
其实我们日常生活之中所面临的问题,八成都是钱的问题,还有一成是需要加钱的问题。国家也一样,大多数问题都是财政问题。
那问题来了,为什么帝国一到后期就都没钱了?
这部分内容我在之前的那期,从唐宪宗到唐玄宗漫谈中晚唐财政危机中提过。我复述一下。那么唐朝又缘何陷入财政危机呢?土地兼并。
回看古代史,各个朝代都亡于土地兼并所引发的财政崩溃。唐朝初年实行均田制,在均田制的基础之上,税收是租庸调制,有田则有租,有家则有调,有身则有庸。注意租庸调制是以人丁数量为基础课税的,不看财产多寡。土地兼并大量开始之后,均田制瓦解了,农民失去了土地,没了土地也就没了收成,没了收成也就交不起税了。要么逃亡,要么是投靠大地主做佃户。而对朝廷来讲,收自耕农的税比较容易,而收大地主的税是很难的。因为大地主往往是贵族豪强,隐瞒人口、隐瞒土地,有各种各样的免税权和逃税手段,所谓“士绅不纳粮”。所以土地兼并之后,国家的税源就少了,最终引发财政危机。到了明清两代,国家税收以田赋为主,田赋是按田产计征,但是继承了同样的病根儿,宗室贵族、官绅豪强本来就有大量的免税土地,还一边兼并土地,一边瞒报土地,导致税基严重萎缩,税收不足,财政紧张。财政没钱了,但是花钱的地方可一样都不少啊。像明朝末年,灾荒、瘟疫、外敌入侵,三合一大礼包哪样都得砸钱。国家既然收不到官绅豪强的税,那就只能向底层加税,辽饷、剿饷、练饷一键三连,底层遭不住,农民起义就上演了。也是为了缓解财政压力,崇祯裁撤驿站,辞退驿卒,而辞退的驿卒中有个人叫李自成。旧王朝灭亡,血洗一遍之后,新王朝建立,重新丈量分配土地,也就重建了税基,于是财政恢复生机。等到王朝中后期,土地兼并旧疾复发,以上循环再来一遍。这个就是王朝周期律。
许多史书或传奇,喜欢把王朝末年的问题统统甩锅给皇帝,其实是一种浅薄的叙事方式,以戏剧化的演绎替代了历史真相。
那么,时至当代,刚才的这套历史周期律还适用吗?适用。古代的兼并是土地兼并,现代的兼并进化为资本兼并,但是依旧会造成贫富悬殊,侵蚀税基,祸及财政。因为“士绅”依旧不纳粮,哪个国家都不例外。
大家应该听过,巴菲特交的税比他秘书还少,他说过,美国占1%的富人所负担的税率比我们的接待员甚至清洁工都要低。那是因为富豪的避税手段花样百出,比如说大家最熟知的一种,美国富豪动辄投设私人慈善基金,巴菲特、比尔盖茨、扎克伯格、贝索斯…无一例外,那当然不是在搞慈善了,而是在避税。按照美国法律,慈善基金免税,每年只要用掉5%,用于慈善相关事宜即可,20年后取消限制。这个相关的说道就大了,富豪让子女或者干脆自己出任基金的管理人,把吃喝玩乐的开销全都列支到这5%的开支里。同时自己只从公司象征性的拿点工资,这么一来,当然就交不了多少税了。但是,这样的避税手段门槛太高,普通人玩不起,结果就是,在税收这个问题上,美国政府只能狠盯着中产薅羊毛,富人的毛就算长到拖地了,他们也薅不着。美国现在联邦财政收入的90%左右都来自个税和社保。翻译过来就是,90%左右的税都来自普通人而不是富人。相比之下美国前1%的富人占据了全社会40%的财富,前10%的富人占据了全社会77%的财富,税收贡献与财富拥有量彻底倒挂。
那么中国情况如何呢?先看贫富问题。虽然没有美国那样的富人财富量占比的数字,但是大家从各类统计数据中也能间接的感受到点什么,比如2020年全年全国居民人居可支配收入32189元,换算到月就是2682元,另外一个数字超7成网民收入低于5000元。反过来再看税收,中国财政收入90%以上来自税收,而税收的70%又来自间接税收入和工资税收入,而间接税和工资税主要是普通人贡献的,也就是说从税收的人群结构上来讲中国面临的问题和美国类似。
说到这大家应该明白,古代最好收税的是自耕农,现代最好收税的是中产。中产没有严格的标准,反正你把这标准线压低一点。要想保证财政长青,就要保证自耕农或者中产阶级作为税基的规模存在。然而,兼并会摧毁税基。古代兼并导致自耕农逃亡,现代兼并导致中产萎缩,最后就是贫富悬殊,穷人没钱交不起税,富人又避税手段太多收不上税,国家陷入财政危机。
尤其是工业社会,生产效率极大提高,物质财富创造速度的同时兼并速度也更加凶猛,尤其是在互联网大数据的加持之下,如果不加干预,贫富差距会以肉眼可见的速度迅即拉大,王朝周期律也会加速运转。
那么怎么解决这个问题呢?就是常说的两个方法:做大蛋糕、重切蛋糕。
大英帝国当年做过示范。一边鼓励底层走出去,变现帝国红利,这是做大蛋糕。另一边是推进税制改革,提高直接税比重,这是重切蛋糕。只要蛋糕能持续的做大,富人吃肉,普通人再怎么也能跟着喝口汤。怕就怕增长放缓,这时候就必须考虑怎么重切蛋糕了。
大家感觉现在社会越来越卷,就和增长放缓是有关系的。
中国美国处境类似,只是中国依旧有做大蛋糕的空间,所以问题还没显得那么紧迫。
说到这中国做大蛋糕的一个方向就是高端制造业,这就抢了美国的蛋糕了,所以近几年中美矛盾才那么激烈。看中国目前的做法大致是这样的,就做大蛋糕这种做法呢,是朝两个方向,横向上以一带一路,纵向上攀爬科技树。重切蛋糕这种做法呢,就是直接税改革(还有抑制兼并)。
做大蛋糕是大家一起赚钱,不过时赚多赚少的区别而已,阻力在外部,主要来自美国。重切蛋糕却是从富人的口袋里掏钱,困难在内部。大家读历史都知道,内部阻力比外部敌人可要难处理多了。
接下来解释解释什么是直接税。
按照学界标准,直接税和间接税的划分标准是税负能否转嫁。
先看间接税。一般来讲,对商品和服务课税的税种属于间接税。比如增值税、消费税、关税,还有以前的营业税。厂商、进口商等这些主体是纳税人,交钱纳税,但是他们会把税额加进价格里,最后让消费者来承担,这个就是税负转嫁。比如本来这个东西不交税只卖10块钱,但是我交了1块钱税,我现在就11块钱卖给你。大家到超市买包薯片、买瓶水交钱的时候,其实都负担了几毛钱的增值税。如果买的是进口薯片,你可能还负担了关税。总的来讲,间接税相对隐蔽,就像温水煮青蛙,如果不说,大多数老百姓这辈子都不会知道。
再看直接税。大致来说,对收入和财产进行课税的税种属于直接税。比如所得税、财产税。直接税并不明显转嫁,由纳税人直接负担。比如,作为搬砖人,大家应该都知道自己每个月的工资扣完了社保,又扣了个税之后才会到手里。和间接税相比,直接税一目了然,就像是割肉一样,税痛非常直接。
说完了这些,就可以明白,间接税收入主要是由普通人所贡献的,因为间接税转嫁的终端通常是消费者,而消费者绝大多数都是普通人。对个人来讲,随着收入和财富值的增加,消费所占的比重会逐渐变小。富人总共就那么些,他们再怎么花天酒地,再怎么一掷千金,也比不上几亿人消费的总和。所以市场上才会有那句话,得屌丝者得天下。抖音、拼多多、樊登读书会、蜜雪冰城…他们都是从下沉市场打出来的。而经济增长放缓的时候,财富就会在内部调整,贫富差距拉大,会侵蚀普通人的购买力,也就是损害税基。大家消费少了,间接税就贡献得少了。还会影响到企业利润,进而影响到给职工开的工资收入。每个环节都会导致税收的减少,而职工收入少了消费就更少了,陷入恶性循环。要解决这个问题就需要对富人征税,做一次再分配,把从富人那挣来的钱拿来夯实底层税基。比方说,投到西部建设、扶贫工作中去,一步步培育出新的购买力,进而消化更多的工业产品,实现内循环。在这个过程之中,他们获得的收入要缴税,从他们手里赚取收益的企业也在缴税。人民有生活、企业有发展、国家有税收,一切步入正向循环。而对富人征税的关键就是直接税,尤其是直接税中的财产税。
直接税并不都是富人税,直接税主要包括所得税和财产税。所得税又分为企业所得税和个人所得税,其中个人所得税主要是个穷人税。众所周知,个人所得税包括很多个税目,工资薪金所得、劳务利得、财产所得、财产转让所得…都要课税。但是从结果上看,我国个人所得税收入六成以上都来自刚才说的第一项——工资薪金所得,也就是俗称的工资税。那么哪些人是拿工资的呢,打工人、上班族。富人的主要收入可不是工资,而是资本利得。这是为什么,我前面提到中国税收的70%来自间接税收入和工资税收入,那里我说的是工资税收入,而不是全部的个人所得税收入。所以,刨掉个税,也暂时不谈企业所得税,这是个大话题这里不展开。直接税改革的关键就在于财产税。
财产税以财产为基础课征。包括但不限于房产税、遗产税、赠与税、资本离境税…现阶段为止,中国几乎没有财产税,之前那个房产税试点是蜻蜓点水,几同于无。所以我们税收之中,富人的贡献率非常低。但是,进一步说,真想征到财产税也没那么容易。美国早就开征了财产税,前面所说的财产税,美国一样没拉下。然而现在还是我前面说的那个情况,联邦财政90%左右来自个税和社保。以遗产税为例,美国的大富豪们几乎就没人交过。前美国白宫经济委员会主任柯恩公开说,只有税务筹划做的很糟糕的有钱人才缴遗产税。不刚美国搞不定,中国古代也搞不定,这种税收改革其实不是什么新鲜事儿,历朝历代都搞过。唐朝时杨炎的两税法,宋朝时王安石的方田均税法,明朝时张居正的一条鞭法,清朝时雍正的摊丁入亩,全都是类似性质的改革。大体思路就是减少按人头征收的人丁税,也就是穷人税,增加按田产征收的财产税,也就是富人税。但是结果如何,大家都有感受,大多都是人亡政息。这几个主持改革的人,翻看史书就会发现,都是负面评价一大把,顶好也是极富争议。原因很简单,改革触犯了既得利益集团,官僚阶层与地主豪强总有千丝万缕的联系,而读书人主要出身于这个阶级,你得罪了他们,也就惹毛了读书人,笔杆子一抖,身后评价,危矣。
最具代表性的就是雍正,给世人留下的都是刻薄寡恩的形象。雍正推行改革,肃清吏治、摊丁入亩、火耗归公、官绅一体当差纳粮,每一项都是对着官绅豪强骑脸输出,也因此给乾隆打了一个相当不错的底子,于是才有了后来的康“乾”盛世,可是雍正本人却不在这个词里面。乾隆上台之后把老爹的改革给废掉了,对官绅豪强宽厚为怀,于是后世的读书人对乾隆大夸特夸,却把雍正给骂惨了。当代有些事是类似的,刚才说的只是王朝中期的改革。事实上放眼中国古代时,中国税制也是从以丁身为本,到以资产为宗。早期主要是收人头税,到了明清两代,田赋已经成了主流。即便如此,政府还是收不到富人的税。宗室贵族、官绅豪强一边兼并土地,一边瞒报偷税,清代的时候最是猖狂。众所周知,清代的国土面积远远大过明朝,然而有清一代登记在册的田亩面积数值居然都没能超过明朝。
时间来到现在,事情依旧不好做。直接税改革喊话很多年了,一直有阻力,这个就有类似的原因。故事中的地主豪强不过是换成资本家而已。
可是,税改这个东西要么主动改,要么被动改。主动改就是直接税,劫富济贫;被动改就是财政危机,触发周期律,社会动荡,推倒重来。后者未免代价太大,而且鉴于我们的政权性质,开征直接税,也是人民主体地位的必然要求。所以难做也得做,或者说正是因为难做才更要做,越是难啃的骨头啃下来收益越大,受益越久。不管国家、公司还是个人都是如此。
以当前的国内环境和国际环境而言,大概就是推行税改最好的时机了。
国内环境,反腐消解了部分阻力,而且人民史观重回舆论视野。群众基础是改开以来空前。
国际环境,各国都开始关注税收问题。经合组织CRS成员国交换涉税银行账户信息,狠挖逃税资产。欧美国家也联手对富人征税,G7集团开会统一所得税政策,防止资产转移。
这样有利的大背景之下,政府正在紧锣密鼓的推进,很多零散的新闻单看没有什么,如果联系到一起,似乎就是一盘大棋了。比如税务部门扩权,政务数字化,金税四期,数字货币…一张有一张的网正在密密叠叠的展开。
与历史相比,今天有更好的技术手段,有空前的组织效率,相信改革的结果会有所不同。
事关国运,也关乎所有人的未来,改革的结果也会揭示历史的走向,让我们拭目以待吧!
]]>宏是一种元编程的方式,和Java1.6引进的AnnotationProcessor类似,它可以在编译时生成源代码。这种元编程技术可以让我们从样板代码中解脱出来,比如Lombok。
Rust对标的是C/C++。C/C++中也有宏(Macro)的概念,可以简单地理解为:宏即编译时将执行的一系列指令。其重点在于「编译时」,尽管宏与函数(或方法)形似,函数是在运行时发生调用的,而宏是在编译时执行的。
不同于C/C++中的宏,Rust的宏并非简单的文本替换,而是在词法层面甚至语法树层面作替换,其功能更加强大,也更加安全。
如下所示的一个C++的宏SQR的定义
1 |
|
我们希望它输出4,但很遗憾它将输出3,因为SQR(1 + 1)在预编译阶段通过文本替换展开将得到(1 + 1 * 1 + 1),并非我们所期望的语义。
而在Rust中,按如下方式定义的宏:
1 | macro_rules! sqr { |
将得到正确的答案4。这是因为Rust的宏展开发生在语法分析阶段,此时编译器知道sqr!宏中的
C/C++中宏很容易出现莫名其妙的问题,所以在很多场景不推荐使用宏。而Rust改进了这点,而且提供了更多的功能,让Rust有了更灵活的表达方式。
先挑声明宏这个软柿子捏,声明宏的语法和match的语法非常类似,区别是使用了macro_rules!
关键字。
1 | match target { |
以一个简化版的vec!为例:
1 |
|
#[macro_export]
注释将宏进行了导出,这样其它的包就可以将该宏引入到当前作用域中,然后才能使用。可能有同学会提问:我们在使用标准库vec!
时也没有引入宏啊,那是因为 Rust 已经通过std::prelude
的方式为我们自动引入了。
紧接着,就使用macro_rules!
进行了宏定义,需要注意的是宏的名称是vec
,而不是vec!
,后者的感叹号只在调用时才需要。
vec 的定义结构跟 match 表达式很像,但这里我们只有一个分支,其中包含一个模式 ( $( $x:expr ),* )
,跟模式相关联的代码就在=>
之后。一旦模式成功匹配,那这段相关联的代码就会替换传入的源代码。
由于 vec 宏只有一个模式,因此它只能匹配一种源代码,其它类型的都将导致报错,而更复杂的宏往往会拥有更多的分支。
虽然宏和 match 都称之为模式,但是前者跟后者的模式规则是不同的。如果大家想要更深入的了解宏的模式,可以查看官方文档。
而现在,我们先来简单讲解下( $( $x:expr ),* )
的含义。
首先,我们使用圆括号()
将整个宏模式包裹其中。紧随其后的是$()
,跟括号中模式相匹配的值(传入的Rust源代码)会被捕获,然后用于代码替换。在这里,模式$x:expr
会匹配任何 Rust 表达式并给予该模式一个名称$x
。
$()
之后的逗号说明在$()
所匹配的代码的后面会有一个可选的逗号分隔符,紧随逗号之后的*
说明*
之前的模式会被匹配零次或任意多次(类似正则表达式)。
当我们使用vec![1, 2, 3]
来调用该宏时,$x
模式将被匹配三次,分别是1、2、3。为了帮助大家巩固,我们再来一起过一下:
1、$()
中包含的是模式$x:expr
,该模式中的expr
表示会匹配任何Rust表达式,并给予该模式一个名称$x
2、因此$x
模式可以跟整数1
进行匹配,也可以跟字符串"hello"
进行匹配: vec!["hello", "world"]
3、$()
之后的逗号,意味着1
和2
之间可以使用逗号进行分割,也意味着3
既可以没有逗号,也可以有逗号:vec![1, 2, 3,]
4、*
说明之前的模式可以出现零次也可以任意次,这里出现了三次
接下来,我们再来看看与模式相关联、在 => 之后的代码:
1 | { |
这里就比较好理解了,$()
中的temp_vec.push()
将根据模式匹配的次数生成对应的代码,当调用vec![1, 2, 3]
时,下面这段生成的代码将替代传入的源代码,也就是替代vec![1, 2, 3]
:
1 | { |
如果是let v = vec![1, 2, 3]
,那生成的代码最后返回的值temp_vec
将被赋予给变量v
,等同于 :
1 | let v = { |
至此,我们定义了一个宏,它可以接受任意类型和数量的参数,并且理解了其语法的含义。
对于macro_rules
来说,它是存在一些问题的,因此,Rust 计划在未来使用新的声明式宏来替换它:工作方式类似,但是解决了目前存在的一些问题,在那之后,macro_rules
将变为 deprecated 状态。
知乎也有一篇文章介绍macro_rules目前存在的一些问题,可以看看这本书 “The Little Book of Rust Macros”。
]]>人猿相揖别。只几个石头磨过,小儿时节。铜铁炉中翻火焰,为问何时猜得?不过几千寒热。人世难逢开口笑,上疆场彼此弯弓月。流遍了,郊原血。
一篇读罢头飞雪,但记得斑斑点点,几行陈迹。五帝三皇神圣事,骗了无涯过客。有多少风流人物?盗跖庄屩流誉后,更陈王奋起挥黄钺。歌未竟,东方白。
贺新郎·读史————毛泽东
人类从饮血茹毛走向刀耕火种的时代,第一次实现了生产力的跃迁,告别了和猩猩那样吃了上顿没下顿的日子。
这个时候的人类靠刀耕火种可以养活整个氏族。把此时的生产力定为1,也就是氏族的劳动力人口干活刚刚好够养活氏族的人口(考虑到有尚无劳动能力的幼儿)。这个时候的社会能实现所谓的民主,应该还不能叫做社会,氏族群体人口还比较少,信息沟通成本比较。只有能让整个氏族利益最大化的人才会被推举为氏族领袖。
随着冶炼技术的发展,从青铜器到铁器技术的演进,铁犁等各种工具的出现以及牛马等畜力的驯化使得生产力快速提高。人类也从刀耕火种的粗旷式生产转变为精耕细作。人口也会因为生产力的提高而增多。
原始氏族公社施行公有制,是因为饮血茹毛刀耕火种的原始人通过个人无法单独生产,个人必须加入集体生产才可能存活,生产方式和狼群类似,集体狩猎、采集果实,此时的公有制是生产力水平极其低下而导致的原始公有制,但实际上这种大家族公社是以家族血缘为纽带的,公社间并不实现生产资料的共享。
此时由于认知水平和生产力的低下,公社内是群婚制,人类两性知识的匮乏以及为了尽可能多的繁衍后代保证族群人口,导致新出生的人类只知其母不知其父,这也就是所谓的“母系氏族公社”。
随着认知水平的提升,人们逐渐对两性关系有所认识,同时由于近亲繁殖会导致遗传病概率的加大,人类开始实行族外群婚制。
随着人类成功驯化五谷家禽,以及陶器、石斧、弓箭等新的生产工具的出现,生产力水平得到发展。同时由于男性天然的体力优势,女性开始依附于男性生存,母系氏族公社逐渐演进为男女婚娶的父系氏族公社。
正因对偶制婚制的形成,以家庭为单位的生产关系出现,同时由于生产力的发展使得劳动形成了劳动剩余,劳动剩余的权属和继承问题成为新的社会矛盾,以家庭为单位的私有制也因此出现。
与此同时,公权力与劳动者分离,形成国家。公权力为维护大众利益无法从事农业生产,如国防安全(炎黄战蚩尤,大禹逐共工征三苗)、兴修水利(大禹治水)。国家为了维持运转需要财税支持,此时存在一些非农业人口,也就出现了一群人供养另一群人的现象,农业税因此出现,《尚书•禹贡》中就记载了大禹治水后定九州并制定税赋的历史,这也是中国第一部税法,此时税赋还比较原始。
当国家统治者的家庭脱离社会生产时间长了,便不再愿意继续从事劳动生产,这是由俭入奢易、由奢入俭难的人性使然。大禹治水成功后威望空前,于涂山之会铸九鼎定九州。从此一个阶级统治另一个阶级的阶级社会出现,夏启袭位使得国家从公天下开始演变为家天下。商汤伐桀、武王伐纣,周朝吸取前朝教训,为了维护家天下的统治,将国家土地分封给各个兄弟一起建设,也就是所谓的封建社会。周朝的农业税以井田制的形式出现,公田的产出归国家,私田的产出归私人家庭所有,孟子说夏后氏五十而贡、殷人七十而助、周人百亩而彻,其实都是十分之一的税率。
此时的农业社会以父权至上,中国上古时期出现的姓氏,姓指代母系(“女”人所“生”),氏指代父系,先秦时期姬,姚,妫,姒,姜,嬴,姞,妘等上古姓以女字旁为主,而氏以地名、国名、官名等为主。比如春秋五霸中齐桓公姜姓吕氏名小白(姜子牙吕尚后代),晋文公姬姓晋氏名重耳,秦穆公嬴姓赵氏名任好,吴王阖闾姬姓吴氏名光,越王勾践姒姓名鸠浅。这种姓氏并存的现象正是由母系氏族公社演变到父系氏族的遗留产物。
封建社会发展到一定时间,统治者血缘关系开始逐渐疏远,同时由于人口增长资源有限,分封的诸侯国之间开始通过战争进行兼并。由于频繁的战争,土地权属经常发生变化,井田制随之瓦解,国家征税不再有公田私田之分。
秦始皇为了国家不再出现战乱分裂,废除分封施行郡县制,加强中央集权,从此进入帝制王朝时代。
秦国之所以胜出,在于秦国施行的商鞅变法认为农民是最重要的生产者,想要富国强兵必须扩大农业人口比重,保持非农业人口比重不超过10%,把人口不断增长而不从事生产的贵族打压到底层从事劳动生产或参军,国家允许私田买卖和开垦荒地,同时提供军功授爵阶级快速上升通道,国家机器得到高效率的运转。此时的生产力可定为1.1。
秦国以此兴,也以此亡。当秦国统一六国后,土地已基本开垦完,战争也结束,阶级出现固化,此时高压的行政手段(修长城、修驰道、重徭役、覆压三百余里的阿房宫)必然激化阶级矛盾,一声“王侯将相宁有种乎”如星火燎原瞬间激发。
农业是以土地为基础,中国历史上自秦朝后的历史周期律本质上都是土地生产力有瓶颈导致的。秦朝以前没有出现这种现象(准确的来说是战国以前),是因为土地还没有被完全开拓,西周时期南方还是蛮荒地区,人口稀少,楚国还被称为“荆蛮”。到了战国人口增多土地资源紧张,国家兼并出现。秦帝国奋六世之余烈,南取百越,北击匈奴,将大陆所有肥沃的土地收入囊中,自此便进入了土地和人口的周期循环:
王朝初期,人口凋敝,百废待兴。人均土地充足,老百姓丰衣足食,人口的迅速恢复,也会迎来王朝的盛世。
王朝中期,人口攀升到顶峰,进入王朝最鼎盛的时候。经历几代人的发展,由于个体能力的差异,贫富分化也开始出现。
王朝末期,人口的继续上涨,导致土地产出不足以养活这么多人,人们在吃饱和吃不饱的临界点徘徊。只要一旦出现洪涝或者干旱等自然灾害,就会出现饿死人的现象,朝廷如果未能及时救灾安抚,农民起义就可能会在这个时期频繁出现。
历史上多次引进等高产农作物提高了生产力。如张骞出塞引进了葡萄、石榴、胡萝卜、胡椒、胡瓜(黄瓜)、胡蒜(大蒜)、胡麻(芝麻)、胡豆(蚕豆)、胡桃(核桃)、胡菜(香菜)。张骞作为第一个睁眼看世界的汉人为中国开辟了陆上丝绸之路,后人引进了很多其他农作物,如西瓜(西域)、菠菜(波斯)、茄子、棉花等。另外,由于造船技术和指南针的发明,中国又开辟了海上丝绸之路引进了玉米、番瓜(南瓜)、番薯、番茄(西红柿)、番椒(辣椒)、洋番薯(马铃薯)、洋白菜、洋葱等。
“胡”字辈大多为两汉两晋时期由西北陆路引入; “番”字辈大多为明朝时期由“番舶”(外国船只)带入; “洋”字辈则大多由清代乃至近代由环球航海的西洋人引入。
也正因为农业生产力的不断提高,中国古代王朝鼎盛时期能容纳的人口也在不断攀升:两汉巅峰期人口6千多万人,唐朝8千万,宋朝鼎盛时期(宋徽宗赵佶时期,靖康之乱前夕)人口突破1亿,元朝只有9千万(还是蒙古人觉得汉人杀不完的情况下),明朝鼎盛时期(万历年间)人口1.5亿,清朝鼎盛时期(乾隆盛世)人口超过3亿。现在能养活14亿人口,得感谢袁隆平等科学家的育种技术以及化肥技术的推广。
除了土地生产力的因素外,制约王朝寿命的另一个因素是以税收为代表的分配制度:
王朝初期,百姓均田实现了相对公平,王朝重新焕发活力,纳税人口占总人口的比例较大,国家税基也相对充盈。
王朝中期,由于个人天赋与能力的差异,出现贫富分化:弱者由于天灾或者人祸,为了活下去被迫卖田,强者使用积累的劳动剩余,购置了更多的田地。这其中特别是士绅贵族以及官僚阶级有免税的特权,使得他们能拥有更多的田地,更甚者他们使用手中的特权土地兼并——巧取豪夺百姓的田地。此时国家税基开始萎缩,国家的税负压在了普通百姓的身上。
王朝末期,土地兼并无以复加,失地的农民被迫沦为地主的佃农依附于地主,以官僚阶级为代表的地主占有大量土地,此时富者田连阡陌,贫者无立锥之地。国家税收已无力维持国防军事、民生赈济等开支。此时王朝极度脆弱,任何外敌入侵、水灾旱灾等问题都会成为压死骆驼的最后一根稻草。不加税直接崩溃,加税则官逼民反,届时农民起义四起,王朝崩溃,新的轮回重新开始。
朝廷如果有足够的税收救灾安抚受灾群众或者及时扑灭农民起义的火苗,王朝还能续命,但是明君也只能通过打击士绅豪强,合理分配财富、增加税基延缓这个进程,并不能逆转这个进程,因为人口增长和自私都是刻在人基因里的天性。
农业时期商品经济十分简单,同时中国士农工商的历史传统导致商业发展受限。但是我们从农业社会发展的本质中发现两个点:
1、土地是生产资料,是农业社会财富的来源。所以历史上的明君都是通过土地改革解放农业生产力的,如清朝雍正的摊丁入亩、士绅一体纳粮,使得之后乾隆盛世人口能达到3亿以上。
2、人多地少是农业社会永恒的矛盾。这是古代王朝周期循环,不超过300年的根本原因。这是客观规律不以人的意志为转移,明君昏君只能延缓或加速这个进程,却无法逆转,如荀子所说“天行有常,不为尧存,不为桀亡”。
伦理道德最早是人类从群婚制演变为对偶婚制过程中产生的,最早追溯到人文始祖伏羲制定婚姻制度,目的是避免近亲繁殖出现遗传疾病,同时通过道德约束合理分配劳动所得使男性能承担起养育后代的责任。可以看到,中原文明对未能遵守这项道德的部落或国家称为未开化的蛮夷,最开始是楚国荆蛮,接着是西戎蛮夷,匈奴蛮夷……
对偶婚制催生以家庭为单位的生产关系,正是家庭的出现,使得私有制产权关系的伦理道德出现。私有制成为了人类奋斗的动力,也是人类历史周期循环的驱动力。马克思设想的公有制只有在这种伦理道德消灭后才可能出现,而彼时父子、母子、夫妻等社会关系也将不复存在,此时的人类或许以另一种方式繁衍或者得到永生。
道德规范的是人的主观意识,只能约束有道德的人;法律规范的是人的客观行为,可以惩罚不遵守规则秩序的人。先秦儒家就是主张以道德治国的主要流派,法家就是主张以法律治国的流派。道德没有强约束力,每个人每个时代的道德观念差别也很大,没有统一的标准,用道德可以约束自己达到自律,但模糊的道德无法约束所有人。因为儒家的理想主义没有考虑人性的幽暗,最终战国主张儒家的国家都灭亡了。法律约束力强,白纸黑字明文规定。特别是税法是是社会财富分配的工具,统治者通过鼓励耕战等社会利益的分配手段,能打造出秦国这样军国主义的战争机器,但严刑峻法也剥夺了个体的人性,使人成为社会机器。而且人客观造成的犯罪,有时并非主观意愿,法律过于严苛死板,法不容情,容易造成冤假错案。另外执法人员也是芸芸众生的一员,也有人的局限性,法家没有考虑执法者内心的幽暗,执法者可能成为破坏社会秩序的力量。中国古代有能力的统治者结合两者,慢慢发展出外儒内法的制度。而现代法律也结合了法治与德治的思想:定罪以客观行为为准,论迹不论心;量刑以主观意识为依据,结合犯罪主体实际情况酌情处理,如犯罪行为分故意犯罪和过失犯罪,刑事责任年龄等成为量刑依据。
法律和道德的目的都是维护秩序,在不同历史时期也会随之发展。随着牛马等畜力的驯化,农业生产力得到发展,法律也因此改变,牛马作为农业时代最重要的生产力,古代王朝通常都会规定私自宰杀牛马会获罪。中国古代重农主义,统治者认为农业为本,工商为末,因此宋明两代会有禁止商人穿绸缎的禁令。中世纪黑死病导致了宗教的崩塌,信上帝并不能保佑你,而后文艺复兴使得封建主义解体,西方重商主义促成了欧洲资本的原始积累,之后由于大航海的地理大发现扩大了世界市场,西欧出现了专制的中央集权国家,圈地运动、殖民掠夺、黑奴贸易、农奴制榨取着劳动人民的剩余价值,萃取成资本。技术作为工业时代最重要的生产力,一个工业国要想发展必须重视知识产权的保护,专利法、知识产权法、著作权法…应运而生。
科学革命引领技术变革,技术变革使得生产力进步,带动生产关系变革和社会制度的变迁。但封建统治者会为了维护自身统治,遏制科学技术的发展。在西方教会会遏制科学思想的发展,对进步思想者施以酷刑。在东方清朝统治者用原子化的家庭男耕女织的生产关系,防止制度变革的发生。
古代王朝罢黜百家独尊儒术,并将儒学改造成“君为臣纲 父为子纲 夫为妻纲”三纲五常的道德秩序,封建王朝会极力宣传“孔融让梨”这样的故事,引进佛教也是为了缓解农业社会生产力落后引起的社会矛盾。封建王朝发展到极致,开始使用道德约束底层人的思想和行为,正如鲁迅所说历史写满了仁义道德,字缝里却只见“吃人”二字。
古代的宗教文化都是当时生产力的产物,因为人多地少是当时的主要矛盾。经济基础决定了政治、法律、文化等上层建筑。
道德化的看问题,容易掩盖历史真相,真正推动人类历史进步的是生产力的发展,只有通过这个视角才能看透历史上所谓的“野蛮征服文明”的假象。
欧洲工业革命的发展,让欧洲的生产力飞速提升。而中国自鸦片战争被迫打开国门,到洋务运动被迫工业化,再到建国后以举国之力进行工业化————中国开始一步步顺应世界潮流进入工业化时代。
为什么工业革命发生在欧洲而不是中国?因为欧洲有科学。首先明确一点,科学和技术要区分开,科学是认识世界,技术是改造世界。中国古代有技术而无科学,中国的齐民要术、梦溪笔谈、农政全书、天工开物等传统丛书都是技术经验的积累,而没有科学的深层解释,中国的哲学思辨还停留在阴阳五行的层次,这使得技术发展只能是无规律的试错,进展缓慢。欧洲文艺复兴后,传统希腊科学的思维得到进一步发展,伽利略、牛顿、拉瓦锡、法拉第等科学家通过定量实验和数学计算让自然规律有了哲学思辨,有了科学指导,技术会爆炸式地发展。这就像中医和西医的区别,中医是经验点的积累,复杂但不系统,底层哲学原理是阴阳五行;西医是基于化学、生物学、有机化学、遗传学、分子生物学等科学体系的积累,比如遗传学使得生物学扩大了认知,分子遗传学再次扩大了人类对生物体的认知,这种基于自然哲学的科学认知是成体系的。因此现在的中医不敢说比500年前李时珍时期的中医牛逼,但西医肯定敢说比500年前的西医更牛逼。
当然,西方科学昌明还有一个客观原因,那就是西方玻璃工业更加先进。西方容器以玻璃为主,东方容器以陶瓷为主。而玻璃透明的性质让西方工匠能制造出凸透镜,使得人类能探索宏观的宇宙、微观的细胞。比如伽利略制作了世界上第一台天文望远镜,促成了他在观测天文学上的成就,也正是天文望远镜的出现使得东西方天文学拉开巨大差距,当伽利略发现木星的卫星时,我们还在用金木水火土阴阳五行命名天上的星星;牛顿在伽利略的基础上制造了更先进的望远镜和棱镜,造就了他在天文学和光学领域的辉煌成绩;罗伯特胡克制作了第一台显微镜成为第一个观察到细胞的人,同一时期的列文虎克改进显微镜使显微镜能放大两百多倍,人类对微观世界的认知更加开阔。西方发展出玻璃为代表的现代科学,东方发展出陶瓷为代表的大陆文化,这也由于各自的地理位置。西方文化发源于地中海,最不缺的是沙子,而沙子是制作玻璃的重要原料;中国发源于大陆,最不缺的是黏土,特别是景德镇瓷器为代表的高岭土,中国的高岭土资源居世界前列,这是陶瓷技术诞生的基础。因此,中国从来不缺充满好奇心和探索欲的人,缺的是探索宏观和微观世界的工具。
正是科学的进步使得西方技术和工业不断革命。1760年代开始,第一次工业革命以牛顿力学为科学基础,以煤炭能源为辅助使得蒸汽机、纺纱机、火车等技术得到发展,人类进入蒸汽时代;1860年代开始,第二次工业革命以拉瓦锡开创的化学、法拉第开创的电磁学为理论基础,以化石能源为辅助,使得内燃机、发电机等技术得到发展,人类进入电气时代;1940年代,第三次工业革命以爱因斯坦的相对论、普朗克薛定谔等人开创的量子力学、艾伦图灵冯诺依开创的计算机科学和曼香农的信息论、沃森和克里克开创的分子遗传学为理论基础,以核裂变、核聚变等原子能为辅助,使得核电、半导体集成电路、航天航空、卫星通信、基因工程等技术得到发展。国内90年代还在用的基于电子显像管的黑白电视没过多少年就换成了大彩电,大彩电还没用多少年,基于发光半导体的液晶电视就占领了主要市场。科技正在以肉眼可见的速度极速发展着。
科学革命引领了技术进步。要想加速技术进步的步伐,还要加强基础理论研究,发现更多的自然规律,开拓人类的世界观。我国近几年在基础理论研究方面加大了投资,各种用于理论研究的基础设施也拔地而起,四川稻城的高海拔宇宙线观测站拉索、圆环阵列射电望远镜千眼天珠,还有落地在四川雅砻江水电站的地下2400米锦屏地下实验室…这些科学设施为我们发现更广阔的自然世界提供了基础。
技术进步,还需要充分利用现有的科学理论。我国的裂变堆已经有两套技术体系了,一套是引进美国技术的“国和一号”,一套是引进法国技术的“华龙一号”。在充分吸收这些技术后,我们还自主研发的模块化核裂变小型堆“玲龙一号”,该技术目前已经在海南建设了,东方超环EAST和环流三号等核聚变技术也在进行不断的突破,有望在2050年前实现可控核聚变能源的利用。这些技术的发展正是对前人科学理论的充分利用。
技术的进步促进了资本积累和产业升级。工业时代的生产力需要资本和技术的支撑。有资本(积累的劳动剩余)才能建厂房扩大再生产、才能供养暂时没有产出的研发人员开发更先进的技术。
而资本的积累本质是劳动剩余的积累,假设一个工厂把赚取的利润都分给员工了,它将无法扩大再生产,这个企业必然在更新的技术浪潮中覆灭。
对于国家而言也是如此,经济要发展,当前劳动者就必须要让渡一部分劳动剩余。
新中国刚建立时,中国的资本来自于苏联。中苏交恶导致苏联撤资,同时遇上美苏冷战,中国必须在美苏的夹缝中寻找发展机会。而资本的稀缺导致我国不得不通过工农业剪刀差将农业的劳动剩余补贴给工业。
国家通过统购统销收取农民的粮食,以此压低粮食价格保证城市生活资料的供应;城市压低工人工资、并通过粮票布票等制度全面管控生活必需品的发放。而人为的扭曲导致城市的岗位稀缺,种田的农民都想进城怎么办,户口制度诞生。也正因此我们才有资本去供养两弹一星的科学家们研发国防科技。有了国防实力,我们才有资格上美苏博弈的牌桌,中美建交也证实了这一点。此时的工业以军用重工业为主,虽然原子弹爆炸了、卫星上天了,但人民的生活迟迟得不到改善。
农业时代的财富来源是土地,而工业时代的财富来源有人口、技术和市场:人通过技术设备对生产资料进行加工制造商品到市场上卖出去。中西方不同时期对这三个要素的侧重点也不同。
1、人口。农业时代人口达到生产力瓶颈后,人口是社会的负担。1982年人口普查时,中国的人口已突破10亿大关,这也是改革开放后计划生育被定为国策的原因。但工业时代人口就是劳动力。在资本主义早期,英国圈地运动的“羊吃人”现象实现了农民与土地的分离,农民们流入城市为城市提供了劳动力。随着工业革命的发展,欧洲国家通过殖民掠夺在世界各地制造了大量的奴隶为其工作,历史上的美国蓄奴制直到南北战争之后才被废除,目的也是为了让南方的大量农奴能进工厂打工。
而中国改革开放通过市场换技术,外资进入中国,带来了资本和技术,使得我国生产力得到飞速发展。而城市劳动力资源不足,于是开放户籍人口流通,农民进城务工蔚然成风。改革开放后的这段时间,沿海的私营企业通过农民工们做衬衫毛衣创造的劳动剩余换取美元,国家通过外汇管制,让私企们将美元换成人民币在国内消费,国家得到美元后再到国际市场进口农产品解决吃饭问题,同时换取飞机、芯片、航母等更先进的技术。此时的中国能迅速发展依靠的是庞大的人口带来的劳动力资源和市场红利。
2、技术。技术是生产力发展的推动力。随着技术的发展,技术的进步带来的效用比单纯叠加人力更有效。西方这一时期的法律也渐渐转变——不再强迫人们去当工人,而是注重知识产权的保护。专利法促进了技术创新,使得发明人可以通过专利许可积累资本,也让发明者的时间和精力可以集中在技术创新上,如上个世纪的奔驰、福特创始人都是汽车工程师出身。
中国改革开放后,很早就有了专利法,但是真正开始注重技术专利、著作权等知识产权也才近十年,要知道10年前盗版光碟、山寨手机市场横行,那时候的中国制造是“盗版山寨高仿”的代名词。因为改革开放的前三十年我们要的是市场,技术大都是引进的,等到技术引进得差不多了,我们必须要自主研发了,就需要保护知识产权了。研发是个巨耗成本的工作,因为技术研发成功前的这段时间是没有产出的,研发成功了就是一本万利,研发失败了就是钱打水漂了。
3、市场。市场决定着商品能卖给多少人。工业化带来了规模化生产:技术和设备投入主要在前期,一旦研发的产品打开了市场,后期只需对生产资料进行加工即可源源不断的生产出商品。只要市场越大,平均下来前期投入的成本就越小。
财富积累度过了原始阶段,形成资本,资本通过技术在一个又一个市场中攻城拔寨逐渐形成垄断地位。市场饱和后,资本为了增值会寻求新的市场。当年的鸦片战争正是西方资本寻求市场扩张发起的。当前国内互联网市场进入存量阶段后,各个互联网大厂也纷纷瞄准海外市场。字节跳动的tiktok、拼多多的temu、南京悄然崛起的Shein…
马克思曾在共产党宣言中预言,物质极大丰富的共产主义必然取代资本主义。我觉得这个问题值得思考,地球的资源和空间是有限的,当物质极大丰富,人类社会没有剥削,人口将会暴涨,人口的暴涨将导致人均资源的匮乏,人均资源的匮乏必然导致个体间甚至国与国之间的竞争与博弈,竞争有输有赢这必然带来不平等。在古代表现为王朝中期人口达到土地生产力能承载的顶峰,人均土地的匮乏导致人们在吃饱和吃不饱的边缘徘徊,土地兼并开始出现,土地兼并最终导致极端的不公平。这种极端的不公平引发社会动荡,如果人均资源的匮乏问题没有解决,必然革命发生内战,内部自我消耗,实现人口的减少,在古代表现为大饥荒人相食、农民起义、国家内战。人类的历史就是相对公平与极度不公平之间不断的轮回。要解决人均资源匮乏的问题,必然要开拓新的资源,比如发动战争对外扩张、大航海发现新大陆、飞往太空开发火星…。内部自我消耗当然是我们都不愿看到的,代价很高,而且生产力也会倒退,而对外开拓新资源,则需要人类走出舒适区探索未知的领域,比如当地球资源或空间有限时,人类应当走出地球这个摇篮,飞往更加广袤的宇宙。人性有其自身的诸多弱点,如懒惰、享乐主义、耽于安逸、不患寡而患不均…,但人性中也有其光辉的一面,求知欲、好奇心、探索欲…人类社会想要摆脱周期律,必须要尽力避免人类自身的弱点,发挥自身光辉的一面。
]]>默认情况下,SpringBoot提供了DefaultErrorAttributes类,该类实现了ErrorAttributes接口,以在发生未处理的错误时生成错误响应。在默认错误的情况下,系统会生成一个 JSON 响应结构,我们可以更仔细地检查它:
1 | { |
虽然此错误响应包含一些关键属性,但它可能无助于查问题。幸运的是,我们可以通过在 Spring WebFlux 应用程序中创建ErrorAttributes接口的自定义实现来修改此默认行为。
从 Spring Framework 6 开始提供了ProblemDetail来支持RFC7807规范的表示。ProblemDetail包括一些定义错误详细信息的标准属性,还有一个扩展详细信息以进行自定义的选项。下面列出了支持的属性:
除了上面提到的标准属性外,ProblemDetail还包含一个Map<String, Object> 以添加自定义参数以提供有关问题的更多详细信息。示例错误响应结构如下:
1 | { |
Spring Framework 还提供了一个名为ErrorResponseException的基本实现。此异常封装了一个ProblemDetail对象,该对象生成有关发生的错误的附加信息。我们可以扩展这个异常来自定义和添加属性。
SpringBoot默认并没有开启ProblemDetail功能,需要通过以下方式任意一种方式开启:
1、yaml配置文件
1 | # webmvc |
2、添加ResponseEntityExceptionHandler
1 |
|
Web Data Extraction Based on Partial Tree Alignment ———— 论文翻译
本文研究了从包含多个结构化数据记录的网页中提取数据的问题。目标是将这些数据记录进行分割,从中提取数据项/字段,并将数据放入数据库表中。这个问题已经被多位研究人员研究过。然而,现有的方法仍然存在一些严重的限制。第一类方法基于机器学习,需要对感兴趣的每个网站进行大量示例的人工标注。由于Web上存在大量的站点和页面,这个过程非常耗时。第二类算法基于自动模式发现。这些方法要么不准确,要么做出很多假设。本文提出了一种新的自动执行任务的方法。它包括两个步骤:(1)识别页面中的单个数据记录,(2)对识别出的数据记录进行对齐和提取数据项。对于第一步,我们提出了一种基于视觉信息的方法来分割数据记录,这比现有方法更准确。对于第二步,我们提出了一种基于树匹配的新颖部分对齐技术。部分对齐意味着我们仅对一对数据记录中可以确凿对齐(或匹配)的数据字段进行对齐,并不对其余的数据字段做出承诺。这种方法能够非常准确地对齐多个数据记录。使用来自不同领域的大量Web页面的实验结果表明,所提出的两步技术能够非常准确地分割数据记录,并从中对齐和提取数据。
结构化数据对象是Web上非常重要的一种信息类型。这些数据对象通常是来自底层数据库的记录,并以一些固定的模板显示在Web页面中。在本文中,我们也称它们为数据记录。在Web页面中挖掘数据记录是有用的,因为它们通常呈现它们所在页面的基本信息,例如产品和服务列表。提取这些结构化数据对象使得我们能够整合来自多个Web页面的数据/信息,提供增值服务,例如比较购物、元查询和搜索。图1展示了Web上一些示例数据记录。图1(A)显示了一个包含两本产品(图书)列表的Web页面段落。每本书的描述是一个数据记录。图1(B)显示了一个包含数据表的页面段落,其中每个数据记录是一个表行。我们的目标有两个:(1)自动识别页面中的这些数据记录,(2)自动对齐和提取数据记录中的数据项。
文献中报道了多种从Web页面中挖掘数据记录的方法。第一种方法是手动方法。通过观察Web页面及其源代码,程序员从页面中找出一些模式,然后编写程序来识别和提取所有的数据项/字段。这种方法在处理大量页面时不具有可扩展性。其他方法都具有一定程度的自动化。主要有两种算法,即包装器归纳和自动提取。在包装器归纳中[11, 19, 23, 25, 33],从一组手动标注的页面或数据记录中学习一组提取规则。然后利用这些规则从类似页面中提取数据项。这种方法仍然需要大量的手动工作。在自动方法中,[12] [1]从包含类似数据记录的多个页面中找出模式或语法。然而,这种方法的一个主要限制是需要找到一组包含类似数据记录的初始页面,而这些页面必须通过手动或其他系统来找到。[20]提出了一种尝试探索当前页面背后的详细信息页面以提取数据记录的方法。然而,需要详细信息页面的需求也是一个严重的限制,因为许多数据记录并没有这样的背后页面(例如,图1(B))。此外,该方法假设详细页面已经存在,这在实践中是不现实的。由于典型Web页面中存在大量链接,自动识别指向详细信息页面的链接是一个非常棘手的任务。[8]提出了一种字符串匹配方法,但其结果不够强大,如[21]所示。大多数当前系统做出的另一个假设是,数据记录的相关信息包含在HTML代码的连续段中。然而,在一些Web页面中,一个对象的描述可能与其他对象的描述交织在一起。例如,HTML源代码中两个对象的描述可能按照以下顺序排列:对象1的部分1,对象2的部分1,对象1的部分2,对象2的部分2。因此,对象1和对象2的描述不是连续的。然而,当它们在浏览器上显示时,它们对于人类观看者来说是连续的。在第2节中,我们详细讨论了这些方法,并与我们提出的方法进行了比较。
本文提出了一个两步策略来解决这个问题。
1、首先,该方法通过对页面进行分割来识别每个数据记录,而无需提取其数据项。我们改进了先前用于此目的的技术MDR [21]。具体而言,新方法也利用视觉线索来寻找数据记录。视觉信息以两种方式帮助系统:
(i)它使系统能够识别分隔数据记录的间隙,这有助于正确分割数据记录,因为数据记录内的间隙(如果有)通常较小于数据记录之间的间隙。
(ii)所提出的系统通过分析HTML标签树或DOM树 [7] 来识别数据记录。构建标签树的一种简单方法是按照HTML代码中的嵌套标签结构进行处理。
然而,必须加入复杂的分析来处理HTML代码中的错误(例如,缺失或格式不正确的标签)。而视觉或显示信息可以在HTML代码被Web浏览器渲染后获得,它还包含有关标签的层次结构的信息。在这项工作中,我们不是分析HTML代码,而是利用视觉信息(即标签在屏幕上的位置)来推断标签之间的结构关系并构建标签树。由于Web浏览器的渲染引擎(例如Internet Explorer)具有较高的容错性,因此这种方法可以实现更强健的树结构构建。只要浏览器能够正确地渲染页面,就可以正确构建其标签树。
2、提出了一种新颖的部分树对齐方法,用于对齐并提取发现的数据记录中的相应数据项,并将数据项放入数据库表中。由于HTML代码的嵌套(或树状)组织方式,使用树对齐是自然而然的选择。根据我们的实验证明,这种新方法非常准确。
具体来说,在确定了所有的数据记录之后,每个数据记录的子树被重新排列成单一的树,因为每个数据记录可能包含在页面的原始标签树中的多个子树中,并且每个数据记录可能不是连续的。然后,使用我们的部分对齐方法对所有数据记录的标签树进行对齐。在部分对齐中,我们指的是对于每对树(或数据记录),我们仅对齐那些可以确定对齐的数据字段,并忽略那些无法对齐的部分,即不确定未对齐数据项的位置。过早地做出不确定的对齐决策可能会对后续涉及其他数据记录的对齐产生不良影响。这种方法在多个树的对齐中非常有效。
由此产生的对齐结果使我们能够从页面中提取所有数据记录的数据项。它还可以作为一个提取模式,用于从使用相同模板生成的其他带有数据记录的页面中提取数据项。
我们的两步方法被称为DEPTA(基于部分树对齐的数据提取),与所有现有方法非常不同,它不会做出现有方法所做的那些假设。只要一个页面包含至少两个数据记录,我们的系统就会自动找到它们(有关更多讨论,请参见第3.5节)。我们使用大量页面的实验结果表明,所提出的技术非常有效。
与我们的相关工作在包装生成领域。包装是从网站或页面中提取数据并将其放入数据库的程序[1, 11, 12, 16, 18, 19, 22, 23, 25]。有两种主要的包装生成方法。
第一种方法是包装归纳,它使用有监督学习从一组手动标记的正负样本中学习数据提取规则。然而,手动标记数据是费力且耗时的工作。此外,对于不同的网站或甚至同一网站的不同页面,手动标记过程需要重复进行,因为它们遵循不同的模板/模式。示例包装归纳系统包括WIEN [19],Softmealy [18],Stalker [23],WL2 [11],[25]等。我们的技术不需要人工标记。它可以自动在页面中挖掘数据记录并从记录中提取数据。
第二种方法是自动提取。在[14]中,研究了自动识别数据记录边界的方法。该方法基于一组启发式规则,例如最高计数标签、重复标签和本体匹配。[5]提出了一些更多的启发式规则来执行任务,而不使用领域本体。然而,[21]表明这些方法产生了较差的结果。此外,这些方法不会从数据记录中提取数据。[8]提出了一种从页面的HTML标签字符串中找到模式的方法,然后使用这些模式提取数据项。该方法使用了Patricia树和序列对齐来找到非精确匹配。然而,[21]表明其性能也较弱。我们的新方法不使用标签字符串进行对齐,而是使用树,利用嵌套的树结构来进行更准确的数据提取。[13]还提供了一组启发式规则来找到单个产品信息,例如价格和其他信息。
在[1, 12, 34]中,提出了另外两种技术。然而,它们需要使用包含相似数据记录的同一网站的多个页面(假定这些页面已给出)来从页面中找到模式或语法来提取数据记录。假设可用的包含相似数据记录的多个页面是一个严重的限制。我们的方法适用于每个单独的页面。
[20]提出了另一种数据提取方法。其主要思想是利用当前页面后面的详细数据来识别数据记录。通常,包含多个数据记录的页面并不包含每个数据记录的完整信息。相反,通常使用链接指向包含产品详细描述的页面。因此,该技术适用于图1(A)中的示例,但不适用于图1(B)中的示例,因为图1(B)中的每个数据记录都没有指向详细页面的链接。此外,[20]中的方法假设详细页面已经存在(在他们的实验中,这些页面是手动确定的),这是不现实的。由于典型网页中存在大量链接,自动识别指向详细页面的正确链接并不是一项简单的任务。我们的技术适用于图1中的两种页面类型,因为它不需要任何详细页面。
大多数现有方法的另一个问题是它们假设数据记录的相关信息包含在HTML代码的连续段中。这并不总是正确的。这个问题在介绍部分已经讨论过。我们提出的方法能够处理这种情况,因为我们的记录分割方法能够识别出这样的数据记录。在[21]中,我们提出了MDR算法,它只识别数据记录,但不对数据记录进行对齐或提取数据项。因此,它只完成了我们任务的第一步。即使对于第一步,它也有两个主要缺点。(1)该算法利用Web页面的HTML标签树从页面中提取数据记录。然而,某些页面的HTML源代码中的错误标签使得构建正确的树变得困难,从而无法在这些页面中找到正确的数据记录。使用视觉(渲染)信息来构建我们的新系统中的树解决了这个问题。(2)单个数据记录可能由多个子树组成。由于噪声信息,MDR可能会找到错误的子树组合。在我们的新系统中,数据记录之间的视觉间隙有助于解决这个问题。请注意,视觉线索已在其他Web任务中使用,例如找到不同的语义块[29, 28]。
最后,在[27]中,树匹配被用于在新闻页面中找到主要内容。然而,他们的任务与我们的不同。
现在我们开始介绍我们提出的技术。本节重点介绍第一步:将Web页面分割为单个数据记录以识别它们。本节不涉及对数据记录进行对齐或提取数据项的工作,这将是下一节的主题。由于这一步是对我们先前的技术MDR [21]的改进,因此我们在下面简要概述MDR算法,并介绍在本工作中对MDR进行的改进。我们也将增强的算法称为MDR-2(MDR的第二个版本)。
MDR算法基于对Web页面中数据记录的两个观察以及一个编辑距离字符串匹配算法[2]来查找数据记录。这两个观察是:
1、 一组包含一组类似对象描述的数据记录通常以连续的区域形式呈现在页面上,并使用相似的HTML标签进行格式化。这样的区域被称为数据记录区域(或简称为数据区域)。例如,在图1(A)中,两本书在一个连续的区域中呈现。它们还使用几乎相同的HTML标签序列进行格式化。如果我们将页面的HTML标签视为一个长字符串,我们可以使用字符串匹配(例如,编辑距离[2])来比较不同的子字符串,以找到表示相似数据记录的子字符串。这种方法的问题是计算量很大,因为数据记录可以从任何标签开始,也可以在任何标签结束。一组数据记录通常在其标签字符串方面长度不同,因为它们可能不包含完全相同的信息片段(参见图1(A))。下一个观察有助于解决这个问题。
2、 Web页面中HTML标签的嵌套结构自然形成一个标签树。例如,图2显示了一个示例标签树。在这个树中,每个数据记录被包裹在3个TR节点中,并且它们的子树位于相同的父节点TBODY下。两个数据记录位于两个虚线框中。我们的第二个观察是,一组相似的数据记录由相同父节点的一些子树组成。一个数据记录不太可能从一个子树的中间开始,在另一个子树的中间结束。相反,它从一个子树的开头开始,并在相同或后续的子树的结尾结束。例如,一个数据记录不太可能从TD*
开始,并在TD#
结束(图2)。这个观察使得基于编辑距离字符串比较的高效算法能够识别数据记录,因为它限制了在标签树中可能起始和结束数据记录的标签范围。
实验结果表明,这些观察非常有效。我们绝不假设一个Web页面只有一个包含数据记录的数据区域。事实上,一个Web页面可能包含几个数据区域,不同的区域可能具有不同的数据记录。给定一个Web页面,算法分为三个步骤(我们还讨论了在我们当前工作中对MDR进行的改进):
对MDR算法的主要改进是利用视觉信息来帮助构建更健壮的树,并找到更准确的数据区域。我们下面对它们进行描述。
在Web浏览器中,每个HTML元素(由起始标签、可选属性、可选嵌入的HTML内容和可能被省略的结束标签组成)都会被渲染为一个矩形。可以根据嵌套的矩形(由嵌套标签产生)构建标签树。具体细节如下:
让我们用一个示例来说明这个过程。假设我们有图3左侧的HTML代码,其中是一个包含两行(tr)和每行两个单元格(td)的表格。浏览器的渲染引擎在图3右侧为每个HTML元素生成了边界坐标(以像素为单位)。
通过视觉信息,我们可以按照打开标签的顺序,并通过包含关系检查,构建出图4中的树结构。树构建算法非常直观,我们在这里不再详细讨论。
该步骤会挖掘包含相似数据记录的页面中的每个数据区域。为了避免直接挖掘数据记录(这很困难),我们首先挖掘数据区域。通过比较单个节点(包括其子节点)的标签字符串和相邻多个节点的组合,我们可以找到每个数据区域。
我们使用图5中的人工标签树来解释。我们发现节点5和6相似(基于编辑距离)并形成标记为1的数据区域,节点8、9和10相似并形成标记为2的数据区域,节点对(14, 15)、(16, 17)和(18, 19)相似并形成标记为3的数据区域。为了避免同时使用单个节点和节点组合,我们使用广义节点的概念来表示每个相似的单个(标签)节点和每个(标签)节点组合。因此,一系列相邻的广义节点形成一个数据区域。图5中的每个阴影单个节点或节点组合都是一个广义节点。广义节点的概念捕捉了这样的情况:数据记录可能包含在几个兄弟标签节点中,而不是一个标签节点,并且数据记录在标签树中可能不是连续的,但广义节点是连续的(见下文)。
由于第3.1节的观察,为了识别数据区域中的广义节点并进行字符串比较,所需的比较次数并不多。我们只需要在父节点的子节点之间进行比较。识别数据区域的过程比较复杂,请参阅[21]了解更多细节。
在我们的新系统中,利用数据记录之间的间隙来消除虚假的节点组合。我们利用以下关于数据记录的视觉观察:
• 数据区域中两个数据记录之间的间隙应不小于数据记录内的任何间隙。例如,在图1(A)中,两个数据记录之间存在较大的间隙。
在确定了所有的数据区域之后,我们从广义节点中识别数据记录。需要注意的是,每个广义节点(标签树中的单个节点或节点组合)可能不代表一个单独的数据记录。情况可能非常复杂。下面,我们只强调两种有趣的情况,其中数据记录不包含在HTML代码的连续段中,以展示我们系统的一些高级功能(详细信息请参阅[21],以及其他更简单的情况)。
在某些网页中,对象(数据记录)的描述不在HTML代码的连续段中。有两种主要情况。图6展示了第一种情况的示例。
在这个示例中,数据区域包含两个广义节点,每个广义节点包含两个标签节点(两行),这意味着这两个标签节点(行)彼此不相似。但是,每个标签节点具有相同数量的子节点,并且子节点彼此相似。一行列出了两个对象的名称,下一行列出了对象的其他信息,也是两个单元格。这导致HTML代码如下:name 1, name 2, description 1, description 2, name 3, name 4, description 3, description 4。
对于这种情况,广义节点中每个标签节点的相应子节点形成一个非连续的数据记录。这由图6底部的标签树进行说明,其中r表示行,n表示名称,d表示描述。G1和G2是广义节点。 (n1, d1), (n2, d2), (n3, d3)和(n4, d4)形成了四个数据记录。
图7展示了第二种情况的示例,其中两个或更多数据区域形成多个数据记录。在这个示例中,第一行和第二行彼此不相似,但第一行形成一个数据区域,第二行形成另一个数据区域。每个数据区域包含两个(小)广义节点。
从图7的标签树中可以看出,这种情况与图6中的情况具有相同的结构。因此,可以应用类似的策略,即将每个数据区域的相应广义节点合并在一起形成非连续的数据记录。这个过程由图7中的标签树进行说明(G1、G2、g1和g2是广义节点)。
最后,需要强调的是,MDR或MDR-2并不知道哪些常规数据记录对用户有用。它仅仅找到了所有的数据记录。然而,在特定的应用中,用户通常只对特定类型的数据记录感兴趣,例如产品列表或数据表。可以设计简单的启发式方法来仅输出所需类型的数据记录。例如,在MDR(或MDR-2)中,可以选择仅基于一些指标(如图像、价格等)输出产品数据记录。
我们现在介绍数据提取的部分树对齐技术。关键任务是如何匹配所有数据记录中对应的数据项或字段。这包括两个子步骤:
在这里需要指出的是,字符串编辑距离在这一步骤中并不适用,因为字符串没有考虑树结构,而树结构对于确定数据项的正确对齐非常重要。由于两个字符串的多个对齐可能导致相同的编辑距离,字符串对齐可能会产生许多错误。而且,由于大多数用于形成数据记录的标签是tr和td,通过字符串匹配很难确定正确的对齐方式,因为有许多可能的对齐方式。然而,树匹配由于树结构约束,显著减少了可能的对齐方式数量。在我们的算法中,我们只使用一个简单的规则来解决存在多个可能的树对齐时的冲突。我们简单地选择最早出现在树中的可能子树对齐。这种方法在我们的实验中表现得非常好。因此,我们没有设计更复杂的冲突解决策略。
类似于字符串编辑距离,两个树A和B之间的树编辑距离[31, 30](我们只关注带标签的有序根树)是将A转换为B所需的最小操作集的相关成本。在经典的定义中,用于定义树编辑距离的操作集包括三种操作:节点删除、节点插入和节点替换。通常为每个操作分配一个成本。解决树编辑距离问题通常通过找到两个树之间的最小成本映射来辅助完成[30]。映射[30]的概念在正式定义上如下:
假设X是一棵树,X[i]是树X的第i个节点,根据树的前序遍历。树A的大小为n1,树B的大小为n2,映射M是一组有序对(i, j),每个来自树的一个节点,对于所有的(i1, j1), (i2, j2) ∈ M,满足以下条件:
(1)i1 = i2当且仅当j1 = j2;
(2)如果A[i1]在A[i2]的左侧,那么B[j1]在B[j2]的左侧;
(3)如果A[i1]是A[i2]的祖先,那么B[j1]是B[j2]的祖先。
直观地说,该定义要求每个节点在映射中最多出现一次,并且保留了兄弟节点之间的顺序和节点之间的层次关系。图8展示了一个映射的示例。
已经提出了几种算法来解决找到将一棵树转换为另一棵树所需的最小操作集(即成本最小)的问题。所有的表述都具有二次以上的复杂性[10]。还证明了如果树没有顺序,问题是NP-complete的[36]。在[30]中,提出了一种基于动态规划的解决方案。该算法的复杂度为O(n1n2h1h2),其中n1和n2是树的大小,h1和h2是树的高度。在[32][10]中,还提出了另外两个具有类似复杂度的算法。
在上述一般设置中,映射可以跨越层级,例如树A中的节点a和树B中的节点a。还存在替换,例如A中的节点b和B中的节点h。在本工作中,我们使用了一种受限制的匹配算法[35],该算法最初用于比较软件工程中的两个计算机程序,被称为简单树匹配(STM)。STM通过动态规划生成最大匹配来评估两个树的相似性,其复杂度为O(n1n2),其中n1和n2分别为树A和B的大小。不允许进行节点替换和层级交叉。
设A和B为两棵树,i ∈ A,j ∈ B为A和B中的两个节点。树之间的匹配被定义为一个映射M,对于M中的每对(i, j)其中i和j为非根节点,有(parent(i), parent(j)) ∈ M。最大匹配是具有最大配对数的匹配。
设A = <RA, A1, A2,…, Am>和B = <RB, B1, B2,…, Bn>为两棵树,其中RA和RB为A和B的根节点,Ai和Bj分别为A和B的第i个和第j个一级子树。当RA和RB包含相同的符号时,A和B之间的最大匹配是MA,B+1,其中MA,B是<A1, A2,…, Am>和<B1, B2,…, Bn>之间的最大匹配。MA,B可以通过以下动态规划方案获得:
在图9中的Simple_Tree_Matching算法中,首先比较A和B的根节点(第1行)。如果根节点包含不同的符号,则两棵树完全不匹配。如果根节点包含相同的符号,则递归地找到A和B的一级子树之间的最大匹配,并将其保存在W矩阵中(第8行)。基于W矩阵,应用动态规划方案来找到两棵树A和B之间最大匹配中的配对数。
算法伪代码: Simple_Tree_Matching(A, B)
1 | if the roots of the two trees A and B contain distinct symbols |
我们使用[35]中的一个例子来解释算法(图10)。为了找到树A和B之间的最大匹配,首先比较它们的根节点N1和N15。由于N1和N15包含相同的符号,返回M1-15[4,2]+1作为树A和B之间的最大匹配值(第11行)。M1-15矩阵是基于W1-15矩阵计算的,而W1-15矩阵中的每个条目,例如W1-15[i, j],是树A和B的第i个和第j个一级子树之间的最大匹配,它是基于其M矩阵递归计算的。例如,通过构建矩阵(E)-(H),递归计算出W1-15[4, 2]。所有相关的单元格都被阴影标记。M矩阵中的零列和零行是初始化。请注意,我们在M和W矩阵中都使用下标来指示它们所操作的节点。
在匹配过程(或匹配之后),我们可以回溯到M矩阵中,找到两个树中匹配/对齐的节点。当一个节点有多个匹配结果时,我们选择在树中出现最早的匹配。例如,在图11中,树A中的节点c可以匹配树B中的第一个节点c或最后一个节点c。我们选择树B中的第一个节点c。这种启发式方法是因为在Web页面中为了视觉效果,如果树A中的较早节点x与树B中的较晚节点y匹配,通常会有一些指示(标签)出现在x之前。根据我们的实验,这种启发式方法效果很好。
由于页面中的每个数据区域都包含多个数据记录,我们需要对多个标签树进行对齐,以便在表的同一列中生成一个包含所有相应数据项/字段的单个数据库表。在这个数据表中,每一行代表一棵树(数据记录),每一列代表每个数据记录中的一个数据字段。存在几种现有算法可以执行多个序列/树的赋值。在[6]中,提出了一种使用多维动态规划的多重对齐方法。该方法是最优的,但其时间复杂度呈指数级增长,因此不适合实际使用。还提出了许多启发式方法[24, 17, 3]。在[8]中使用的居中字符串方法是一种特殊的启发式方法,用于多个序列的对齐,也可以用于树的对齐。在这种方法中,选择一个序列xc,使得(D(xi, xc)代表两个字符串的距离)的值最小。
$$
\sum_{k}^{i=0}D(x_i,x_c)
$$
然后,针对每对(xi, xc),其中i ≠ c,执行一对一的对齐操作。假设有k个序列,且所有序列的长度为n,寻找中心序列的时间复杂度为O(k^2n^2),而每一步的迭代一对一对齐操作的时间复杂度为O(n^2)。因此,总体的时间复杂度为O(k^2n^2)。类似地,我们可以找到一个中心树Tc,并将所有其他树与Tc对齐。这种技术存在两个主要缺点:首先,尽管该算法具有多项式时间复杂度,但对于包含许多数据记录或包含许多属性的数据记录的页面,其运行速度较慢。其次,如果中心树没有特定的数据项,那些包含相同数据项的其他数据记录将无法对齐。我们实施了这种方法,但结果很差。其他流行的多重对齐方法包括渐进对齐[17]和迭代对齐[3]。它们的工作原理类似于分级聚类,并且都需要提前进行O(k^2)的一对一匹配。对于我们的任务,我们可以做得更好,因为我们知道数据记录遵循某些预定义的模板。
我们提出的方法通过逐步扩展种子(标签)树来对齐多个标签树。种子树记为Ts,最初选择具有最多数据字段的树作为种子树。需要注意的是,种子树类似于中心树,但不需要进行O(k^2)的一对一树匹配来选择。选择这棵种子树的原因很明确,因为这棵树更有可能与其他数据记录中的数据字段良好对齐。然后,对于每个Ti(i ≠ s),算法试图为Ti中的每个节点找到与Ts中的匹配节点。当找到节点ni的匹配时,在ni和ns之间创建一个链接,表示在种子树中的匹配。如果找不到节点ni的匹配,则算法尝试通过将ni插入到Ts中来扩展种子树。扩展后的种子树Ts随后用于后续匹配。需要注意的是,在匹配或对齐过程中不使用标签树节点中的数据项。
在介绍完对齐多个树的完整算法之前,让我们先讨论一下两个树的部分对齐的概念。如上所述,在Ts和Ti匹配之后,Ti中的一些节点可以与Ts中对应的节点对齐,因为它们互相匹配。对于那些未匹配的Ti中的节点,我们希望将它们插入到Ts中,因为它们可能包含可选的数据项。当从Ti中插入一个新节点ni到种子树Ts时,可能存在两种情况,取决于能否确定在Ts中唯一的位置来插入ni。事实上,我们不仅考虑单个节点ni,还可以一起考虑Ti中一组未匹配的连续兄弟节点nj…nm。不失一般性,我们假设nj…nm的父节点在Ts中有一个匹配,并且我们希望将nj…nm插入到Ts中的相同父节点下。只有当可以在Ts中唯一确定插入nj…nm的位置时,我们才会将它们插入到Ts中。否则,它们将不会被插入到Ts中,保持未对齐状态。因此,这种对齐是部分的。插入nj…nm的位置可以唯一确定在以下情况下:
否则,我们无法唯一确定Ti中未匹配节点插入Ts的位置。这在图12(C)中有所说明。在这种情况下,Ti中未匹配的节点x可以插入到Ts的两个位置之间,即节点a和节点b之间,或者节点b和节点e之间。在这种情况下,我们将不会将其插入到Ts中。
图13给出了基于两个标签树部分对齐的多树对齐的完整算法。
我们在图14中使用一个简单的示例来解释算法。我们有三个示例树,它们都只有两个层级。
第1行和第2行(图13)基本上是找到具有最多数据项的树,这就是种子树。在图14中,种子树是第一棵树(我们省略了T1左侧的许多节点)。第3行进行一些初始化操作。第4行开始while循环,对每个未对齐的树与Ts进行对齐。第5行选择下一个未对齐的树,第6行进行树的匹配。第7行通过追踪第6行的矩阵结果找到所有匹配的节点对。这个过程类似于使用编辑距离对齐两个字符串。我们不会进一步讨论这个问题。注意,第5行和第6行可以集成在一起。为了简单起见,我们将它们分开展示。在图14中,Ts和T2产生一个匹配,即节点b。节点n、c、k和g未与Ts匹配。第8行进行检查。第9行尝试将它们插入Ts,这就是上面讨论的部分树对齐。在图14中,T2中的节点n、c、k和g都无法插入Ts,因为找不到唯一的位置。第14行将T2插入R,R是可能需要进一步处理的树的列表。在图14中,当将T3与Ts进行匹配时,所有未匹配的节点c、h和k都可以插入Ts。因此,T3将不会被插入R。第14-16行设置“flag = true”以指示找到了一些新的对齐/匹配,或者插入了一些未匹配的节点到Ts中。
第17-21行检查停止条件。“S = ∅ and flag = true”表示我们已经处理了S中的所有树,并且找到了一些新的对齐或插入操作。然后应该再次处理R中的树。在图14中,R中只有T2,它将与下一轮中的新Ts进行匹配。现在T2中的每个节点都可以匹配或插入。过程完成。第23行根据生成的对齐结果输出每个树的数据项。请注意,如果算法完成后仍然存在未匹配的节点和数据,那么每个未匹配的数据将占据单独的一列。表1显示了图14中树的数据表。我们使用“1”表示一个数据项。
该算法的复杂度在不考虑树匹配的情况下为O(k^2),其中k是树的数量。然而,在实践中,我们几乎总是只需要遍历S一次(即R = ∅)。
应注意,生成的对齐结果Ts还可以用作从使用相同模板生成的其他页面中提取数据项的提取模式。
本节评估了我们的系统DEPTA(基于部分树对齐的数据提取),该系统实现了提出的技术。评估分为两个部分:
数据记录提取(步骤1):我们将DEPTA的第一步(也称为MDR-2)与我们现有的系统MDR进行比较,以确定数据记录。在这里,我们不将其与[5]和[8]中的方法进行比较,因为[21]表明MDR已经比它们更有效。
数据项/字段对齐和提取(步骤2):这是DEPTA的第二步。[8]能够执行相同的任务。然而,正如[21]所示,它在找到正确的数据记录方面表现不佳,因此无法很好地提取数据项。我们不与[1][12]中的系统进行比较,因为它们需要多个页面,并且所有页面都包含类似的数据记录以从页面中提取模式。[20]中的技术需要页面背后的详细信息页面(待提取页面),在他们的实验中,这些详细信息页面是手动识别和下载的,这在实践中是不现实的。DEPTA更加通用。给定单个页面,它能够从中提取数据记录和数据项。
我们的实验结果如表2所示。
第1列:列出了每个站点的URL。在某些站点上尝试了多个页面(具有不同的数据记录格式)。我们在实验中使用的站点数量为49。使用的页面总数为72。我们的实验Web页面是随机收集的。由于大多数页面的URL较长,我们无法在此列出它们。我们将在我们的网站上发布所有测试页面的URL。
第2列和第4列:它们分别给出了MDR和MDR-2(DEPTA的步骤1)从每个站点的页面中提取的正确(Cor.)记录数。这些数据记录是页面中明显的记录(例如产品列表)。它们不包括导航区域,这些区域也可能具有规律的模式。请注意,尽管MDR-2框架能够处理嵌套的数据记录(记录中的记录),但在这项工作中我们没有明确处理此类数据记录,因为在记录列表中它们相对较少。我们将在未来添加这一点。提出的部分树对齐方法能够对齐嵌套记录中的数据项。
第3列和第5列:它们分别给出了MDR和MDR-2(DEPTA的步骤1)从每个站点的页面中错误提取的数据记录数。x/y表示提取结果中有x个是不正确的,y个是未提取的。
第6列和第7列:它们分别给出了DEPTA的第2步从每个站点的数据记录中提取的正确数据项和错误数据项的数量。具有错误对齐的提取的数据项也被视为错误。
表2的最后三行给出了每列的总计,以及每个系统的召回率和精确度。对于MDR和MDR-2(DEPTA的第1步),召回率和精确度是基于在所有页面中找到的正确数据记录总数和这些页面中实际数据记录的数量计算的。对于DEPTA的数据项提取,精确度和召回率的计算考虑了因DEPTA的第1步导致的20个丢失的数据记录。
我们可以看到,数据提取非常有效。几乎所有的错误都是由于数据记录提取引起的。我们还观察到,MDR-2的性能明显优于MDR。
在本文中,我们提出了一种从网页中提取结构化数据的新方法。尽管这个问题已经被几位研究人员研究过,但现有的技术要么不准确,要么做出了许多强假设。我们的方法不做这些假设,只需要页面包含多个数据记录,而对于包含数据记录的页面来说,这几乎总是成立的。我们的技术包括两个步骤:(1)在不提取数据记录中的每个数据字段的情况下识别数据记录,以及(2)对多个数据记录中的相应数据字段进行对齐,以从中提取数据并放入数据库表中。我们针对步骤(1)提出了一种基于视觉信息的增强方法,显著提高了我们之前算法的准确性。对于步骤(2),我们提出了一种新颖的部分树对齐技术,用于对齐多个数据记录的相应数据字段。使用大量网页的实证结果表明,这种新的两步技术能够非常准确地分割数据记录并从中提取数据。
这项工作得到了美国国家科学基金会(NSF)的支持(项目编号:IIS-0307239)。
时序数据特点:
1、写多读少:持续高并发的写入,由于定期采样的特点,写入量平稳,几乎不会有更新操作。
2、冷热明显:查询一般查近期产生的数据,很少查过期数据。
3、数据关联性小:数据之间几乎不存在关系,不需要Join。
4、查询场景不同,索引特殊:数据都包含设备信息(服务器、传感器)和时间戳,大部分都是聚合查询。
应用场景:IOT传感器数据、服务器监控数据、金融交易数据
绝大多数关系型数据库的特点:
1、读多写少:大部分RDBMS都是基于B+树实现的,B+树主要通过增加节点大小降低树的层级从而减小IO,索引更多数据,从而优化读取性能。
2、随机读写:B+树读写会直达叶子节点,读写性能稳定,少部分热数据直接通过LRU进行缓存。
3、表间关系紧密:关系型数据库的特点就是关联关系,通过范式来避免数据冗余,查询数据库时通过Nested Loop Join,Hash Join,Sort Merge Join等算法进行关联。
4、按查询场景建立二级索引:为了提升查询性能,会对查询字段额外创建另一个B+树索引,也就是所谓的“二级索引”,这也导致写操作需要同时更新这些索引。
应用场景:一致性较强的应用数据
优点:
1、随机读写:根据B+树索引查询,能对磁盘中的数据实现快速读写。按照InnoDB的16KB块大小计算,对8字节的ID值进行索引,包括物理指针在内,一个节点可以存储1000左右的数据。这样算内两层数据只有1M左右,可以缓存在内存中,第三层可以索引1G左右的数据,所以三层B+树基本只要一次I/O操作。
2、范围查询:B+树叶子节点通过双向链表连起来了,可以直接顺着叶子节点的链表进行范围扫描,避免了B树回溯节点的问题。
3、查询稳定:数据都存在叶子节点,内部节点只存key,一方面可以让内部节点索引更多的数据,降低树的层级,另一方面能让查询都沉到叶子节点,性能相对稳定,不会像B树那样,有的查询读到第二层,有的查询读到第三层。
4、WAL保证写操作的持久性:将修改先写入预写日志(Write-Ahead Log),如果修改的脏页因为宕机等原因没有落盘,可以通过WAL进行恢复。
缺点:
1、页分裂,写性能不稳定:写操作可能导致页面数据已满,触发页分裂,页分裂会产生多次磁盘I/O,导致写放大。
2、大数据量I/O性能下降:数据量过大,会导致层级过深,I/O次数上升,性能下降。通常的策略是分库分表,但是分库分表需要根据系统业务选择列进行路由,通用性不强。
Log-Structure MergeTree将写操作写入内存树,当达到一定阈值再Flush进磁盘,磁盘中的小树会逐级合并大树。
优点:
1、写性能吞吐量极高:随机写合并成大批量的顺序写。支持高吞吐的写,写入性能能得到大幅度提升。
2、没有B+树的页分裂,修改操作会直接追加到
缺点:
1、读放大:读取操作需要,从
2、写放大:对数据的修改和删除,需要延迟到Compact阶段进行后台合并,写操作次数实际上增多了。
3、空间放大:LSM-Tree的深层树中可能存在未合并的垃圾数据,会占用更多空间。
优化:
1、读优化:可以在内存中使用BloomFilter对树中的数据进行预判,如果不存在就不需要进行读操作了。
2、写优化和空间优化:优化Compact算法,尽量减少写操作。
LSM-Tree非常适合存储时序数据。
1、首先LSM-Tree的写性能满足了时序数据持续性高并发的写入和读多写少的场景。
2、时序数据基本不会修改和删除,所以避免了不必要的Compact操作,也就没有空间放大的问题,写放大也能避免。
3、老数据会随着层级的层架,进入更深层,契合了是冷热数据查询的场景。
]]>使用过docker的人都知道,在正常情况下。我们使用multi-stage构建利用docker镜像缓存机制,可以加快构建速度。
但是缓存的镜像一多,没有及时释放磁盘空间,磁盘就容易爆满。
1 | [root@report ~]# df -h |
如果每次构建完手动docker rmi
又达不到加快构建速度的效果。
尤其是在持续集成环境中,大家公用一个build machine的时候。大家各自打扫门前雪,更加不会有人care磁盘会不会被占满。
为了一劳永逸的解决这个问题,最好的办法莫过于通过定时任务来清理旧的image。 这个方法听起来高大上,用起来简单的很。 运行crontab -e命令编辑定时任务。
1 | crontab -e |
在打开的文本编辑器最后添加如下一行,然后保存退出。
1 | 0 1 * * * docker image prune -a --force --filter "until=48h" |
然后执行下面的命令使定时任务生效。
1 | systemctl restart crond.service |
其实,到这里,整个配置就结束了。接下来我们简单解释一下。
上面的定时任务是每天夜里1点钟删除2天(48h)之前的image。
具体的操作时间,具体的image保留时间,可以根据自己的情况修改。
OpenResty是基于Lua即时编译器(LuaJIT)对Nginx进行扩展的模块——最核心的就是lua-nginx-module
这个模块。其他的都是OpenResty基于lua开发的相关模块,当然也可以基于lua开发自己的第三方模块。
所以要想使用OpenResty首先必须安装lua-nginx-module
。
下载并安装LuaJIT。可以使用源码方式安装,这个可以参考官方文档非常详细。这里为了方便直接用apt安装了
1 | sudo apt install luajit libluajit-5.1-dev |
下载ngx_devel_kit
模块
1 | # 在nginx目录下创建一个modules目录 |
下载lua-nginx-module
模块
1 | git clone https://github.com/openresty/lua-nginx-module modules/ngx_http_lua_module |
如果是使用alibaba/tengine,这个模块已经被包含在tengine的modules/ngx_http_lua_module
目录下了。
另外注意lua-nginx-module与nginx的兼容性,nginx1.6.0之前的版本是不支持的。
如果需要对nginx进行debug的话,需要修改 /auto/cc/conf 文件,将ngx_compile_opt="-c"
修改为 ngx_compile_opt="-c -g"
-g
用来生成调试信息:详见gcc文档
设置luajit的头文件和静态库的路径,ubuntu下可以用dpkg看看libluajit被安装到哪个目录了
1 | $ dpkg -L libluajit-5.1-dev |
然后设置两个环境变量
1 | export LUAJIT_LIB=/usr/lib/x86_64-linux-gnu/ |
执行auto/configure
1 | auto/configure --prefix=nginx \ |
执行make install
编译过程可能会比较慢,可以执行
make -j2 && make install
调大编译任务的个数
这里以一个第三方的lua模板引擎为例——lua-resty-template
1 | # 在nginx下创建一个放lua脚本的目录 |
在nginx.conf
中对lua模块进行配置
1 | http { |
在html/templates
目录下添加模板文件
1 |
|
访问localhost/templates/view.html
,能看到下面的结果
]]>Read More:
https://www.lua.org/pil/23.html
http://lua-users.org/wiki/DebuggingLuaCode
http://notebook.kulchenko.com/zerobrane/debugging-openresty-nginx-lua-scripts-with-zerobrane-studio
vscode调试nginx源码
1 | git clone https://github.com/nginx/nginx |
除了官方的nginx,也可以考虑用阿里的Tengine或OpenResty。
这两个发行版添加了各自的三方module。
修改 /auto/cc/conf 文件,将ngx_compile_opt=”-c” 修改为 ngx_compile_opt=”-c -g”
-g
用来生成调试信息:详见gcc文档
执行 sudo ./auto/configure –prefix=nginx工程目录 ,如果遇到错误 “the HTTP rewrite module requires the PCRE library”,说明少了用来匹配正则表达式的pcre
依赖包,可以自行根据平台进行安装
1 | apt install pcre2-utils |
执行 sudo make
执行 ./objs/nginx,打开浏览器访问下 127.0.0.1,没问题的话就可以看到Nginx的欢迎界面了。
具体源码编译内容可以参考nginx文档
nginx是多进程架构:一个Master进程,若干个Worker进程。
Master进程负责管理 Worker 进程,处理nginx命令行指令
Worker进程负责接收处理客户端请求
Worker进程数通常设置成CPU核数,worker_processes: auto;
可以自动检查CPU设置成核心数。
Worker进程和redis类似使用单线程+IO多路复用实现高并发处理IO请求。
修改/conf/nginx.conf
1 | # 关闭Master守护进程的功能 |
daemon默认都是on
,开发阶段关闭
添加VSCODE调试配置
1 | { |
如果是MacOS,不愿意装gdb
,也可以用llvm的lldb
进行调试。具体配置可以参考vscode文档
打断点,Debug起来
查看 Worker 进程pid
1 | ps aux | grep nginx |
编辑launch.json
,Attach到worker进程
1 | { |
这里的进程id填进去就行了
切换到Attach Worker
接受请求的地方打个断点,浏览器重新刷新一下,请求就进来了
从函数堆栈中,可以看到请求的处理过程
启动两个进程的方式debug确实挺麻烦的,nginx提供了配置关闭多进程架构,这样就可以在一个进程里对nginx的整个流程进行debug了,避免上面繁琐的配置。
只需要在nginx.conf
文件中把daemon
和master_process
设成off
即可。
1 | # 关闭Master守护进程的功能 |
]]>ReadMore: https://github.com/agile6v/awesome-nginx
在这篇文章中,我想从性能的角度探讨ElasticSearch 为我们存储了哪些字段,以及在查询检索时这些字段如何工作。实际上,ElasticSearch和Solr的底层库Lucene提供了两种存储和检索字段的方式:store_fields
和doc_values
。此外,ElasticSearch默认提供了 _source
字段,这是在索引时由文档的所有字段构造的一个大json。
为什么 ElasticSearch使用 _source
字段作为默认值,所有这些可用的字段从性能的角度来看有什么区别?让我们一探究竟!
当我们在 Lucene 中索引一个文档时,已经被索引的原始字段的信息丢失了。字段根据schema配置进行分词、转换然后索引形成倒排索引。没有任何额外的数据结构,当我们搜索一个文档时,我们得到的是这个文档的 docId 而不是原始字段。为了获得这些原始信息,我们需要额外的数据结构。Lucene为此提供了两种可用的方式:store_fields
和doc_values
。
store_fields
的目的是存储字段的原始值(没被分词),以便在查询时检索它们。正如前面所说Lucene的倒排索引查询出来的是一个个docId,为了得到原始值就得把原始值存储起来。
引入了doc_values
是为了对排序、聚合、分组等操作进行加速。doc_values
也可用于在查询时返回字段值。唯一的限制是我们不能在text字段上使用doc_values
。
store_fields
和doc_values
是在 Lucene 库中实现的,在 Solr 和 ElasticSearch 中都可以使用。
这里有一篇文章,比较了 Solr 中store_fields
和doc_values
检索性能:
DocValues VS Stored Fields : Apache Solr Features and Performance SmackDown.
可以找到关于store_fields
和doc_values
的更详细的使用方法以及各自局限性。
如果我们在映射中明确定义store_fields
和doc_values
,则可以在 elasticsearch 中使用它们:
1 | "properties" : { |
默认情况下,每个字段的
store
都设置为 false。相反,所有支持doc_values
的字段都会默认开启doc_values
。
根据store_fields
和doc_values
的默认配置,在查询时仍然会返回查询命中的文档中的每个字段值。发生这种情况是因为 ElasticSearch 使用另一种工具进行字段检索:Elasticsearch 提供的_source
字段。
_source
字段是在索引时传递给 ElasticSearch 的 json。此字段在 ElasticSearch 中默认设置为 true,可以通过以下方式使用mappings
禁用_source
:
1 | "mappings": { |
有两种方式检索_source
字段的内容:
1、查询时用field
选项可以提取在mappings
中已经定义的字段
1 | POST my-index-000001/_search |
还可以用format
选项对一些特殊的字段进行格式化处理,比如可以将时间戳转成字符串。
这种方式命中的结果也会在的hits
对象下有对应的fields
字段作为响应。
1 | { |
2、通过_source
选项提取原始的文档内容。前面的例子中,查询时_source
都置成了false。
默认情况下_source
为true,也就是默认返回_source
原始内容的所有字段。
也可以指定要在响应中返回的_source
中的一部分字段,这应该是为了提高网络传输的响应速度。
1 | GET /_search |
可以通过适当的配置将_source
的某些字段在索引的时候就排除掉:
1 | PUT logs |
索引时,从_source
中排除字段将减少磁盘空间使用,但被排除的字段将永远不会在响应中返回。
如果禁用 elasticsearch _source
字段,更新文档时需要从头开始重新索引。实际上,为了更新文档,我们需要从旧文档中获取字段的值。从逻辑上讲,使用store_fields和doc_values从旧文档中获取字段的值应该是可行的(这就是 Solr 中原子更新的工作方式)。但是,由于设计决定,这在 ElasticSearch 中是不允许的,如果您需要更新文档,则必须在 elasticsearch 索引配置中启用_source
字段。
在 elasticsearch 中,您可以启用或禁用_source
字段并使Stored Field或doc_values
。但是如何在查询时检索字段?
默认情况下,如果启用了_source
,则返回包含整个文档的_source
。您可以避免它并仅返回源的一个子集,如下所示:
1 | POST my-index-000001/_search |
但是,如果您没有启用_source
字段,并且想要从Stored Field和doc_values
返回字段,则必须以另一种方式告诉它给 ElasticSearch。对于您使用的每个源,您必须以不同的方式指定字段列表:
1 | ... |
例如,如果您有一个字段既存储了store_fields
也存储了doc_values
,您可以选择是从store_fields
还是doc_values
中检索它。从功能的角度来看,这完全相同,但您的选择可能会影响查询的执行时间。
在本节中,我只想简要概述store_fields
、_source
字段和 doc_values
的内部结构,以便来了解使用这些方法进行字段检索时对性能的期望。
store_fields以行方式放置在磁盘中:对于每个文档,都有一行连续包含所有需要存储的字段。
以上图为例。为了访问文档 x 的 field3,我们必须先访问文档 x 的行起始位置,并跳过存储在 field3 之前的所有字段。跳过字段需要获取其长度。跳过字段虽然不像读取那么繁琐,但此操作并非不耗时。
doc_values以列方式存储。多个文档的同一个字段的值一起连续存储在一起,因为同一个字段的格式基本是一致的,所以可以“几乎”直接访问某个文档的某个字段。计算一个想要的值的地址不是一个简单的操作,它有一个计算成本,但我们可以想象,如果我们只想要一个字段,使用这种访问会更有效率。但是对于磁盘来说,这种随机访问会非常影响性能,所以一般只有在排序和聚合这种需要大批量提取一个字段的情况下会使用doc_values
。
_source
呢?好吧,如上所述,_source
是一个包含 json 的大字段,其中包含在索引时提供给 ElasticSearch 的所有输入。但是,这个字段实际上是如何存储的?毫不奇怪,ElasticSearch 利用了一种已经由 Lucene 现成的机制:store_fields
。而且,_source
字段是行中第一个存储的字段。
正因为它是包含整个文档内容的json,所以必须读取整个_source
才能使用它包含的信息。如果我们要返回一个文档的所有字段,这个过程直观上是最快的。另一方面,如果我们只需要返回它包含的信息的一小部分,读取这个巨大的字段可能会浪费计算能力。
为了对 3 种类型的字段进行基准测试,我在 ElasticSearch 中创建了 3 个不同的索引。我索引了来自维基百科的 100 万个文档,对于每个文档,我用三种不同的方法索引了 100 个包含 15 个字符的字符串字段:在第一个索引中,我将字段设置为store_fields
,在第二个索引中设置为doc_values
。在这两个索引中,我都禁用了_source
字段。相反,在第三个索引中,我只是启用了_source
字段。
文档和查询集合来自 https://github.com/tantivy-search/search-benchmark-game。 我使用真实的集合来模拟真实的场景。
执行细节:
对于每个查询,我请求了最好的 200 个文档,并重复测试——将返回的字段数量(在我创建的 100 个随机字符串字段中)从 1 逐步提升到 100。
这是基准测试的结果:
结果正好显示了我们期望看到的结果。
1、**如果我们需要每个文档的字段很少,建议使用 doc_values
**。
2、当我们想要返回整个文档_source
字段是最好的
3、而store_field
是其他两者之间的完美折中。
在我执行的基准测试场景中,如果我们只需要一个字段,doc_values
的速度几乎是 _source
字段的两倍 ,而在相反的极端情况下,如果我们想返回所有字段,使用_source
字段代替doc_values
,图表显示速度几乎提高了 2倍。
总之,性能不是我们必须考虑的唯一参数。正如我们在这篇文章中简要解释的那样,使用一种或另一种方法存在一些限制。由于您的用例的一些限制,您可能被迫使用这三个中的一个。而且即使从表现来看,我们也没有明显的赢家。
如果磁盘空间不是问题,**甚至可以混合不同的方法并将字段设置为store_field
和doc_values
,并保持开启_source
**。在查询时,elasticsearch 使您可以选择所需的字段列表,以及是否希望从 _source
、_store_field
或 doc_values
返回它们。
当然三个都存储,也会导致索引阶段速度很慢,容易出现EsReject异常。所以软件工程没有银弹。根据场景合适选择吧!
参考:
https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html
https://sease.io/2021/02/field-retrieval-performance-in-elasticsearch.html
]]>对菜鸟教程网站的思考。菜鸟教程一个看上去很平常的网站,只是提供HTML、CSS、JS等Web相关的基础教程,但是据站长之家统计,这个网站的日流量达到接近300万的PV。
大多数行业都是金字塔结构,行业头部的人往往是少数。很多时候我们把目标瞄向行业头部的专家,却忽视了专家也是从菜鸟成长起来的,忽视了普通从业者,忽视了“菜鸟“们的需求。“菜鸟教程”这个网站就是瞄准行业底部占大头的客户。也就是所谓的”下沉市场“。
很多时候,因为自己的孤傲,忽略了这部分下沉市场的需求:当你成为专家的时候,往往会对CSDN上的用户嗤之以鼻;当企业做大了,经常会忽视用户的小众需求;当阿里不断打造”消费升级“的概念,财报年年增长,股票欣欣向荣,商家抽成越来越狠,流量成本越来越高,逐渐背离三四线城市的需求,所以拼多多迅速崛起。
有人说:只要产品做的好,人无我有,人有我优,总会有大量用户来使用我的产品。但是这个依赖于用户对产品的认知,在互联网环境下,酒香也怕巷子深,没有好的流量手段推广,产品再好也无人问津。
也有人说:我只要把用户基数做大了,流量做大了,不管卖啥,都能赚的盆满钵满。这种做法不能说有错,但是不可持续,没有好的产品支撑,用户无法回购,一锤子买卖做完就没了,流量再大,也是涸泽而渔。
好的产品需要创业者根据用户痛点用心打磨,针对每个行业,每个需求点做法不一。这篇文章下面重要讲的是流量的获取。
传统模式,如发传单、地推、电话营销、展会营销。这种以人力为主,转化率比较高。
SEO
SEO是成本最低的获取流量的方式,只需要懂技术,对搜索引擎关键词进行优化,尽可能让你的网站在搜索结果的前几页,自然就有流量了。
SEO 网站优化的步骤和技巧有哪些? 可以参考 - 纵横SEO的回答 - 知乎 https://www.zhihu.com/question/19808905/answer/1324352720
SEO流量的优点是质量很高,人家都是冲着关键词找到你的,只要产品做的足够好,转化率是非常高的。
SEO的缺点是应用必须部署在Web端,得让爬虫能访问到,这对于移动端App不友好。
今天次把我的网站所有的访问量清0了,验证一下SEO的效果
联盟推广
可以使用百度、谷歌的广告联盟,投放广告后,用户点击就能跳到landing页了。
联盟推广要看平台的算法,目前谷歌的质量比百度的质量好很多。
移动互联网平台引流
联盟推广主要针对Web上的用户,对于移动端用户,可以在微信、抖音等社交媒体上推广。这中类型的广告转化率也要看平台的广告算法的准确率,千人千面——能根据用户画像精准的推荐广告,这样转化率才会更高。
我发现微信广告,每个人看到的内容都是不一样的,这种有针对性的投放,流量转化率就高很多。
便宜的渠道优先:这个比较好理解,优先需要考虑到成本问题,当然如果不缺钱,可以忽略这条。缺点吧,便宜的渠道一般转化率可能较差。
和目标用户群比较匹配的渠道优先:这个很好理解,渠道匹配,转化率高。如果你产品的目标客户群是女性,你最好去小红书上去获客,比如你要推广的是化妆品,你应该选择美妆类的主播帮忙推广产品。
有可追溯性的渠道优先:一般流量的转化率需要数据作为基础。具备可追溯性 ,才可能优化广告投放的效果。
互联网广告通过大数据进行计算汇总:前面提到的SEO,各个所有引擎都有相关的统计平台,比如google的Google Search Console,Bing的BingWebMaster;联盟推广和其他互联网推广平台也有相应的统计报表。
投放灵活性的渠道优先:线下广告和一些合作mcn机构,一般都是一个季度、半年起投,如果你的产品和商业模式还不清晰,这种渠道暂时不要考虑。
大体量的渠道优先:大体量的渠道决定你获客的上限,一般中小企业暂时不需要考虑,这个属于爆发期企业需要考虑的问题。
天下大势,分久必合,合久必分——《三国演义》
分布式未开之际,软件世界里网站后台还是单体模式。有问题可以看日志,查性能有top
以及sysstat下的一批工具包:mpstat
、pidstat
、iostat
,要对日志做分析统计可以上文本三剑客awk
、sed
、grep
。
随着互联网的突飞猛进,摩尔定律的失效,单体模式已经承接不了互联网下的滔天流量,应用的日志体量也在飙升,由于分布式集群环境日志也不再单独分布在一台机器上,怎么监控应用的性能、怎么基于日志做指标统计也变得异常复杂。于是衍生了分布式系统的可观测性的三大基石:Logging、Metrics、Tracing。今天的主角就是Tracing。
谷歌作为互联网的巨擘,手握独步天下的搜索引擎——Google。搜索引擎作为互联网的要隘,流量增长异常惊人,这也促使谷歌发展出自己的一套分布式系统。谷歌为了增加公司影响力便于吸引人才,将内部的系统设计以一篇篇论文的形式公布给业界。
谷歌就是这么自信,要知道谷歌的创始人也是基于自己发表的一篇论文创建的谷歌搜索引擎。
这其中每一篇论文都曾掀起过层层巨浪,比如号称三驾马车的GFS、MapReduce、Bigtable开创了大数据时代,Spanner、F1给分布式数据库指明了方向,TensorFlow引领了人工智能机器学习的发展,Borg衍生出基于K8S云原生的大生态。而Dapper也只是谷歌浩瀚冰山下的一隅,却引领了分布式系统链路追踪的发展。
Dapper中描述了谷歌内部系统的链路追踪技术。论文在2010年一经发出,Twitter根据论文研发了Zipkin,Uber根据论文研发了Jaeper。当然这只是最早开源出来的非常有名气的两个项目,还有像阿里的EagleEye等众多为开源出来的内部项目都是以谷歌的Dapper为原型设计的。
但是随着各大厂商对链路追踪系统的实现趋于碎片化,社区开始制定OpenTracing标准来统一链路追踪中涉及的相关概念和规范——这是一套平台无关、厂商无关的Trace协议,使得开发人员能够方便的添加或更换分布式追踪系统的实现。
在2016年11月的时候CNCF技术委员会投票接受OpenTracing作为Hosted项目,这是CNCF的第三个项目,第一个是Kubernetes,第二个是Prometheus,可见CNCF对OpenTracing背后可观察性的重视。大名鼎鼎的Zipkin、Jaeger也都开始遵循OpenTracing协议。
既然有了OpenTracing,OpenCensus又来凑什么热闹?
对不起,你要知道OpenCensus的发起者可是谷歌,也就是最早提出Tracing概念的公司(教练下场指导工作了:smirk:),而OpenCensus也就是Google Dapper的社区版。
OpenCensus和OpenTracing最大的不同在于除了Tracing外,它还把Metrics也包括进来,这样也可以在OpenCensus上做基础的指标监控;还有一点不同是OpenCensus并不是单纯的规范制定,他还把包括数据采集的Agent、Collector一股脑都实现了。OpenCensus也有众多的追随者,最大的新闻就是微软也宣布加入,OpenCensus可谓是如虎添翼。
OpenTracing这边有Elastic、Uber、Twitter、DataDog等互联网新秀作为拥趸已发展多年,而OpenCensus有谷歌、微软两大巨头撑腰。一时间也难分高低。
既然没办法分个高低,谁都有优劣势,咱们就别干了,统一吧。于是OpenTelemetry横空出世。
那么问题来了:统一可以,起一个新的项目从头搞吗?那之前追随我的弟兄们怎么办?不能丢了我的兄弟们啊。
放心,这种事情肯定不会发生的。要知道OpenTelemetry的发起者都是OpenTracing和OpenCensus的人,所以项目的第一宗旨就是:兼容OpenTracing和OpenCensus。对于使用OpenTracing或OpenCensus的应用不需要重新改动就可以接入OpenTelemetry。
OpenTelemetry可谓是一出生就带着无比炫目的光环:OpenTracing支持、OpenCensus支持、直接进入CNCF sanbox项目。但OpenTelemetry也不是为了解决可观察性上的所有问题,他的核心工作主要集中在3个部分:
可以看到OpenTelemetry只是做了数据规范、SDK、采集的事情,对于Backend、Visual、Alert等并不涉及,官方目前推荐的是用Prometheus去做Metrics的Backend、用Jaeger去做Tracing的Backend。
看了上面的图大家可能会有疑问:Metrics、Tracing都有了,那Logging为什么也不加到里面呢?
其实Logging之所以没有进去,主要有两个原因:
OpenTelemetry的终态就是实现Metrics、Tracing、Logging的融合,作为CNCF可观察性的终极解决方案。
Tracing:提供了一个请求从接收到处理完毕整个生命周期的跟踪路径,通常请求都是在分布式的系统中处理,所以也叫做分布式链路追踪。
Metrics:提供量化的系统内/外部各个维度的指标,一般包括Counter、Gauge、Histogram等。
Logging:提供系统/进程最精细化的信息,例如某个关键变量、事件、访问记录等。
这三者在可观测性上缺一不可:基于Metrics的告警发现异常,通过Tracing定位问题(可疑)模块,根据模块具体的日志详情定位到错误根源,最后再基于这次问题调查经验调整Metrics(增加或者调整报警阈值等)以便下次可以更早发现/预防此类问题。
实现Metrics、Tracing、Logging融合的关键是能够拿到这三者之间的关联关系.其中我们可以根据最基础的信息来聚焦,例如:时间、Hostname(IP)、APPName。这些最基础的信息只能定位到一个具体的时间和模块,但很难继续Digin,于是我们就把TraceID把打印到Log中,这样可以做到Tracing和Logging的关联。但这还是解决不了很多问题:
在OpenTelemetry中试图使用Context为Metrics、Logging、Tracing提供统一的上下文,三者均可以访问到这些信息,由OpenTelemetry本身负责提供Context的存储和传播:
目前OpenTelemetry还处于策划和原型阶段,很多细节的点还在讨论当中,目前官方给的时间节奏是:
从Prometheus、OpenTracing、Fluentd到OpenTelemetry、Thanos这些项目的陆续进入就可以看出CNCF对于Cloud Native下可观察性的重视,而OpenTelemetry的出现标志着Metrics、Tracing、Logging有望全部统一。
但OpenTelemetry并不是为了解决客观性上的所有问题,后续还有很多工作需要进行,例如:
更多参考文章:
A brief history of OpenTelemetry (So Far) | Cloud Native Computing Foundation (cncf.io)
open-telemetry/opentelemetry-specification: Specifications for OpenTelemetry (github.com)
]]>线上skywalking架构:
两台skywalking-oap接受并分析由agent采集的trace数据,但是问题是两台oap服务负载不均衡。
为了排除k8s的service负载均衡的问题,在线下环境还原了请求的过程。
skywalking提供了grpc(11800端口)和rest(12800端口)两种协议的服务。
从下图可以看到,skywalking提供了11800和12800的监听端口,以及连接ElasticSearch的9200端口
第一次请求连上了skywalking-oap1
第二次请求连上了skywalking-oap2
多次请求负载均衡是没有问题的。但是请求会断开连接,实际上是两次连接连向了两台不同的server。这个概念很重要,请求和连接不是一个事物,多个请求可以复用一个连接。
观察线上的两台oap发现,两台server都维持了大量的长连接,其中负载高的一台明显连接数更多。
由于线上skywalking-agent和skywalking-oap使用的是grpc进行通信的,grpc基于http/2会维持一个长连接。k8s的service无法识别应用层的负载均衡。
dubbo与SpringCloudRibbon的客户端负载均衡
Dubbo因为有自己的注册中心可以直接获取服务ip,负载均衡直接由Dubbo客户端实现,两次调用会路由到不同的service,即使client持有多个service实例的连接,客户端也能根据连接个数进行负载均衡。这本质上和SpringCloud中的Ribbon原理一样。这种情况直接就不需要k8s的service来实现负载均衡。
基于反向代理的负载均衡
Nginx,Apache,HAProxy等反向代理服务器都支持负载均衡的功能。
像Nginx是能识别HTTP消息的,即使是维持了长连接,也能截取出完整的http消息,从而实现应用层的负载均衡。
Nginx在1.9.0添加了ngx_stream_proxy_module
模块支持TCP/UDP的反向代理,但是它只能处理TCP连接,但是处理不了应用层的请求负载均衡。
比如使用nginx为mysql建立高可用的反向代理,它解析不了同一个mysql连接中的两条sql请求。比如想进行读写分离,nginx就实现不了,这些大多是在应用端实现的,或者使用专业的反向代理,比如ProxySQL。
k8s的service就有点类似于nginx的tcp反向代理。当然只是说很像,实际上区别还是很大的。
下面看一下k8s的service实现原理。
k8s的service是基于虚拟ip实现的:使用iptables实现路由转发。k8s的service代理有三种运行模式:
这种模式,kube-proxy 会监控 Kubernetes control plane 对 Service 对象和 Endpoints 对象的添加和移除操作。 对每个 Service,它会在本地 Node 上打开一个端口(随机选择)。 任何连接到“代理端口”的请求,都会被代理到 Service 后端的某个Pod上(如 Endpoints
所报告的一样)。 使用哪个后端 Pod,是 kube-proxy 基于 SessionAffinity
来确定的。
最后,它配置 iptables 规则,将到达该 Service 的 clusterIP
(是虚拟 IP) 和 Port
的请求重定向到代理的后端Pod的端口。
默认情况下,用户空间模式下的 kube-proxy 通过轮询算法选择后端服务。
这种模式,kube-proxy
会监控 Kubernetes control plane 对 Service 对象和 Endpoints 对象的添加和移除。对每个 Service,它会配置 iptables 规则,从而捕获到达该 Service 的 clusterIP
和端口的请求,进而将请求重定向到 Service 的后端中的某个 Pod 上面。 对于每个 Endpoints 对象,它也会配置 iptables 规则,这个规则会选择一个后端Pod。
默认情况下,kube-proxy 在 iptables 模式下随机选择一个后端。
使用 iptables 处理流量具有较低的系统开销,因为流量由 Linux netfilter 处理, 而无需在用户空间和内核空间之间切换。 这种方法也可能更可靠。
如果 kube-proxy 在 iptables 模式下运行,并且所选的第一个 Pod 没有响应, 则连接失败。 这与用户空间模式不同:在这种情况下,kube-proxy 将检测到与第一个 Pod 的连接已失败, 并会自动使用其他后端 Pod 重试。
你可以使用 Pod 就绪探针 验证后端 Pod 可以正常工作,以便 iptables 模式下的 kube-proxy 仅看到测试正常的Pod。 这样做意味着你避免将流量通过 kube-proxy 发送到已知已失败的 Pod。
在 ipvs
模式下,kube-proxy 监控 Kubernetes Services和Endpoints,调用 netlink
接口相应地创建 IPVS 规则, 并定期将 IPVS 规则与 Kubernetes 服务和端点同步。 该控制循环可确保IPVS 状态与所需状态匹配。访问Service时,IPVS 将流量定向到后端Pod之一。
IPVS代理模式基于类似于 iptables 模式的 netfilter hook函数, 但是使用哈希表作为基础数据结构,并且在内核空间中工作。 这意味着,与 iptables 模式下的 kube-proxy 相比,IPVS 模式下的 kube-proxy 重定向通信的延迟要短,并且在同步代理规则时具有更好的性能。 与其他代理模式相比,IPVS 模式还支持更高的网络流量吞吐量。
IPVS 提供了更多选项来平衡后端 Pod 的流量。 这些是:
rr
:轮替(Round-Robin)lc
:最少链接(Least Connection),即打开链接数量最少者优先dh
:目标地址哈希(Destination Hashing)sh
:源地址哈希(Source Hashing)sed
:最短预期延迟(Shortest Expected Delay)nq
:从不排队(Never Queue)说明:
要在 IPVS 模式下运行 kube-proxy,必须在启动 kube-proxy 之前使 IPVS 在节点上可用。
当 kube-proxy 以 IPVS 代理模式启动时,它将验证 IPVS 内核模块是否可用。 如果未检测到 IPVS 内核模块,则 kube-proxy 将退回到以 iptables 代理模式运行。
在这些代理模型中,绑定到服务 IP 的流量: 在客户端不了解 Kubernetes 或服务或 Pod 的任何信息的情况下,将 Port 代理到适当的后端。
如果要确保每次都将来自特定客户端的连接传递到同一 Pod, 则可以通过将 service.spec.sessionAffinity
设置为 “ClientIP” (默认值是 “None”),来基于客户端的 IP 地址选择会话关联。 你还可以通过适当设置 service.spec.sessionAffinityConfig.clientIP.timeoutSeconds
来设置最大会话停留时间。 (默认值为 10800 秒,即 3 小时)。
前面已经分析了k8s的service是无法实现应用层的负载均衡的。grpc基于http/2,因为http/2是长连接,负载均衡需要发生在每次调用,而非每次连接。K8s识别不了http/2的请求,就无法实现grpc的负载均衡。
既然grpc基于http/2,那么可以使用Nginx进行grpc的反向代理,因为Nginx在1.9.5开始支持Http/2协议。这个方案能完全保证流量的均匀分配。
但是架构上就比较复杂,为了防止skywalking-oap的pod重启,ip改变后nginx需要重新修改配置。那么需要为每个skywalking-oap创建一个Service。另外为了防止Nginx重启后Pod的ip改变,Nginx也需要创建一个Service。
一个简单的方法是使用K8s的Ingress代替Nginx,Ingress本身也有nginx的实现。
我们退而求其次,无法实现每次grpc调用的负载均衡,保证连接数的均衡,也算进一大步了。
前面分析K8s的Service运行原理的时候,Service有三种运行模式:
rr
:轮替(Round-Robin)lc
:最少链接(Least Connection),即打开链接数量最少者优先dh
:目标地址哈希(Destination Hashing)sh
:源地址哈希(Source Hashing)sed
:最短预期延迟(Shortest Expected Delay)nq
:从不排队(Never Queue)使用轮询和随机的方式创建连接,问题是连接断了后重连,会出现连接不均衡的问题。
那么可以使用IPVS的lc
模式,让连接优先分配给打开链接数少的server。这样能保证连接数的均衡。
原生grpc就没有服务发现这个概念,而是使用另外的LoaderBalancer组件实现负载均衡。
这种方式类似于Dubbo的方案,让客户端实现负载均衡。但是实现起来就比较复杂了,除了要多部署一个LoadBalancer,还需要在skywalking-agent中配置grpc负载均衡的策略。skywalking-oap注册到LoadBalancer中也是一个大问题。
]]>日志记录本质上是一个事件。大多数语言、应用程序框架或库都支持日志,表现形式可以是字符串这样原始的非结构化数据,也可以是JSON等半结构化数据。开发者可以通过日志来分析应用的执行状况,报错信息,分析性能…… 正因为日志极其灵活,生成非常容易,没有一个统一的结构,所以它的体量也是最大的。
对于单体应用,查看日志我们可以直接登上服务器,用head
、tail
、less
、more
等命令进行查看,也可以结合awk
、sed
、grep
等文本处理工具进行简单的分析。但是分布式应用,面对部署在数十数百台机器的应用,亟需一个日志收集、处理、存储、查询的系统。
开源社区最早流行的是Elastic体系的ELK。Logstash负责收集,ElasticSearch负责索引与存储,Kibana负责查询与展示。ElasticSearch支持全文索引可以进行全文搜索,而且支持DocValue可以用于结构化数据的聚合分析。再加上MetricBeats提供了监控指标的收集,APM提供的链路收集,Elastic俨然已是一个集Logging、Metrics、Trace的大一统技术体系。这主要是因为早期的
Elastic野心很大,但是这也导致ElasticSearch并不专注在其中的一个领域。
1、使用全文索引受限于分词器,对于日志查询非常鸡肋(两个单词能搜索到,三个单词就搜索不到的现象也不少)。
2、而且索引阶段特别耗时,很多用户都无法忍受ElasticSearch索引不过来时抛出的EsReject。
3、另外,ElasticSearch除了用于全文搜索的倒排索引,还有store
按行存储,在_source
字段中存储JSON文档,docValue
列式存储,对于不熟悉ElasticSearch的开发者来说,意味着存储体量翻了好几倍,ElasticSearch的高性能查询严重依赖于索引缓存,官方建议机器的内存得预留一半给操作系统进行文件缓存,这套吃内存的东西对普通的日志查询简直就是小题大做。
4、还有ElasticSearch在生产环境至少得部署三个节点,否则由于网络波动容易出现脑裂。
5、基于JVM的Logstash极其笨重,经常因为GC无响应导致日志延时,作为采集日志的agent有点喧宾夺主,为此Elastic专门用Go语言开发了轻量级的FileBeat日志采集工具。由FileBeat负责采集,Logstash负责解析处理。
目前K8s生态下以Fluentd和C语言编写的fluent-bit为主作为日志收集工具,Grafana开发的Loki负责存储。Loki去掉了全文索引,使用最原始的块存储,对时间和特定标签做索引,这和Metrics领域的Prometheus类似。
单体应用中应用的性能可以通过Linux自带的各种工具进行观测。
比如最常用的top命令可以看到每个进程的CPU、内存等使用情况,mpstat
、vmstat
、iostat
可以看到系统的CPU、内存、磁盘读写等情况。
对于不同语言编写的应用也有针对应用内部的监控工具,如Java体系有jmap
、jconsole
、Eclipse Memory Analyzer等工具,JDK也提供了JMX对应用指标接口进行标准化。
在分布式系统中,由于应用部署在多台机器上,应用性能的观测面临着巨大的挑战。
指标数据和日志数据的区别在于它更加结构化,而且这些metrics的结构化数据与传统的OLTP数据库中的数据,区别在于:
这类数据也被称为时序数据,时序数据库的发展就是专门用来解决这类数据的存储问题。
正因为时序数据是按照时间依次生成顺序追加的,所以除了早期的VividCortex(基于MySQL)和TimeScaleDB(基于PostgreSQL)使用就地更新的B+树来存储,其他大多数时序数据库都是用LSM-Tree(Log-Structured Merge Tree)作为底层的索引结构(在InfluxDB中被叫做Time-Structured Merge Tree)。
时序数据并不局限于系统与应用的性能指标,还包括了各种业务指标,如互联网应用的流量分析、接口性能、消息发送量……物联网传感器的功率、风速、温度等各种信号……金融领域的股票交易数据……
目前prometheus+grafana的组合几乎成了分布式系统指标观测的事实标准。
单体应用的调用只局限于内存的堆栈,可以通过stack trace进行调用链追踪,调用的性能分析可以通过这些堆栈生成相应的火炬图进行可视化。github上也有大多数语言应用生成火炬图的工具,使用火炬图能方便地分析各个函数的调用深度和调用消耗。
但是分布式应用,不再是内存内的堆栈调用了,而是穿透网络的RPC调用。用户一个请求过来,从A服务到B服务再到C服务……这个调用链可能很长。再也不能像单机版应用一样直接看程序堆栈,直接用火炬图就能分析应用的性能了。而且一个应用部署了多台机器,具体调用了集群中哪台机器也是不知道的。
这就催生了调用链收集的工具,将分布式应用的调用链整成一个跟程序堆栈类似的东西,最好还能告诉我每个服务调用过程中的耗时,这就是Distributed Tracing。
最早谷歌的Dapper论文就介绍了谷歌是怎么实现这个功能的。然后开源社区便产出了Zipkin、Jaeger等优秀工具,Spring Cloud也有一个Sleuth,这种组件很多,每种实现可能有所差别。
在谷歌论文里服务之间的调用被称为Span,整个链路被叫做Trace,但是在一些实现里服务间的调用被称为Rpc或者其他的名字,为了规范链路追踪的技术,有了OpenTracing标准和OpenCensus标准。
但是,OpenTracing和OpenCensus的出发点不一样。由社区发起的OpenTracing专注于链路追踪相关概念的统一,与具体实现无关,是一套链路追踪的规范。由谷歌主导的OpenCensus包括了Metrics和Trace两者数据收集的规范与对应的实现,后面微软的加入也更能证明这个项目的实力。
随着K8s催生的云原生的发展,OpenTracing和OpenCensus合并到了OpenTelemetry,并且将Traces, Metrics, Logs进行了统一。目前OpenTelemetry是云原生基金会的孵化项目,是K8s生态分布式观测系统的未来。
Logging、Metrics、Tracing三者架构上基本是一致统一的,但是这里面可选的组件可谓是百花齐放。
比如FileBeat和MetricBeat可以负责Logging和Metrics的日志采集,由Logstash处理后存入ElasticSearch;ElasticAPM Agent可以负责Tracing的数据采集,由APM Server处理后存入ElasticSearch;Kibana中提供了Logging、Metrics、Tracing的可视化;Elastic官方提供了kibana-alerting用于告警,开源社区也有一个ElastAlert插件可以提供告警功能。
1、Library:应用内生成数据。Logging领域的库不胜枚举,Metrics和Tracing领域也有很多。
2、 Collector Agent:负责在服务器节点上采集数据,有一些能做简单的数据处理,如从日志中拆解字段,过滤清洗日志等。
3、Transport:负责中间转储,防止日志丢失,为日志处理流程提供相对高的可靠性。
4、Storage:负责数据的存储,可以根据数据的不同schema(非结构化的大文本日志类,半结构化的JSON文档类型,结构化的)选用适合的存储。
5、Visualization & Dashboarding:负责将数据可视化展示,生成相应的仪表盘
6、Alerting:负责对异常进行告警
最早Grafana是为了弥补Kibana没有Metrics指标统计功能的一个分支,2014年首次发布,目标是为Prometheus、InfluxDB、OpenTSDB等时序数据库提供可视化界面,后面逐渐支持传统关系型数据库。
Kibana和Grafana走向了两个不同的发展道路。Kibana作为ElasticSearch的可视化工具,最早只支持日志查询,之后围绕着ElasticSearch存储功能的不断升级改进,才有了后面的Metrics和Tracing的功能。Grafana最早没有自己的存储功能,它通过接入各种数据库(也支持ElasticSearch),来实现Metrics功能。
在2019年~2021年获得三轮融资后,依次有了Loki、Tempo专门支持Logging、Tracing的开源产品。
OpenTelemetry是由两个项目合并而成:由开源社区主导的OpenTracing,由谷歌主导微软支持的OpenCensus。OpenTracing只是制定了Tracing相关的标准,OpenCensus提供了Tracing和Metrics采集与收集的相关实现。两者合并后目标是将Logging、Tracing、Metrics三者的采集与收集进行统一,并指定一个数据编码与传输规范——OpenTelemetry Protocol Specification。
截至当前(2021年11月),OTLP规范的Tracing和Metrics已经稳定,Logging目前还在Beta版本。
opentelemetry-cpp-contrib还提供了nginx的支持(C/C++语言实现的)。
日志数据的写是由Loki中的Distributor和Ingester两个组件处理,整体的流程如下图红线部分,读取过程由蓝线部分表示。
除此之外,Loki还提供了一个独立的应用loki-canary用于监控日志抓取性能。
使用grafana即可通过LogQL查询日志
参考链接:
^ 1. 分布式系统观测性的三大基石:https://www.oreilly.com/library/view/distributed-systems-observability/9781492033431/ch04.html
^ 2. 云原生生态远景图:https://landscape.cncf.io/
^ 3. 面向列的数据库:http://www.timestored.com/time-series-data/what-is-a-column-oriented-database
^ 4. OpenMetrics: https://openmetrics.io/
^ 5. OpenTracing: https://opentracing.io/
^ 6. OpenCensus: https://opencensus.io/
^ 7. OpenTelemetry: https://opentelemetry.io/
^ 8. OpenAPM: https://openapm.io/landscape
^ 9. Loki Architecture: https://grafana.com/docs/loki/latest/architecture/
]]>spring-cloud-bus是用来实现服务间异步通信的服务总线,有基于kafka和rabbitmq的两个实现。
kafka和rabbitmq的消息处理逻辑本身也被抽象成了spring-cloud-stream,所以就有了上图中的依赖结构
spring-cloud-config-monitor是一个通过spring-cloud-bus实现配置实时更新的依赖库。
monitor一般是作为config-server的一个依赖库放在config-server上,这个库里提供了一个PropertyPathEndpoint的Controller接口。流程大致如下:
1、使用者在gitlab上修改并提交配置
2、gitlab在收到新的push后,把调用预先配置的webhook接口。这个webhook接口一般就是monitor提供的http接口。通过这个接口把git的push事件发送给monitor。
3、monitor收到事件后,解析事件中修改的文件,广播一个RefreshRemoteApplicationEvent事件到eventbus。这个事件有三个字段。
1 | public abstract class RemoteApplicationEvent extends ApplicationEvent { |
4、所有依赖了eventbus的服务会收到这个Refresh事件,根据事件中的destinationService
字段判断,这个事件是否需要处理,如果匹配成功,会由RefreshListener
接受并处理这个事件。
5、RefreshListener
中会调用ContextRefresher
,拉取最新的配置,并更新Environment,重建所有加了@RefreshScope注解的Bean。
1 | spring: |
1、refresh
代表是否要接受远程EventBus发送过来的RefreshRemoteApplicationEvent
事件。事件由RefreshListener
处理。处理过程中,会从config-server拉取最新配置,并发送EnvironmentChangeEvent通知ConfigurationPropertiesRebinder刷新配置Bean
2、env
是用来刷新部分配置的,事件里面要提供刷新的name
和value
值。
3、ack
是收到EventBus远程发来的事件后,是否返回确认事件。
4、trace
是用来记录这些事件的,目前SpringCloud没有实现,只是打了debug日志。但是SpringCloud提供了HttpTraceRepository接口,后期可能会扩展。
1、如果config-server
存在多个实例,webhook没办法广播到多个config-server
实例,config-server
本地备份的git仓库如何更新。如果有实例没有更新,就会导致第5步,拉取不到最新的配置。
2、gitlab中通常只配置一个webhook,测试、预发、生产环境的config-server
怎么能都收到这个配置进行仓库的更新。
1、首先mq必须全局共享,开发、测试、预发、生产都用同一套mq
2、gitlab中配置的webhook是生产环境的config-server,收到push事件后,根据修改的文件提取出来环境和应用,通知对应环境的config-server
更新git仓库
3、对应环境的config-server
更新git仓库完成后,需要返回响应,响应中需要带上该环境下其他的config-server
实例的信息(这个信息可以从eureka中获取,eureka是环境隔离的)。
4、需要等其他的config-server
都更新完成后,再推送Refresh事件到其他需要更新配置的服务,这个时候能保证所有的服务拉到的配置都是最新的
1、gitlab中配置多个环境的webhook,这要求各环境的config-server
域名区分开
2、webhook调用接口后,config-server
检查更新的文件是否涉及当前环境,如果没有不需要做任何处理,相反如果涉及当前环境执行第3步
3、发送事件给当前环境的所有config-server
实例,更新完成后返回确认
4、收到所有的确认后再由config-server
发送Refresh事件到需要更新配置的服务
SpringCloudBus的问题:
1、通过rabbitmq广播到每个环境,最后等所有环境确认,这段延时比较严重
2、通过rabbitmq推送刷新事件到应用,这段延时也比较严重
3、SpringCloudContext刷新时会重建Context,不可用的情况不能忍
抛弃SpringCloudBus,使用与Nacos、Consul一样的长轮询
1、首先mq必须全局共享,开发、测试、预发、生产都用同一套mq
2、gitlab中配置的webhook是生产环境的config-server,收到push事件后,根据修改的文件提取出来环境和应用,通知对应环境的config-server
更新git仓库
3、config-server
一旦收到更新仓库的通知后,更新完仓库,即刻通知自身hold住的长连接返回结果
压测节点拓扑图:
jmeter是三节点:每个节点都是默认的1500cpu,1.5G内存
redis-provider是一个转发redis请求的代理服务,配置为:3000cpu,1.5G内存
redis是RedisSentinel部署一主两从,版本为redis-5.0.7,只有主节点接受写。
jmeter的监控线程从200逐渐增大到500,但是吞吐量并没有明显提升,响应时间也基本没有变化。从jmeter两个slave节点的监控可以看到,cpu和内存仍有很大空闲,吞吐量没有提升,说明下游有瓶颈。
redis服务的cpu被提升到了3000,可以看到实际达到1500后就没有再增长了。jmeter的流量到redis-provider后cpu就压不上来了,说明cpu不是瓶颈,那就是I/O有瓶颈了,但是redis-provider服务的带宽远没有到顶。那说明下游的redis数据库有瓶颈。
可以看到redis的CPU每次压测调用虽然使用率上升了,但是仍有80%以上的CPU资源空闲着,说明cpu也不是redis的瓶颈,IO带宽也远没有打满。但是从系统监控中看到,redis的Context Switches有飙升,和压测节点的波形基本吻合,还有一点比较特别的是三台redis中,有一台redis每秒执行的命令数是另外两台的四倍。
原本以为context switches是线程切换导致的,但是redis使用的5.x版本仍然是单线程,6.x开始才支持multi-thread。在redis官网查了一下,context-switch是SocketIO每次调用read/write系统调用时内核态与用户态之间切换导致的。redis推荐用pipeline来减少SocketIO导致的IO性能影响。
RedisSentinel是主从异步同步的,只有主节点能接受写操作,所有节点都能接受读操作,所以集群中主节点会接受更多的请求。
redis-provider应用CPU在2000即可达到1.4K的吞吐量,CPU不是应用的瓶颈。redis数据库的瓶颈在于SocketIO的内核态与用户态的切换,以及RedisSentinel单个主节点接受写操作导致写入速度有瓶颈。
]]>正常模式。也就是正常启动Android操作系统的样子。启动操作系统后,可以在开发者选项中打开USB调试。打开后可以用adb
命令对连线的手机执行一些操作:
1 | # 查看当前连接的android设备 |
更多adb命令可以参考这个sheet
fastboot类似于pc的bios,也被人叫做“刷机模式”,可以看到一些设备信息。
1 | # 使用adb重启bootloader进入fastboot模式 |
大部分手机关机后,同时按住音量“+”或“-”,再长按开机键也可以进入fastboot模式
可以使用fastboot
命令,查看fastboot模式下的设备
1 | fastboot devices # 查看fastboot连接设备 |
recovery模式类似于Windows PC的U盘启动器或者MacOS的还原模式。Android的recovery模式和MacOS的还原模式类似,将存储盘的一部分划成recovery分区当作U盘启动器用,里面有一个微型还原系统,可以对手机进行设备还原,重新写入操作系统。
1 | # 使用adb命令以recovery模式重启手机 |
除了
/recovery
分区,android还有/boot
、/system
、/data
、/cache
、/misc
等分区,可以参考有没有哪位能详细讲解一下安卓手机的分区? - 知乎 (zhihu.com)和Android系统分区与升级 - 知乎 (zhihu.com)看看各分区的作用,关于Android A/B system分区可以参考A/B(无缝)系统更新。使用下面命令可以看到安卓预设的各种分区。
1 adb shell ls -al /dev/block/platform/soc/1da4000.ufshc/by-name
PC想要重装操作系统,还是挺简单的。ghost备份的方式安装或者U盘装机。但是安卓刷机就有点麻烦了。首先一个原因就是几乎所有的安卓机都有OEM锁。
OEM全称Original Equipment Manufacturer,也就是原始设备制造商为了防止用户刷成其他系统设置的锁。bootloader是fastboot模式下的加载程序,OEM锁就类似于PC上BIOS的密码。
PC操作系统基本被Windows占领了,Windows是闭源的,微软一家发售,所以在PC上顶多就只能对BIOS设置个密码。但Android是谷歌开源的操作系统,华为、小米、VIVO等手机制造商都可以对Android进行定制化。这些定制化系统有很多预装软件,预装软件是很有商业价值的,比如说小米视频等各大预装软件广告如云,小米金融、小米之家等软件带来用户转化。所以手机制造商给设备上锁不让用户装其他系统,就是为了避免给别人徒作嫁衣。如果用户硬要解锁呢,那手机厂商不给保修。
每家手机制造商的解锁方式都不一样,我这里找了华为、小米、一加的解锁方案:
这里以一加为例:
1、在系统设置的开发者选项里找到“OEM解锁”,把它置成允许。
2、正常模式连接adb
后可以执行adb reboot bootloader
进入fastboot模式(或者关机后同时按下音量+/-
后,再长按开机键开机,即可进入fastboot模式)。
3、fastboot模式下执行fastboot oem device-info
可以看到boot-loader目前解锁状态,执行fastboot oem unlock
后出现一个界面让你选择,选择“unlock the bootloader”即可解锁。
1 | $ fastboot oem unlock |
解锁bootloader后,我们可以继续使用原来的操作系统,也可以自由刷机。刷机有几个部分可以刷,一个是用来恢复的/recovery
分区,还有一个是正常使用的android操作系统也就是/system
分区,还有一个就是/boot
内核启动分区。
首先来说刷Recovery系统。
第三方的Recovery系统有很多,其中最有名的就是TWRP(Team Win Recovery Project)项目。
他们的项目在github上也开源了:https://github.com/TeamWin
TWRP支持的设备很多:https://twrp.me/Devices/,大部分机型都可以在这里面找到。
以oneplus5为例:
1 | # 将下载的twrp镜像刷入recovery分区 |
请注意,许多设备会在启动期间自动替换自定义recovery。所以需要在刷完分区后第一次进入,按住键组合并启动到 TWRP。启动 TWRP 后,TWRP 将修补库存 ROM,以防止原有ROM 替换 TWRP。如果不遵循此步骤,将不得不重复安装。
目前都是使用magisk来进行root权限管理,早期的supersu和kingroot已经被淘汰了,
1、从官方ROM包中,提取出boot.img
,这个文件就是linux内核boot分区的镜像。
以oneplus5T为例,OnePlus5T官方论坛提供了ROM包下载,解压后找到boot.img
1 | total 6921336 |
2、将boot.img
拷贝到手机上,用magisk.apk
将boot.img重写一遍,magisk会插入一段代码到boot.img
中,生成新的magisk_patched-23000_msSoR.img
(文件名后面是一个随机字符串)
3、将magisk生成的img文件拷贝到电脑上
1 | adb reboot-bootloader # 进入fastboot模式 |
4、重启手机后,进入magisk应用,会发现已经root成功了。
5、好用的Magisk模块
https://github.com/Magisk-Modules-Repo
Refs:
Installation | Magisk (topjohnwu.github.io)
目前ROOT成功率最高的软件是什么? - 知乎 (zhihu.com)
刷了recovery
分区和boot
分区,我们还可以刷system
分区自定义的ROM,第三方的ROM多如牛毛(毕竟大多数就只是做做界面):
Android Open Source Project: https://github.com/aosp-mirror
直接基于AOSP的扩展:https://github.com/AospExtended
谷歌自己的Pixel,自然就带了很多谷歌的产品:https://github.com/PixelExperience
基于Pixel魔改的:https://github.com/PixelExtended
小米的MIUI,带了很多小米的产品:https://github.com/MiCode
国内的一个开源ROM:https://github.com/MoKee
最小化的miniOS:https://github.com/StatiXOS
还有lineageos、GrapheneOS、RevengeOS、arrowos、aokp、Corvus-ROM、BlissRoms、
ProjectSakura)、Project-Xtended…好多好多项目。
ROM开发
在Android上玩终端,甚至将Android改装成Linux服务器,这些都可以用下面的软件实现。
Termux:提供了android端的终端命令以及pkg
包管理器,也支持chroot方式挂在Linux发行版到指定目录,具体可以参考PRoot - Termux Wiki。
meefik/linuxdeploy:使用chroot将Linux发行版挂载到android的某个目录,也支持docker容器的方式
Refs:
^ https://android.gadgethacks.com/how-to/best-phones-for-rooting-modding-2020-0175988/
^ https://nexus5.gadgethacks.com/how-to/elementalx-only-custom-kernel-you-need-your-nexus-5-0157196/
^ https://oneplus.gadgethacks.com/how-to/unroot-revert-your-oneplus-5-5t-100-stock-0182460/
^https://forums.oneplus.com/threads/oneplus-5t-unlock-bootloader-flash-twrp-root-nandroid-efs.685403/
]]>《解读中国经济》是最近在读的非常有感触的一本经济类书籍,把中国近现代经济发展的逻辑理的很清楚。这里记录一下部分章节的读后感。
科学是第一生产力,经济增长的一般路径是科学革命引领技术进步,然后技术进步带动资本积累、产业升级和制度变迁。
中国古代强盛和近现代衰败的原因:
1、人口多,无规律的试错次数比较多,可以解释古代中国的发达。
2、科举制度,中国的优秀人才都在念八股文,被官场吸收,好脑子没有用在发明创造而是用在官场的勾心斗角。
西方强盛的原因:
1、数学模型和实验室内的可控实验,远比无规律试错高效。希腊科学体系和东方的工匠知识体系的最大区别在于,前者有一个完整的体系,任何发明和发现都是可以叠加的,比如你给几何学贡献了一个新的定理,几何学就扩大一圈。而后者是不成体系的,是零碎的知识点(甚至算不上知识点,只是经验点),每一项新的改进都是孤立的,因此很多后来就失传了,以后的人又要重新开始。今天三甲医院的西医肯定比50年前的任何一个牛逼的西医更牛逼,但是今天的中医没有谁敢说自己比500年前的知名中医水平高,原因就在于前者有累积效应而后者没有。
2、近代西方文艺复兴后,思想得到全面解放,从而使西方在人文与自然科学等多个领域得到空前发展。
一言以蔽之,经济增长始于科学革命和技术进步,在前现代社会中国有人口优势,技术进步快,因此社会文明鼎盛;而在现代,技术进步依靠科学,中国只有科举八股文没有自然科学,而西方自然科学鼎盛,自然科学的发展带来了技术进步速度的空前加快(工业革命的本质就是技术变迁速度的加快,而不是某一项具体技术的应用),在这种情况下人口根本不再是优势,自然科学才是优势,这也就导致了现代社会中国的远远落后于西方。
中国与苏联的国情不一样。
二月革命虽然推翻了沙皇统治,临时政府上台,但是士兵和工人的处境并没有改善,所以发起工人暴动临时政府镇压时,更加激化了工人与政府之间的矛盾,最终导致十月革命的成功。
而中国当时是半殖民地,工人大多在租界,工人暴动遭到的是列强的镇压,并不会激化工人与国民政府的矛盾。中国当时大多数人是农民,团结大多数,打土豪分田地,发动农民运动,当国民政府镇压时,可以将民众的矛盾指向国民政府。
新中国刚成立时为了有利于恢复生产稳定发展,颁布《土地改革法》孤立地主、保护中农、稳定民族资产阶级,但是10月中国参与抗美援朝,一些腐败的官僚与民族资本勾结发战争财,所以毛泽东在次年发动了”三反无反“打击官僚的贪污腐败和民族资本的行贿偷税。也是经历了朝鲜战争,这个时候毛泽东政府认识到中国要真正站起来,需要自己的重工业打造自己的武器,同时不能依靠民族资本来发展重工业。
重工业是资本高度集中的产业,主要有以下三个特征:
1、建设周期长
2、关键技术、设备需要国外进口,自己无法生产
3、项目一次性投入大,动辄上百亿上千亿
而当时中国是发展中国家,以农业为主,这也意味着:
1、资金积累少,资金的价格就非常高
2、可出口的产品少,外汇少,外汇的价格高
3、农业生产分散,要收税动员资金非常困难
所以从53年第一个五年计划开始宏观上扭曲价格信号、在行政上计划配置资源、在微观上剥夺企业自主权,其目的是把劳动剩余都集中起来,全部投入重工业部门。具体做法有:
1、为了保证剩余全部掌握在国家手里,民族资本企业以公私合营的方式全部收为国有
2、为了让建设周期长的项目能被建立起来,只好压低利率
3、为了让这些项目能以低廉的价格进口设备,只好扭曲汇率,人为地抬高本国汇率
4、为了让建成的企业有较高的利润,压低原材料价格、压低工人工资、压低所有生活必需品等各种投入的价格
5、为了压低生活必需品的价格,所有的农产品统购统销,所有生活必需品按国家统一计划(粮票、棉票、布票…)购买
6、为了增加农产品的产量,让农村给城市输血,推行规模经济,搞农业合作化
7、由于余粮省不愿多生产粮食给缺粮省发展工业,导致地区性粮食需要自给自足
8、重工业就业机会不多,为了不让农民进城分享城市里的各种补贴,让农村持续为城市输血,于是推行了城乡隔绝的户籍制度。
这样的战略唯一的优点是能快速建立起一个初步完备的工业体系,原子弹能爆炸,卫星能上天,但缺点就是个体没有积极性,人民生活水平长期得不到提高,搞不好还会有农业危机(59年到61年的三年大饥荒)。而且低价收购农产品,高价出售工业品的“工农业剪刀差”其实是来回地剥削农民贡献的价值。
作者将这个归因于发展中国家在现代化发展过程中的“后发优势”:依靠从发达国家引进先进技术和经验,发展中国家可以在较短时间内用较低成本实现自身的技术创新,从而带来效率的提高,增加资本回报率,促进产业升级和经济增长。日本、亚洲四小龙和改革开放之后的中国经济的崛起,都是这样。
不过现在中国经济进入了减速增质的阶段,因为中国的技术水平相对于发达国家,不像以前一样那么落后了,在很多特定方面还是世界领先位置,没有了那么多可引进的技术,没有了那么多的后发优势,就得靠自己高成本试错自主研发了。
]]>