1. 程式人生 > >Springboot通過整合Webmagic實現資料抓取功能。

Springboot通過整合Webmagic實現資料抓取功能。

一、什麼是Webmagic.
要使用Webmagic首先需要了解什麼是Webmagic.
webmagic是一個開源的Java垂直爬蟲框架,目標是簡化爬蟲的開發流程,讓開發者專注於邏輯功能的開發。webmagic主要由Downloader(下載器)、PageProcesser(解析器)、Schedule(排程器)和Pipeline(管道)四部分組成。
webmagic採用完全模組化的設計,功能覆蓋整個爬蟲的生命週期(連結提取、頁面下載、內容抽取、持久化),支援多執行緒抓取,分散式抓取,並支援自動重試、自定義UA/cookie等功能。
webmagic包含頁面抽取功能,開發者可以使用css selector、xpath和正則表示式進行連結和內容的提取,支援多個選擇器鏈式呼叫。
二、示例程式碼
(1)pom.xml 檔案新增新的依賴

<!-- webmagic 爬蟲框架 -->
      <dependency> 
            <groupId>us.codecraft</groupId>`這裡寫程式碼片`
            <artifactId>webmagic-core</artifactId>
            <version>0.5.3</version>
     </dependency>
     <dependency>
            <groupId
>
us.codecraft</groupId> <artifactId>webmagic-extension</artifactId> <version>0.5.3</version> </dependency> </dependencies>
(2)Scheduling定時任務設定,也可稱為排程器。
import javax.annotation.Resource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import
org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import com.zhibo.xmt.common.webmagic.xpager.popeline.XpaperZgtcbPopeline; import com.zhibo.xmt.common.webmagic.xpager.processor.XpaperZgtcbProcessor; import us.codecraft.webmagic.Spider; /** * 爬取 xpaper http://i.xpaper.net/cnsports 版面資訊資料 * 每週 二 、 四、日釋出新期刊 * @author Bruce * */ @Component @EnableScheduling public class XpaperWebmagicSchedulingConfig { private final Logger logger = LoggerFactory.getLogger(XpaperWebmagicSchedulingConfig.class); public static final String BASE_URL = "http://i.xpaper.net/cnsports"; @Resource private XpaperZgtcbPopeline xpaperZgtcbPopeline; /** * 中國體彩報 xpaper全媒體數字報 版面內容抓取 */ /** * "0 0/1 18 * * ?" 每天18:00到18:59 沒分鐘執行一次 * * "0 10 4 ? * *" 每天上午4:10觸發 */ @Transactional @Scheduled(cron = "0 10 4 ? * *") public void createLotteryInfo(){ System.out.println("中國體彩報 xpaper全媒體數字報 版面內容抓取"); long startTime, endTime; System.out.println("【爬蟲開始】"); startTime = System.currentTimeMillis(); logger.info("爬取地址:" + BASE_URL); try { Spider spider = Spider.create(new XpaperZgtcbProcessor()); spider.addUrl(BASE_URL); spider.addPipeline(xpaperZgtcbPopeline); spider.thread(5); spider.setExitWhenComplete(true); spider.start(); spider.stop(); } catch (Exception e) { logger.error(e.getMessage(),e); } endTime = System.currentTimeMillis(); System.out.println("【爬蟲結束】"); System.out.println("中國體彩報 xpaper全媒體數字報 版面內容抓取耗時約" + ((endTime - startTime) / 1000) + "秒,已儲存到資料庫."); } }

(3)XpaperZgtcbProcessor解析器,解析要爬取的頁面

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.zhibo.xmt.common.enums.common.EnumCommonStatus;
import com.zhibo.xmt.common.util.DateUtil;
import com.zhibo.xmt.common.vo.pagesub.Journal;
import com.zhibo.xmt.common.vo.pagesub.JournalPage;

import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.selector.Selectable;

/**
 * 中國體彩報 xpaper全媒體數字報 版面內容抓取
 * http://i.xpaper.net/cnsports
 * @author Bruce
 *
 */
@Component
public class XpaperZgtcbProcessor implements PageProcessor{
    private static Logger logger = LoggerFactory.getLogger(XpaperZgtcbProcessor.class);

    // 正則表示式\\. \\轉義java中的\ \.轉義正則中的.
    // 主域名
    public static final String BASE_URL = "http://i.xpaper.net/cnsports";

    private Site site = Site.me()
            .setDomain(BASE_URL)
            .setSleepTime(1000)
            .setRetryTimes(30)
            .setCharset("utf-8")
            .setTimeOut(30000)
            .setUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31");

    @Override
    public Site getSite() {
        return site;
    }

    @Override
    public void process(Page page) {
         if (page.getUrl().regex(BASE_URL).match()) {

             String contentTitle = page.getHtml().xpath("//title/text()").toString();

             /**
              * System.out.println("issue:" + issue);
                System.out.println("issueDesc:" + issueDesc);
                System.out.println("contentTitle:" + contentTitle);

              * contentTitle:中國體彩報 - 第1151期 - 第01版 - A1
                issue: 1151 
                issueDesc:中國體彩報 - 第1151期 
              */

             String[] contentTitles = contentTitle.trim().split("-");
             String issueStr = contentTitles[1].replaceAll("第", "").replaceAll("期", "").replaceAll(" ", "").trim().replaceAll("\\s*", "");

             String issue = new String(issueStr);

             //由於裡面有空格,因此使用了多種方式去空格。
             Pattern p = Pattern.compile("\\s*|\t|\r|\n");
             Matcher m = p.matcher(issue);
             issue = m.replaceAll("");
             issue = issue.replaceAll("\\u00A0","");  

             String issueDesc = contentTitles[0] + "-" + contentTitles[1];

             Journal journal = new Journal();
             journal.setTitle(issueDesc);
             journal.setTitleDesc(contentTitle);
             journal.setIssue(issue);
             journal.setDate(DateUtil.getDateByFormat(DateUtil.getDateByFormat(new Date(), "yyyy-MM-dd"), "yyyy-MM-dd"));
             journal.setDateStr(DateUtil.getDateByFormat(new Date(), "yyyy-MM-dd"));
             journal.setType(1);
             journal.setStatus(EnumCommonStatus.NORMAL.getValue());
             journal.setGrabDate(new Date());
             journal.setCreatedAt(new Date());
             journal.setUpdatedAt(new Date());

             logger.info("期刊資料:" + journal.toString());

             List<Selectable> list = page.getHtml().xpath("//div[@id='m1']/a").nodes();

             if(list != null && list.size() > 0){
                 List<JournalPage> journalPages = new ArrayList<JournalPage>();

                 for(int i = 0; i < list.size(); i++){
                     Selectable s = list.get(i);



                     String link = s.links().toString();

                     String titleStr = s.xpath("//b/text()").toString();
                     if(StringUtils.isBlank(titleStr)){
                         titleStr = s.toString().split(">")[1].replaceAll("</a", "").replaceAll(" ", "").replaceAll("&nbsp;", " ");
                     }

                     String title = new String(titleStr);
//                     title = title.replaceAll("\\s*", "");

//                  Pattern p = Pattern.compile("\\s*|\t|\r|\n");
                    Matcher ma = p.matcher(title);
                    title = ma.replaceAll("");
                    title = title.replaceAll("\\u00A0","");
                    title= title.replaceAll("版", "版 ");


//                     System.out.println("title:" + title);

                     /**
                      * System.out.println("s:" + s.toString());
                        System.out.println("link:" + link);
                        System.out.println("title:" + title);
                      * s:<a href="http://i.xpaper.net/cnsports/release/539/2040.shtml"> <b>第01版 &nbsp; A1</b> </a>
                        link:http://i.xpaper.net/cnsports/release/539/2040.shtml
                        title:第01版   A1
                      */
                     if(StringUtils.isNotBlank(title) && StringUtils.isNotBlank(link)){
                         if(i == 0){
                             journal.setUrl(link);
                             journal.setStageDesc(title);
                         }
                         JournalPage  journalPage = new JournalPage();
                         journalPage.setJournalId(journal.getId());
                         journalPage.setPageHtmlTitle(title);
                         journalPage.setPageHtmlUrl(link);
                         journalPage.setStatus(EnumCommonStatus.NORMAL.getValue());
                         journalPage.setGrabDate(new Date());
                         journalPage.setCreatedAt(new Date());
                         journalPage.setUpdatedAt(new Date());

                         logger.info("版面資料:" + journalPage.toString());

                         journalPages.add(journalPage);
                     }
                 }
                 journal.setJournalPages(journalPages);
                 logger.info("journal.toString():" + journal.toString());
             }
             page.putField("journal", journal); 
          }

    }

//  public static void main(String[] args) {
//        Spider spider = Spider.create(new XpaperZgtcbProcessor());
//        spider.addUrl(BASE_URL);
//        spider.addPipeline(new XpaperZgtcbPopeline());
//        spider.thread(1);
//        spider.setExitWhenComplete(true);
//        spider.start();
////        spider.stop();
//    }

}

(4)XpaperZgtcbPopeline-Pipeline(管道)

import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.zhibo.xmt.common.mapper.pagesub.JournalMapper;
import com.zhibo.xmt.common.mapper.pagesub.JournalPageMapper;
import com.zhibo.xmt.common.vo.pagesub.Journal;
import com.zhibo.xmt.common.vo.pagesub.JournalPage;
import com.zhibo.xmt.common.webmagic.xpager.processor.XpaperZgtcbJournalPageContentProcessor;

import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;


@Service("xpaperZgtcbPopeline")
public class XpaperZgtcbPopeline implements Pipeline{
    private static Logger logger = LoggerFactory.getLogger(XpaperZgtcbPopeline.class);

    @Autowired
    private JournalMapper journalMapper;

    @Autowired
    private JournalPageMapper journalPageMapper;

    @Autowired
    private XpaperZgtcbJournalPageContentPopeline xpaperZgtcbJournalPageContentPopeline;

    @Override
    public void process(ResultItems resultItems, Task task) {
        for (Map.Entry<String, Object> entry : resultItems.getAll().entrySet()) {
            if (entry.getKey().contains("journal")) {
                Journal journal = (Journal) entry.getValue();
                if(journal != null){
                    Journal oldJournal = journalMapper.selectByIssue(journal.getIssue());
                    if(oldJournal == null){
                        try {

                            /**
                             * //替換URL為 appurl
                             * http://i.xpaper.net/cnsports/release/542/2069.shtml
                             * 替換為
                             * http://i.xpaper.net/cnsports/apprelease/542/2069.shtml
                             */
                            journal.setUrl(journal.getUrl().replaceAll("release", "apprelease"));
                            journalMapper.insertSelective(journal);
                            logger.info("journalMapper-insert:" + journal.toString());

                            String oldPageHtmlUrl = null;
                            for(JournalPage journalPage : journal.getJournalPages()){


                                journalPage.setJournalId(journal.getId());
                                /**
                                 * //替換URL為 appurl
                                 * http://i.xpaper.net/cnsports/release/542/2069.shtml
                                 * 替換為
                                 * http://i.xpaper.net/cnsports/apprelease/542/2069.shtml
                                 */
                                oldPageHtmlUrl = journalPage.getPageHtmlUrl();//儲存原有url地址,用於資料頁面內容抓取

                                journalPage.setPageHtmlUrl(journalPage.getPageHtmlUrl().replaceAll("release", "apprelease"));

                                journalPageMapper.insertSelective(journalPage);

                                logger.info("journalPageMapper-insert:" + journalPage.toString());

                                logger.info("XpaperZgtcbJournalPageContentProcessor-start");
                                //這裡我們對後面的頁面進行了深度的抓取,這裡就不再進行過//多的表述,如果需要可以聯絡我。
                                Spider spider = Spider.create(new XpaperZgtcbJournalPageContentProcessor());
                                spider.addUrl(oldPageHtmlUrl);
                                spider.addPipeline(xpaperZgtcbJournalPageContentPopeline);
                                spider.thread(1);
                                spider.setExitWhenComplete(true);
                                spider.start();
                                logger.info("XpaperZgtcbJournalPageContentProcessor-end");
                            }

                        } catch (Exception e) {
                            logger.error(e.getMessage(), e);
                        }
                    }else{
                        logger.info("期號為" + journal.getIssue() + "的期刊已經存在!");
                    }


                }


            }

        }

    }

}

(5)資料入庫
這個部分就不再過多的贅述,因為我們用的是mybatis,所以入庫的部分都是使用的mapper,還有很多專案使用的是JPA,只需要在相應的mappper部分進行替換即可。
(6)資料表設計

①s_journals 期刊資訊表

-- ----------------------------
-- Table structure for s_journals
-- ----------------------------
DROP TABLE IF EXISTS `s_journals`;
CREATE TABLE `s_journals` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `title` varchar(100) DEFAULT NULL COMMENT '標題',
  `title_desc` varchar(100) NOT NULL COMMENT '標題全本',
  `issue` varchar(50) NOT NULL COMMENT '期號 唯一',
  `date` datetime NOT NULL COMMENT '期刊日期 yyyy-MM-dd',
  `date_str` varchar(50) NOT NULL COMMENT '期刊日期字串格式 yyyy-MM-dd',
  `url` varchar(255) NOT NULL COMMENT '版面地址',
  `stage_desc` varchar(255) DEFAULT NULL COMMENT '版面備註',
  `type` int(11) NOT NULL COMMENT '期刊型別\r\n1.中國體彩報\r\n2.其他',
  `status` int(11) NOT NULL COMMENT '狀態\r\n1.正常\r\n2.刪除\r\n',
  `grab_date` datetime NOT NULL COMMENT '抓取時間 yyyy-MM-dd HH:mm:ss',
  `created_at` datetime NOT NULL COMMENT '建立時間 yyyy-MM-dd HH:mm:ss',
  `updated_at` datetime NOT NULL COMMENT '更新時間 yyyy-MM-dd HH:mm:ss',
  PRIMARY KEY (`id`),
  UNIQUE KEY `UNIQUE_ISSUE` (`issue`) USING BTREE,
  KEY `NORNAM_DATE_STR` (`date_str`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8 COMMENT='期刊資訊表\r\n';

②s_journal_pages 期刊版面表

-- ----------------------------
-- Table structure for s_journal_pages
-- ----------------------------
DROP TABLE IF EXISTS `s_journal_pages`;
CREATE TABLE `s_journal_pages` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `journal_id` bigint(20) NOT NULL COMMENT '期刊資訊表ID  journals表id',
  `page_html_title` varchar(100) NOT NULL COMMENT '版面HTML格式標題 與journals_id欄位聯合唯一索引',
  `page_html_url` varchar(500) NOT NULL COMMENT '版面HTML格式地址',
  `page_html_desc` varchar(100) DEFAULT NULL COMMENT '版面html格式備註資訊',
  `page_pdf_title` varchar(100) DEFAULT NULL COMMENT '版面PDF格式標題',
  `page_pdf_url` varchar(500) DEFAULT NULL COMMENT '版面PDF格式地址',
  `page_pdf_desc` varchar(100) DEFAULT NULL COMMENT '版面PDF格式備註資訊',
  `page_desc` varchar(500) DEFAULT NULL COMMENT '版面備註資訊',
  `status` int(11) NOT NULL COMMENT '狀態\r\n1.正常\r\n2.刪除\r\n',
  `grab_date` datetime NOT NULL COMMENT '抓取時間yyyy-MM-dd HH:mm:ss',
  `created_at` datetime NOT NULL COMMENT '建立時間yyyy-MM-dd HH:mm:ss',
  `updated_at` datetime NOT NULL COMMENT '更新時間yyyy-MM-dd HH:mm:ss',
  PRIMARY KEY (`id`),
  UNIQUE KEY `UNIQUE_JOURNALID_PAGEHTMLTITLE` (`journal_id`,`page_html_title`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=197 DEFAULT CHARSET=utf8 COMMENT='期刊版面表(s_ journal_pages)\r\n備註: 由於一個期刊擁有多個版面,因此期刊資訊表與期刊版面表為一對多關係。\r\n因此要建立journal_id 與page_heml_title兩者建立聯合唯一的索引。\r\n';

③s_journal_page_contents 期刊版面內容表

-- ----------------------------
-- Table structure for s_journal_page_contents
-- ----------------------------
DROP TABLE IF EXISTS `s_journal_page_contents`;
CREATE TABLE `s_journal_page_contents` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `journal_id` bigint(20) NOT NULL COMMENT '期刊資訊表ID  Journals表ID',
  `journal_page_id` bigint(20) NOT NULL COMMENT '期刊版面表ID  Journal_pages表ID',
  `content_title` varchar(200) NOT NULL COMMENT '內容標題',
  `content_url` varchar(500) NOT NULL COMMENT '內容url',
  `content_id` varchar(50) DEFAULT NULL COMMENT '內容ID',
  `content_id_desc` varchar(50) DEFAULT NULL COMMENT '內容ID原文',
  `content_desc` varchar(50) DEFAULT NULL COMMENT '內容資訊備註',
  `status` int(11) NOT NULL COMMENT '狀態\r\n1.正常\r\n2.刪除',
  `grab_date` datetime NOT NULL COMMENT '抓取時間yyyy-MM-dd HH:mm:ss',
  `created_at` datetime NOT NULL COMMENT '建立時間yyyy-MM-dd HH:mm:ss',
  `updated_at` datetime NOT NULL COMMENT '更新時間yyyy-MM-dd HH:mm:ss',
  PRIMARY KEY (`id`),
  UNIQUE KEY `UNIQUE_JOURNALID_JOURNALPAGEID_CONTENTTITLE` (`journal_id`,`journal_page_id`,`content_title`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=178 DEFAULT CHARSET=utf8 COMMENT='期刊版面內容表\r\n備註: 由於一個期刊擁有多個版面,因此期刊資訊表與期刊版面表為一對多關係;由於一個版面含有多個內容和標題,因此期刊版面表與期刊版面內容表為一對多關係。\r\n因此要建立journal_id 、journal_page_id 、content_title三者建立聯合唯一的索引。\r\n';