java爬虫抓取动态网页(Java爬虫服务器被屏蔽,不要慌,咱们换一台中)
优采云 发布时间: 2022-03-27 04:09java爬虫抓取动态网页(Java爬虫服务器被屏蔽,不要慌,咱们换一台中)
Java爬虫服务器被封了,别慌,换个服务器,我们简单说一下反爬策略和反反爬方法,主要是IP封堵及其对应的方法。之前的文章文章我们讲了爬虫的基础知识。在这篇文章中,我们来谈谈爬虫架构。
在前面的章节中,我们的爬虫程序都是单线程的。我们在调试爬虫程序的时候,单线程爬虫是没有问题的,但是当我们使用单线程爬虫程序去在线环境下的采集网页时,单线程爬虫会不行。暴露了两个致命问题:
线上的环境不能像我们本地的测试一样,不关心采集的效率,只要能正确提取结果即可。在这个时间就是金钱的时代,不可能给你时间慢慢来采集,所以单线程爬虫程序就不行了,我们需要把单线程改成多线程模式来改进采集 效率和提高计算机利用率。想学习交流HashMap、nginx、dubbo、Spring MVC、分布式、高性能、高可用、MySQL、redis、jvm、多线程、netty、kafka、家微信(佟英文):1253431195 扩展栏目获取资料供学习,无不加工作经验!
多线程爬虫的设计要比单线程的程序复杂得多,但又不同于其他需要在高并发下保证数据安全的业务。被视为一个独立的实体。做多线程爬虫,必须做好两件事:第一点是维护统一的采集 URL,第二点是对URL进行去重。简单说一下这两点。
将 URL 维护为 采集
多线程爬虫不能像单线程一样,每个线程独立维护自己的采集 URL,如果是这样的话,那么每个线程的网页采集都是一样的,你会不是多线程采集,而是要多次采集 一个页面。为此,我们需要将 URL 统一维护为 采集。每个线程从统一的URL维护处接收到采集 URL,完成采集任务。如果在页面上找到新 URL 链接将添加到统一 URL 维护的容器中。这里有几个适合统一 URL 维护的容器:
URL重复数据删除
URL去重也是多线程采集中的一个关键步骤,因为如果我们不去重,那么我们会采集到大量重复的URL,这并不能改善我们的采集 效率,例如在一个分页的新闻列表中,我们可以在 采集 的第一页获取到 2、3、4、5 个页面的链接,并且在采集的第二页会再次得到1、3、4、5个页面链接,采集的URL队列中会有大量列表页面链接@>,会重复采集甚至进入死循环,所以需要进行URL去重。有很多方法可以对 URL 进行重复数据删除。以下是几种常用的URL去重方法:
关于多线程爬虫的两个核心知识点大家都知道。下面我画一个简单的多线程爬虫架构图,如下图所示:
多线程爬虫架构图
以上,我们主要了解了多线程爬虫的架构设计。接下来,我们不妨试试Java多线程爬虫。我们以采集虎扑新闻为例,实战Java多线程爬虫,在Java多线程爬虫中设计的采集URL的维护和URL的去重,由于这里只是演示,所以我们将使用JDK的内置容器来完成它。我们使用 LinkedBlockingQueue 作为 URL 维护容器为 采集,HashSet 作为 URL 到 Heavy 容器。以下是Java多线程爬虫的核心代码。详细代码可以上传到 GitHub。地址在文末:想学习和交流HashMap,nginx,dubbo,Spring MVC,分布式,高性能高可用,
2. public class ThreadCrawler implements Runnable {
3. // 采集的文章数
4. private final AtomicLong pageCount = new AtomicLong(0);
5. // 列表页链接正则表达式
6. public static final String URL_LIST = “https://voice.hupu.com/nba”;
7. protected Logger logger = LoggerFactory.getLogger(getClass());
8. // 待采集的队列
9. LinkedBlockingQueue taskQueue;
10. // 采集过的链接列表
11. HashSet visited;
12. // 线程池
13. CountableThreadPool threadPool;
15. public ThreadCrawler(String url, int threadNum) throws InterruptedException {
16. this.taskQueue = new LinkedBlockingQueue();
17. this.threadPool = new CountableThreadPool(threadNum);
18. this.visited = new HashSet();
19. // 将起始页添加到待采集队列中
20. this.taskQueue.put(url);
21. }
23. @Override
24. public void run() {
25. logger.info(“Spider started!”);
26. while (!Thread.currentThread().isInterrupted()) {
27. // 从队列中获取待采集 URL
28. final String request = taskQueue.poll();
29. // 如果获取 request 为空,并且当前的线程采已经没有线程在运行
30. if (request == null) {
31. if (threadPool.getThreadAlive() == 0) {
32. break;
33. }
34. } else {
35. // 执行采集任务
36. threadPool.execute(new Runnable() {
37. @Override
38. public void run() {
39. try {
40. processRequest(request);
41. } catch (Exception e) {
42. logger.error(“process request “ + request + ” error”, e);
43. } finally {
44. // 采集页面 +1
45. pageCount.incrementAndGet();
46. }
47. }
48. });
49. }
50. }
51. threadPool.shutdown();
52. logger.info(“Spider closed! {} pages downloaded.”, pageCount.get());
53. }
55. protected void processRequest(String url) {
56. // 判断是否为列表页
57. if (url.matches(URL_LIST)) {
58. // 列表页解析出详情页链接添加到待采集URL队列中
59. processTaskQueue(url);
60. } else {
61. // 解析网页
62. processPage(url);
63. }
64. }
66. protected void processTaskQueue(String url) {
67. try {
68. Document doc = Jsoup.connect(url).get();
69. // 详情页链接
70. Elements elements = doc.select(” div.news-list > ul > li > div.list-hd > h4 > a”);
71. elements.stream().forEach((element -> {
72. String request = element.attr(“href”);
73. // 判断该链接是否存在队列或者已采集的 set 中,不存在则添加到队列中
74. if (!visited.contains(request) && !taskQueue.contains(request)) {
75. try {
76. taskQueue.put(request);
77. } catch (InterruptedException e) {
78. e.printStackTrace();
79. }
80. }
81. }));
82. // 列表页链接
83. Elements list_urls = doc.select(“div.voice-paging > a”);
84. list_urls.stream().forEach((element -> {
85. String request = element.absUrl(“href”);
86. // 判断是否符合要提取的列表链接要求
87. if (request.matches(URL_LIST)) {
88. // 判断该链接是否存在队列或者已采集的 set 中,不存在则添加到队列中
89. if (!visited.contains(request) && !taskQueue.contains(request)) {
90. try {
91. taskQueue.put(request);
92. } catch (InterruptedException e) {
93. e.printStackTrace();
94. }
95. }
96. }
97. }));
99. } catch (Exception e) {
100. e.printStackTrace();
101. }
102. }
104. protected void processPage(String url) {
105. try {
106. Document doc = Jsoup.connect(url).get();
107. String title = doc.select(“body > div.hp-wrap > div.voice-main > div.artical-title > h1”).first().ownText();
109. System.out.println(Thread.currentThread().getName() + ” 在 “ + new Date() + ” 采集了虎扑新闻 “ + title);
110. // 将采集完的 url 存入到已经采集的 set 中
111. visited.add(url);
113. } catch (IOException e) {
114. e.printStackTrace();
115. }
116. }
118. public static void main(String[] args) {
120. try {
121. new ThreadCrawler(“https://voice.hupu.com/nba”, 5).run();
122. } catch (InterruptedException e) {
123. e.printStackTrace();
124. }
125. }
126. }
我们用5个线程去采集hupu新闻列表页面看看效果。如果我们运行程序,我们会得到以下结果:
多线程采集结果
从结果可以看出,我们启动了 5 个线程 采集 到 61 个页面,总共耗时 2 秒。可以说效果还是不错的。我们用单线程对比一下,看看差距有多大?我们将线程数设置为1,再次启动程序,得到如下结果:
单线程运行结果
可以看到,单线程采集Hupu 61消息耗时7秒,几乎是多线程的4倍。如果你仔细想想,这只有 61 页。如果有更多的页面,差距会增加。该值越大,多线程爬虫的效率越高。想学交流HashMap、nginx、dubbo、Spring MVC、分布式、高性能、高可用、MySQL、redis、jvm、多线程、netty、kafka、家微信(佟英文):1253431195 扩展栏目获取资料供学习,无不加工作经验!
分布式爬虫架构
分布式爬虫架构是只有大型采集程序才需要使用的架构。一般可以使用单机多线程来解决业务需求。反正我没有分布式爬虫项目的经验,所以和这个没什么关系。是的,但作为技术人员,我们需要保持技术的热度。虽然我们不需要它,但理解它是可以的。我查了很多资料,得出以下结论:
分布式爬虫架构在思路上和我们的多线程爬虫架构是一样的。我们只需要在多线程的基础上稍加改进,就可以成为一个简单的分布式爬虫架构。由于在分布式爬虫架构中爬虫程序部署在不同的机器上,我们等待的URL采集和已经是采集的URL不能存储在爬虫程序机器的内存中,我们需要存储在某台机器上统一维护,比如存储在Redis或者MongoDB中。每台机器都从这里获取采集链接,而不是从LinkedBlockingQueue等内存队列中获取链接。这样一个简单的分发爬虫架构就出现了。当然会有很多细节,因为我没有分布式架构的经验,也谈不上。如果您有兴趣,欢迎交流。