大专就不配月薪3万么,我月薪是28000,总包在50左右,而他薪资预估在18000左右,还是211高材生,于是开始对我充满了敌意。
刚看到个贴子,大概意思是:一个大专生在公司做到月薪2万8,总包快50万,结果被一个211同事阴阳怪气,觉得“大专不配拿3万”。
网友回复我看了看,有人替211抱不平,说学历白读了;也有人说挣钱靠本事,学历只是入场券。我觉得这事吧,说到底是“鄙视链”害人。职场发工资,看的是岗位价值和解决问题的能力,不是毕业证上印了几个字。你能帮公司赚钱、扛得住活儿,老板才没空查你是哪一批次。
换个角度想,见不得别人挣得多,本质还是对自己不满意。与其酸同事,不如想想怎么提升自己,把预估18K干到实打实3万。别把学历当优越感,当成工具才值钱。总的来说,尊重每一种路径,大家日子都能越过越好。
面试题:多线程网页爬虫
我先说个事儿哈,昨天晚上十一点多,我在公司楼下抽烟,手机一刷,运维在群里喊:“谁的爬虫又把CPU打满了?日志全是一个线程在那儿慢悠悠地扒页面。” 我一看 IP,嘿,还是我们组的小李写的那个单线程爬新闻页面的玩意儿,活活跑了一晚上才爬了几百条,急死人那种。
小李那会儿就特别委屈:“哥,我不是不会多线程,就是一上并发我脑袋就浆糊了,感觉一堆线程同时抢 URL,会不会把服务器爬死,还会不会乱套啊?”
我当时困得眼睛都睁不开了,但还是给他在白板上画了个简化版“多线程网页爬虫”,你们可以想象一下那个画面:便利贴一张一张往墙上贴,其实就是爬虫的几个核心东西。
那个…多线程爬虫到底是个啥思路
你别一上来就想着线程池、锁这些吓人的词,先把场景想简单点:
有一堆还没爬的 URL,像待办事项一样堆在桌子上 有一堆“工人”(线程),一个人拿一张 URL 去干活 干完就顺手把新发现的链接再丢回桌子上 防止重复:桌上/仓库里要记一下“这个 URL 我来过了”
所以核心就三个小东西:
待爬队列:线程安全的队列,比如 ConcurrentLinkedQueue<String>已访问集合:用 ConcurrentHashMap.newKeySet()之类,防止重复爬线程池: ExecutorService,专门派工人干活的
等你能把这三个组合起来,基本上一个能跑的多线程爬虫雏形就出来了。
先看一眼关键代码,大致长这样
我给小李写了个最简版 demo,你们感受下,别纠结细节,整体结构先有个印象:
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
publicclassMultiThreadCrawler{
// 线程池,IO 密集型任务,线程数可以是 CPU 核心数的 2 倍左右
privatefinal ExecutorService pool =
new ThreadPoolExecutor(
8, // core
16, // max
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new LinkedBlockingQueue<>(1000),
r -> {
Thread t = new Thread(r);
t.setName("crawler-" + t.getId());
return t;
});
// 待爬 URL 队列
privatefinal ConcurrentLinkedQueue<String> urlQueue = new ConcurrentLinkedQueue<>();
// 已访问 URL 集合,防止重复爬
privatefinal Set<String> visited = ConcurrentHashMap.newKeySet();
// 简单限制一下总页面数,防止一不小心爬爆
privatefinal AtomicInteger pageCount = new AtomicInteger(0);
privatefinalint maxPages;
publicMultiThreadCrawler(String seedUrl, int maxPages){
this.maxPages = maxPages;
urlQueue.offer(seedUrl);
}
publicvoidstart(){
// 提交一批 worker 线程
for (int i = 0; i < 8; i++) {
pool.submit(this::worker);
}
}
privatevoidworker(){
while (true) {
if (pageCount.get() >= maxPages) {
break;
}
String url = urlQueue.poll();
if (url == null) {
// 没活干了,稍微等等看有没有新 URL
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
continue;
}
// 防止重复访问
if (!visited.add(url)) {
continue;
}
try {
crawlPage(url);
} catch (Exception e) {
System.err.println("抓取失败: " + url + " , err = " + e.getMessage());
}
}
}
privatevoidcrawlPage(String url)throws IOException {
if (pageCount.incrementAndGet() > maxPages) {
return;
}
// 这里你可以随便换成 OkHttp、HttpClient,或者 Jsoup
String html = HttpUtils.get(url); // 假装有这么个工具类
System.out.println(Thread.currentThread().getName()
+ " 爬到页面: " + url + " | 已爬: " + pageCount.get());
// 从 html 里解析出新链接,这里伪代码演示一下
for (String link : HtmlParser.extractLinks(html)) {
// 简单过滤一下域名、重复等
if (shouldVisit(link) && !visited.contains(link)) {
urlQueue.offer(link);
}
}
}
privatebooleanshouldVisit(String url){
// 比如只抓取某个站点的链接
return url.startsWith("https://example.com");
}
publicvoidshutdown(){
pool.shutdown();
}
publicstaticvoidmain(String[] args){
MultiThreadCrawler crawler = new MultiThreadCrawler("https://example.com", 500);
crawler.start();
}
}
上面有两个“我懒得写”的类 HttpUtils 和 HtmlParser,其实就是发 HTTP 和解析 HTML,你自己用 OkHttp + Jsoup 封一下就行了,跟算法关系不大,我就没展开写。
多线程这块最容易翻车的几个点
我当时跟小李说,别啥都怕“多线程很难”,其实真翻车的地方就那仨:
第一,共享数据要线程安全刚才代码里,队列用了 ConcurrentLinkedQueue,visited 用的是 ConcurrentHashMap.newKeySet()。 你要是图省事整一个 ArrayList + LinkedList,在 8 个线程一起改的时候,基本稳稳的出幺蛾子:重复爬、漏爬、甚至直接 ConcurrentModificationException。
第二,停止条件要想清楚比如你设了 maxPages = 500,那就得像上面那样用 AtomicInteger 去统计。 还有一种写法是:当队列空了并且 pageCount 到上限,就让线程 break 掉。很多人写着写着就变成了:线程一直在 while(true) 里空转,CPU 100%,人还以为是“性能很高”。
第三,别一上来就开 100 个线程爬虫大部分是 IO 密集,粗暴一点可以按照 “CPU 核心数 * 2~3” 来估个线程数,再慢慢压测调。 小李一开始直接 newFixedThreadPool(200),结果外网带宽、目标站点、自己机器的 IO,全都不是这个线程数能撑起来的,反而到处排队,还难调。
再唠两句爬虫的“规矩”
这个我每次都要强调一下,怕你们光顾着写代码忘了:
尊重对方网站, robots.txt该看要看,别瞎爬请求间隔可以搞个随机 sleep,别像 DDoS 一样怼 header 里带上合理的 UA,别搞得跟攻击脚本似的 真的要大规模爬,最好跟对方网站沟通一下,别让运维半夜被叫起来找你
反正那天在楼下我跟小李讲完,他回去把单线程那一坨 while 循环拆成了“队列 + visited + 线程池”这一套,第二天早上看监控,CPU 稳定,QPS 上去了,页面数也翻了几倍,人还挺开心,说之后要把 HttpUtils 和 HtmlParser 再封装一下,我说行,你先把 bug 修完再开心…
先这样吧,我咖啡也凉了,有空再跟你们扯扯怎么加“深度限制”“按层爬”“带优先级的队列”这些进阶玩法,不然小李又要喊我加班了。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html