V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
panyanyany
V2EX  ›  分享创造

思路分享:如何抓取 不同厂商、不同结构的 VPS Plan

  •  
  •   panyanyany · 2017-04-01 18:02:24 +08:00 · 2110 次点击
    这是一个创建于 2784 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近做了个抓取 VPS Plan 的爬虫

    • 一开始是写了个解析器,根据 Plan 的结构来自动去识别 HTML 中的 Plan ,并根据 Specs 特点去自动识别 Specs 中的值,比如 1 core, 500 GB Storage, 256 MB RAM,并把这些值提取出来
    • 写完后才发现,各个 VPS 厂商的网页结构差别太大,太不统一,最终正确解析率只有 20%,也就是说,你给我 10 个 VPS 的厂商,虽然都能识别出里面的 Plan 结构,但只有 2 个是能完整解析出各项参数的。其他的要么是少了 Price ,要么是少了 CPU
    • 其次就是这样爬太费时间,一个官网几十个页面,每个都去解析一遍,这计算量真是够呛。

    后来改变思路,决定采用人工识别,但是通过配置表的方式来提取数据,这就是下面的内容。

    小广告:
    GitHub 地址 vps_spider
    项目展示官网 Find All VPS


    如何抓取结构复杂的 VPS Plan

    Plan 分类

    由于不同厂商使用不同的网站,所以他们的 Plan 表也是不一样的,为了能设计出一套通用的解析器,我们先研究下它们的特点:

    第一种

    Linode: 一个节点就是一个 Plan
    Linode: 像这样的情况属于多数,一个节点包含了一个完整的 Plan

    第二种

    DomainVPS: 一个 Plan 被分割到多个节点中
    DomainVPS: 这种就比较反直觉了,一个好好的 Plan 被分割到不同的节点里,简直是 VPS 中的杀马特

    Plan 解析器逻辑(配置表)

    为了避免给每一个网站写一个 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 列表,其中每一个元素都包含一个 Plan
    • td: 此 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 解析单元 的实例

    对 Plan 表格中 DOM 结构的预处理

    上面的代码只是说明了处理正确表结构的情况,然而现实中有大量表结构是不正常的,那么这种不正常的表,应该如何写配置呢?

    如下所示,为 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 结构是很不一样的,所以配置表的这种特性就可以很好地解决这个问题。

    对特殊 Specs 的特别处理

    除了 Plan 表格 会出现特殊结构外,有些奇葩厂商还会弄些标新立异的 Specs 格式。比如:

    • 同一个表格中 Disk 的格式不一样,一种是 "10 GB", 另一种是 "2x 30 GB"
    • 同一个表格中 Traffic 的格式不一样,一种是 "100.000 Gbps", 另一种是 "100,000 Gbps"

    像这样的情况,我会这样处理:

    {
        '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

    5 条回复    2017-04-02 06:53:11 +08:00
    sunorg
        1
    sunorg  
       2017-04-01 18:23:37 +08:00 via iPhone
    然后有什么用?

    做 resell?
    panyanyany
        2
    panyanyany  
    OP
       2017-04-01 18:25:19 +08:00
    r#1 @sunorg 没啥用,练手
    panyanyany
        3
    panyanyany  
    OP
       2017-04-01 18:26:44 +08:00
    r#1 @sunorg 你不觉得用这种配置表的形式,避免了一个个去写 spider 吗?
    isCyan
        4
    isCyan  
       2017-04-01 18:36:41 +08:00 via Android
    @sunorg 做 affman 啊
    ryd994
        5
    ryd994  
       2017-04-02 06:53:11 +08:00 via Android
    不错的
    参数和逻辑充分解耦了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1043 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 22:12 · PVG 06:12 · LAX 14:12 · JFK 17:12
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.