理想和现实

无论我有多么愚蠢,我都知道我需要有一个目标来让我达到我想达到样子。那个我需要达到的地方一定不会让我轻易地到达,到达的那个地方并不美好,或许没有自己真真切切能够触及到的东西,又或许只是平凡无奇的一片草地。但是,我想达到我理想的样子。
理想
C/C++ 指针指向着一个雪花,我想表达什么?雪花在自然界中是不会相同的,然而在画图中我们总能得到相同的雪花。C/C++ “->”(指针成员运算符)总是得到一个类型的内容(或许这个类型是你定义的)。背景百搭黑(我朋友说黑色是百搭的)可以无限的延伸,或许它代表世界?或许它代表我自己?不管这样, 
我想说明的是我要表达的东西是:编码出独一无二(哪来的独一无二?黑盒技术给我们带来的任意代码实现重复的可能性,这大概是无法完成的)。我想我会继续做下去吧?
现实
然后呀,愚蠢的我用 CSS 写出了这样的东西(好吧,做成这样让我萌生放弃的念头)。我想我没有做到现实中我要编码的东西呀,而且在现实中我也没有我想成为的那样子啊。那个理想的自己总是在旁边提醒着,引导着。而我却无耻地担心着,害怕着选择了最为容易的方式。正确的方式在理想的自己一声又一声的叹息中埋没,那个懈怠的自己不断地鼓励着自己走向不好的地方。一次接着一次,愚蠢地重蹈覆辙。不断重复地说出没经过脑子的的话语,不断地想回到起初的时间点。但是,我依然想达到自己想要达到的样子。
实现理想只需完成
我可以达到自己想要的样子吗?我用 CSS 做出了与刚开始不一样的东西,这个和刚开始完完全全不一样,风格也许大概是在哪里看过的吧,有我自己的东西吗?有我自己的印记吗?加以自己的 CSS 能力,发挥自己的创造力了吗?理想的那个自己会不会满意地给我一个答复了呢?那个懈怠的自己会不会嘲笑我:这个傻子做出了一个奇怪的,又不是自己的东西呢?那我自己会想什么?会是——我可以做的更好,达到我想要达到的样子吗?

实现理想是一个及其艰难的过程,也许你会为自己寻找着借口,让自己不去达到旁边那个理想的自己。但是你只需要付出一点点的努力便会让自己有改变的地方。改变自己总是对自己有着强大的帮助。如果你不知道理想的自己,我想你会在某本书、某句话语、某个人或者自己的内心深处得到正确的答案吧。
最后,我想达到我想达到的样子。

爬虫服务化

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