博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
搜索引擎的预料库 —— 万恶的爬虫
阅读量:2442 次
发布时间:2019-05-10

本文共 4158 字,大约阅读时间需要 13 分钟。

640?wx_fmt=png

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

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,先导入依赖

    
org.jsoup
    
jsoup
    
1.12.1

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

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

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 结构存储处理后的干净的文本内容。

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 我们也要存起来。

db = redis.getResource();db.sadd("invalid_article_ids", String.valueOf(id));db.close();

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

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() 来等待所有线程终止,如果没有这行代码,程序会立即退出,想想为什么?

var idGen = new AtomicInteger();var threads = new Thread[16];var redis = new JedisPool();for(int i=0;i
 {        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 篇文章。这作为搜索引擎的语料库也差不多够用了,再死磕下去似乎会很不划算,所以今天的爬虫就到此为止。下一篇文章我们来使用 Lucene 构建文章的索引。

如果你想要仔细阅读今天的爬虫代码,可以点击「阅读原文」进入 Github Gist 查看(有可能需要翻墙)。如果你要尝试本节代码,请将线程数调低一点,以免对果壳网产生 DDOS 攻击 —— 后果自负。

下面是有钱(有老钱)的字节跳动内推入口,找出自己心意的职位后,请勇于投递你的个人简历,内部系统的数据安全性做得非常到位,我是看不到你们的简历内容的,所以不必太担心个人隐私问题。北京、上海、深圳、杭州、成都、广州等城市的职位都有,Python、Java、Golang、算法、数据、分布式计算和存储的岗位也都有,各人发挥自己的搜商自行搜寻。请认真投递自己的简历,否则可能连面试的机会都没有。

640?wx_fmt=png

转载地址:http://csbqb.baihongyu.com/

你可能感兴趣的文章
ansible权威指南_如何使用Ansible:参考指南
查看>>
git克隆github_如何使用Git和Github分叉,克隆和推送更改
查看>>
golang中使用指针_了解Go中的指针
查看>>
盖茨比乔布斯_使用盖茨比的useStaticQuery挂钩的快速指南
查看>>
如何在Go中定义和调用函数
查看>>
react 分页_如何使用React构建自定义分页
查看>>
angular 模块构建_使用传单在Angular中构建地图,第4部分:形状服务
查看>>
服务周期性工作内容_使服务工作者生命周期神秘化
查看>>
如何在Ubuntu 20.04上安装TensorFlow
查看>>
react项目设置全局变量_如何使用宗地设置React项目
查看>>
如何在Python 3中使用sqlite3模块
查看>>
如何在Node.js中编写异步代码
查看>>
nuxt.js 全局 js_如何在Nuxt.js应用程序中实现身份验证
查看>>
优雅编写js条件语句_如何在Go中编写条件语句
查看>>
debian文件系统_如何在Debian 10上设置文件系统配额
查看>>
angular id标记_使用传单在Angular中构建地图,第2部分:标记服务
查看>>
命令行基础知识:使用ImageMagick调整图像大小
查看>>
通过Angular,Travis CI和Firebase托管进行连续部署
查看>>
debian docker_如何在Debian 10上安装和使用Docker
查看>>
python pyenv_如何使用Pyenv和Direnv管理Python
查看>>