网络爬虫:使用多线程爬取网页链接
前言:
? 經過前面兩篇文章,你想大家應該已經知道網絡爬蟲是怎么一回事了。這篇文章會在之前做過的事情上做一些改進,以及說明之前的做法的不足之處。
思路分析:
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.對數據庫的同步操作
? 上面的做法是對數據庫操作進行同步時出現的問題,如果不進行同步,我們會得到數據庫連接數超過最大連接數的異常信息。對于這個問題有望在下篇文章中進行解決。
? 不知道大家對上面的做法有沒有什么疑問。當然,我希望你有一個疑問就是在于,我們去同步數據庫的操作。當我們開始進行同步的時候就已經說明我們此時的同步只是做了單線程的無用功。因為我開始以為對數據庫的操作是需要同步的,數據庫是一個共享資源,需要互斥訪問(如果你學習過“操作系統”,對這些概念應該不會陌生)。實際上還是單線程,解決的方法就是不要對數據庫的操作進行同步操作。而這些引發的數據庫連接數過大的問題,會在下篇文章中進行解決。
總結
以上是生活随笔為你收集整理的网络爬虫:使用多线程爬取网页链接的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 网络爬虫初步:从一个入口链接开始不断抓取
- 下一篇: MySQL存储结构的使用