实战:分布式 selenium 爬虫,突破 craigslist 反反爬虫机制

2 minute read

爬虫和反爬虫的军备竞赛

爬虫是很传统的应用。 通过爬虫,我们可以从互联网服务中自动化大规模收割我们需要的数据和信息。 服务供应方,也就是被爬虫抓取数据的网站,出于保护数据版权,降低服务器负载等目的,设置各种各样的反爬机制。 工程技术领域的攻防问题大多是道高一尺魔高一丈的军备竞赛。 面对防御方的各种防御机制,可以通过对具体问题的针对性分析,逐一破解。

如同所有的工程开发一样,这一过程中最昂贵的成本就是摸着石头过河的技术探索成本。 本文就是对 Craigslist 这一具体案例的技术探索过程的记录,希望可以帮助后人降低学习成本,迅速掌握爬虫技术。

爬虫的法律相关

Disclaimer:本节内容不构成法律建议!!!

笔者对我国法律实践了解较少。 美国的爬虫相关案例中,较近的有 hiQ 诉 LinkedIn 案1,联邦第九巡回上诉法院于2022年底裁定,“‘未经授权’的概念不适用于公共网站”,因此 爬取公开数据 的行为并不违反美国《计算机欺诈和滥用法案》(CFAA, Computer Fraud and Abuse Act)23

第九巡回法院在裁决中指出,

公共网站的一大基本特征,就是其中公开可见的部分不受访问限制;换言之,这些部分将对任何拥有网络浏览器的访问者开放。

也就是说,如果将这些托管公开页面的计算机视为房屋,那么公共网站设备在部署之初就没有设置任何“前门”,自然不存在提高或降低访问门槛一说。因此,Van Buren 案强化了我们的裁定,即 “未经授权”概念确实不适用于公共网站

作为判例法国家,在联邦最高法院接受本案上诉并重审之前,在美国法律管辖范围内,并无判例支撑 爬取公开数据 的行为违法。

Disclaimer:本节内容不构成法律建议!!!

实际问题: Craigslist 网站的反爬虫机制

Craigslist (下称 CL )是北美应用最广泛的同城分类信息平台之一,自然深受爬虫之害,无数人通过爬取 CL 的数据,构建 app 原型。 针对于此, CL 设置了若干反爬虫机制,大体可以概括成以下两类:

  1. 用户信息识别:这包括很多种机制,比如 User-agent , cookie ,访问者 IP 等:
    1. 访问者 IP 的请求频率: CL 服务器会识别访问者的 IP 地址,当某一 IP 地址访问频率过高,明显高于人类操作可能性时,这一 IP 地址将被屏蔽。
      1. 屏蔽 IP 段:可能是由于 CL 并不向中国大陆提供服务, CL 站方屏蔽了一切来自中国大陆地区的访问请求。
    2. User-agent 等访问头(request header):和其他根据访问头识别访问者身份的机制一样,可以通过提供若干个 User-agent 预备来解决。
    3. cookie :初次 http 访问请求一般是无状态的。服务器为了识别身份,在初次访问之后,会返回给浏览器一段数据,即 cookie ,方便用户下次使用。
      1. 重复大量使用同一 cookie 自然会引起服务器屏蔽这一 cookie 的访问。
      2. 可以使用 selenium 模拟浏览器行为,每次访问重新生成 cookie ,以破解其用户识别机制
  2. 动态渲染:
    1. 现代 web 前端网页一般不会在初次加载时载入所有信息,而是使用动态载入,异步获取数据。比如 AJAX ,网页根据用户行为,实时访问后端,在得到回复后即时渲染。
    2. 我们无法使用诸如 python-requests 一类访问静态网页的库一次性抓取动态网页中的所有信息,因此必须使用 selenium 一类库模拟浏览器的行为,完整渲染整个网页以抓取信息。

分析结论:需要使用哪些反反爬虫手段,破解 Craigslist 的反爬虫防御措施?

根据上文的分析,可以简单归纳出三种针对性攻击措施,以破解 Craigslist 的防御措施。

  1. 反侦察措施:使用高匿代理服务器池子,掩盖自身 IP 地址,避免被服务器识别。
    1. 同理,对 User-agent 等访问头,也应做好相应的反侦察措施。
    2. 我们可以选择免费的高匿代理,也可以选择收费的,为了缩短开发周期,尽快完成原型,我们暂时选择收费高匿代理。
      1. 收费高匿代理是按照流量计费的,这意味着必须关闭图片接收,尽可能缓存不同网页的重复内容。
  2. 动态渲染:我们使用 selenium 模拟浏览器渲染网页,以实时生成 cookie ,模拟浏览器的 AJAX 行为。
  3. 分布式爬虫:由于 selenium 对爬虫服务器的资源消耗较大,我们使用分布式系统设计:
    1. 使用消息队列分发任务,由工作者接受任务,使用 selenium 和相应的浏览器驱动程序进行爬取,比较流行的有 firefox 系和 chrome 系

扩展阅读:更多更复杂的反爬虫机制

反爬虫和反反爬虫的军备竞赛是永无止境的,用一张网图表示4

剽窃来的军备竞赛网图

十分幸运的是, Craigslist 并未设置更为复杂的反爬虫机制:

  1. 数据只允许登录后访问:
    1. 这个是比较麻烦的,也是我国企业的主流选项。
  2. 验证码机制:
    1. 在计算机视觉技术大为发展的今天,解决这一问题并不难。
  3. 数据加密机制:
    1. 有很多种手段,比如字体模糊,如下图所示,并不以文本方式渲染全文,而是随机使用与原文本相似的图片替换掉一些文本,以中断我们对文本的抓取。
      1. 与验证码类似,也可以使用 OCR 技术破解已经被 selenium 渲染的网页来抓取所需的信息。

图片对文本的遮断

开发的第一步:技术选型、开发流程、系统设计

孙子兵法中说过,

夫未战而庙算胜者,得算多也;未战而庙算不胜者,得算少也。多算胜,少算不胜,而况于无算乎!吾以此观之,胜负见矣。

我们把一个项目的开发比喻为一场战役的话,了解作战的环境(天时、地利、人和),分析敌情,提出作战方案和作战计划,就是开发中的庙算。 庙算较多,可以磨刀不误砍柴工,节约后续的开发周期和成本。 “多算胜,少算不胜”的道理,也同样适用于市场的竞争。

庙算的目的是什么? 不同的作战方案必然会有不同的效果。通过开发前的技术讨论,选择一个较好的作战方案,可以缩短开发周期,节约开发成本,夺取市场竞争优势。

在庙算之中,我们需要:

  1. 分析己方的人和(团队组成和人员结构、技术栈)。
  2. 分析假想敌(项目需求,在本文中是敌方的反爬虫机制)。
  3. 提出作战方案(技术选型、分解项目需求到开发流程和系统设计)
  4. 根据作战方案制定作战计划(安排和推进开发流程到团队日程表上,可以使用甘特图( Gantt chart )5推进项目)

技术选型的标准和过程

根据个人经验,笔者个人在进行技术选型的时候,一般会有如下标准(排名不分先后):

  1. 这一技术我会不会?如果我不会的话,学习成本有多高?(人和)
    1. 很多情况下开发周期都是最重要的课题,不论对于处在任何阶段的企业,高速抢占市场都是至关重要的。
  2. 这一技术在市场上的流行度?(人和)
    1. 雇佣一个掌握这一技术的工程师,大概的难易度和成本,决定了后续维护和开发的成本。
  3. 这一技术的后续伸缩难度和成本?
    1. 互联网的魅力之一就是可以以较低成本进行大规模伸缩,所以有必要在系统设计之初就考虑到后续的伸缩需求。
  4. 这一技术的开发、调试和维护成本?
    1. 开发成本,或者说工程师的工时工资,一般是开发过程的最大成本。
    2. 稳定性和可维护性是非常重要的。
  5. 通用技术 vs 专用技术?
    1. 专用技术(如 Scrapy )往往有较丰富的功能,更加贴合于具体的应用场景(爬虫)。
    2. 通用技术(如 Celery )则是更高层次的抽象(任务队列),适用于更多的应用场景,但具体到特定的应用场景,则未必有专用技术强大广泛的功能适配。
    3. 很多情况下,项目早就采用了某种通用技术,此时有必要讨论是否有必要引入新的专用技术:开发成本是多少?我们真的有必要引入那些功能吗?这些都是权衡与取舍。我们总是说“重复发明轮子”,事实上重复学习轮子也是要规避的。

现实工程中需要考虑的问题往往都是权衡与取舍,技术本身的可行性一般都是没什么问题的,需要考虑的更多都是开发成本、开发周期、维护成本等因素。

即使是对于创业企业而言,技术选型的讨论过程也是值得留档记录的。作为项目文档的一部分,方便新人工程师接手项目。

我们的技术选型

  1. 开发语言: Python
    1. 选型原因:开发快,懂的人多
  2. 爬虫框架: Selenium
    1. 选型原因:上文叙述过,必须使用 Selenium 模拟浏览器行为,动态渲染网页
  3. 浏览器驱动程序: Chrome
    1. 这个其实经历了反复改换,在 Firefox 和 Chrome 之间斟酌不定,最后选择 Chrome 的原因是我在 StackOverflow 上找到了如何使用 Chrome 保存浏览器缓存
      1. 对于 Craigslist 上的大部分网页,我们下载的内容都是模板 + 数据,为了节约流量,我们只希望下载数据,不希望重复下载模板
  4. 包管理: Conda
    1. 其他项目会用,一般来说其实用 pip 就好
    2. 由于我们使用 conda ,可以在 iPython Notebook 里进行原型开发,缩短开发周期,也算一个优势
  5. 消息队列: Celery + RabbitMQ
    1. Python 的任务队列库,进行分布式任务分配
    2. 一般需要配合 RabbitMQ 或 Redis 等消息队列服务作为 Celery 的 broker ,这里选用 RabbitMQ
    3. 不选择 Scrapy 的原因:相比起 Scrapy 是高度特化的爬虫框架, Celery 是更基础的抽象,可以广泛应用到其他的业务开发中
  6. 集群管理: Kubernetes + Docker
    1. 选型原因:负责集群管理和容器的自动化运维:可以以非常低的成本扩张,大幅度降低运维成本,适配其他服务的开发也十分简易
  7. 与本文不相关的技术选型:
    1. 数据库及驱动程序: PostgreSQL + SqlAlchemy
    2. 后端框架: FastAPI

开发探索步骤

上边讨论了这么多,终于产品经理要出场了,提出并分析技术需求( PRD, Project Requirements Document )。

需求:我们想要获得 Craigslist 上旧金山湾区所有的租房房源信息。

探索(即开发任务):

  1. 使用代理:学会使用高匿代理、 Selenium 及其 webdriver 刮取网页……
  2. 列表抓取
    1. 抓取第一个网页:尝试抓取 Craigslist 的 list 部分6,即房源列表中所有房源的链接。
    2. 抓取后续网页:仅仅有一页房源列表肯定是不够的,需要探索如何用可靠的手段抓取房源列表的下一页。
    3. 更新任务队列:当我们得到列表中的所有这些链接之后,我们把这些链接推送到 Celery 队列中,作为下一步爬虫工人( celery worker )们的任务( task )。
    4. 定时任务:为了确保房源数据库与 Craigslist 尽可能同步,我们需要周期性运行 列表抓取,抓取新的房源。
      1. 为了避免重复抓取,每次运行 列表抓取 的时候,我们从新到旧抓取房源,当我们抓取到的最旧一条房源,早于数据库中尚存的最新一条房源的时候,我们中止 列表抓取 这一过程。
      2. 这一过程可以做成 Kubernetes 中的定时任务 Crontab ,简化运维
    5. 针对 Craigslist 特化的需求:对于某一个选定的区域, Craigslist 仅仅提供前 10000 条记录(或最近一个月的记录),但旧金山湾区是个很大的区域,前 10000 条记录仅仅包括了三天的更新量。
      1. 对每一个选定的大区域(旧金山湾区),Craigslist 另外提供一些子区域给我们搜索,旧金山湾区包括城里(即旧金山市)、东湾、南湾、北湾、半岛、 Santa Cruz 等地区,通过追踪这些地区的更新,可以获得一个较久的数据储备。
  3. 房源抓取
    1. 抓取第一个网页:需要剖析 Craigslist 网页内容,输入数据库
    2. 数据库设计:选择哪些网页内容进行剖析和储存?选择什么样的数据类型储存这些内容?
      1. 处理重复项:如何判定两个房源是重复的?这可以节约后续 定时淘汰 步骤耗费的流量。
    3. 抓取后续网页:使用 K8s 的 deployment 部署若干个 Celery worker 进行抓取
      1. Celery worker 的容器化和集群部署
      2. 如何配置 Celery worker 的容器,使其共享浏览器缓存,节约流量
      3. 在后续运行中,我们可能还会面对如何处理 Celery worker 的优雅失败
  4. 定时淘汰
    1. 数据库中的房源可能会随着时间流逝而失效,我们需要定期扫描数据库的内容,访问其链接,删除已经失效的内容
    2. 这同样也是一个定时任务 Crontab

我们需要评定开发任务是否阻碍其他任务,其先后顺序和优先级,估算其消耗时长,进而形成 ticket ,即工单。

把工单排列进入甘特图( Gantt Chart ),管理项目进度。

简单的系统设计,及后续开发中需要注意的原则

Docker 容器化和 Kubernetes 集群管理的使用,大幅度简化了系统设计的思维负担。 我们已经在上述的任务分解中简述了这个简单系统的设计,包含

  1. K8s 集群(在规模较小时,我们可以直接使用 Minikube )
  2. 一个消息队列( Celery + RabbitMQ )
  3. 一个容器部署( K8s deployment ),即 Celery worker
  4. 两个定时任务( K8s crontab )
  5. 一个数据库( PostgreSQL )

我们剽窃一张前人撰写的 Celery 爬虫架构图7,给予一个较为直观的理解:

剽窃来的架构图

我们根据云原生应用开发的最佳实践,遵循 12 因素应用( 12 Factor Application )8的原则进行开发,以节约后续的维护成本,减少潜在的bug,提高团队的开发体验。 其中较重要的开发原则,比如:

  1. 在环境中存储配置
  2. 以无状态进程运行应用
  3. 快速启动和优雅终止

等。

小结

经过一定的庙算,分析敌情,分析人和,跟踪技术潮流,追随系统设计的最佳实践,提出作战方案,我们可以缩短开发周期,大幅度简化开发流程,以很小的团队完成过去非常困难的工作。 对于创业企业而言,迅速完成原型,就是站稳脚跟的第一步。

但仅仅有第一步是不充分的,通过周密考虑的系统设计和恰到好处的文档,我们可以构建非常健壮的应用程序,节约后续的维护成本,有效应对未来潜在的规模扩张和功能扩展需求,良好应对技术团队的新旧交替,站稳企业生命周期中的每一步。

(回顾本文,我发现我一行代码都没写。 如果本文有下篇,我一定写。)

Categories:

Updated: