大数据之网络爬虫-一个简单的多线程爬虫

   本文介绍一个简单的多线程并发爬虫,这里说的简单是指爬取的数据规模不大,单机运行,并且不使用数据库,但保证多线程下的数据的一致性,并且能让爬得正起劲的爬虫停下来,而且能保存爬取状态以备下次继续。

  爬虫实现的步骤基本如下:

  • 分析网页结构,选取自己感兴趣的部分;

  • 建立两个Buffer,一个用于保存已经访问的URL,一个用户保存带访问的URL;

  • 从待访问的Buffer中取出一个URL来爬取,保存这个URL中感兴趣的信息;并将这个URL加入已经访问的Buffer中,然后将这个URL中的所有外链URLs中没有被访问过的URL加到待访问Buffer;

  • 只要待访问的Buffer不为空,重复上一步。

  这次是为了给博客园的用户进行一次pagerank排名,爬取了博客园各个用户的粉丝与关注者。博客园的用户用17万多个。爬取的页面是http://home.cnblogs.com/u/+userId,每个用户的url只需要用用户的id表示就可以了,用户id按平均10B来计算,保存所有用户也只需1.7Mb内存。因此我把两个Buffer都放在内存中。

  这个项目的就四个java文件,结构如下:  

  下面对整个爬虫的实现过程进行详细的介绍。

一、登录

  要获取用户的粉丝与关注,必须先登录,博客园的模拟登陆算是比较简单,找到登录时要上传的参数,然后Pos发送即登录成功,可以使用Chrome的工具,打开登录页面,调好账号和密码后,按F12弹出工具,按登录就能看到要传的参数了,再POST一个这些参数就好了。  

  我之前是使用这样的方式,后来用使用Jsoup解析的参数,代码实现如下:


 1   /**
2 * 使用Joup解析登录参数,然后POST发送参数实现登录
3 *
4 * @throws UnsupportedEncodingException
5 * @throws IOException
6 */
7 private static void login() throws UnsupportedEncodingException,
8 IOException {
9 CookieHandler.setDefault(new CookieManager());
10 // 获取登录页面
11 String page = getPage(LOGIN_URL);
12 // 从登录去取出参数,并填充账号和密码
13 Document doc = Jsoup.parse(page);
14 // 取登录表格
15 Element loginform = doc.getElementById("frmLogin");
16 Elements inputElements = loginform.getElementsByTag("input");
17 List<String> paramList = new ArrayList<String>();
18 for (Element inputElement : inputElements) {
19 String key = inputElement.attr("name");
20 String value = inputElement.attr("value");
21 if (key.equals("tbUserName"))
22 value = Test.Name;
23 else if (key.equals("tbPassword"))
24 value = Test.passwd;
25 paramList.add(key + "=" + URLEncoder.encode(value, "UTF-8"));
26 }
27 // 封装请求参数
28 StringBuilder para = new StringBuilder();
29 for (String param : paramList) {
30 if (para.length() == 0) {
31 para.append(param);
32 } else {
33 para.append("&" + param);
34 }
35 }
36 // POST发送登录
37 String result = sendPost(LOGIN_URL, para.toString());
38 if (!result.contains("followees")) {
39 cookies = null;
40 System.out.println("登录失败");
41 } else
42 System.out.println("登录成功");
43 }

二、获取粉丝与关注

  登录成功就可以爬取粉丝和关注了,关注在http://home.cnblogs.com/u/userid/followees/链接中,而粉丝在http://home.cnblogs.com/u/userid/followers/,两个网页结构基本相同,只需要把选择一下followees(被关注者)和(followers)关注者,用Jsoup解析avatar_list中avatar_name就好了,代码如下:


 1   /**
2 * 获取一页中的关注or粉丝
3 *
4 * @param pageHtml
5 * @return
6 */
7
8 private List<String> getOnePageFriends(Document doc) {
9 List<String> firends = new ArrayList<String>();
10 Elements inputElements = doc.getElementsByClass("avatar_name");
11 for (Element inputElement : inputElements) {
12 Elements links = inputElement.getElementsByTag("a");
13 for (Element link : links) {
14 //从href中解析出用户id
15 String href = link.attr("href");
16 firends.add(href.substring(3, href.length() - 1));
17 }
18 }
19 return firends;
20 }

  每一页显示50个粉丝or关注者,需要分页爬取,获取下一页跟获取用户粉丝差不多,找到元素就好。

三、爬取单个用户

  爬取的一个用户的过程就是先分页爬取粉丝,再爬取关注者,然后把爬过的用户放入访问Buffer中,再把爬到的用户放到未访问队列中。


 1     @Override
2 public void run() {
3 while (stop.get() == false) {
4 // 取出一个待访问
5 String userId = mUserBuffer.pickOne();
6 try {
7 // 爬取粉丝
8 List<String> fans = crawUser(userId, "/followers");
9 // 爬取关注者
10 List<String> heros = crawUser(userId, "/followees");
11 // 只需要保持粉丝关系即可
12 StringBuilder sb = new StringBuilder(userId).append("\t");
13 for (String friend : fans) {
14 sb.append(friend).append("\t");
15 }
16 sb.deleteCharAt(sb.length() - 1).append("\n");
17 saver.save(sb.toString());
18 // 被关注者应该放进队列里面,以供下次爬取他的粉丝
19 fans.addAll(heros);
20 mUserBuffer.addUnCrawedUsers(fans);
21 } catch (Exception e) {
22 saver.log(e.getMessage());
23 // 访问错误时,放入访问出错的队列中,以备以后重新访问。
24 mUserBuffer.addErrorUser(userId);
25 }
26 }

  一页一页爬取单个用户如下:


 1       /**
2 * 爬取用户,根据tag来决定是爬该用户关注的人,还是该用户的粉丝
3 *
4 * @param userId
5 * @return
6 * @throws IOException
7 */
8 private List<String> crawUser(String userId, String tag) throws IOException {
9 //构造URL
10 StringBuilder urlBuilder = new StringBuilder(USER_HOME);
11 urlBuilder.append("/u/").append(userId).append(tag);
12 //请求页面
13 String page = getPage(urlBuilder.toString());
14 Document doc = Jsoup.parse(page);
15 List<String> friends = new ArrayList<String>();
16 //爬取第一页
17 friends.addAll(getOnePageFriends(doc));
18 String nextUrl = null;
19 //不断地爬取下一页
20 while ((nextUrl = getNextUrl(doc)) != null) {
21 page = getPage(nextUrl);
22 doc = Jsoup.parse(page);
23 friends.addAll(getOnePageFriends(doc));
24 }
25 return friends;
26 }

   整个爬虫结构就是这样了:


  1 public class UserCrawler implements Runnable {
2 // 停止任务标志
3 private static AtomicBoolean stop;
4 // 当前爬虫的id
5 private int id;
6 // 用户缓存
7 private UserBuffer mUserBuffer;
8 // 日志与粉丝保存工具
9 private Saver saver;
10
11 static {
12 stop = new AtomicBoolean(false);
13 try {
14 // 登录一次即可
15 login();
16 // 保存数据线程先启动
17 Saver.getInstance().start();
18 } catch (IOException e) {
19 e.printStackTrace();
20 }
21 // new Thread(new CommandListener()).start();
22 }
23
24 public UserCrawler(UserBuffer userBuffer) {
25 mUserBuffer = userBuffer;
26 mUserBuffer.crawlerCountIncrease();
27 id = c++;
28 saver = Saver.getInstance();
29 }
30
31 @Override
32 public void run() {
33 if (id > 0) {
34 // 等第一个线程启动一段时候再开始新的线程
35 try {
36 TimeUnit.SECONDS.sleep(20 + id);
37 } catch (InterruptedException e) {
38 e.printStackTrace();
39 }
40 }
41 System.out.println("UserCrawler " + id + " start");
42 int retry = 3;// 重置尝试次数
43 while (stop.get() == false) {
44 // 取出一个待访问
45 String userId = mUserBuffer.pickOne();
46 if (userId == null) {// 队列元素已经为空
47 retry--;// 重试3次
48 if (retry <= 0)
49 break;
50 continue;
51 }
52 ...//爬取用户
53 }
54 System.out.println("UserCrawler " + id + " stop");
55 // 当前线程停止了
56 mUserBuffer.crawlerCountDecrease();
57 }
58
59 private List<String> crawUser(String userId, String tag) throws IOException {
60
61 }
62
63 /**
64 * 获取一页中的关注or粉丝
65 *
66 * @param pageHtml
67 * @return
68 */
69
70 private List<String> getOnePageFriends(Document doc) {
71 ...
72 }
73
74 /**
75 * 获取下一页的地址
76 *
77 * @param doc
78 * @return
79 */
80 private String getNextUrl(Document doc) {
81
82 }
83
84 private static String getPage(String pageUrl) throws IOException {
85
86 }
87
88 /***
89 * 终止所有爬虫任务
90 */
91 public static void stop() {
92 System.out.println("正在终止...");
93 stop.compareAndSet(false, true);
94 UserBuffer.getInstance().prepareForStop();
95 }
96
97 private static void login() throws UnsupportedEncodingException,
98 IOException {
99 ...
100 }
101
102
103 private static String sendPost(String url, String postParams)
104 throws IOException {
105 ...
106
107 }

四、Buffer并发控制

  在用户Buffer设置一个已经访问的用户集合、一个访问出错的用户集合和一个待访问的队列。

1     private UserBuffer() {
2 crawedUsers = new HashSet<String>();// 已经访问的用户,包括访问成功和访问出错的用户
3 errorUsers = new HashSet<String>();// 访问出错的用户
4 unCrawedUsers = new LinkedList<String>();// 未访问的用户
5 }

  UserBuffer全局唯一,因此采用单例模式,使用的集合和队列都不是线程安全的数据结构,我认为没有必要使用线程安全的ConcurrentSkipListSet与ConcurrentLinkedQueue,因为将一个用户插入unCrawedUsers队列时需要先判断是否已经存在于crawedUsers用户集合中了,这需要用锁来同步访问,如果不使用锁来控制,单个线程安全的set和queque不能保证多个变量之间的test-try有效性。而使用了锁后,再使用线程安全的数据结构只会增加加锁和解锁的次数,反而降低了性能。


 1     /***
2 * 添加未访问的用户
3 *
4 * @param users
5 * @return
6 */
7 public synchronized void addUnCrawedUsers(List<String> users) {
8 // 添加未访问的用户
9 for (String user : users) {
10 if (!crawedUsers.contains(user))
11 unCrawedUsers.add(user);
12 }
13 }
14
15 /**
16 * 从队列中取一个元素,并把这个元素添加到已经访问的集合中,以免重复访问。
17 *
18 *
19 * @return
20 */
21 public synchronized String pickOne() {
22 String newId = unCrawedUsers.poll();
23 // 队列中可能包含重复的id,因为插入队列时只检查是否在访问集合里,
24 // 没有检查是否已经出现在队列里
25 while (crawedUsers.contains(newId)) {
26 newId = unCrawedUsers.poll();
27 }
28 //访问前先把添加到已经访问的集合中
29 crawedUsers.add(newId);
30 return newId;
31 }
32
33 /**
34 * 添加访问出错的用户
35 *
36 * @param userId
37 */
38 public synchronized void addErrorUser(String userId) {
39 errorUsers.add(userId);
40 }

 

五、安全地终止爬虫

  大丈夫能伸能屈,能走能停,爬虫也当如此。爬博客园的用户时,我真是提心吊胆,担心管理员封我的账号,我尽量挑凌晨的时间爬数据,另外就是爬了一会后,我就停下来,过一段时间再爬。要让一个多线程的程序平稳的停下来还真不简单,最担心的就是死锁,发送停止命令后,线程不动却也没有终止,爬了好久的Buffer空间没有保存,那个恨啊。

  我的思路:

  1、爬虫启动时,向Buffer注册一下,buffer记录启动的爬虫数量;

  2、对爬虫设置一个全局的标志,爬虫在每次爬取一个用户前检查终止标志是否被设置;

  3、当发生停止命令时,爬虫检查到停止标志被设置,于是通知Buffer自己将要停止,通知完后就结束运行;

  4、Buffer收到爬虫停止通知后,将爬虫计数器减1,当计数器为0时,保存工作空间,同时通知关闭日志;

  5、为了避免有些爬虫在运行异常时推出而没有通知Buffer,在发出停止命令时,同时通知Buffer准备停止,Buffer设置一个计时器,2分钟后,强制保存爬虫状态和日志。

  完整的代码还是放在Github上,有兴趣的同学可以看看。

  感谢阅读,转载请注明出处:http://www.cnblogs.com/fengfenggirl/

http://www.cnblogs.com/fengfenggirl/p/cnblogs-crawler.html

时间: 2024-10-22 01:57:47

大数据之网络爬虫-一个简单的多线程爬虫的相关文章

一个简单的多线程爬虫

   本文介绍一个简单的多线程并发爬虫,这里说的简单是指爬取的数据规模不大,单机运行,并且不使用数据库,但保证多线程下的数据的一致性,并且能让爬得正起劲的爬虫停下来,而且能保存爬取状态以备下次继续. 爬虫实现的步骤基本如下: 分析网页结构,选取自己感兴趣的部分; 建立两个Buffer,一个用于保存已经访问的URL,一个用户保存带访问的URL; 从待访问的Buffer中取出一个URL来爬取,保存这个URL中感兴趣的信息:并将这个URL加入已经访问的Buffer中,然后将这个URL中的所有外链URL

一个简单的分布式爬虫

下载scrapy-redis: https://github.com/rmax/scrapy-redis 下载zip文件之后解压 建立两个批处理文件,start.bat和clear.batstart.bat的内容为redis-server redis.windows.confclear.bat的内容为redis-cli flushdb双击start.bat启动 这样就说明下好了,运行正常. 我们需要构建一个分布式爬虫系统:由一个master爬虫和slave爬虫组成,master端部署了redis

【大数据】大数据时代--网络数据与科学的时代

大数据_大数据时代_大数据概念_网络大数据 随着大数据时代的来临,大数据也吸引了越来越多的关注.网络大数据(http://www.raincent.com)整合了大数据,大数据概念,大数据处理,大数据分析,cdn,cdn加速,idc,网络测量,网络监测,网络安全测量,网站性能监测,行业分析报告,行业研究报告,免费行业报告等服务为一体,力争打造中国最大的网络大数据中心. 这两个词最早出现是在上世纪90年代.按照当时的解释,大科学时代主要是指单打独斗的时代结束了,要搞集团军式的科研.也有一种说法是,

一个简单的多线程Python爬虫

最近想要抓取拉勾网的数据,最开始是使用Scrapy的,但是遇到了下面两个问题: 前端页面是用JS模板引擎生成的 接口主要是用POST提交参数的 目前不会处理使用JS模板引擎生成的HTML页面,用POST的提交参数的话,接口统一,也没有必要使用Scrapy,所以就萌生了自己写一个简单的Python爬虫的想法. 一个爬虫的简单框架 一个简单的爬虫框架,主要就是处理网络请求,Scrapy使用的是Twisted(一个事件驱动网络框架,以非阻塞的方式对网络I/O进行异步处理),这里不使用异步处理,等以后再

海量数据查询关系型数据库存储大数据,要点就是:简单存储、分区分表、高效索引、批量写入

海量数据查询 https://www.cnblogs.com/nnhy/p/DbForBigData.html 相当一部分大数据分析处理的原始数据来自关系型数据库,处理结果也存放在关系型数据库中.原因在于超过99%的软件系统采用传统的关系型数据库,大家对它们很熟悉,用起来得心应手. 在我们正式的大数据团队,数仓(数据仓库Hive+HBase)的数据收集同样来自Oracle或MySql,处理后的统计结果和明细,尽管保存在Hive中,但也会定时推送到Oracle/MySql,供前台系统读取展示,生成

2019.03.30 云计算和大数据时代网络技术揭秘

云计算  大数据   时代 来源<云计算和大数据时代网络技术揭秘> 第一章 云计算的兴起 云计算的本质是一种服务提供模型,通过这种模型可以随时,随地,按需地通过网络访问共享资源池的资源,这个资源池的内容包括计算资源,网络资源,存储资源等,这些资源你能够被动态的分配和调整,在不同用户之间灵活的划分.范式符合这些特征的IT服务都可以称为云计算服务 为了能将这个定义更方便的匹配到显示世界的IT架构中: IaaS 通过虚拟化技术奖服务器等计算平台同存储和网络资源打包,通过API接口的形式提供给用户.用

大数据营销为什么是一个趋势?

今天给大家分享一个案例,为什么说大数据营销会是一个趋势呢? 为什么那么受欢迎,为什么那么有用?总结来说还是因为在这个流量即金钱的时代,大部分老板都是不知道怎么去运作流量的,现在不管是抖音,快手,今日头条,还是微博等等,这些主流的流量战场对于不懂如何操作的老板来说无疑是浪费自己的时间和经历,但是AI智能营销系统却刚好可以帮助不懂网络的老板在网络的时代快速找到自己的流量并且建立自己的流量池! 除了快手,抖音,微博这些大型的流量聚集地以外,还有一个最大的流量平台,那就是微信,只要可以运用好这个平台,效

python实现的一个简单的网页爬虫

学习了下python,看了一个简单的网页爬虫:http://www.cnblogs.com/fnng/p/3576154.html 自己实现了一个简单的网页爬虫,获取豆瓣的最新电影信息. 爬虫主要是获取页面,然后对页面进行解析,解析出自己所需要的信息进行进一步分析和挖掘. 首先需要学习python的正则表达式:http://www.cnblogs.com/fnng/archive/2013/05/20/3089816.html 解析的url:http://movie.douban.com/ 查看

MAC COCOA一个简单的多线程程序

功能: 实现多线程:2个线程同时工作,一个用时间计数器,一个用来打印信息 STEP1 XCODE ->New Application ->Cocoa中的Command Line 自动增加: #include <CoreFoundation/CoreFoundation.h> STEP2 // // main.c // test_runloop1 // // Created by DMD on 20/6/14. // Copyright (c) 2014 EDU. All right