最近做了个抓取 VPS Plan 的爬虫
1 core
, 500 GB Storage
, 256 MB RAM
,并把这些值提取出来后来改变思路,决定采用人工识别,但是通过配置表的方式来提取数据,这就是下面的内容。
小广告:
GitHub 地址 vps_spider
项目展示官网 Find All VPS
由于不同厂商使用不同的网站,所以他们的 Plan 表也是不一样的,为了能设计出一套通用的解析器,我们先研究下它们的特点:
Linode: 像这样的情况属于多数,一个节点包含了一个完整的 Plan
DomainVPS: 这种就比较反直觉了,一个好好的 Plan 被分割到不同的节点里,简直是 VPS 中的杀马特
为了避免给每一个网站写一个 spider ,有必要设计一套配置表,通过配置表来告诉解析器不同的网站应该怎么解析
{
'https://www.linode.com/pricing': PlanSet([
(('#pricing-larger-plans > div > div > div > div > table > tbody > tr', 'td'), {
0: values.Name,
1: values.RAM,
2: values.CPU,
3: values.Disk,
4: values.Traffic,
5: values.NetworkIn,
6: values.NetworkOut,
7: values.Price,
}),
]),
}
如代码如示,这个是 Linode 的配置表,这个配置表结构简单,却具有相当的可扩展性和通用性。
https://www.linode.com/pricing
: 此项指示 Plan 所在页面PlanSet
: 此类封装了对配置表的解析逻辑,下面是具体的参数#pricing-larger-plans > div > div > div > div > table > tbody > tr
: 此 CSS Selector 指示了 Plan 所在节点,若使用 $() 去 select 就会发现,它会返回一个 tr 列表,其中每一个元素都包含一个 Plantd
: 此 CSS Selector 指示了具体 Specs 所在节点,其返回结果也是数组{0: values.Name, 1: values.RAM, ...}
: 此项是在遍历 Specs 时 DOM 数组 对 values.* 的映射关系,其 key 对应了上一步中数组的下标values.CPU
: 此类是封装好的 Specs 解析单元,主要是针对常见的格式,比如 1 cores
, 1 CPU 2.6 GHz
等进行解析。一般会提取出有用的信息,比如 1 CPU 2.6 GHz
中会提取成 {'core': 1, 'freq': 2.6, 'freq_unit': 'GHz'}以上代码运行后会得到一个数组:[Plan, Plan, Plan, ...]
其中每个 Plan 里都包含了 Specs 解析单元 的实例
上面的代码只是说明了处理正确表结构的情况,然而现实中有大量表结构是不正常的,那么这种不正常的表,应该如何写配置呢?
如下所示,为 DomainVPS 的配置表:
{
'http://www.domain.com/hosting/vps/': PlanSet([
(('#main > div.compgrid > table > tr', 'td', True), {
1: values.Disk,
2: values.Traffic,
11: values.Price,
}),
(('#main > div.compgrid > table > tr', 'th', True)), {
0: values.Name,
}),
]),
}
True
: 大家可以看到在第 2 个 CSS Selector 后面还跟了个参数。这个参数的意思是,此表结构非主流,需要预处理,在获取 DOM 数组 后进行转换values.Name
: 这里, Name 被分到另一组 CSS Selector 中处理。如果大家查看该 HTML 的元素,就会发现, Name 所在节点跟其他 Specs 所在节点的种类不是同一个, 甚至很多情况下, Name, Price 的 DOM 结构跟 其他 Specs 的 DOM 结构是很不一样的,所以配置表的这种特性就可以很好地解决这个问题。除了 Plan 表格 会出现特殊结构外,有些奇葩厂商还会弄些标新立异的 Specs 格式。比如:
像这样的情况,我会这样处理:
{
'http://xxx.ooo': PlanSet([
(('#main > div.compgrid > table > tr', 'td', True), {
1: ValueMatch(values.Disk, re.compile("((?P<disk_cnt>\d+)x ?)?(?P<size>\d+)(?P<size_unit>\w+) (?P<disk_type>SSD)")),
3: ValuePipe(values.Traffic,
[ lambda t: t.replace(',', '').replace('.', '') + ' GB'],
values.Traffic
),
}),
]),
}
ValueMatch(value_class, regex)
: 使用 regex 对特殊格式进行识别,识别的结果是一个 dict, 里面的 key 直接就是 values.Disk 的字段ValuePipe(value_class, pre_process, value_class)
: 在使用 value_class 自带的解析单元进行解析之前,先用 pre_process 把 Specs 处理下,这里,我把 ',' 替换成 '.', 方便后续的识别更多配置案例请移步至: urlsolutions.py
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.