使用JavaScript进行Web抓取

2020-10-27 00:57:06

如果你尝试用谷歌搜索“网络抓取教程”,你会得到一堆关于这个主题的技术文章,告诉你如何使用python来达到这个结果。这个工具包对于这些帖子来说是相当标准的:Python3(希望不是第二个)作为引擎,Requests Library用于抓取,以及Beautiful Soup4(已有6年历史)用于Web解析。

我也看过一些他们教你如何用正则表达式解析HTML内容的文章,剧透:不要这么做。

问题是,我在5年前就看过这样的文章,而这一堆文章几乎没有改变。更重要的是,该解决方案不是javascript开发人员原生的。如果您想使用您更熟悉的技术,如ES2020、节点和浏览器API,您将错过直接的指导。

我试着填补空缺,创造出“失踪的医生”。

在开始进行任何编程之前,请始终检查是否有最简单的可用方法。在我们的例子中,它将是对数据的直接网络请求。

打开开发人员工具-大多数浏览器中的F12-然后切换到网络选项卡并重新加载页面。

如果数据没有像在一半的现代Web应用程序中那样在HTML中烘焙,那么您很可能根本不需要抓取和解析。

如果你没有那么幸运,仍然需要刮,下面是这个过程的一般概述:

处理数据:对其进行过滤、转换以满足您的需要,并为将来的使用做好准备。

这将是最简单的解析情况,在复杂的情况下,您可以遇到一些分页、链接导航、处理bot保护(验证码),甚至实时站点交互。但所有这些都不会在目前的指南中涵盖,抱歉。

作为本指南的一个示例,我们将从Transfermarkt收集梅西的进球数据。你可以在网站上查看他的数据。要从节点环境加载页面,您需要使用您喜欢的请求库。您也可以使用原始HTTP/S模块,但是它甚至不支持异步,所以我为这个任务选择了Node-Fetch。您的代码将如下所示:

Const FETCH=REQUIRED(';NODE-FETCH';)CONST DATA_URL=';https://www.transfermarkt.com/lionel-messi/alletore/spieler/28003/plus/1';常量loadMessiGoals=Async()=>;{Const Response=等待FETCH(DATA_URL)返回响应。Text()}模块.exports={loadMessiGoals}。

这项任务有两个主要的替代方案,这两个方案由两个高质量、最星级和最活跃的库方便地表示。

第一种方法只是从标记文本构建语法树,然后使用熟悉的类似浏览器的语法导航它。这个被声明为jQuery for server(IMO,他们需要修改2020年的营销氛围)的cheiio完全覆盖。

第二种方法是构建整个浏览器DOM,但不使用浏览器本身。我们可以使用非常棒的jsdom来做到这一点,它是许多Web标准的node.js实现。

尽管有这些类比,cheio在依赖项中没有jQuery,但它只是尝试从头开始重新实现大多数已知的方法:

";依赖";:{";@类型/节点";:";^14.11.2";,";CSS-SELECT";:";~3.1.0";,";随机序列化器";:";~1.1.0";,";实体";:";~2.1.0";,";htmlparser2";:";^5.0.0";,";loash";:";^4.17.20";,";parse5";:";^6.0.0";,";parse5-htmlparser2-树适配器";:";^6.0.0";}。

如果您需要节省大小(cheilio是轻量级且快速的),或者您非常熟悉jQuery语法,并且出于某种原因想要将其引入到新项目中,那么您可能可以选择这个方法。Cheerio是一种很好的方式,可以用来处理应用程序中需要的任何类型的HTML。

这个稍微复杂一些:它试图模拟使用HTML和JS的整个浏览器的一部分(除了呈现结果之外)。它被大量用于测试和…。刮得很好。

Jsdom要重得多,它做的工作也多得多。你应该明白,为什么要选择它而不是其他选项。

在我们的示例中,我想坚持使用jsdom。它将帮助我们在文章末尾展示最后一种方法。解析部分非常重要,但是非常短。

然后,您可以使用CSS选择器和浏览器API选择表内容。不要忘了从querySelectorAll返回的NodeList创建一个实数组。

现在您有了一个要使用的二维数组。这一部分已经完成,现在您需要处理此数据以获得干净的、随时可以使用的统计数据。

首先,让我们检查一下我们的行的长度。每一行都是关于进球的统计数据,我们大多不关心我们有多少个进球。但是每行可以包含不同格式的数字,所以我们必须处理它们。

我们逐行映射,得到长度。然后,我们对结果进行重复数据消除,看看这里有哪些选项。

因为我们在15个单元格的情况下不需要额外单元格的排名数据,所以删除它是安全的。

只有一个单元格的行实际上是无用的:它只是一个季节名称,所以我们将跳过它。

对于5个单元格的情况(当球员在一场比赛中进了几个球),我们需要找到上一个整行,并使用它的数据作为空的统计数据。

Const getLastMatch=(IDX,目标)=>;目标[IDX].length=14?目标[IDX]:getLastMatch(IDX-1,Goals)const Match=getLastMatch(IDX,Goals)const isSameMatch=row.length=14。

现在我们只需要手动将数据映射到键,这里没有什么科学的东西,也没有聪明的方法来避免它。

返回{竞赛:比赛[1],比赛日期:比赛[2],日期:比赛[3],地点:比赛[4],对手:比赛[7],结果:比赛[8],位置:比赛[9],分钟:行[1+isSameMatch*9],atScore:行[2+isSameMatch*9],goalType:行[3+isSameMatch*9],辅助:行[4+isSameMatch*9],}。

我们只需将结果转储到一个文件中,首先使用JSON.stringify方法将其转换为字符串。

因为我们将jsdom与浏览器兼容的API一起使用,所以我们实际上不需要任何节点环境来解析数据。如果我们只需要一次来自特定页面的代码,我们只需在浏览器的开发人员工具的控制台选项卡上运行一些代码即可。尝试打开Transfermarkt上的任何玩家统计数据,并将此巨大的不可读代码片段粘贴到控制台:

让data=Array。发件人(文档。QuerySelectorAll(';.response-table table tr';))。MAP(ROW=>;Array。发件人(排.孩子)。Map(node=>;node.textContent。Trim()。MAP((行)=>;行.length=15?[...行。切片(0,5),...行。Slice(6)]:行)。Map((row,idx,Goals)=>;{if(row.length=1)return nul Const getLastMatch=(IDX,Goals)=>;Goals[IDX].length=14?Goals[IDX]:getLastMatch(idx-1,Goals Const Match=getLastMatch(IDX,Goals Const isSameMatch=row.length=14 return{竞赛:Match[1],Matchday:Match[2],Date:Match[3],地点:Match[4],对手:Match[7],Result:Match[8],Position:Match[9],分钟:ROW[1+isSameMatch*9],atScore:ROW[2+isSameMatch*9],GoalType:行[3+isSameMatch*9],辅助:行[4+isSameMatch*9],}})。筛选器(布尔值)。

现在只需应用集成到浏览器DevTools中的魔术复制功能。它会将数据复制到您的剪贴板。

没那么难,对吧?再也不用和皮普打交道了。我希望你觉得这篇文章有用。请继续关注,下一次我们将使用现代JS库可视化这些抓取的数据。