聊聊爬虫背后的技术

技术发展真的是日新月异。当我的记忆还停留在PhantomJS时,却发现PhantomJS早已经停止维护了。(因为chrome等浏览器都推出headless模式,2018-05时作者就已经发issue说将要archive这个项目,2023-05-30 archive了此项目。此项目的最后一个版本是2016-01-25发布的2.1.1版本)

目前实现headless浏览的有两种方式:WebDriver 和 DevTools Protocol。

以chrome为例,可以查阅如下资料:

 

那么,DevTools Protocol 和 WebDriver之间的区别是什么呢?

可以参考:https://stackoverflow.com/questions/50939116/what-is-the-difference-between-webdriver-and-devtool-protocol

Main difference between WebDriver protocol and DevTools protocol is that WebDriver protocol needs a middle man like browser-driver (eg: chrome-driver) which is a server that sits between the automation script and browser enabling browser control, but in case of DevTools protocol the automation script can directly talk to browser running in debug mode making headless automation pretty straight forward.

And Chrome driver internally uses DevTools protocol to control browser, so if we are using WebDriver protocol it will in turn use Devtools protocol to control browser.

If cross-browser testing is something important for the new testing tool, DevTools protocol may not be suitable now, as there is no standard yet and it is mostly work in progress. Otherwise DevTools protocol will be a great choice as it gives more control like intercepting request header, simulating network etc and makes headless automation way easier.

其他:

  • 涉及无头浏览器操作的具体编码上,建议使用Selenium或者Playwright等工具,它们都提供了多种语言的绑定,支持所有主流的浏览器。
  • 至于爬虫框架的话,可以使用scrapy。

JavaScript(JS)、JSON和int64

大家在前后端联合开发时经常会遇到一个问题,js不能处理服务端返回的JSON中的大整数。处理方法往往是将这个大整数字段设置为string。

为什么js不能处理大整数了?
因为之前js只有number类型来表示数字(整数+浮点数),其遵循IEEE 754规范,是一个双精度 64 位二进制格式值,其整数的最大支持2的53次方-1(2^53-1):9007199254740992。内置常量Number.MAX_SAFE_INTEGER值为9007199254740991(2^53-1),因为当赋值给Number类型变量大于此值时(比如2^53,2^53+1,……),都会被存储为2^53。顺带提一下,还有一个常量:Number.MAX_VALUE。

后来新增了BitInt这种内置类型,它提供了一种能力来表示大于 2^53 – 1 的整数。而且BigInt 可以表示任意大的整数。
可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数 BigInt()(但不包含 new 运算符)并传递一个整数值或字符串值。

const theBiggestInt = 9007199254740991n;
const alsoHuge = BigInt(9007199254740991);
// 9007199254740991n
const hugeString = BigInt("9007199254740991");
// 9007199254740991n
const hugeHex = BigInt("0x1fffffffffffff");
// 9007199254740991n
const hugeBin = BigInt("0b11111111111111111111111111111111111111111111111111111");
// 9007199254740991n

 

那么又有新问题了,既然js支持大整数,那么为什么服务端不能返回大整数了?
因为我们服务端返回数据设定Content-Type: application/json时,我们告诉客户端这是一段json,而根据json规范的定义,其只支持js的number类型,而不支持BitInt类型,所以当服务端返回的json包含了一个大于Number.MAX_SAFE_INTEGER的整数时(其实这个时候严格来说服务端没有遵循json规范),客户端不能识别是正常的现象(按照上文提到的,会被转换成2^53),是符合json的规范的行为。那所以web客户端这边没有把服务端返回的大整数转换成BitInt类型是源于遵循JSON规范。既然web客户端不支持大于2^53-1的整数,那么服务端就应该避免返回这些数据,所以如果某个字段是int64类型,那么为了安全,建议将int64转换成string类型返回,服务端接收web客户端参数时也一样。

当然我们期待json规范也能更新(到时出现一个Content-Type: application/json2,哈哈),能够原生支持BitInt类型,然后内置的JSON对象的parse()和stringify()两个方法也升级支持BitInt类型。这样我们开发者就可以安全方便的在前后端之间传输int64类型数据了。

References:

https://zh.wikipedia.org/wiki/雙精度浮點數

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/BigInt

Go中struct的内存对齐

直接看代码

var i int32
var ii int64
var b bool
var s string
fmt.Printf("width:%d\n", unsafe.Sizeof(i))  // width:4
fmt.Printf("width:%d\n", unsafe.Sizeof(ii)) // width:8
fmt.Printf("width:%d\n", unsafe.Sizeof(b))  // width:1
fmt.Printf("width:%d\n", unsafe.Sizeof(s))  // width:16

{
   //空结构体的宽度为 0
   type S struct{}
   var ss S
   fmt.Printf("width:%d\n", unsafe.Sizeof(ss)) // width:0
}

{
   type stringStruct struct {
      str unsafe.Pointer // 8
      len int            // 8
   }
   var ss stringStruct
   fmt.Printf("width:%d\n", unsafe.Sizeof(ss)) // width:16
}

//以下涉及内存对齐
// https://dave.cheney.net/2014/03/25/the-empty-struct#comment-2815
{
   type S struct {
      a uint64 // 8
      b uint32 // 4
   }
   var s S
   fmt.Printf("width:%d\n", unsafe.Sizeof(s)) // width:16, not 12
}

{
   type S struct {
      a uint64    // 8
      b uint32    // 4
      c [3]uint32 // 12
   }
   var s S
   fmt.Printf("width:%d\n", unsafe.Sizeof(s)) // width:24
}

{
   type S struct {
      a uint64 // 8
      b bool   //1
   }
   var s S
   fmt.Printf("width:%d\n", unsafe.Sizeof(s)) // width:16, not 9
}
{

   type S struct {
      a uint64    // 8
      b uint32    // 4
      c bool      //1
      d [3]uint32 // 12
   }
   var s S
   fmt.Printf("width:%d\n", unsafe.Sizeof(s)) // width:32, not 25(8+4+1+12), not 28(8+4+4+12)
}

{
   type S struct {
      a uint64 // 8
      b uint32 // 4
      c bool   //1
      d uint32 // 4
   }
   var s S
   fmt.Printf("width:%d\n", unsafe.Sizeof(s)) // width:24
}

 

关键要读懂Russ Cox的这段话:

It’s not true that “a value must be aligned in memory to a multiple of its width.” Each type has another property, its alignment. Alignments are always powers of two. The alignment of a basic type is usually equal to its width, but the alignment of a struct is the maximum alignment of any field, and the alignment of an array is the alignment of the array element. The maximum alignment of any value is therefore the maximum alignment of any basic type. Even on 32-bit systems this is often 8 bytes, because atomic operations on 64-bit values typically require 64-bit alignment.

To be concrete, a struct containing 3 int32 fields has alignment 4 but width 12.

It is true that a value’s width is always a multiple of its alignment. One implication is that there is no padding between array elements.

 

References:

https://dave.cheney.net/2014/03/25/the-empty-struct#comment-2815

https://ijayer.github.io/post/tech/code/golang/20200419_emtpy_struct_in_go

Linux FHS

我们常用Linux发行版本,各种类型文件通常放到不同的目录里,比如二进制文件放置到/bin、/sbin、/usr/bin、/usr/sbin,系统和应用配置文件放置到/etc,/home是非root用户的主目录(/home/{username}),/root是root用户的主目录……

那么为什么要这样放置文件?

这个其实就是Linux基金会制定的FHS标准。

文件系统层次结构标准(Filesystem Hierarchy Standard,FHS)定义了Linux操作系统中的主要目录及目录内容。FHS由Linux基金会维护。 当前版本为3.0版,于2015-06-03发布。具体的FHS规范内容可以看FHS官网。

注意:

  1. 虽然主流的发行版本基本实现了FHS标准,但是在实现细节上与标准还是有一些不一致的地方。还有一些小众的发行版本压根就没有遵循FHS标准。
  2. 这些年新起的Snap、Flatpak、AppImage等应用程序包管理方式,不再像以前的apt、rpm、dnf等遵循FHS规范,而是将应用程序的二进制文件、配置文件、依赖文件等都打包到一个目录里。

 

References:

  1. FHS Wiki https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard
  2. FHS 3.0 https://refspecs.linuxfoundation.org/FHS_3.0/fhs-3.0.html

 

闰秒

说道闰秒,可能很多人没有听说过,而闰年和闰月应该被很多人熟知。

闰年是指该年有366日,即较平常年份多出一日。闰年是为了弥补因人为历法规定的年度天数365日和平均回归年的大约365.24219日的差距而设立的。不同历法有不同置闰方法。儒略历每4年置1闰日,平均1年是365.25日。格里高利历每400年少3次闰日,平均是365.2425日。多出来的一天为2月29日。(参考:https://zh.m.wikipedia.org/zh-sg/%E9%97%B0%E5%B9%B4

我们生活里说的闰月通常是指农历(阴阳历)的闰月。闰月没有固定是一年的中的哪一个月,是动态的,总的规则是19年加7个闰月,即“19年7闰法”。(参考:http://www.gov.cn/govweb/fwxx/kp/2009-06/16/content_1341232.htm

那什么是闰秒呢?

为确定时间,世界上有两种常用的时间计量系统:基于地球自转的世界时(UT)(参考:https://zh.wikipedia.org/zh-sg/%E4%B8%96%E7%95%8C%E6%97%B6
)和基于原子振荡周期的国际原子时(TAI)(参考:https://zh.wikipedia.org/zh-sg/%E5%9C%8B%E9%9A%9B%E5%8E%9F%E5%AD%90%E6%99%82)。由于两种测量方法不同,随着时间推移,两个计时系统结果会出现差异,因此有了协调世界时的概念。
协调世界时以国际原子时秒长为基础,在时刻上尽量接近世界时。1972年的国际计量大会决定,当国际原子时与世界时的时刻相差达到0.9秒时,协调世界时就增加或减少1秒,以尽量接近世界时,这个修正被称作闰秒。
闰秒实际上是为适应地球自转的脚步而对国际原子时的人为增减。依据国际地球自转服务组织对国际原子时与世界时的监测数据,当两者之差达到0.9秒时,该机构就向全世界发布公告,在下一个6月或12月最后一天的最后一分钟,实施正闰秒或负闰秒。
自1972年协调世界时正式使用至今,全球已经实施了27次正闰秒调整,最近一次的闰秒调整是格林尼治时间2016年12月31日。从协调世界时正式使用以来,地球自转一直处于不断减慢的趋势,因此迄今为止的闰秒都是正闰秒,但相关科研发现,自2020年年中以来,地球自转速率呈现加快趋势,这意味着未来也可能会出现负闰秒。(参考:闰秒是什么,将何去何从  http://www.news.cn/tech/2022-11/19/c_1129142173.htm)

历史上已实行的闰秒日期
630 1231
1972 +1 +1
1973 0 +1
1974 0 +1
1975 0 +1
1976 0 +1
1977 0 +1
1978 0 +1
1979 0 +1
1980年 0 0
1981 +1 0
1982 +1 0
1983 +1 0
1984年 0 0
1985 +1 0
1986年 0 0
1987 0 +1
1988年 0 0
1989 0 +1
1990 0 +1
1991年 0 0
1992 +1 0
1993 +1 0
1994 +1 0
1995 0 +1
1996年 0 0
1997 +1 0
1998 0 +1
1999年 0 0
2000年 0 0
2001年 0 0
2002年 0 0
2003年 0 0
2004年 0 0
2005 0 +1
2006年 0 0
2007年 0 0
2008 0 +1
2009年 0 0
2010年 0 0
2011年 0 0
2012 +1 0
2013年 0 0
2014年 0 0
2015 +1 0
2016 0 +1
2017年 0 0
2018年 0 0
2019年 0 0
2020年 0 0
2021年 0 0
2022年 0
总计 11 16
27
目前TAIUTC秒差
37

之前我有写过关于monotonic clock的内容。当时就提到闰秒,正的闰秒会导致wall clock比较时出现负值,所以我们需要使用monotonic clock来克服这一个问题。那为什么呢?我们来看看目前如何处理正值的闰秒的,1998-12-31日的闰秒为例:

TAI (1 January 1999) UTC (31 December 1998 to 1 January 1999) Unix time
1999-01-01T00:00:30.00 1998-12-31T23:59:59.00 915148799.00
1999-01-01T00:00:30.50 1998-12-31T23:59:59.50 915148799.50
1999-01-01T00:00:31.00 1998-12-31T23:59:60.00 915148800.00
1999-01-01T00:00:31.50 1998-12-31T23:59:60.50 915148800.50
1999-01-01T00:00:31.75 1998-12-31T23:59:60.75 915148800.75
1999-01-01T00:00:32.00 1999-01-01T00:00:00.00 915148800.00
1999-01-01T00:00:32.25 1999-01-01T00:00:00.25 915148800.25
1999-01-01T00:00:32.50 1999-01-01T00:00:00.50 915148800.50
1999-01-01T00:00:33.00 1999-01-01T00:00:01.00 915148801.00

可以看到,发生闰秒的1998-12-31 23:59:59之后不是1999年1月1日00:00:00,而是中间多了一个1998-12-31 23:60:60,这两秒的Unix timestamp是一样的。所以如果在使用毫秒级、微秒级、纳秒级的时间戳时,在这两秒里就可能出现负的时间戳差值,比如:23:59:59.600 ~ 23:60:60.000,差值就是一个负值。

写在最后,使用闰秒这种处理方法已被证明具有破坏性,特别是在二十一世纪,尤其是在依赖精确时间戳或时间关键程序控制的服务中。相关国际标准机构一直在讨论是否继续这种做法。(参考:https://zh.wikipedia.org/zh-sg/%E9%97%B0%E7%A7%92

MySQL的Character Set和collate

常见的charset:

一般常用的charset是utf8、utf8mb4。

如下是MySQL 8的文档描述:

  • utf8mb4: A UTF-8 encoding of the Unicode character set using one to four bytes per character.
  • utf8mb3: A UTF-8 encoding of the Unicode character set using one to three bytes per character. This character set is deprecated in MySQL 8.0, and you should use utfmb4
  • utf8: An alias for utf8mb3. In MySQL 8.0, this alias is deprecated; use utf8mb4utf8 is expected in a future release to become an alias for utf8mb4.

总结来说,utf8mb4字符集支持最多使用4个字节来存储一个字符,这样就支持很多额外的特殊字符,比如emoji符号。可以说utf8mb3是个历史产物,当年MySQL支持UTF-8时,UTF标准里一个字符最多只占用3个字节。而后UFT-8标准更新后最多使用4个字节后,MySQL在5.5版本开始新增utf8mb4以支持4个字节存储一个字符。而到目前为止(2023-01)MySQL的utf8还是utf8mb3的别名,当然将来可能uft8会被指向utf8mb4。

 

以上都是老生常谈的知识了,大家无脑的使用utf8mb4就可以了。今天要说的其实是collate,collate主要用于排序和字符比较。因为最近遇到一个问题,就是sql查询时,使用 name like ‘%keywords%’时,MySQL是区分大小写匹配的,一看表的collate是utf8mb4_bin,破案了。那么我们来看看MySQL有哪些常用的collate。

 

utf8mb4_bin: 将字符串每个字符用二进制数据编译存储,区分大小写,而且可以存二进制的内容。

utf8mb4_general_ci:ci即case insensitive,不区分大小写。是一个遗留的校对规则,不支持扩展,它仅能够在字符之间进行逐个比较,没有实现Unicode排序规则,在遇到某些特殊语言或者字符集,排序结果可能不一致。但是,在绝大多数情况下,这些特殊字符的顺序并不需要那么精确。

utf8mb4_unicode_ci:是基于标准的Unicode来排序和比较,能够在各种语言之间精确排序,Unicode排序规则为了能够处理特殊字符的情况,实现了略微复杂的排序算法。

utf8mb4_general_cs:基本同utf8mb4_general_ci,区别是区分大小写。

utf8mb4_unicode_cs:基本同utf8mb4_unicode_ci,区别是区分大小写。

大家可以看到规律了吧:

*_bin: 表示的是binary case sensitive,也就是说是区分大小写的

*_cs: case sensitive,区分大小写

*_ci: case insensitive,不区分大小写

 

我本地MySQL8 配置文件中字符集相关变量

show variables like '%character%';

Variable_name    Value

character_set_client  utf8mb4

character_set_connection       utf8mb4

character_set_database   utf8mb4

character_set_filesystem  binary

character_set_results utf8mb4

character_set_server utf8mb4

character_set_system       utf8mb3

……


show CHARSET like 'utf%'; -- 查看MySQL的default COLLATE


Charset Description  Default collation Maxlen

utf16     UTF-16 Unicode utf16_general_ci 4

utf16le  UTF-16LE Unicode    utf16le_general_ci     4

utf32     UTF-32 Unicode utf32_general_ci 4

utf8mb3       UTF-8 Unicode   utf8_general_ci   3

utf8mb4       UTF-8 Unicode   utf8mb4_0900_ai_ci  4

这里我们会发现一个特殊的collate:utf8mb4_0900_ai_ci ,那这个0900表示什么了?原来utf8mb4_0900_ai_ci 是MySQL8新增的collate,中间的0900,表示的是Unicode 9.0的规范。对应的之前就有的utf8mb4_unicode_520_ci、utf8mb4_unicode_ci等。

 

References:

  1. https://dev.mysql.com/doc/refman/8.0/en/charset-unicode-sets.html
  2. https://dev.mysql.com/doc/refman/8.0/en/charset-unicode-utf8mb4.html
  3. https://zh.wikipedia.org/zh-hans/UTF-8
  4. https://dev.mysql.com/doc/refman/8.0/en/charset-database.html
  5. https://dev.mysql.com/doc/refman/8.0/en/charset-collation-names.html
  6. https://www.lifesailor.me/archives/2676.html

 

防空警报和防灾警报

防空警报

每年9月的第三个星期六是(中华人民共和国)全民国防教育日,这一天部分城市会试鸣防空警报。今年(2022年)是9月17日。

防空警报通常分为:“预先警报、空袭警报、解除警报”三种形式。预先警报是在敌方对我方空袭有预兆时鸣响;空袭警报是在敌方对我方空袭即将或者已经开始时鸣响;解除警报是在敌方空袭结束时鸣响。

预先警报:鸣36秒,停24秒,反复三遍为一个周期。

空袭警报:急促短音鸣6秒,停6秒,反复十五遍为一个周期。

解除警报:连续鸣三分钟为一个周期。

国内不同城市和地区试鸣防空警报的日期并不一样,具体可以参考维基百科或者当地政府网站。

 

防灾警报

不同城市和地区的防灾警报形式并不相同,比如参考3和4对应(山东省)日照市的和浙江省的防灾警报形式就不一样。所以具体到各省市请查询当地政府网站。

 

References:

  1. https://zh.wikipedia.org/zh-cn/防空警报
  2. https://video.sina.cn/news/2022-09-17/detail-imqmmtha7647263.d.html
  3. http://www.rizhao.gov.cn/art/2022/9/14/art_207870_10435896.html
  4. https://www.zj.gov.cn/art/2022/5/12/art_1554467_59699356.html

在Mac中的根目录下创建文件(比如创建/data目录)

从El Capitan (OS X 10.11)引入System Integrity Protection (SIP)开始,mac就已经开始逐步加强对系统文件的写限制,到Catalina(macOS 10.15)时完全限制了在根目录下进行写操作。从Catalina开始,官方提供了synthetic.conf文件以支持在根目录下创建软链。

可以通过man synthetic.conf查看文档。

操作步骤:

  1. sudo vi /etc/synthetic.conf

进行文件映射 or 软连接,如:

data Users/username/log
data1 Users/username/log1

注意:
1)  每行的两项配置不是以/开头。(可以理解系统会帮我们加入前缀/)
2)  data 与 Users/username/log 之间是使用tab进行分隔,否则重启后无效。如果指定目录不存在记得mkdir目录。

  1. 重启Mac,然后ls -l 就会发现/data, /data1就会存在了。

如果发现/data目录没有被创建,那么检查下你的/etc/synthetic.conf文件里的Tab分隔符是否被正确配置了。有的机器的vim配置了set expandtab,导致Tab被自动转换成了多个空格。这个时候可以在编辑模式下,先按ctrl+v再按tab键,就可以输入Tab了。可以用xxd查看,Tab是ASCII码是09,而空格的是20。

xxd  /etc/synthetic.conf

空格的情况(错误的情况)
00000000: 6461 7461 2020 2020 5573 6572 732f …  data    Users/…

Tab的情况(正确的情况)
00000000: 6461 7461 0955 7365 7273 2f…  data.Users/…

 

References:

https://javabase.cn/p/114

https://developer.apple.com/forums/thread/670391