实现爬虫即服务的实战指南
在当今数字化时代,数据的获取和处理变得至关重要。本文将详细介绍如何利用 Elasticsearch 进行特定技能的工作列表查询,如何修改 API 以支持按技能搜索工作,以及如何将容器部署到 AWS ECS 等内容。
1. 使用 Elasticsearch 查询特定技能的工作
我们可以使用之前创建的爬虫从 StackOverflow 抓取工作列表,并将其存储在 Elasticsearch 中。然后,扩展此功能以查询 Elasticsearch,找到包含一个或多个指定技能的工作列表。
1.1 准备工作
示例代码默认使用本地 Elastic Cloud 引擎,你可以根据需要进行更改。目前,我们将在本地运行的单个 Python 脚本中执行此过程,而不是在容器或 API 之后执行。
1.2 操作步骤
以下是具体的代码实现:
from sojobs.scraping import get_job_listing_info
from elasticsearch import Elasticsearch
import json
if __name__ == "__main__":
es = Elasticsearch()
job_ids = ["122517", "163854", "138222", "164641"]
for job_id in job_ids:
if not es.exists(index='joblistings', doc_type='job-listing', id=job_id):
listing = get_job_listing_info(job_id)
es.index(index='joblistings', doc_type='job-listing', id=job_id, body=listing)
search_definition = {
"query": {
"match": {
"JSON.skills": {
"query": "c#"
}
}
}
}
result = es.search(index="joblistings", doc_type="job-listing", body=search_definition)
print(json.dumps(result, indent=4))
上述代码的第一部分定义了四个工作列表,如果它们尚未存在于 Elasticsearch 中,则将其放入。它会遍历这些工作的 ID,如果不存在,则检索它们并将其放入 Elasticsearch。
剩余部分定义了一个要对 Elasticsearch 执行的查询,并遵循相同的搜索执行模式。唯一的区别在于搜索条件的定义。最终,我们希望将工作技能列表与工作列表中的技能进行匹配。
这个查询只是将单个技能与我们工作列表文档中的技能字段进行匹配。示例指定我们要匹配目标文档中的 JSON.skills 属性。这些文档中的技能就在文档的根目录下,因此在这种语法中,我们在前面加上 JSON 。
在 Elasticsearch 中,这个属性是一个数组,如果该属性数组中的任何值为 “c#”,则查询值将匹配该文档。
运行这个搜索,在 Elasticsearch 中只有这四个文档的情况下,结果如下:
{
"took": 4,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1.031828,
"hits": [
...
]
}
}
如果我们不想为每个匹配项返回整个文档,可以修改查询以仅返回匹配项中的 ID。将 search_definition 变量更改为以下内容:
search_definition = {
"query": {
"match": {
"JSON.skills": {
"query": "c# sql"
}
}
},
"_source": ["ID"]
}
执行这个查询的结果如下:
{
"took": 4,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1.031828,
"hits": [
{
"_index": "joblistings",
"_type": "job-listing",
"_id": "164641",
"_score": 1.031828,
"_source": {
"ID": "164641"
}
},
{
"_index": "joblistings",
"_type": "job-listing",
"_id": "122517",
"_score": 0.9092852,
"_source": {
"ID": "122517"
}
}
]
}
}
现在,每个匹配项只返回文档的 ID 属性。如果有很多匹配项,这将有助于控制结果的大小。
要识别包含多个技能的文档,只需对 search_definition 进行简单更改:
search_definition = {
"query": {
"match": {
"JSON.skills": {
"query": "c# sql",
"operator": "AND"
}
}
},
"_source": [
"ID"
]
}
这表示我们只想要技能中同时包含 “c#” 和 “sql” 的文档。运行脚本的结果如下:
{
"took": 4,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1.031828,
"hits": [
{
"_index": "joblistings",
"_type": "job-listing",
"_id": "164641",
"_score": 1.031828,
"_source": {
"ID": "164641"
}
},
{
"_index": "joblistings",
"_type": "job-listing",
"_id": "122517",
"_score": 0.9092852,
"_source": {
"ID": "122517"
}
}
]
}
}
结果集现在减少到两个匹配项,如果你检查,这是技能中包含这些值的仅有的两个文档。
2. 修改 API 以按技能搜索工作
我们将修改现有的 API,添加一个方法以支持按一组技能搜索工作。
2.1 操作步骤
我们将对 API 实现进行两个基本更改:
- 添加一个额外的 Flask-RESTful API 实现以支持搜索功能。
- 通过环境变量使 Elasticsearch 和我们自己的微服务的地址可配置。
API 实现位于 11/04_scraper_api.py 文件中。默认情况下,该实现尝试连接到本地系统上的 Elasticsearch。如果你使用的是 Elastic Cloud,请确保更改 URL(并确保索引中有文档)。
启动 API 只需执行以下脚本:
$ python scraper_api.py
Starting the job listing API ...
* Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
* Restarting with stat
Starting the job listing API ...
* Debugger is active!
* Debugger pin code: 449-370-213
要进行搜索请求,我们向 /joblistings/search 端点发送 POST 请求,以 “skills=<用空格分隔的技能>” 的形式传递数据。以下是搜索包含 C# 和 SQL 技能的工作的示例:
$ curl localhost:8080/joblistings/search -d "skills=c# sql"
{
"took": 4,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1.031828,
"hits": [
{
"_index": "joblistings",
"_type": "job-listing",
"_id": "164641",
"_score": 1.031828,
"_source": {
"ID": "164641"
}
},
{
"_index": "joblistings",
"_type": "job-listing",
"_id": "122517",
"_score": 0.9092852,
"_source": {
"ID": "122517"
}
}
]
}
}
我们现在可以通过 REST 在互联网上访问搜索功能了。
2.2 工作原理
这是通过添加另一个 Flask-RESTful 类实现的:
from flask_restful import Resource, Api
import os
from elasticsearch import Elasticsearch
import sys
from flask import request
class JobSearch(Resource):
def post(self):
skills = request.form['skills']
print("Request for jobs with the following skills: " + skills)
host = 'localhost'
if os.environ.get('ES_HOST'):
host = os.environ.get('ES_HOST')
print("ElasticSearch host: " + host)
es = Elasticsearch(hosts=[host])
search_definition = {
"query": {
"match": {
"JSON.skills": {
"query": skills,
"operator": "AND"
}
}
},
"_source": ["ID"]
}
try:
result = es.search(index="joblistings", doc_type="job-listing", body=search_definition)
print(result)
return result
except:
return sys.exc_info()[0]
api = Api()
api.add_resource(JobSearch, '/', '/joblistings/search')
这个类实现了一个 post 方法,作为映射到 /joblistings/search 的资源。使用 POST 操作的原因是我们要传递一个由多个单词组成的字符串。虽然这可以在 GET 操作中进行 URL 编码,但 POST 允许我们将其作为键值传递。而且,虽然我们只有一个键 skills ,但未来扩展以支持其他搜索参数的其他键可以简单地添加。
3. 在环境中存储配置
为了使应用程序更具可移植性,我们应该通过环境变量传递配置。在我们的工作列表实现中,使用以下代码确定 Elasticsearch 的主机:
host = 'localhost'
if os.environ.get('ES_HOST'):
host = os.environ.get('ES_HOST')
print("ElasticSearch host: " + host)
es = Elasticsearch(hosts=[host])
这是一个简单直接的操作,但对于使我们的应用程序能够轻松部署到不同环境非常重要。默认情况下使用 localhost ,但我们可以通过 ES_HOST 环境变量定义不同的主机。
技能搜索的实现也进行了类似的更改,允许我们更改抓取微服务的默认 localhost :
CONFIG = {'AMQP_URI': "amqp://guest:guest@localhost"}
if os.environ.get('JOBS_AMQP_URL'):
CONFIG['AMQP_URI'] = os.environ.get('JOBS_AMQP_URL')
print("AMQP_URI: " + CONFIG["AMQP_URI"])
with ClusterRpcProxy(CONFIG) as rpc:
...
4. 创建 AWS IAM 用户和密钥对以访问 ECS
为了将我们的爬虫和 API 打包成 Docker 容器并部署到 AWS ECS,我们需要创建一个具有 ECS 权限的 IAM 用户账户和密钥对。
4.1 准备工作
假设你已经创建了一个 AWS 账户。我们需要创建一个非根用户,该用户具有使用 ECS 的权限。
4.2 操作步骤
创建具有 ECS 权限的 IAM 用户和密钥对的说明可以在 这里 找到。
在创建 IAM 账户时,需要确保分配了以下权限,请参考 详细说明 。
创建用户时,需要记录访问密钥 ID 和关联的秘密密钥。如果没有记录,可以在用户账户页面的安全凭证选项卡中创建另一个。
5. 配置 Docker 以与 ECR 进行身份验证
为了将我们的容器推送到 Elastic Container Repository (ECR),需要配置 Docker 进行身份验证。
5.1 准备工作
确保安装了 AWS 命令行工具,安装说明可以在 这里 找到。安装验证后,使用 aws configure 命令配置 CLI 以使用上一步创建的账户:
$ aws configure
AWS Access Key ID [None]: AKIA---------QKCVQAA
AWS Secret Access Key [None]: KEuSaLgn4dpyXe-------------VmEKdhV
Default region name [None]: us-west-2
Default output format [None]: json
将密钥替换为你之前检索到的密钥,并设置默认区域和数据类型。
5.2 操作步骤
执行以下命令,该命令返回一个用于对 Docker 与 ECR 进行身份验证的命令:
$ aws ecr get-login --no-include-email --region us-west-2
docker login -u AWS -p <secret> https://270157190882.dkr.ecr.us-west-2.amazonaws.com
在 Mac(和 Linux)上,通常可以简化为以下命令:
$(aws ecr get-login --no-include-email --region us-west-2)
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login Succeeded
此时,我们可以使用 docker 命令将容器推送到 ECR。
6. 将容器推送到 ECR
我们将重建 API 和微服务容器,并将它们推送到 ECR。同时,我们还将推送一个 RabbitMQ 容器到 ECR。
6.1 准备工作
由于 ECS 无法直接从 Docker Hub 拉取 RabbitMQ 容器,我们需要将其推送到 ECR。从家庭网络连接将这些容器推送到 ECR 可能需要很长时间,建议在与 ECR 相同区域的 EC2 上创建一个 Linux 映像,从 GitHub 下载代码,在该 EC2 系统上构建容器,然后推送到 ECR。
6.2 操作步骤
首先,在本地系统上重建 API 容器:
$ docker build ../.. -f Dockerfile-api -t scraper-rest-api:latest
Dockerfile-api 的内容如下:
FROM python:3
WORKDIR /usr/src/app
RUN pip install Flask-RESTful Elasticsearch Nameko
COPY 11/11/scraper_api.py .
CMD ["python", "scraper_api.py"]
然后构建爬虫微服务容器:
$ docker build ../.. -f Dockerfile-microservice -t scraper-microservice:latest
Dockerfile-microservice 的内容如下:
FROM python:3
WORKDIR /usr/src/app
RUN pip install nameko BeautifulSoup4 nltk lxml
RUN python -m nltk.downloader punkt -d /usr/share/nltk_data all
COPY 11/05/scraper_microservice.py .
COPY modules/sojobs sojobs
CMD ["python", "-u", "scraper_microservice.py"]
现在我们使用 python 而不是 nameko run 命令来运行微服务。这是因为在 ECS 中启动容器的顺序存在问题, nameko run 命令在 RabbitMQ 服务器尚未运行时表现不佳,而在 ECS 中不能保证 RabbitMQ 服务器已经运行。因此,我们使用 python 启动微服务,并且实现中有一个启动过程,本质上复制了 nameko run 的代码,并使用一个 while 循环和异常处理程序进行包装,在容器停止之前重试连接。
总结
通过以上步骤,我们实现了使用 Elasticsearch 查询特定技能的工作列表,修改了 API 以支持按技能搜索工作,将配置存储在环境变量中,创建了 AWS IAM 用户和密钥对,配置了 Docker 以与 ECR 进行身份验证,并将容器推送到 ECR。这些步骤为将我们的爬虫应用程序转变为真正的云服务奠定了基础。
流程图
graph LR
A[准备工作] --> B[使用 Elasticsearch 查询工作]
B --> C[修改 API 支持搜索]
C --> D[存储配置到环境变量]
D --> E[创建 AWS IAM 用户和密钥对]
E --> F[配置 Docker 与 ECR 认证]
F --> G[推送容器到 ECR]
表格
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 使用 Elasticsearch 查询工作 | 从 StackOverflow 抓取工作列表,存储到 Elasticsearch 并查询特定技能的工作 |
| 2 | 修改 API 支持搜索 | 添加搜索功能,使 API 可通过环境变量配置 |
| 3 | 存储配置到环境变量 | 通过环境变量传递配置,提高应用程序可移植性 |
| 4 | 创建 AWS IAM 用户和密钥对 | 创建具有 ECS 权限的用户和密钥对 |
| 5 | 配置 Docker 与 ECR 认证 | 配置 Docker 以将容器推送到 ECR |
| 6 | 推送容器到 ECR | 重建并推送 API、微服务和 RabbitMQ 容器到 ECR |
实现爬虫即服务的实战指南
7. 深入探讨搜索操作
在前面的内容中,我们已经了解了如何使用 Elasticsearch 进行特定技能的工作列表查询。这里我们进一步探讨搜索操作的一些细节。
7.1 搜索条件的灵活性
在之前的示例中,我们使用了“match”查询来匹配技能。实际上,Elasticsearch 提供了丰富的查询 DSL(Domain Specific Language),可以根据不同的需求构建复杂的查询。例如,我们可以使用“bool”查询来组合多个条件:
search_definition = {
"query": {
"bool": {
"must": [
{
"match": {
"JSON.skills": {
"query": "c#"
}
}
},
{
"match": {
"JSON.location": {
"query": "New York"
}
}
}
]
}
},
"_source": ["ID"]
}
这个查询表示我们要查找技能包含“c#”且工作地点在“New York”的工作列表。
7.2 搜索结果的排序
默认情况下,Elasticsearch 根据相关性对搜索结果进行排序。但我们也可以根据其他字段进行排序。例如,根据工作的发布时间进行排序:
search_definition = {
"query": {
"match": {
"JSON.skills": {
"query": "c#"
}
}
},
"sort": [
{
"JSON.publish_date": {
"order": "desc"
}
}
],
"_source": ["ID"]
}
这里我们使用“sort”字段,按照“JSON.publish_date”字段降序排列结果。
8. API 扩展与优化
我们已经修改了 API 以支持按技能搜索工作,接下来讨论一些 API 的扩展和优化方向。
8.1 增加搜索参数
目前我们的 API 只支持按技能搜索,我们可以扩展它以支持更多的搜索参数,如工作地点、薪资范围等。在 JobSearch 类中进行相应修改:
class JobSearch(Resource):
def post(self):
skills = request.form.get('skills', '')
location = request.form.get('location', '')
salary_min = request.form.get('salary_min', 0)
salary_max = request.form.get('salary_max', float('inf'))
print(f"Request for jobs with skills: {skills}, location: {location}, salary between {salary_min} and {salary_max}")
host = 'localhost'
if os.environ.get('ES_HOST'):
host = os.environ.get('ES_HOST')
print("ElasticSearch host: " + host)
es = Elasticsearch(hosts=[host])
search_definition = {
"query": {
"bool": {
"must": []
}
},
"_source": ["ID"]
}
if skills:
search_definition["query"]["bool"]["must"].append({
"match": {
"JSON.skills": {
"query": skills,
"operator": "AND"
}
}
})
if location:
search_definition["query"]["bool"]["must"].append({
"match": {
"JSON.location": {
"query": location
}
}
})
if salary_min:
search_definition["query"]["bool"]["must"].append({
"range": {
"JSON.salary": {
"gte": int(salary_min)
}
}
})
if salary_max:
search_definition["query"]["bool"]["must"].append({
"range": {
"JSON.salary": {
"lte": int(salary_max)
}
}
})
try:
result = es.search(index="joblistings", doc_type="job-listing", body=search_definition)
print(result)
return result
except:
return sys.exc_info()[0]
8.2 性能优化
为了提高 API 的性能,我们可以考虑使用缓存机制。例如,使用 Redis 缓存搜索结果,当有相同的搜索请求时,先检查缓存中是否存在结果,如果存在则直接返回,避免重复查询 Elasticsearch。
9. 容器部署的最佳实践
在将容器推送到 ECR 并部署到 ECS 的过程中,有一些最佳实践可以遵循。
9.1 容器编排
使用容器编排工具,如 Docker Compose 或 Kubernetes,可以更方便地管理多个容器的部署和运行。例如,使用 Docker Compose 可以定义多个服务(API、微服务、RabbitMQ 等)之间的依赖关系和配置:
version: '3'
services:
api:
build:
context: .
dockerfile: Dockerfile-api
ports:
- "8080:8080"
environment:
ES_HOST: elasticsearch
microservice:
build:
context: .
dockerfile: Dockerfile-microservice
environment:
AMQP_URI: amqp://rabbitmq
elasticsearch:
image: elasticsearch:7.17.3
ports:
- "9200:9200"
rabbitmq:
image: rabbitmq:3.9-management
ports:
- "5672:5672"
- "15672:15672"
9.2 监控与日志
在容器部署后,需要对其进行监控和日志记录。可以使用工具如 Prometheus 和 Grafana 进行监控,使用 ELK Stack(Elasticsearch、Logstash、Kibana)进行日志收集和分析。
10. 安全考虑
在整个过程中,安全是至关重要的。
10.1 Elasticsearch 安全
对于 Elasticsearch,需要设置合适的访问权限。可以使用 Elasticsearch 的安全功能,如角色和用户管理,限制对 Elasticsearch 的访问。
10.2 容器安全
在构建和运行容器时,要确保容器的安全性。例如,使用最小化的基础镜像,及时更新容器中的软件包,避免在容器中运行不必要的服务。
11. 未来展望
随着技术的不断发展,我们可以对现有的爬虫即服务系统进行进一步的扩展和优化。
11.1 集成机器学习
可以使用机器学习技术对工作列表进行分类和推荐。例如,根据用户的搜索历史和偏好,推荐更符合用户需求的工作。
11.2 多数据源支持
目前我们只从 StackOverflow 抓取工作列表,可以扩展到支持更多的数据源,如 Indeed、LinkedIn 等。
总结回顾
通过本文的学习,我们深入了解了如何使用 Elasticsearch 进行工作列表的查询,如何修改 API 以支持搜索功能,如何将配置存储在环境变量中,以及如何将容器部署到 AWS ECS。同时,我们还探讨了搜索操作的细节、API 的扩展和优化、容器部署的最佳实践、安全考虑以及未来的发展方向。这些知识和技能将帮助我们构建一个高效、安全、可扩展的爬虫即服务系统。
流程图
graph LR
A[搜索操作] --> B[API 扩展与优化]
B --> C[容器部署最佳实践]
C --> D[安全考虑]
D --> E[未来展望]
表格
| 步骤 | 操作 | 说明 |
|---|---|---|
| 7 | 深入探讨搜索操作 | 了解 Elasticsearch 查询 DSL 的灵活性和搜索结果的排序 |
| 8 | API 扩展与优化 | 增加搜索参数,优化 API 性能 |
| 9 | 容器部署最佳实践 | 使用容器编排工具,进行监控和日志记录 |
| 10 | 安全考虑 | 确保 Elasticsearch 和容器的安全性 |
| 11 | 未来展望 | 集成机器学习,支持多数据源 |
超级会员免费看
5751

被折叠的 条评论
为什么被折叠?



