Brisk Voyage为我们的会员提供便宜的、最后一分钟的周末旅行。基本的想法是,我们不断地检查机票和酒店的价格,当我们发现一次旅行是低价的异常值时,我们会发送一封带有预订说明的电子邮件。
为了找到机票和酒店价格,我们搜索了谷歌航班和谷歌酒店。酒店相对简单:要在100个目的地中找到最便宜的酒店,每个目的地超过5个不同的日期,我们必须总共抓取500个Google Hotels页面。
削减机票价格是一个更大的挑战。要找到100个目的地的最便宜的往返航班,每个目的地有5个不同的日期,那就是500页。然而,航班不仅仅有目的地机场-它们也有始发机场。这500个航班必须从每个始发机场进行检查。我们必须检查从纽约市周围的每个机场到阿斯彭的航班,洛杉矶周围的每个机场,以及介于两者之间的每个机场。我们在打理酒店的时候没有这个额外的机场“维度”。这意味着与我们的问题相关的机票价格比酒店价格高出几个数量级。
目标是找到给定日期组中每个始发地/目的地对之间最便宜的航班。为了做到这一点,我们每天从25,000个谷歌航班页面中收取大约300,000个航班价格。这不是一个天文数字,但它足够大,以至于我们(至少作为一家自力更生的公司)必须关心成本效益。在过去一年左右的时间里,我们反复使用我们的擦除方法,以得到一个相当健壮和灵活的解决方案。下面,我将描述我们在抓取堆栈中使用的每个工具,大致按数据流排序。
我们使用SQS为要爬行的URL队列提供服务。谷歌航班网址如下:https://www.google.com/flights?hl=en#flt=BOS.JFK,lga,EWR.2020-11-13*jfk,lga,EWR.BOS.2020-11-16;c:USD;e:1;sd:1;t:f。
上面URL中的三个字母代码是IATA机场代码。如果单击该链接,请注意有多个目的地机场。这是高效谷歌航班搜索的关键之一!由于谷歌航班允许一次搜索多个行程,我们可以在一个页面上获取多个往返行程的最便宜价格。谷歌有时不会显示查询到的所有行程的航班。为了确保我们拥有所需的所有数据,当结果中没有显示目的地/目的地时,我们会检测到,然后将行程重新排队,以便自己进行搜索。这确保了我们收集每次旅行可获得的最便宜航班的价格。
单个SQS队列存储所有需要爬行的Google航班URL。当Crawler运行时,它将从队列中提取一条消息。顺序并不重要,因此使用标准队列(而不是FIFO)。下面是队列充满消息时的样子:
Lambda是爬行器实际运行的地方。我们使用Chalice,这是一个优秀的Python Lambda微框架,将函数部署到Lambda。虽然Serverless是最流行的Lambda框架,但它是用NodeJS编写的。考虑到我们最熟悉Python,并且希望保持堆栈的一致性,这对我们来说是一个障碍。我们对Chalice非常满意--它的使用就像使用Flask一样简单,而且它允许整个轻快的Voyage后端成为Lambda上的Python。
主要的lambda函数从SQS队列摄取消息、爬行Google航班并存储输出。当此函数运行时,它在Lambda实例上启动Chrome浏览器并爬行页面。我们将其定义为具有Chalice的纯Lambda函数,因为此函数将单独触发:
第二个Lambda函数触发第一个函数的多个实例。这可以并行运行我们需要的任意数量的爬行器。此功能定义为每天UTC时间15-22小时过后2分钟运行。它为50个并行爬网程序启动50个主爬网功能实例:
@app.Schedule(“cron(2 15,16,17,18,19,20,21,22?*)”)def start_Crawler(Event):app.log.info(“正在启动爬虫”)。n_Crawler=50 client=boto3.client(“lambda”),范围内的n(N_Crawler):Response=client.voke(FunctionName=“Collector-dev-Crawl”,InvocationType=“event”,Payload=‘{“Crawler_id”:’+str(N)+“}”,)app.log.info(“已启动的爬网程序。”)。
另一种方法是将SQS队列用作爬网功能的事件源,这样当队列填充时,爬网程序会自动向上扩展。我们最初使用的是这种方法。然而,有一个很大的缺点:一次调用可以接收的最大消息数(批大小)是10,这意味着必须为每10条消息组新调用该函数。这不仅会导致计算效率低下,而且会大幅增加带宽,因为浏览器缓存在每次重新启动时都会被破坏。有一些方法可以绕过这个问题,但根据我们的经验,它们会导致很多额外的复杂性。
关于Lambda成本的说明Lambda的价格是0.00001667美元/GB/秒,而许多ec2实例的成本是这个数字的六分之一。我们目前每月为Lambda支付约50美元,因此这将意味着我们可以大幅降低这些成本。然而,Lambda有两个很大的好处:第一,它可以立即向上和向下伸缩,我们不需要付出任何努力,这意味着我们永远不会为空闲的服务器买单。其次,它是我们堆栈的其余部分的基础。更少的技术意味着更少的认知开销。如果我们爬行的页面数量增加,重新考虑EC2或类似的计算服务将是有意义的。在这一点上,我认为每月额外支付40美元(Lambda为50美元,EC2为10美元)对我们来说是值得的。
Pyppeteer是一个Python库,用于与Puppeteer交互,Puppeteer是一个无头Chrome API。因为Google航班需要Javascript来加载价格,所以有必要实际呈现整个页面。这50个爬行功能中的每一个都启动了自己的无头Chrome浏览器副本,该浏览器由Pyppeteer控制。
在Lambda上运行无头Chrome是一个挑战。我们必须将必要的库预先打包到Chalice中,而这些库并没有预先安装在运行Lambda的操作系统Amazon Linux上。这些库被添加到我们的Chalice项目内的供应商目录中,该目录告诉Chalice在每个Lambda爬网实例上安装它们:
当50个爬行函数在~5秒内启动时,每个函数中都会启动Chrome实例。这是一个功能强大的系统;通过更改一行代码,它可以扩展到数千个并发Chrome实例。
Crawl函数从SQS队列读取URL,然后Pyppeteer告诉Chrome导航到旋转的住宅代理后面的页面。住宅代理是必要的,以防止Google阻止Lambda发出请求的IP。加载并呈现页面后,可以提取呈现的HTML,并且可以从页面读取航班价格、航空公司和时间。每页有10-15个我们想要提取的航班结果(我们最关心的是最便宜的,但其他的也可以是有用的)。目前,这是通过遍历页面结构手动完成的,但这很脆弱。如果Google更改页面上的一个元素,爬行器可能会中断。将来,我想使用对页面结构的依赖程度较低的内容。
一旦提取了价格,我们将删除SQS消息,并对航班结果中未显示的任何始发地/目的地重新排队。然后我们转到下一页-从队列中拉出一个新的URL,并重复该过程。在每个爬行实例中抓取第一个页面后,由于Chrome的缓存,页面需要加载的带宽要少得多(约100KB而不是3MB)。这意味着我们希望尽可能长时间地保持实例处于活动状态,并尽可能多地爬行,以便保留缓存。因为持久化函数来保留缓存是有利的,Crawl的超时是15分钟,这是AWS当前允许的最大值。
从页面中提取数据后,我们必须存储航班数据。我们为此选择了DynamoDB,因为它具有按需伸缩功能。这对我们来说很重要,因为我们不确定需要什么样的货物。它也很便宜,根据AWS的免费级别,25 GB是免费的。
DynamoDB已经做了一些工作来使其正确。通常情况下,表只能有一个具有一个排序关键字的主索引。添加二级索引是可能的,但要么是有限的,要么需要额外的资源调配,这会增加成本。由于索引的这一限制,如果事先充分考虑了用法,DynamoDB的工作效果最好。我们花了几次时间才把桌子的设计做好。回想起来,DynamoDB对于我们正在构建的产品类型来说有点僵化。既然Aurora Serverless提供了PostgreSQL,那么我们在某个时候切换到它可能是明智的。
无论如何,我们都将所有航班数据存储在一个表中。该索引具有目的地机场的IATA代码的主键和作为ULID的次要范围键。
ULID对于DyanmoDB非常有用,因为它们是惟一的,具有嵌入的时间戳,并且可以按该时间戳按字典顺序排序。这使得范围键既可以作为唯一标识符,又可以支持诸如“给我过去30分钟内爬过的最便宜的床上行程”之类的查询:
我们使用Dashbird来监视爬行器和在Lambda下运行的其他一切。良好的监控是抓取应用程序的要求,因为页面结构更改是一个持续的危险。在任何时候(就像我们最近在谷歌航班上看到的那样,甚至一天多次),页面结构都可能改变,这会破坏爬虫。当这种情况发生时,我们需要得到警告。我们有两种不同的机制来跟踪这一点:
每3小时运行一次的GitHub Action,运行一次测试爬网并验证结果是否有意义。因为爬虫并不总是在运行,所以当谷歌航班在运营时间之外改变页面结构时,它会向我们发出警报。这样,爬行器可以在当天开始之前修复。
SQS、Lambda、Chalice、Pyppeteer、DynamoDB、Dashbird和GitHub操作的组合非常适合我们。这是一个完全使用Python的低开销解决方案,不需要提供实例,并且保持相对较低的成本。
虽然我们对我们目前每天大约30万个价格和25K个页面的规模感到满意,但随着我们数据需求的增长,还有一些改进的空间:
首先,明智的做法是将Crawler移动到EC2服务器,这些服务器在需要时会自动提供。随着计算爬网需求的增加,Lambda和EC2成本之间的增量将增加到在EC2上运行有意义的程度。EC2在计算方面的成本大约是Lambda的五分之一,但需要更多的开销,而且不会降低带宽成本。
其次,我们将从DynamoDB迁移到无服务器Aurora,这允许更灵活的数据使用。随着我们的价格库和对替代数据用途的胃口的增长,这将是有用的。
如果您觉得这很有趣,您可能会喜欢轻快旅行-我们的免费时事通讯每隔几天就会从您附近的机场向您发送一次廉价的周末旅行。如果您喜欢这项服务,我们还有一个高级版,它将为您提供更多的行程,并且有更多的功能。
当然,有很多方法可以改进这个系统。如果你有什么想法,请告诉我们!