如何使用 Scrapy 抓取网页

网络抓取是从公共网站下载数据的过程。例如,您可以从 ESPN 中获取棒球运动员的统计数据,并构建一个模型来根据他们的球员统计数据和获胜率来预测球队的获胜几率。下面是一些网页抓取的用例。

监控竞争对手的价格以进行价格匹配(竞争性定价)。
从各种网站收集统计信息以创建仪表板,例如 COVID-19 仪表板。
监控金融论坛和推特以计算特定资产的情绪。
我将演示的一个用例是抓取网站 Indeed.com 来发布招聘信息。假设您正在寻找一份工作,但您对列表的数量感到不知所措。您可以设置一个过程来确实每天进行刮擦。然后您可以编写一个脚本来自动应用于满足特定条件的帖子。

免责声明:网络抓取确实违反了他们的使用条款。本文旨在为教育目的只。在抓取网站之前,请务必阅读他们的服务条款并遵循其 robots.txt 的指导方针。

数据仓库注意事项
我们的蜘蛛每天都会抓取给定搜索查询可用的所有页面,因此我们希望存储大量重复项。如果帖子持续多天,那么我们将在帖子发布的每一天都有一个副本。为了容忍重复,我们将设计一个管道来捕获所有内容,然后过滤数据以创建可用于分析的规范化数据模型。

首先将从网页中解析出数据,然后将其放入半结构化数据结构中,例如 JSON。从这里开始,数据结构将存储在对象存储中(例如 S3、GS)。对象存储是捕获数据的有用起点。它便宜、可扩展,并且可以随着我们的数据模型灵活变化。一旦数据在我们的对象存储中,网络爬虫的工作就完成了,并且数据已经被捕获。

下一步是将数据非规范化为对分析更有用的数据。如前所述,数据包含重复项。我会选择使用 SQL 数据库,因为它具有强大的分析查询。它还使我能够区分不同的实体,例如公司、职位发布和地点。首先,所有帖子都将进入事实表(大型只写表),并带有时间戳,显示帖子何时被抓取,以及何时插入到表中。从这里我们可以将数据非规范化为一个表示当前活动发布的有状态表。

可以编写合并语句来更新和插入表示实时发布的表中的发布。从这里我们还想删除已删除或已过期的帖子。现在我们有一个已经标准化的表,或者换句话说,所有的重复项都被删除了。

设置项目
对于这个项目,我将使用Scrapy,因为它带有有用的功能和抽象,可以节省您的时间和精力。例如,scrapy 可以轻松地将结构化数据推送到 S3 或 GCS 等对象存储中。这是通过将您的凭据以及存储桶名称和路径添加到由 scrapy 生成的配置文件来完成的。目的是为了长期存储,并在我们每次运行刮刀时生成一个不可变的副本。因为 S3 是一个无限的对象存储,它是长期存储的理想场所,可以轻松地随任何项目扩展。

为了突出更多功能,scrapy 使用 Twisted 框架来处理异步 Web 请求。这意味着程序可以在等待网站服务器响应请求时完成工作,而不是通过无所事事的等待来浪费时间。Scrapy 有一个活跃的社区,因此您可以寻求帮助并查看其他项目的示例。它还提供了一些更高级的选项,例如在带有 Redis 的集群中运行和用户代理欺骗,但这些不在本教程的范围内。

让我们从在 python 中创建一个虚拟环境并安装依赖项开始。然后初始化一个空白项目,我们将在其中拥有我们的网络爬虫。请务必在项目的顶级目录中执行此代码。

python3 -m venv venv
source ./venv/bin/activate
pip install scrapy lxml BeautifulSoup4 jupyterlab pandas
scrapy startproject jobs
cd jobs/jobs/
scrapy genspider indeed indeed.com

解析网页的代码会indeed.py放在spiders/目录中的文件中。蜘蛛是网络爬虫的抽象,它生成 HTTP 请求并解析返回的页面。有一个单独的抽象用于处理和存储称为 ItemPipeline 的信息。这种抽象的分离允许解耦、灵活性和水平扩展。例如,您可以将蜘蛛的结果发送到多个地方,而无需修改蜘蛛代码的内部逻辑。一个好的做法是将结果作为文件保存在对象存储中以进行长期存储,并保存在数据库中以进行重复数据删除和临时查询。

开发环境
您可以将网络抓取视为对其他人的工作进行逆向工程。有许多工具可以使 Web 开发更易于组合和管理,例如使用可重用模板或 ES6 模块。这些允许 Web 开发人员在多个地方重用相同的代码,目标是将简单的部分组合成更复杂的部分。当您抓取网站时,您所拥有的只是实际呈现的网页,我们无法访问用于构建该页面的组件。因此,我们必须使用我们可以使用的任何技巧向后工作以获得我们想要的东西。

任何 Web 抓取项目的第一步都是在 Web 浏览器中打开要抓取的页面,并在您选择的浏览器中使用“检查元素”探索 DOM。使用浏览器中的开发人员工具,您可以探索 DOM 的结构或页面的骨架。现在可以随意打开页面并使用开发人员工具进行探索。在整个过程中,我们将使用浏览器以可视化和交互方式快速浏览 dom。

我喜欢使用带有 iPython 的 scrapy shell 为我的网页抓取开发解析代码,交互性允许快速反馈循环,允许进行大量试验和错误。scrapy shell 将您带入已实例化的所有帮助程序和便利函数的 scrapy 上下文。这些相同的对象在运行时可供蜘蛛使用。它还使您可以访问 iPython 的所有功能。您可以使用以下代码启动您的 shell。

scrapy shell 'https://www.indeed.com/q-medical-assistant-l-Boston,-MA-jobs.html'

这里最重要的对象是响应。这包含从 Web 服务器到我们的 HTTP GET 请求的 HTTP 响应。它包含页面的 HTML,以及与 HTTP 响应相关的标头和其他信息。基本的反馈循环是使用浏览器来识别我们想要解析的内容,然后在终端中测试解析代码。

起始网址
如果您查看上面的链接,您可能会注意到该 URL 包含我们的搜索查询。这就是在 HTTP GET 请求中传递参数的方式(本示例不使用标准化格式)。我们可以利用这些信息以编程方式尝试不同的搜索查询。

在 url 中,在字母 q 之后,我们看到与职位相对应的查询。字母 l 之后是位置。假设在我们的用例中,我们要搜索多个位置和职位。例如,医疗助理也称为患者护理助理。

在scrapy中,我们可以通过我们的蜘蛛几个url作为抓取的起点。我们可以向它传递与多个位置和职位相对应的 URL 以获得此行为。在这个例子中,我使用 product 函数来生成位置和职位的每个组合,然后我将它传递给蜘蛛作为我们的起点。

from itertools import product
job_titles = ["Medical Assistant", "Patient Care Technician", "Patient Care Assistant"]
states = ["MA"]
cities = ["Boston", "Cambridge", "Somerville", "Dorchester"]
urls = []
for (job_title, state, city) in product(job_titles, states, cities):
urls.append(f"https://www.indeed.com/q-{'-'.join(job_title.split())}-l-{city},-{state}-jobs.html")
class IndeedSpider(scrapy.Spider):
name = "indeed"
allowed_domains = ["indeed.com"]
start_urls = urls
也许您注意到在相邻城市搜索相同的职位会产生重叠的结果。换句话说,在剑桥和波士顿搜索相同的标题将返回重复项。任何数据项目的一项主要挑战是重复数据删除。我们可以使用的一种策略是拥有一个像 redis 这样的应用程序缓存,我们的程序可以根据由职位、公司名称、位置和发布日期组成的自然主键来检查列表是否已经被解析。我们甚至可以在服务器生成的 DOM 中查找唯一 ID。为简单起见,一旦所有内容都保存到我们的对象存储中,我们将在最后进行重复数据删除。

解析页面
我将使用 python 库BeautifulSoup4来解析 HTML,因为这是我最熟悉的库。默认情况下,scrapy 带有 CSS 选择器和 XPath 选择器,两者都是针对 DOM 编写查询的强大方法。在本教程中,您需要将 Beautiful Soup 导入我们的 shell,然后将 HTML 解析为 BeautifulSoup 对象。我喜欢 BeautifulSoup,因为 find API 很简单。

from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, features="lxml")
请注意,当他们重命名 CSS 类时,此解析代码将被破坏。如果您发现这些示例不起作用,请尝试修复它们以适应今天网站的结构和命名方式。

首先,我们需要找到一种方法来解析给定页面上所有作业的列表。我们想找到一种方法来捕获每个列表的顶级节点。一旦我们有了每个列表的父节点,我们就可以迭代如何解析每个列表的属性。目前,每个列表都有一个顶级锚元素 ( ),带有class="tapItem". 我们可以使用 CSS 类来选择所有这些代表单个列表的节点。

listings = soup.find_all("a", {"class": "tapItem"})
在本教程中,我们将定位属性职位、雇主、地点和职位描述。前三个属性可以在搜索结果页面中找到,而职位描述将需要点击职位描述页面的链接。从这个页面上可用的属性开始,我们可以使用 CSS 类来定位每个父节点中列表的不同属性。

for listing in listings:
job_title = listing.find("h2", {"class": "jobTitle"}).get_text().strip()
summary = listing.find("div", {"class": "job-snippet"}).get_text().strip() # strip newlines
company = listing.find("span", {"class": "companyName"}).get_text().strip()
location = listing.find("div", {"class": "companyLocation"}).get_text().strip()

我通过在检查元素 devtools 中找到职位然后在 iPython 中迭代代码来编写这段代码。在每种情况下,我发现通过 HTML 元素和 CSS 类进行选择足以获得我需要的信息。

现在我们需要检索位于单独页面上的职位描述。为此,我们将发送另一个 HTTP 请求,以使用我们在搜索结果页面上找到的链接检索带有职位描述的页面。我们通过将在锚标记的 href 中找到的相对路径与在我们的响应对象中找到的搜索结果页面的 URL 相结合来获得链接 url。然后我们会要求scrapy用异步事件循环来调度请求。

我们会将职位描述 (jd) 与我们在此页面页面上找到的信息结合起来,因此我们会将解析后的属性传递给回调函数,以便它们都可以存储在同一个项目中。Scrapy 需要使用 yield 语句,因为函数是由异步调度程序执行的。该parse_jd回调函数将返回代表招聘启事一本字典。

posting = {"job_title": job_title, "summary": summary, "company": company, "location": location}
jd_page = listing.get("href")
if jd_page is not None:
yield response.follow(jd_page, callback=self.parse_jd, cb_kwargs=posting)

现在剩下要做的就是解析工作描述,然后生成要收集的项目。幸运的是,职位描述有一个唯一的 ID。这是选择特定元素的最简单方法。我们想要保存职位描述的 URL,因为如果我们最终申请,我们将需要找到申请按钮的链接。

def parse_jd(self, response, **posting):
soup = BeautifulSoup(response.text, features="lxml")
jd = soup.find("div", {"id": "jobDescriptionText"}).get_text()
url = response.url
posting.update({"job_description": jd, "url": url})
yield posting

解析通常是编写蜘蛛程序中最具挑战性和最耗时的阶段。网站会随着时间的推移而变化,因此您需要在其中断时修改代码。添加一个验证步骤来检查None字符串或空字符串,然后引发错误会很有用。这样,当代码不再工作时,您会收到通知。这应该只对关键路径信息进行,因为丢失的信息可能很常见。

最后一步是告诉我们的爬虫去搜索结果的下一页。我们希望爬虫检索当前可用的每个帖子,而不仅仅是第一页上的结果。当下一页按钮不再可用时,我们就会知道我们已经完成了。

next_page = soup.find("a", {"aria-label": "Next"}).get("href")
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)

通过这些简单的指令集,我们现在有一个相当强大的过程来提取所有重要的细节。跟随下一页链接的能力意味着爬虫将抓取所有可用的结果,而无需任何额外的编码。现在我们已经完成了蜘蛛的编写,我们可以继续研究结果。

保存结果
现在解析器已经写好了,我们可以开始使用一些scrapy特性了。我们可以选择使用ItemPipline将每个发送Item到文件对象存储或数据库中。我们可以利用具有高写入吞吐量的高可用性分布式数据库(例如 DynamoDB、Cassandra),并在表运行时将项目插入到表中。

对于这个项目,我将使用内置提要选项从命令行创建提取。我会选择 JSON 行,因为 JSON 编码器将正确转义换行符和引号,作为编组到 JSON 的过程的一部分。与 CSV 相比,这可以在以后为您节省一些麻烦,其中一个额外的换行符或引号可能会导致解析错误和头痛。当架构不是静态的时,半结构化格式也很有用,每个项目可能在其包含的属性方面有所不同。

有一种方法可以在配置文件中指定提要,但我将向您展示如何从命令行执行此操作。我们将创建一个包含所有抓取到本地文件系统的数据的 JSON Lines 文件。从那里我们可以开始与结果交互以提取价值。

scrapy crawl indeed -o jobs.jl
这将需要一些时间来运行,具体取决于您配置的职位数量和位置。所有网站都有某种形式的速率限制。速率限制可以通过像 Cloudflare 这样的 CDN 或负载均衡器/反向代理来实现。速率限制可防止拒绝服务 (DoS) 攻击关闭 Web 服务器。Scrapy 将执行指数退避直到它得到 200 响应代码,这意味着它会在每次失败后等待更长的时间,直到请求成功。

分析结果
现在我们有了一个包含所有帖子的文件,我们可以开始分析了。进行分析的一种方法是使用 Jupyter 笔记本。Jupyter Notebook 可用于探索性数据分析和非线性编程。当我们为了发现和分析而编码时,我们不知道最终状态是什么样的。当我们编写代码时,我们需要改变事物的顺序,并做出重大改变,这就是它被称为非线性编程的原因。Jupyter 使这种编程风格更容易。

Jupyter notebooks 通过利用单元格和 iPython 促进了这种类型的开发。通过使用 iPython 作为后端,它允许您在 REPL 环境中工作。REPL 允许您快速查看所执行代码的输出,并在对象创建后保留它们。第二部分是可以移动、剪切、复制和删除的单元格。单元格使更改执行顺序和更改对象范围变得简单。我发现笔记本是我对结果进行分析的好地方。

结论
下一步是聚合和规范化数据,分析它,然后创建某种用户界面来访问它。例如,您可以有一个网站,该网站显示根据自定义条件排序和过滤的所有抓取的网站。您可以使用关键字检测来确定提供您最感兴趣的机会的列表的优先级。

使用scrapy 编写spider 将使您通过第一步,解析网页中的数据并保存它。这是任何依赖网络爬行数据的数据管道中的第一个组件。一旦您捕获了数据,您就可以开始为您希望的任何应用程序从中提取价值。

您可以从本教程下载包含所有代码的 python 文件。

版权声明:itnav123 发表于 2021-10-27 16:11:08。
转载请注明:如何使用 Scrapy 抓取网页 | 堆栈导航

暂无评论

暂无评论...