前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >搜索引擎的预料库 —— 万恶的爬虫

搜索引擎的预料库 —— 万恶的爬虫

作者头像
老钱
发布2019-09-08 22:25:21
6070
发布2019-09-08 22:25:21
举报
文章被收录于专栏:码洞码洞

本节我们来生产一个简单的语料库 —— 从果壳网爬点文章。后面我们将使用这些文章来完成索引构建和关键词查询功能。

https://www.guokr.com/article/438188

果壳网的文章很容易遍历,因为它的文章 id 是自增的。我查阅了站点的最新文章,发现这个 id 还没有超过 45w,所以我打算从 1 开始遍历,扫描出所有的有效文章。

但是扫描 45w 个 URL 会非常漫长,所以我开启了多线程。但是线程也不敢开太多,网站可能有反扒策略快速封禁 IP(我可不想去整 IP 代理池),也可能服务器计算能力有限,爬一爬网站就挂了。

我并不期望自己能扫描出所有的文章,有那么几百篇也就够了,做人也不宜太贪婪。

有同学建议我使用 Go 语言来爬,开启协程比线程方便多了。这个还是留给读者当作学习 Go 语言的练习题吧,我是打算一杆子 Java 写到底了 —— 因为玩 Lucene 是离不开 Java 的。

45w 个文章 ID 如何在多个线程之间分配,需要将所有的 id 塞进一个队列,然后让所有的线程来争抢么?这也是一个办法,不过我选择了使用 AtomicInteger 在多个线程之间共享。

爬到的文章内容放在哪里呢?只放在内存里会丢失,存储到磁盘上有需要序列化和反序列化也梃繁琐,还需要考虑文件内容如何存储。所以我打算把内容统统放到 Redis 中,这会非常方便。但是会不会放不下呢?我们来计算一下,一篇文章的内容量大概会占用 5k,如果有 45w 篇文章,那么就需要 45w * 5k 的内存,大概也只有 2个多G,我的苹果本内存 16G,存这点内容还是绰绰有余的。

爬到的文章是 HTML 格式的,每个网页除了文章内容本身之外,还有很多其它的外链以及广告。那如何将其中的核心文章内容抽取出来,这又是一个问题。我这里选择了 Java 的 HTML 解析库 JSoup,它使用起来有点类似于 JQuery,可以使用选择器来快速定位节点抽取内容。同时它还可以作为一个非常方便的抓取器,自带了 HTTP 的请求工具类。也许读者会以为我会使用高级的机器学习来自动抽取文章内容,很抱歉,实现成本有点高。

下面我们来看看如何使用 JSoup,先导入依赖

代码语言:javascript
复制
<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.12.1</version>
</dependency>

抓取文章,将自己浏览器的 UserAgent 拷贝过来作为机器人的 UserAgent,伪装成一个正常的浏览器。当文章不存在时,果壳网并不是返回标准的 404 错误码。我们需要通过抽取网页内容来判断,如果抽取到的文章标题或者内容是空的,那么我们就认为这篇文章无效不存在。

抽取内容成功后,将内容存储到 Redis 中。因为抽取 45w 个网页时间上会有点漫长,我担心程序可能跑到一半就崩溃了,然后又不得不重新开始遍历。所以我打算记录一下抽取的状态,将抽取成功的文章 id 记录到一个 Redis 集合中。同时因为这 45w 个整数 id 有效的文章有可能连一半都占不到,所以我还会将无效的文章 id 也给记录下来,减少因为程序重启带来的无效爬虫抓取动作。

代码语言:javascript
复制
var agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36";
var url = String.format("https://www.guokr.com/article/%d/", id);
var res = Jsoup.connect(url)
        .header("HOST", "www.guokr.com")
        .header("User-Agent", agent)
        .execute();
if(res.statusCode() == 200) {
    var doc = Jsoup.parse(res.body());
    // 像极了 JQuery 有木有
    var divTitle = "div[class~=layout__Skeleton.*__ArticleTitle.*]";
    var divHTML = "div[class~=layout__Skeleton.*__ArticleContent.*]";
    var title = doc.selectFirst(divTitle).html();
    var html = doc.selectFirst(divHTML).html();
    if(!title.isEmpty() && !html.isEmpty()) {
      save2Redis(id, title, html);
      return;
    }
}
saveFailedId2Redis(id);

将文章内容存储到 Redis 中我打算使用 Hash 结构,分别存储 title 和 html 内容。因为后续我们还需要对文本进行去标签化等操作,这时就可以继续使用 Hash 结构存储处理后的干净的文本内容。

代码语言:javascript
复制
var redis = new JedisPool();
var db = redis.getResource();
// 将有效的文章 id 存储到一个集合中
db.sadd("valid_article_ids", String.valueOf(id));
db.hset(String.format("article_%d", id), "title", title);
db.hset(String.format("article_%d", id), "html", html);
db.close();

代码中的 db.Close() 表示将当前的 Jedis 链接归还给连接池,而不是关闭链接。

无效的文章 id 我们也要存起来。

代码语言:javascript
复制
db = redis.getResource();
db.sadd("invalid_article_ids", String.valueOf(id));
db.close();

这样当每个线程抢到一个 ID 之后,它要做的第一件事就是判断这个 ID 是否在有效的和无效的文章 ID 列表中,如果已经存在了,那就直接去抢下一个文章 ID。

代码语言:javascript
复制
var db = redis.getResource();
if(db.sismember("valid_article_ids", String.valueOf(id))) {
    db.close();
    return;
}
if(db.sismember("invalid_article_ids", String.valueOf(id))) {
    db.close();
    return;
}
db.close();
// 去爬吧
fetchArticle(id);

下面我们再来看看并发线程是如何开启和结束的,我只用了 16 个线程。最后需要使用 thread.join() 来等待所有线程终止,如果没有这行代码,程序会立即退出,想想为什么?

代码语言:javascript
复制
var idGen = new AtomicInteger();
var threads = new Thread[16];
var redis = new JedisPool();
for(int i=0;i<threads.length;i++) {
    threads[i] = new Thread(() -> {
        while(true) {
            var id = idGen.incrementAndGet();
            if(id > 450000) {
                break;
            }
            crawlArticle(redis, id);
        }
    });
    threads[i].start();
}
for(var thread: threads) {
    thread.join();
}
System.out.println("Game Over!");

程序总算跑起来了,但是跑了一段时间后我去 Redis 中查看了一下有效文章 ID 集合,发现里面之后 200 多个有效的文章 ID。果壳网难道只有 200 多篇文章,不可能,果壳网里面的科学文章可是包罗万象,少说应该也有几万篇吧,那这个究竟是怎么回事呢?

于是我将 Redis 中无效的文章 ID 集合清空,又重新跑了一下程序,打印了 HTTP 请求的状态码,发现非常非常多的 503 Service Unavailable 响应。我明白了 —— 网站的反爬策略起作用了,或者是服务扛不住 —— 挂了。我倾向于后者,因为我发现 HTTP 响应时好时坏,服务处于不稳定状态。通常反爬策略会持续一段时间封禁 IP,不会让你一会难受一会爽。

很无奈,我多跑了几次程序,最终收集了不到 1000 篇文章。这作为搜索引擎的语料库也差不多够用了,再死磕下去似乎会很不划算,所以今天的爬虫就到此为止。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-09-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码洞 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档