网络爬虫:使用多线程爬取网页链接

gavanwanggw gavanwanggw     2022-09-04     198

关键词:

前言:

  经过前面两篇文章,你想大家应该已经知道网络爬虫是怎么一回事了。这篇文章会在之前做过的事情上做一些改进,以及说明之前的做法的不足之处。


思路分析:

1.逻辑结构图

  技术分享

  上图中展示的就是我们网络爬虫中的整个逻辑思路(调用Python解析URL,这里仅仅作了简略的展示)。


2.思路说明:

  首先。我们来把之前思路梳理一下。之前我们採用的两个队列Queue来保存已经訪问过和待訪问的链接列表,并採用广度优先搜索进行递归訪问这些待訪问的链接地址。并且这里使用的是单线程操作。

在对数据库的操作中。我们加入了一个辅助字段cipher_address来进行“唯一”性保证,由于我们操心MySQL在对过长的url链接操作时会有一些不尽如人意。

  我不知道上面这一段是否能让你对之前我们处理Spider的做法有一个大概的了解,假设你还没有太明确这是怎么一回事。你能够訪问《网络爬虫初步:从訪问网页到数据解析》和《网络爬虫初步:从一个入口链接開始不断抓取页面中的网址并入库》这两篇文章进行了解。

  以下我就来说明一下,之前的做法存在的问题:

  1.单线程:採用单线程的做法,能够说相当不科学,尤其是对付这样一个大数据的问题。

所以,我们须要採用多线程来处理问题。这里会用到多线程中的线程池。


  2.数据存储方式:假设我们採用内存去保存数据,这样会有一个问题。由于数据量很大。所以程序在执行的过种中必定会内存溢出。而事实也正是如此:

  技术分享


  3.Url去重的方式:假设我们对Url进行MD5或是SHA1进行加密的方式进行哈希的话,这样会有一个效率的隐患。只是的确这个问题并不那么复杂。对效率的影响也非常小。只是。还好Java自身就已经对String型的数据有哈希的函数能够直接调用:hashCode()


代码及说明:

LinkSpider.java

public class LinkSpider {
	
    private SpiderQueue queue = null;
    
	/**
	 *  遍历从某一节点開始的全部网络链接
	 * LinkSpider
	 * @param startAddress
	 * 			 開始的链接节点
	 */
	public void ErgodicNetworkLink(String startAddress) {
	    if (startAddress == null) {
            return;
        }
	    
	    SpiderBLL.insertEntry2DB(startAddress);
	    
	    List<WebInfoModel> modelList = new ArrayList<WebInfoModel>();
		queue = SpiderBLL.getAddressQueue(startAddress, 0);
		if (queue.isQueueEmpty()) {
            System.out.println("Your address cannot get more address.");
            return;
        }
		
		ThreadPoolExecutor threadPool = getThreadPool();
		int index = 0;
        boolean breakFlag = false;
        
		while (!breakFlag) {
		    
		    // 待訪问队列为空时的处理
		    if (queue.isQueueEmpty()) {
		        System.out.println("queue is null...");
		        modelList = DBBLL.getUnvisitedInfoModels(queue.MAX_SIZE);
		        if (modelList == null || modelList.size() == 0) {
                    breakFlag = true;
                } else {
                    for (WebInfoModel webInfoModel : modelList) {
                        queue.offer(webInfoModel);
                        DBBLL.updateUnvisited(webInfoModel);
                    }
                }
		    }
		    
			WebInfoModel model = queue.poll();
			
			if (model == null) {
                continue;
            }
			
			// 推断此站点是否已经訪问过
			if (DBBLL.isWebInfoModelExist(model)) {
			    // 假设已经被訪问。进入下一次循环
			    System.out.println("已存在此站点(" + model.getName() + ")");
				continue;
			}
			
			poolQueueFull(threadPool);
			
			System.out.println("LEVEL: [" + model.getLevel() + "] NAME: " + model.getName());
			SpiderRunner runner = new SpiderRunner(model.getAddress(), model.getLevel(), index++);
			threadPool.execute(runner);
			
			SystemBLL.cleanSystem(index);
			
			// 对已訪问的address进行入库
			DBBLL.insert(model);
		}
		
		threadPool.shutdown();
	}
	
	/**
	 * 创建一个线程池的对象
	 * LinkSpider
	 * @return
	 */
	private ThreadPoolExecutor getThreadPool() {
	    final int MAXIMUM_POOL_SIZE = 520;
        final int CORE_POOL_SIZE = 500;
        return new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(MAXIMUM_POOL_SIZE), new ThreadPoolExecutor.DiscardOldestPolicy());
	}
	
	/**
	 * 线程池中的线程队列已经满了
	 * LinkSpider
	 * @param threadPool
	 *         线程池对象
	 */
	private void poolQueueFull(ThreadPoolExecutor threadPool) {
	    while (getQueueSize(threadPool.getQueue()) >= threadPool.getMaximumPoolSize()) {
            System.out.println("线程池队列已满,等3秒再加入任务");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
	}
	
	/**
	 * 获得线程池中的活动线程数
	 * LinkSpider
	 * @param queue
	 *         线程池中承载线程的队列
	 * @return
	 */
	private synchronized int getQueueSize(Queue queue) {
        return queue.size();
    }
	
	/**
	 * 接收一个链接地址,并调用Python获取该链接下的关联的全部链接list
	 * 将list入库
	 */
	class SpiderRunner implements Runnable {
	    private String address;
	    private SpiderQueue auxiliaryQueue; // 记录訪问某一个网页中解析出的网址
	    
	    private int index;
	    private int parentLevel;
	    
	    public SpiderRunner(String address, int parentLevel, int index) {
	        this.index = index;
	        this.address = address;
	        this.parentLevel = parentLevel;
        }
	    
        public void run() {
            auxiliaryQueue = SpiderBLL.getAddressQueue(address, parentLevel);
            System.out.println("[" + index + "]: " + address);
            DBBLL.insert2Unvisited(auxiliaryQueue, index);
            auxiliaryQueue = null;
        }
    }
}

  在上面的ErgodicNetworkLink方法代码中,大家能够看到我们已经把使用Queue保存数据的方式改为使用数据库存储。这样做的优点就是我们不用再为OOM而烦恼了。并且。上面的代码也使用了线程池。使用多线程来运行在调用Python获得链接列表的操作。

  而对于哈希Url的做法。能够參考例如以下关键代码:

/**
     * 加入单个model到等待訪问的数据库中
     * DBBLL
     * @param model
     */
	public static void insert2Unvisited(WebInfoModel model) {
	    if (model == null) {
            return;
        }
	    
        String sql = "INSERT INTO unvisited_site(name, address, hash_address, date, visited, level) VALUES('" + model.getName() + "', '" + model.getAddress() + "', " + model.getAddress().hashCode() + ", " + System.currentTimeMillis() + ", 0, " + model.getLevel() + ");";
        DBServer db = null;
        try {
            db = new DBServer();
            db.insert(sql);
            
            db.close();
        } catch (Exception e) {
            System.out.println("your sql is: " + sql);
            e.printStackTrace();
        } finally {
            db.close();
        }
	}


  PythonUtils.java

  这个类是与Python进行交互操作的类。代码例如以下:

public class PythonUtils {

	// Python文件的所在路径
	private static final String PY_PATH = "/root/python/WebLinkSpider/html_parser.py";
		
	/**
	 * 获得传递给Python的运行參数
	 * PythonUtils
	 * @param address
	 * 			网络链接
	 * @return
	 */
	private static String[] getShellArgs(String address) {
		String[] shellParas = new String[3];
    	shellParas[0] = "python";
    	shellParas[1] = PY_PATH;
    	shellParas[2] = address.replace(""", "\"");
    	
    	return shellParas;
	}
	
	private static WebInfoModel parserWebInfoModel(String info, int parentLevel) {
		if (BEEStringTools.isEmptyString(info)) {
			return null;
		}
		
		String[] infos = info.split("\$#\$");
		if (infos.length != 2) {
			return null;
		}
		
		if (BEEStringTools.isEmptyString(infos[0].trim())) {
            return null;
        }
		
		if (BEEStringTools.isEmptyString(infos[1].trim()) || infos[1].trim().equals("http://") || infos[1].trim().equals("https://")) {
            return null;
        }
		
		WebInfoModel model = new WebInfoModel();
		
		model.setName(infos[0].trim());
		model.setAddress(infos[1]);
		model.setLevel(parentLevel + 1);
		
		return model;
	}
	
	/**
	 * 调用Python获得某一链接下的全部合法链接
	 * PythonUtils
	 * @param shellParas
	 * 			传递给Python的运行參数
	 * @return
	 */
	private static SpiderQueue getAddressQueueByPython(String[] shellParas, int parentLevel) {
		if (shellParas == null) {
			return null;
		}
		
		Runtime r = Runtime.getRuntime();
		SpiderQueue queue = null;
		
    	try {
			Process p = r.exec(shellParas);
			
			BufferedReader bfr = new BufferedReader(new InputStreamReader(p.getInputStream()));
			
			queue = new SpiderQueue();
			String line = "";
			WebInfoModel model = null;
			while((line = bfr.readLine()) != null) {
//			    System.out.println("----------> from python: " + line);
			    
			    if (BEEStringTools.isEmptyString(line.trim())) {
                    continue;
                }
			    
			    if (HttpBLL.isErrorStateCode(line)) {
                    break;
                }
			    
			    model = parserWebInfoModel(line, parentLevel);
			    if (model == null) {
                    continue;
                }
			    
				queue.offer(model);
			}
			
			model = null;
            line = null;
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
            r = null;
		}
    	
    	return queue;
	}
	
	/**
	 * 调用Python获得某一链接下的全部合法链接
	 * PythonUtils
	 * @param address
	 * 			网络链接
	 * @return
	 */
	public static SpiderQueue getAddressQueueByPython(String address, int parentLevel) {
		return getAddressQueueByPython(getShellArgs(address), parentLevel);
	}
}

遇到的问题:

1.请使用Python2.7

  由于Python2.6中HTMLParser还是有一些缺陷的,比例如以下图中展示的。只是在Python2.7中。这个问题就不再是问题了。

  技术分享


2.数据库崩溃了

  数据库崩溃的原因可能是待訪问的数据表中的数据过大引起的。

  技术分享


3.对数据库的同步操作

  上面的做法是对数据库操作进行同步时出现的问题,假设不进行同步,我们会得到数据库连接数超过最大连接数的异常信息。对于这个问题有望在下篇文章中进行解决。

  不知道大家对上面的做法有没有什么疑问。当然。我希望你有一个疑问就是在于,我们去同步数据库的操作。当我们開始进行同步的时候就已经说明我们此时的同步仅仅是做了单线程的无用功。由于我開始以为对数据库的操作是须要同步的,数据库是一个共享资源,须要相互排斥訪问(假设你学习过“操作系统”。对这些概念应该不会陌生)。实际上还是单线程,解决办法就是不要对数据库的操作进行同步操作。

而这些引发的数据库连接数过大的问题。会在下篇文章中进行解决。













c#网络爬虫--多线程处理强化版

上次做了一个帮公司妹子做了爬虫,不是很精致,这次公司项目里要用到,于是有做了一番修改,功能添加了网址图片采集,下载,线程处理界面网址图片下载等。说说思路:首相获取初始网址的所有内容 在初始网址采集图... 查看详情

python爬虫深造篇——多线程网页爬取(代码片段)

一、前情提要相信来看这篇深造爬虫文章的同学,大部分已经对爬虫有不错的了解了,也在之前已经写过不少爬虫了,但我猜爬取的数据量都较小,因此没有过多的关注爬虫的爬取效率。这里我想问问当我们要爬... 查看详情

爬取豆瓣top250

...掌握基本的多线程技术;(5)能够根据问题需求,指定网络爬虫方案,并编码实现。(6)具备撰写项目实验报告的能力。2.实验内容豆瓣电影TOP250:​​https://movie.douban.c 查看详情

java实现网页爬虫

...页的数据爬取6、多线程的网页爬取7、总结爬虫实现原理网络爬虫基本技术处理网络爬虫是数据采集的一种方法,实际项目开发中,通过爬虫做数据 查看详情

多线程爬虫介绍

...。单线程爬虫每次只访问一个页面,不能充分利用电脑的网络带宽。一个页面最多也就几百KB,所以爬虫在爬取一个页面的时候,多出来的网速就浪费掉了。而如果我们可以让爬虫同时访问10个页面,就相当于我们的爬取速度提... 查看详情

多线程爬虫爬取详情页html

注意:如果想爬取详情页的信息请按须添加方法importrequestsimportosimportreimportthreadingfromlxmlimportetree#爬去详情页得HTML内容classCnBeta(object):defget_congtent(self,url):#获取网页首页HTML信息r=requests.get(url)#将获取得HTML页面进行解码html=r.co 查看详情

python多线程爬虫爬取顶点小说内容(beautifulsoup+urllib)

...?下一步打算搞点能被封ip的爬取行为,然后学学分布式爬虫。加油~ 查看详情

爬虫爬取网易云歌单

一、主题式网络爬虫设计方案1.主题式网络爬虫名称:爬取网易云音乐歌单2.主题式网络爬虫爬取的内容与数据特征分析  爬取网易云音乐歌单前十页歌单,轻音乐类型的歌单名称、歌单播放量、歌单链接、用户名称。 ... 查看详情

python多线程爬虫爬取电影天堂资源(代码片段)

...python希望可以获得宝贵的意见。  先来简单介绍一下,网络爬虫的基本实现原理吧。一个爬虫首先要给它一个起点,所以需要精心选取一些URL作为起点,然后我们的爬虫从这些起点出发,抓取并解析所抓取到的页面,将所需要... 查看详情

python网络爬虫技巧小总结,静态动态网页轻松爬取数据

很多人学用python,用得最多的还是各类爬虫脚本:有写过抓代理本机验证的脚本,有写过自动收邮件的脚本,还有写过简单的验证码识别的脚本,那么我们今天就来总结下python爬虫抓站的一些实用技巧。 静态网页对于静态网... 查看详情

python爬虫宝典:从爬取网页到app所有思路总结(代码片段)

...高爬取速度1.多线程、多进程2.异步3.分布式4.优化5.架构爬虫在工作生活中使用非常广泛,无论是论文数据准备还是市场调研还是工作工具等等都十分有用,爬取的目标大部分是网页或App,所以今天就从这两大类分别... 查看详情

,遇到的爬虫问题与解决思路(代码片段)

爬虫问题分析回顾之前写了一个爬取小说网站的多线程爬虫,操作流程如下:先爬取小说介绍页,获取所有章节信息(章节名称,章节对应阅读链接),然后使用多线程的方式(pool=Pool(50)),通过章节的阅读... 查看详情

使用线程池多线程爬取链接,检验链接正确性

...因为程序bug造成访问出错,因此对主站写了个监控脚本,使用python爬取主站设置的链接并访问,统计访问出错的链接,因为链接有上百个,所以使用了多线程进行,因为http访问是io密集型,所以python多线程还是可以很好的完成并... 查看详情

网络爬虫初探网络爬虫(代码片段)

爬虫基础入门什么是爬虫:爬虫又称为网页蜘蛛/网络机器人,是一种按照一定规则,自动爬取万维网的程序或者脚本,是搜索引擎的重要组成。爬虫的作用:1.搜索引擎2.数据分析,发现规律,商品活... 查看详情

多线程爬虫

1#多线程爬虫2#map函数的使用3#frommultiprocessing.dummyimportPool4#pool=Pool(4)5#results=pool.map(爬取函数,网址列表)6#实例演示:7frommultiprocessing.dummyimportPoolasThreadPool8importrequests9importtime1011defgetsource(url): 查看详情

python网络爬虫爬取静态数据详解

目的爬取http://seputu.com/数据并存储csv文件导入库lxml用于解析解析网页HTML等源码,提取数据。一些参考:https://www.cnblogs.com/zhangxinqi/p/9210211.htmlrequests请求网页chardet用于判断网页中的字符编码格式csv用于存储文本使用。re用于正则... 查看详情

网络爬虫简介

什么是网络爬虫为什么要学网络爬虫网络爬虫的组成网络爬虫的类型网络爬虫的工作流程网络爬虫的爬行策略网络爬虫的更新策略扩展:网页分析算法扩展:GooSeeker工具  1.什么是网络爬虫(1)网络爬虫又称网络蜘蛛,可以... 查看详情

网络爬虫,如何做到“盗亦有道”?

网络爬虫的实质,其实是从网络上“偷”数据。通过网络爬虫,我们可以采集到所需要的资源,但是同样,使用不当也可能会引发一些比较严重的问题。因此,在使用网络爬虫时,我们需要做到“盗亦有道”。网络爬虫主要分为... 查看详情