网络爬虫初步:从一个入口连接开始不断抓取页面中的网址并入库

前言:

  在上一篇《网络爬虫初步:从访问网页到数据解析》中,咱们讨论了如何爬取网页,对爬取的网页进行解析,以及访问被拒绝的网站。在这一篇博客中,咱们能够来了解一下拿到解析的数据能够作的事件。在这篇博客中,我主要是说明要作的两件事,一是入库,二是遍历拿到的连接继续访问。如此往复,这样就构成了一个网络爬虫的雏形。html


笔者环境:

  系统:     Windows 7
            CentOS 6.5
  运行环境:  JDK 1.7
            Python 2.6.6
  IDE:      Eclipse Release 4.2.0
            PyCharm 4.5.1
  数据库:    MySQL Ver 14.14 Distrib 5.1.73
java


效果图:

  这里只截取开始的一部分数据。这些数据我是保存在MySQL中的。
python

 

 

思路梳理:

  前面说到,咱们拿到数据要作两件:数据保存与数据分析。web

  咱们整个逻辑过程是这样的:算法

    1.Java传递连接参数给Python;sql

    2.Python解析HTML返回必要信息给Java;数据库

    3.Java拿到数据进行入库;网络

    4.对解析出的有效连接进行继续遍历(这里是采用图的广度优先遍历)多线程

    5.反复以上的4个步骤,直到没有可继续访问的有效连接为止,这里是使用递归迭代。
app

  关于数据保存,却是没有什么好说的。由于我是在Linux(CentOS)下运行程序的。因此,你的Linux中必需要有MySQL,另外,我是经过Java来进行数据库操做的,因此这里你的系统中也有要Mysql的Java驱动包。

 

开发过程:

1.Python解析数据

get_html_response.py


# encoding=utf-8

import HTMLParser

import utils as utils

class ListWebParser(HTMLParser.HTMLParser):
    def __init__(self):
        HTMLParser.HTMLParser.__init__(self)
        self.tagAFlag = False

        self._name = None
        self._address = None
        self._info = []

    def handle_starttag(self, tag, attrs):
        if tag == 'a':
            for name, value in attrs:
                if name == 'href' and utils.isMatch(value, '^http'):
                    self._info.append((self._name, self._address))
                    self.tagAFlag = True
                    self._address = value
                    self._name = None

                    # print 'Address: ', value

    def handle_endtag(self, tag):
        if tag == 'a':
            self.tagAFlag = False

    def handle_data(self, data):
        if self.tagAFlag:
            name = data.decode('utf-8')
            if self._name:
                self._name = str(self._name) + ' ' + name
            else:
                self._name = name

    def getLinkList(self):
        return self._info

html_parser.py

# encoding=utf-8

'''
    对Html文件进行解析
'''

import sys
reload(sys)
sys.setdefaultencoding('utf8')

from list_web_parser import ListWebParser
import get_html_response as geth

def main(html):
    myp = ListWebParser()
    get_html = geth.get_html_response(html)
    myp.feed(get_html)
    link_list = myp.getLinkList()
    myp.close()

    for item in link_list:
        if item[0] and item[1]:
            print item[0], '$#$', item[1]

if __name__ == "__main__":
    if not sys.argv or len(sys.argv) < 2:
        print 'You leak some arg.' # http://www.cnblogs.com/Stone-sqrt3/
    main(sys.argv[1])

2.Java入库

对于Java中对数据库的操做,也没什么好说说明的。若是你写地JDBC,那么这对于你而言将是小菜一碟。关键代码以下:

public class DBServer {

	private String mUrl = DBModel.getMysqlUrl();
	private String mUser = DBModel.getMysqlUesr();
	private String mPassword = DBModel.getMysqlPassword();
	private String mDriver = DBModel.getMysqlDerver();
	
	private Connection mConn = null;
	private Statement mStatement = null;
	
	public DBServer() {
		initEvent();
	}
	
	private void initEvent() {
		mUrl = DBModel.getMysqlUrl();
		mUser = DBModel.getMysqlUesr();
		mPassword = DBModel.getMysqlPassword();
		mDriver = DBModel.getMysqlDerver();
		
		try {
			Class.forName(mDriver).newInstance();
			mConn = DriverManager.getConnection(mUrl, mUser, mPassword);
			
			mStatement = mConn.createStatement();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 数据库查询
	 * TODO
	 * DBServer
	 * @param sql
	 * 			查询的sql语句
	 */
	public void select(String sql) {
		try {
			ResultSet rs = mStatement.executeQuery(sql);
			while (rs.next()) {
                String name = rs.getString("name");
                System.out.println(name);
            }
			
			rs.close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 插入新数据
	 * DBServer
	 * @param sql
	 * 			插入的sql语句
	 */
	public int insert(String sql) {
		try {
			int raw = mStatement.executeUpdate(sql);
			return raw;
		} catch (SQLException e) {
			e.printStackTrace();
			return 0;
		}
	}
	
	/**
	 * 某一个网址是否已经存在
	 * DBServer
	 * @param sql
	 * 			查询的sql语句
	 */
	public boolean isAddressExist(String sql) {
		try {
			ResultSet rs = mStatement.executeQuery(sql);
			if (rs.next()) {
				return true;
			}
			rs.close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		
		return false;
	}
	
	public void close() {
		try {
			if (mConn != null) {
				mConn.close();
			}
			
			if (mStatement != null) {
				mStatement.close();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

3.Java进行递归访问连接

/**
	 *  遍历从某一节点开始的全部网络连接
	 * LinkSpider
	 * @param startAddress
	 * 			 开始的连接节点
	 */
	private static void ErgodicNetworkLink(String startAddress) {
		SpiderQueue queue = getAddressQueue(startAddress);
//		System.out.println(queue.toString());
		
		SpiderQueue auxiliaryQueue = null; // 记录访问某一个网页中解析出的网址
		while (!queue.isQueueEmpty()) {
			WebInfoModel model = queue.poll();
			
			// TODO 判断数据库中是否已经存在
			if (model == null || DBBLL.isWebInfoModelExist(model)) {
				continue;
			}
			
			// TODO 若是不存在就继续访问
			auxiliaryQueue = getAddressQueue(model.getAddres());
			System.out.println(auxiliaryQueue);
			
			// TODO 对已访问的address进行入库
			DBBLL.insert(model);
			
			if (auxiliaryQueue == null) {
				continue;
			}
			
			while (!auxiliaryQueue.isQueueEmpty()) {
				queue.offer(auxiliaryQueue.poll());
			}
		}
	}
	
	/**
	 * 得到某一连接下的全部合法连接
	 * LinkSpider
	 * @param htmlText
	 * 			网络连接
	 * @return
	 */
	private static SpiderQueue getAddressQueue(String htmlText) {
		if (htmlText == null) {
			return null;
		}
		
    	SpiderQueue queue = PythonUtils.getAddressQueueByPython(htmlText);
    	
    	return queue;
    }

本程序的内存及线程状况:

内存:


线程:


爬取速度:



要点说明:

1.系统中的MySQL及MySQL包

  你的Linux中必需要有MySQL,另外,我是经过Java来进行数据库操做的,因此这里你的系统中也有要Mysql的Java驱动包。这一点在上面也有说明,不过这里仍是要强调一下。若是你写过JDBC的程序,那么这个驱动包,我想你应该是有的,若是你没写过,那就去下一个吧。


2.须要一个辅助Queue

  在上面的代码中,咱们能够看到咱们有两个SpiderQueue。一个是咱们待访问的队列queue,保存咱们将要访问的连接列表;另外一个是辅助队列auxiliaryQueue,用于得到从Python解析出来的数据。


3.使用图的广度优先搜索算法进行连接爬取

  这是从连接的相关性上考虑的。若是选择深度优先,那么随着遍历的深刻,可能连接的相关性就会愈来愈小了。而广度优先搜索则不会这样,由于咱们都知道在同一个页面中的连接老是会由于一些因素要展现在同一个页面中,那么它们的相关性就会比较靠谱。


4.单线程与多线程

  完成以上操做,若是你的程序正常运行。在前期是比较快的,但是到了稳按期的时候就通常是1s钟出一条数据。这个有点慢,我会在下一篇博客利用多线程来解决这个问题。


5.MySQL中添加一个叫cipher_address的字段

  此字段用于address的加密生成(MD5 or SHA1)。下面举个例子:

可能你有一个疑问,为何要这个字段?若是你这样思考了,那么对于你,是有益的。咱们知道其实MySQL对一个很长的字符串进行select的时候,是相对来讲比较慢的。这时,咱们能够把这个address进行哈希一下,造成一个长度适中,又比较相近的字符串,这样MySQL在比较时会容易一些(固然,你能够不使用这个字段)。


6.OOM异常

  彻底按照本文中的代码和讲解来进行编码的话,会得到一个OOM的异常(个人程序是跑了1天半的时时间)。以下:


数量大概在23145条左右


对于这一点在上面关于内存和线程的展现图中能够看到缘由。

转载于:https://www.cnblogs.com/fengju/p/6336052.html