爬虫服务化

Web 应用中,一个重要的话题就是服务化,服务化意味着 Web 服务的体积得到减小,并提高了 Web 服务的内聚和解藕程度,并给用户提供了更加可扩展的服务。本文将会带大家了解如何服务化爬虫。

原因

在基于爬虫的 Web 服务中,爬虫关乎整个系统的正常运作。在以爬虫为核心的Web 服务中,我们面临着爬虫进程的管理、爬取数据的管理和爬取数据的分析等问题。在处理爬虫运行状态管理时,爬虫的运行状态需要记录起来以方便找出爬虫中止的原因,排除爬虫的故障。在处理爬虫数据时,根据爬虫的数据进而结构化存储的对应操作需要具备一定的灵活性和纠错能力。分析爬虫爬取的数据时,数据的分析和处理需要额处的处理,如使用其它的服务等等。同时,为了给用户或者开发者带来开放能力,又必须给用户提供爬虫服务的接口,方便进行二次开发,大大提高服务的可重用性。

做法

通常一个应用的服务化,总是离不开操作系统,在处理服务化时离不开以下几种形式:

命令行模式

命令行模式在处理爬虫服务故障时,是最简单,把日志重定向到文件就可以方便日后的故障排查。但数据的处理和分析需要单独对输出的数据文件进行处理,中间有一定的烦琐,如果数据量多的话,很考验服务器的 IO 处理能力和Web 服务读取数据的内存优化能力。通常命令行模式的做法与以下步骤相似:
  1. 执行爬虫命令,爬虫日志与数据单独保存
  2. 记录爬虫运行状态和退出值,如创建,运行,终止。保存爬虫的日志文件和数据文件。
  3. Web 服务在爬虫进程结束后,读取爬虫数据,和日志数据(如果需要)。

RESTful API/RPC

RESTful API 或者远程过程调用的形式在第一种形式的基础上,减少了服务器的 IO 处理能力。但无法优化内存,一但传输结构确定下来便难以提高结构与算法相适应的改进。

第三方托管(PaaS)

以 Scrapy 为例,Scrapy Hub 提供了 Scrapy Cloud ,可以替我们管理和运行爬虫。优点与 RESTful/RPC 形式相似。极大地减轻服务器负担(别人帮我们完成了!)并以 RESTful API 形式开放接口。
可以前往 Scrapy Cloud 了解更多信息。

示例

本示例以命令行模式进行构建爬虫服务,在开始之前,需要读者具备一定的 Python 编程能力,大致上能够想起怎样写类就差不多了。另外,本示例使用 Scrapy 作为爬虫框架。
我们首先需要一个 Pyhton 编译器,我使用的是 Python 3.x 可以在这里下载:https://python.org/ 。 安装好后,接着安装 Scrapy 爬虫框架:
pip install scrapy
接下来,讲述如何简单地创建一个爬虫项目,让大家熟悉一下。
可以直接把 loveletter-spider (https://github.com/codimiracle/loveletter-spider) 克隆下来这样可以节约不少的时间。 安装完 Scrapy 后,执行scrapy会得到以下结果:
$ scrapy
Scrapy 1.5.2 - no active project
Usage:
scrapy [options] [args]
Available commands:
bench Run quick benchmark test
fetch Fetch a URL using the Scrapy downloader
genspider Generate new spider using pre-defined templates
runspider Run a self-contained spider (without creating a project)
settings Get settings values
shell Interactive scraping console
startproject Create new project
version Print Scrapy version
view Open URL in browser, as seen by Scrapy
[ more ] More commands available when run from project directory
Use "scrapy -h" to see more info about a command
接着,运行 scrapy startproject [project_name] 创建一个爬虫项目。 进入 loveletter-spider 目录,再次执行 scrapy 你应该可以看到以下内容
$ scrapy
Scrapy 1.5.2 - project: loveletter
Usage:
scrapy [options] [args]
Available commands:
bench Run quick benchmark test
check Check spider contracts
crawl Run a spider
edit Edit spider
fetch Fetch a URL using the Scrapy downloader
genspider Generate new spider using pre-defined templates
list List available spiders
parse Parse URL (using its spider) and print the results
runspider Run a self-contained spider (without creating a project)
settings Get settings values
shell Interactive scraping console
startproject Create new project
version Print Scrapy version
view Open URL in browser, as seen by Scrapy
Use "scrapy -h" to see more info about a command
接着,我们可以使用 scrapy list 显示当前可以运行的 spider。使用 scrapy crawl [spider_name] 可以运行一个爬虫。不管怎样,您可以在上面给出的爬虫链接中得到提示。 我们需要的只是爬虫给我们产生的日志和数据。在上边给出的 loveletter-spider 中是 *.json 和 *.log 在这个爬虫运行结束后会产生这两个文件,我们的服务化过程就有了输入点。然后,我们需要做的就是运行 spider 和处理 json 和 log 文件。在 Java 中运行一条命令使用的是 Runtime#exec() 方法。具体要如做呢?
@Async
public ListenableFuture<CrawlingResult> crawlFully() {
String runId = getRunId();
String logfile = "crawled/" + runId + ".log";
String outfile = "crawled/" + runId + ".json";
String command = getCommand(outfile, logfile);
//插入爬取结果
CrawlingResult result = getCrawlingResult(runId, outfile, logfile);
crawlingResultRepository.insert(result);
try {
//执行爬虫
Process process = Runtime.getRuntime().exec(command);
result.setStatus(CrawlingResult.RUNNING);
crawlingResultRepository.updateIdempotently(result, CrawlingResult.CREATED);
//等待爬虫执行完成
while (process.isAlive()) {
Thread.sleep(3000);
}
//处理爬虫进程结束退出值
result.setExitValue(process.exitValue());
if (result.getExitValue() == 0) {
result.setStatus(CrawlingResult.FINISHED);
//载入爬取的数据
loadCrawledData(runId);
//分析爬取的数据
analysisCrawledData(runId);
} else {
result.setStatus(CrawlingResult.SPIDER_ERROR);
}
crawlingResultRepository.updateIdempotently(result, CrawlingResult.RUNNING);
return AsyncResult.forValue(result);
} catch (Exception e) {
log.error("spider service terminated:", e);
int previousStatus = result.getStatus();
result.setStatus(CrawlingResult.SYSTEM_ERROR);
crawlingResultRepository.updateIdempotently(result, previousStatus);
return AsyncResult.forExecutionException(e);
}
}
这样可以记录好爬虫执行状态。处理好爬虫的运行接着我们就需要处理爬取到的数据了,这时候我们可以这样处理例如:
try {
//读取爬取的数据
InputStream inputStream = Files.newInputStream(Paths.get(result.getOutfile()));
String rawData = StreamUtils.copyToString(inputStream, Charset.defaultCharset());
List<CrawledData> crawledDataList = JSON.parseArray(rawData, CrawledData.class);
//主题摘要
String[] rawThemeData = null;
Map<String, Theme> map = new HashMap<>();
for (CrawledData crawlData : crawledDataList) {
//处理爬虫数据,结构化地存入数据库
Theme theme = map.get(crawlData.title);
if (Objects.isNull(theme)) {
theme = new Theme();
BeanUtils.copyProperties(crawlData, theme);
rawThemeData = crawlData.title.split("\\|");
theme.setEpisode(rawThemeData[0]);
theme.setId(getThemeId(crawlData.title, crawlData.getPublishTime()));
theme.setRunId(runId);
map.put(crawlData.title, theme);
log.debug("read title data of crawled data: [{}]", theme);
themeRepository.insert(theme);
}
Letter letter = new Letter();
BeanUtils.copyProperties(crawlData, letter);
letter.setTheme(theme);
letter.setRunId(runId);
letter.setId(getLetterId(theme.getId(), crawlData.letterIndex));
log.debug("read letter data of crawled data: [{}]", letter);
letterRepository.insert(letter);
}
} catch (IOException e) {
log.error("can not load the data of crawled data by runId [{}], it throws [{}]", runId, e);
throw e;
}
这样如果中途出错应该怎么办呢?我们可以使用事务来帮助处理。在 Spring Boot 中我们使用 @Transaction(rollbackFor = IOException.class) 注解处理,若发生问题回滚操作。
@Async
@Transactional(rollbackFor = IOException.class)
public void loadCrawledData(String runId) throws IOException {
CrawlingResult result = crawlingResultRepository.findByRunId(runId);
if (Objects.nonNull(result)) {
// 与上一致
}
}
如果需要控制爬虫本身呢?我们需要把处理这个爬虫的线程Thread.currentThread()保存下来,并对其进行相应的结束,暂停等操作,不过需要注意的是一旦服务重启这个就会失去效用。

具体的爬虫和该示例的源代码:
爬虫:https://github.com/codimiracle/loveletter-spider
服务:https://github.com/codimiracle/loveletter-service

注意:Java 程序在运行时,PATH 路径要包含 Python 解析器。

总结

我们通过命令行模式进行爬虫的服务化,了解了 Web 应用中如何爬虫的服务化设计。在本次学习中,我们可以得到爬虫服务化的过程中需要注意以下几点:
  • 爬虫运行的方式,其运行方式决定了爬虫自身服务化的方式。
  • 爬虫状态管理,通常服务需要管理爬虫的状态,以方便进行爬虫的管理。这里笔者只是视作一项成功或失败的任务。
  • 爬取数据的处理与分析,爬取数据是需要处理和分析的。没有处理爬虫的数据,没有分析爬虫的数据,就得不到这次爬虫的成果。