1. 程式人生 > >爬蟲實戰:一個簡易 Java 爬蟲程式的實現

爬蟲實戰:一個簡易 Java 爬蟲程式的實現

前面,我們分別介紹了爬蟲程式的 3 個部分:原始碼下載、內容解析、頁面連結提取。現在,讓我們把這些部分串起來,構建一個完整的簡易爬蟲程式。

爬蟲流程

構建程式前,我們首先需要了解爬蟲的具體流程。

一個簡易的爬蟲程式,具備以下流程:

image

若以文字表述,就是:

  1. 從任務庫(可以是 MySQL 等關係型資料庫)選取種子 URL;

  2. 在程式中初始化一個 URL 佇列,將種子 URL 加入到佇列中;

  3. 若 URL 佇列不為空,則位於隊頭的 URL 出隊;若 URL 佇列為空,則退出程式;

  4. 程式根據出列的 URL,反射出對應的解析類,同時新建執行緒,開始解析任務;

  5. 程式將下載 URL 指向的網頁,並判斷該頁面是詳情頁還是列表頁(如部落格中的部落格詳情與博文列表),若為詳情頁,則解析出頁面內容併入庫,若為列表頁,則提取出頁面連結,加入到 URL 佇列中;

  6. 解析任務完成後,重複第 3 步。

程式結構

我們已經清楚爬蟲的具體流程,現在,我們需要一個合理的程式結構來實現它。

首先,介紹一下該簡易爬蟲程式的主要結構組成:

類名 作用
SpiderApplication.java 程式入口,負責任務排程,初始化 URL 佇列,執行緒排程
DownloadService.java 下載服務類,負責下載 URL 指向的頁面
PluginFactory.java 外掛工廠,根據 URL 反射對應的外掛類(解析類)
Plugin.java 外掛註解類,用於外掛(解析類)的註解
AbstractPlugin.java 抽象外掛類,所有外掛(解析類)的父類,包含公用方法
XmuPlugin.java 具體外掛類,負責指定 URL 的解析任務

然後,再瞭解一下程式中的工具類與實體類。

類名 作用
LinkFilter.java 連結過濾介面,規範過濾方法
LinkExtractor.java 基於 htmlparser 的連結提取類
HttpUtil.java 封裝 HttpClient 的工具類
CommonUtil.java 程式通用工具類
Task.java 任務物件
HttpParams.java http 請求物件
Proxy.java 代理配置類
StructData.java 結構化資料

最後,我們根據類的作用,將其放置到上述流程圖中對應的位置中。具體的示意圖如下所示:

image

現在,我們已經完成了實際流程到程式邏輯的轉換。接下來,我們將通過原始碼的介紹,深入到程式中的各個細節。

任務排程、初始化佇列

在簡易爬蟲程式中,任務排程、初始化佇列都在 SpiderApplication 類中完成。

package main;

import entity.Task;
import factory.PluginFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import plugins.AbstractPlugin;

import java.util.*;
import java.util.zip.CRC32;

/**
 * 應用入口
 *
 * @author panda
 * @date 2017/10/28
 */
public class SpiderApplication {

    private static final Logger logger = LoggerFactory.getLogger(SpiderApplication.class);

    // URL 佇列
    private static Queue<String> urlQueue = new LinkedList<String>();

    // URL 排重
    private static Map<Long, Integer> urlPool = new HashMap<Long, Integer>();


    public static void main(String[] args) {

        // 例項化任務物件
        Task task = Task.getBuilder()
                .setUrl("http://sm.xmu.edu.cn/")
                .build();

        // 初始化 URL 佇列
        urlQueue.add(task.getUrl());
        addUrlPool(task.getUrl(), 1);

        // 迴圈排程
        String taskUrl;
        while ((taskUrl = urlQueue.poll()) != null) {
            logger.info("當前任務URL:" + taskUrl + ",當前層深:" + getDepth(taskUrl));

            try {
                task.setUrl(taskUrl);
                AbstractPlugin plugin = PluginFactory.getInstance().getPlugin(task);
                plugin.run();

                if (plugin.getUrlList() != null) {
                    int depth = getDepth(taskUrl) + 1;
                    for (String url : plugin.getUrlList()) {
                        if (!isUrlExist(url)) {
                            urlQueue.add(url);
                            addUrlPool(url, depth);
                        }
                    }
                }

                Thread.sleep(300);
            } catch (Exception e) {
                continue;
            }
        }

    }

    /**
     * 新增連結到 url 池
     *
     * @param url
     * @param depth
     */
    private static void addUrlPool(String url, int depth) {
        CRC32 c = new CRC32();
        c.update(url.getBytes());

        urlPool.put(c.getValue(), depth);
    }

    /**
     * 判斷 url 是否重複
     *
     * @param url
     * @return
     */
    private static boolean isUrlExist(String url) {

        CRC32 c = new CRC32();
        c.update(url.getBytes());

        return urlPool.containsKey(c.getValue());
    }

    /**
     * 獲得層深
     *
     * @param url
     * @return
     */
    private static Integer getDepth(String url) {

        CRC32 c = new CRC32();
        c.update(url.getBytes());

        return urlPool.get(c.getValue()) == null ? 1 : urlPool.get(c.getValue());
    }

}

我們看到,SpiderApplication 有兩個靜態成員變數,分別用於存放 URL 佇列和 URL 排重集合。

首先,我們介紹任務排程部分。由於本例是簡易的爬蟲程式,因此不從資料庫抓取任務,而是直接在程式中通過建造者模式例項化一個任務物件。

Task task = Task.getBuilder()
                .setUrl("http://sm.xmu.edu.cn/")
                .build();

然後,我們將種子 URL 新增到 URL 佇列中。

urlQueue.add(task.getUrl());
addUrlPool(task.getUrl(), 1);

其中,addUrlPool(String url, int depth) 方法會將 url 加入到排重池中,並標記 url 的採集深度。

接著,我們開始對 URL 佇列進行迴圈排程,直至 URL 佇列為空,才退出程式。

while ((taskUrl = urlQueue.poll()) != null) {
    logger.info("當前任務URL:" + taskUrl + ",當前層深:" + getDepth(taskUrl));
    ...
}

外掛工廠

在 URL 迴圈排程中,有個語句需要我們注意:

AbstractPlugin plugin = PluginFactory.getInstance().getPlugin(task);

其中,AbstractPlugin 是一個繼承 Thread 的抽象外掛類。

該語句的意思是,由外掛工廠,根據 url,反射例項化繼承了 AbstractPlugin 的指定外掛。

外掛工廠,也可以理解為解析類工廠。

為什麼會存在這個定義呢?

我們知道,一個完整的爬蟲程式,要能實現大量不同網站的採集,而不是僅僅針對單一網站。由於每個網站的頁面佈局不同,解析方法也不盡相同,這時候,就需要由一個個外掛來負責對應網站的解析。而外掛工廠的存在,就是為了管理這些外掛,使程式能根據任務 url 來動態地選擇外掛(解析類)。

在本程式中,外掛工廠的實現主要依靠三方面:

Plugin:

package plugins;

import java.lang.annotation.*;

/**
 * 外掛註解
 *
 * @author panda
 * @date 2017/12/01
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Plugin {
    String value() default "";
}

Plugin 實際上是一個註解介面,有 Plugin 的支援,我們可以實現讓程式通過註解 @Plugin 來識別外掛類。這就像 SpringMVC 中,我們通過 @Controller@Service 等識別一個個 Bean。

XmuPlugin:

@Plugin(value = "sm.xmu.edu.cn")
public class XmuPlugin extends AbstractPlugin {

}

XmuPlugin 是眾多外掛(解析類)之一,由註解 @Plugin 標誌角色,由註解中的 value 標誌其具體身份(即對應哪個 url)。

PluginFactory:

package factory;

import entity.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import plugins.AbstractPlugin;
import plugins.Plugin;
import util.CommonUtil;

import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 外掛工廠
 *
 * @author panda
 * @date 2017/12/01
 */
public class PluginFactory {

    private static final Logger logger = LoggerFactory.getLogger(PluginFactory.class);

    private static final PluginFactory factory = new PluginFactory();

    private List<Class<?>> classList = new ArrayList<Class<?>>();

    private Map<String, String> pluginMapping = new HashMap<String, String>();

    private PluginFactory() {
        scanPackage("plugins");
        if (classList.size() > 0) {
            initPluginMapping();
        }
    }

    public static PluginFactory getInstance() {
        return factory;
    }

    /**
     * 掃描包、子包
     *
     * @param packageName
     */
    private void scanPackage(String packageName) {
        try {
            String path = getSrcPath() + File.separator + changePackageNameToPath(packageName);
            File dir = new File(path);
            File[] files = dir.listFiles();

            if (files == null) {
                logger.warn("包名不存在!");
                return;
            }

            for (File file : files) {
                if (file.isDirectory()) {
                    scanPackage(packageName + "." + file.getName());
                } else {
                    Class clazz = Class.forName(packageName + "." + file.getName().split("\\.")[0]);
                    classList.add(clazz);
                }
            }
        } catch (Exception e) {
            logger.error("掃描包出現異常:", e);
        }
    }

    /**
     * 獲取根路徑
     *
     * @return
     */
    private String getSrcPath() {
        return System.getProperty("user.dir") +
                File.separator + "src" +
                File.separator + "main" +
                File.separator + "java";
    }

    /**
     * 將包名轉換為路徑格式
     *
     * @param packageName
     * @return
     */
    private String changePackageNameToPath(String packageName) {
        return packageName.replaceAll("\\.", File.separator);
    }

    /**
     * 初始化外掛容器
     */
    private void initPluginMapping() {
        for (Class<?> clazz : classList) {
            Annotation annotation = clazz.getAnnotation(Plugin.class);
            if (annotation != null) {
                pluginMapping.put(((Plugin) annotation).value(), clazz.getName());
            }
        }
    }

    /**
     * 通過反射例項化外掛物件
     * @param task
     * @return
     */
    public AbstractPlugin getPlugin(Task task) {

        if (task == null || task.getUrl() == null) {
            logger.warn("非法的任務!");
            return null;
        }

        if (pluginMapping.size() == 0) {
            logger.warn("當前包中不存在外掛!");
            return null;
        }

        Object object = null;

        String pluginName = CommonUtil.getHost(task.getUrl());
        String pluginClass = pluginMapping.get(pluginName);

        if (pluginClass == null) {
            logger.warn("不存在名為 " + pluginName + " 的外掛");
            return null;
        }

        try {
            logger.info("找到解析外掛:" + pluginClass);
            Class clazz = Class.forName(pluginClass);
            Constructor constructor = clazz.getConstructor(Task.class);
            object = constructor.newInstance(task);
        } catch (Exception e) {
            logger.error("反射異常:", e);
        }

        return (AbstractPlugin) object;
    }

}

PluginFactory 的作用主要是兩個:

  • 掃描外掛包下有 @Plugin 註解的外掛類;

  • 根據 url 反射指定外掛類。

解析外掛

我們上面說到,解析外掛實際上就是對應一個個網站的解析類。

由於實際爬蟲的解析中,總有許多解析工作是相似甚至相同的,例如連結提取,因此,在解析外掛中,我們首先要實現一個父介面,來提供這些公用方法。

本程式中,外掛父介面即上面提到的 AbstractPlugin 類:

package plugins;

import entity.Task;
import filter.AndFilter;
import filter.FileExtensionFilter;
import filter.LinkExtractor;
import filter.LinkFilter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import service.DownloadService;
import util.CommonUtil;

import java.util.ArrayList;
import java.util.List;

/**
 * 外掛抽象類
 *
 * @author panda
 * @date 2017/12/01
 */
public abstract class AbstractPlugin extends Thread {

    private static final Logger logger = LoggerFactory.getLogger(AbstractPlugin.class);

    protected Task task;

    protected DownloadService downloadService = new DownloadService();

    private List<String> urlList = new ArrayList<String>();

    public AbstractPlugin(Task task) {
        this.task = task;
    }

    @Override
    public void run() {
        logger.info("{} 開始執行...", task.getUrl());
        String body = downloadService.getResponseBody(task);
        if (StringUtils.isNotEmpty(body)) {
            if (isDetailPage(task.getUrl())) {
                logger.info("開始解析詳情頁...");
                parseContent(body);
            } else {
                logger.info("開始解析列表頁...");
                extractPageLinks(body);
            }
        }
    }

    public void extractPageLinks(String body) {

        LinkFilter hostFilter = new LinkFilter() {
            String urlHost = CommonUtil.getUrlPrefix(task.getUrl());

            public boolean accept(String link) {
                return link.contains(urlHost);
            }
        };

        String[] fileExtensions = (".xls,.xml,.txt,.pdf,.jpg,.mp3,.mp4,.doc,.mpg,.mpeg,.jpeg,.gif,.png,.js,.zip," +
                ".rar,.exe,.swf,.rm,.ra,.asf,.css,.bmp,.pdf,.z,.gz,.tar,.cpio,.class").split(",");
        LinkFilter fileExtensionFilter = new FileExtensionFilter(fileExtensions);

        AndFilter filter = new AndFilter(new LinkFilter[]{hostFilter, fileExtensionFilter});

        urlList = LinkExtractor.extractLinks(task.getUrl(), body, filter);
    }

    public List<String> getUrlList() {
        return urlList;
    }

    public abstract void parseContent(String body);

    public abstract boolean isDetailPage(String url);

}

父介面定義了兩個規則:

  • 解析規則,即什麼時候解析正文,什麼時候提取列表連結;

  • 提取連結規則,即過濾掉哪些不需要的連結。

但是我們注意到,父介面中用來解析網站正文內容的 parseContent(String body) 是抽象方法。而這,正是實際的外掛類應該完成的事情。這裡,我們以 XmuPlugin 為例:

package plugins;

import entity.Task;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import util.CommonUtil;
import util.FileUtils;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * xmu 外掛
 *
 * @author panda
 * @date 2017/12/01
 */
@Plugin(value = "sm.xmu.edu.cn")
public class XmuPlugin extends AbstractPlugin {

    private static final Logger logger = LoggerFactory.getLogger(XmuPlugin.class);

    public XmuPlugin(Task task) {
        super(task);
    }

    @Override
    public void parseContent(String body) {

        Document doc = CommonUtil.getDocument(body);

        try {
            String title = doc.select("p.h1").first().text();
            String publishTimeStr = doc.select("div.right-content").first().text();
            publishTimeStr = CommonUtil.match(publishTimeStr, "(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})")[1];
            Date publishTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(publishTimeStr);
            String content = "";
            Elements elements = doc.select("p.MsoNormal");
            for (Element element : elements) {
                content += "\n" + element.text();
            }

            logger.info("title: " + title);
            logger.info("publishTime: " + publishTime);
            logger.info("content: " + content);

            FileUtils.writeFile(title + ".txt", content);
        } catch (Exception e) {
            logger.error(" 解析內容異常:" + task.getUrl(), e);
        }
    }

    @Override
    public boolean isDetailPage(String url) {
        return CommonUtil.isMatch(url, "&a=show&catid=\\d+&id=\\d+");
    }

}

在 XmuPlugin 中,我們做了兩件事:

  • 定義詳文頁的具體規則;

  • 解析出具體的正文內容。

採集示例

至此,我們成功地完成了 Java 簡易爬蟲程式,下面,讓我們看下實際的採集情況。

image

image

如動態圖所示,程式成功地將網站上的新聞內容採集下來了。

相關推薦

爬蟲實戰一個簡易 Java 爬蟲程式實現

前面,我們分別介紹了爬蟲程式的 3 個部分:原始碼下載、內容解析、頁面連結提取。現在,讓我們把這些部分串起來,構建一個完整的簡易爬蟲程式。 爬蟲流程 構建程式前,我們首先需要了解爬蟲的具體流程。 一個簡易的爬蟲程式,具備以下流程: 若

Python爬蟲實戰股票資料定向爬蟲

功能簡介 目標: 獲取上交所和深交所所有股票的名稱和交易資訊。 輸出: 儲存到檔案中。 技術路線: requests—bs4–re 語言:python3.5 說明 網站選擇原則: 股票資訊靜態存在於html頁面中,非js程式碼生成,沒有Robbts

Python網絡爬蟲實戰根據天貓胸罩銷售數據分析中國女性胸部大小分布

直方圖 回調 ams find tags ram 可視化分析 discus 綜合應用 本文實現一個非常有趣的項目,這個項目是關於胸罩銷售數據分析的。是網絡爬蟲和數據分析的綜合應用項目。本項目會從天貓抓取胸罩銷售數據,並將這些數據保存到SQLite數據庫中,然後對數據進行清洗

Python網絡爬蟲實戰天貓胸罩銷售數據分析

顯示 來講 數據顯示 display colors python網絡 java 讀者 rep 本文實現一個非常有趣的項目,這個項目是關於胸罩銷售數據分析的。是網絡爬蟲和數據分析的綜合應用項目。本項目會從天貓抓取胸罩銷售數據,並將這些數據保存到SQLite數據庫中,然後對數據

python爬蟲實戰利用scrapy,短短50行代碼下載整站短視頻

start mongodb efi 本地 rtp 公司 loader 右鍵 more 近日,有朋友向我求助一件小事兒,他在一個短視頻app上看到一個好玩兒的段子,想下載下來,可死活找不到下載的方法。這忙我得幫,少不得就抓包分析了一下這個app,找到了視頻的下載鏈接,幫他解決

Python爬蟲實戰抓取並儲存百度雲資源(附程式碼)

尋找並分析百度雲的轉存api 首先你得有一個百度雲盤的賬號,然後登入,用瀏覽器(這裡用火狐瀏覽器做示範)開啟一個分享連結。F12開啟控制檯進行抓包。手動進行轉存操作:全選檔案->儲存到網盤->選擇路徑->確定。點選【確定】前建議先清空一下抓包記錄,這樣可以精確定位到轉存的api,這就是

Scrapy爬蟲實戰使用代理訪問

Scapy爬蟲實戰:使用代理訪問 Middleware 中介軟體設定代理 middlewares.py settings.py spider 配置meta使用proxy 快代理 前面我們簡單的設定了h

scrapy爬蟲實戰偽裝headers構造假IP騙過ip138.com

scrapy爬蟲實戰:偽裝headers構造假IP騙過ip138.com Middleware 中介軟體偽造Header Util.py middlewares.py settings.py ip138.py

Scrapy爬蟲實戰百度搜索找到自己

Scrapy爬蟲實戰:百度搜索找到自己 背景 分析 怎麼才算找到了自己 怎麼才能拿到百度搜索標題 怎麼爬取更多頁面 baidu_search.py 宣告BaiDuSearchItem Items

Python網路爬蟲實戰抓取和分析天貓胸罩銷售資料

本文實現一個非常有趣的專案,這個專案是關於胸罩銷售資料分析的。Google曾給出了一幅世界女性胸部尺寸分佈地圖 ,從地圖中可以明顯看出中國大部分地區呈現綠色(表示平均胸部尺寸為A罩杯),少部分地區呈現藍色(表示平均胸部尺寸為B罩杯) 現在李寧老師也來驗證一下這個

python爬蟲實戰利用scrapy,短短50行程式碼下載整站短視訊

近日,有朋友向我求助一件小事兒,他在一個短視訊app上看到一個好玩兒的段子,想下載下來,可死活找不到下載的方法。這忙我得幫,少不得就抓包分析了一下這個app,找到了視訊的下載連結,幫他解決了這個小問題。 因為這個事兒,勾起了我另一個念頭,這不最近一直想把python爬蟲方面的知識梳理梳理嗎,乾脆藉機行事,正湊

Python3+Selenium爬蟲實戰微博粉絲榜水分大揭祕

高能預警!分析到最後,我不得不感慨這個世界太真實了! 文中有大量程式碼,注重閱讀體驗的請在PC站開啟!或者直接去我的個人部落格(www.data-insights.cn)閱讀! 一、微博粉絲榜:一潭深水 微博粉絲榜爭奪戰由來已久,每個明星在榜單上的位置似乎就象徵著他(她)在粉

零基礎Python爬蟲實戰豆瓣電影TOP250

我們曾經抓取過貓眼電影TOP100,並進行了簡單的分析。但是眾所周知,豆瓣的使用者比較小眾、比較獨特,那麼豆瓣的TOP250又會是哪些電影呢? 我在整理程式碼的時候突然發現一年多以前的爬蟲程式碼竟然還能使用……那今天就用它來演示下,如何通過urllib+BeautifulSoup來快

python3 爬蟲實戰爬蟲新增 GUI 影象介面

  From:https://blog.csdn.net/Fan_shui/article/details/81611752     一、前言     前面我們寫的爬蟲只能執行在具有python環境的電腦上,若是把原始碼發給別人,很大可

python3 爬蟲實戰mitmproxy 對接 python 下載抖音小視訊

  From:https://blog.csdn.net/Fan_shui/article/details/81461253     一、前言   前面我們已經用 appium 爬取了微信朋友圈,今天我們學習下 mitmproxy,mi

python3 爬蟲實戰 用 Appium 抓取手機 app 微信 的 資料

  From:https://blog.csdn.net/Fan_shui/article/details/81413595   本編教程從 appium 的環境配置開始,到抓取手機 app 微信朋友圈結束。 知乎:https://zhuanlan.zhihu.c

Python爬蟲實戰 批量採集股票資料,並儲存到Excel中

小編說:通過本文,讀者可以掌握分析網頁的技巧、Python編寫網路程式的方法、Excel的操作,以及正則表示式的使用。這些都是爬蟲專案中必備的知識和技能。本文選自《Python帶我起飛》。 例項描述:通過編寫爬蟲,將指定日期時段內的全部上市公司股票資料爬取下來,並按照股

Python爬蟲入門筆記一個簡單的爬蟲架構

      上次我們從對爬蟲進行簡單的介紹,今天我們引入一個簡單爬蟲的技術架構,解釋爬蟲技術架構中的幾個模組,對爬蟲先有一個整體的認知,方便對爬蟲的理解和後面的程式設計。      簡單的爬蟲架構:URL管理、網頁下載、網頁解析、輸出部分,如下圖:       1、UR

高階Python爬蟲實戰破解極驗滑動驗證碼

今天給大家帶來的是極驗驗證碼的selenium破解之法,是不是有點小激動呢,小夥伴們等不及了,讓

python3程式設計08-爬蟲實戰爬取網路圖片

本篇部落格爬取內容如下: 爬取校花網的圖片   準備工作: 1.安裝python3 2.安裝pycharm 3.安裝Scrapy,參考:Scrapy安裝   cmd命令新建Scrapy工程 1. 在D:\PythonProjects目錄下新建