詳細介紹如何自研一款"博客搬家"功能

前言

  如今的技術博客(社區)愈來愈多,好比:imooc、spring4All、csdn、cnblogs或者iteye等,有不少朋友可能在這些網站上都發表過博文,當有一天咱們想本身搞一個博客網站時就會發現好多東西已經寫過了,咱們不可能再從新寫一遍,何況多個平臺上都有本身發表的文章,也不可能挨個去各個平臺ctrl c + ctrl v。鑑於此, 我在個人開源博客裏新開發了一個「博客遷移」的功能,目前支持imooc、csdn、iteye和cnblogs,後期會適配更多站點。html

功能介紹

  以下視頻所示:java

  抓取展現:git

功能特色

  使用方便,抓取規則已內置,只需修改不多的配置就可運行。支持同步抓取文章標籤descriptionkeywords,支持轉存圖片文件。使用開源的國產爬蟲框架webMagic,方便擴展爬蟲功能。web

使用教程

  目前,該功能已內置瞭如下幾個平臺(imooc、csdn、cnblogs和iteye),根據不一樣的平臺,程序已默認了一套抓取規則,以下圖系列ajax

  cnblogs抓取規則:正則表達式

  使用時,只須要手動指定如下幾項配置便可spring

  其餘信息在選擇完博文平臺後,程序會自動補充完整。圈中必填的幾項配置以下:數據庫

選擇博文平臺:選擇待操做的博文平臺(程序會自動生成對應平臺的抓取規則)後端

自動轉存圖片:勾選時默認將文章中的圖片轉存到七牛雲中(需提早配置七牛雲瀏覽器

文章分類:是指抓取的文章保存到本地數據庫中的文章分類

用戶ID:是指各平臺中,登錄完成後的用戶ID,程序中已給出了對應獲取的方法

文章總頁數:是指待抓取的用戶全部文章的頁數

Cookie(非必填:只在必須須要登錄才能獲取數據時指定,獲取方式如程序中所示

  在指定完博文平臺用戶ID文章總頁數後,爬蟲的其餘配置項就會自動補充完整,最後直接執行該程序便可。 注意:默認同步過來的文章爲「草稿」狀態,主要是爲了防止抓取的內容錯誤,而直接顯示到網站前臺,形成沒必要要的麻煩。因此,須要手動確認無誤後修改發佈狀態。另外,針對一些作了防盜鏈的網站,咱們在使用「文章搬運工」時,還要勾選上「自動轉存圖片」,至於爲什麼要這麼作,在下面會有解釋。

關於「文章搬運工」功能的實現

   「文章搬運工」功能聽起來以爲高大上,相似的好比CSDN和cnblogs裏的「博客搬家」功能,其實實現起來很簡單。下面聽我道一道,你也能夠輕鬆作出一個「博客搬家」功能!

  「博客搬家」首先須要克服的問題無非就是:怎麼從別人的頁面中提取出相關的文章信息後保存到本身的服務器中。說到頁面提取,可能不少同窗不約而同的就想到了:爬蟲!沒錯,就是經過最基礎的網絡爬蟲就可實現,而OneBlog的文章搬運工功能就是基於爬蟲實現的。

  OneBlog中選用了國產的優秀的開源爬蟲框架:webMagic

  WebMagic是一個簡單靈活的Java爬蟲框架。之因此選擇該框架,徹底依賴於它的優秀特性:

  1. 徹底模塊化的設計,強大的可擴展性。
  2. 核心簡單可是涵蓋爬蟲的所有流程,靈活而強大,也是學習爬蟲入門的好材料。
  3. 提供豐富的抽取頁面API。
  4. 無配置,可是可經過POJO+註解形式實現一個爬蟲。
  5. 支持多線程。
  6. 支持分佈式。
  7. 支持爬取js動態渲染的頁面。
  8. 無框架依賴,能夠靈活的嵌入到項目中去

  關於webMagic的其餘詳細介紹,請去webMagic的官網查閱,本文不作贅述。

  下面針對OneBlog中的「文章搬運工」功能作一下簡單的分析。

  第一步,添加依賴包

 1 <dependency>
 2     <groupId>us.codecraft</groupId>
 3     <artifactId>webmagic-core</artifactId>
 4     <version>0.7.3</version>
 5     <exclusions>
 6         <exclusion>
 7             <groupId>org.slf4j</groupId>
 8             <artifactId>slf4j-log4j12</artifactId>
 9         </exclusion>
10     </exclusions>
11 </dependency>
12 <dependency>
13     <groupId>us.codecraft</groupId>
14     <artifactId>webmagic-extension</artifactId>
15     <version>0.7.3</version>
16     <exclusions>
17         <exclusion>
18             <groupId>org.slf4j</groupId>
19             <artifactId>slf4j-log4j12</artifactId>
20         </exclusion>
21     </exclusions>
22 </dependency>

  第二步,抽取爬蟲規則

爲了方便擴展,咱們要抽象出webMagic爬蟲運行時須要的基本屬性到BaseModel.java

  1 /**
  2  * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
  3  * @website https://www.zhyd.me
  4  * @version 1.0
  5  * @date 2018/7/23 13:33
  6  */
  7 @Data
  8 public class BaseModel {
  9     @NotEmpty(message = "必須指定標題抓取規則(xpath)")
 10     private String titleRegex;
 11     @NotEmpty(message = "必須指定內容抓取規則(xpath)")
 12     private String contentRegex;
 13     @NotEmpty(message = "必須指定發佈日期抓取規則(xpath)")
 14     private String releaseDateRegex;
 15     @NotEmpty(message = "必須指定做者抓取規則(xpath)")
 16     private String authorRegex;
 17     @NotEmpty(message = "必須指定待抓取的url抓取規則(xpath)")
 18     private String targetLinksRegex;
 19     private String tagRegex;
 20     private String keywordsRegex = "//meta [@name=keywords]/@content";
 21     private String descriptionRegex = "//meta [@name=description]/@content";
 22     @NotEmpty(message = "必須指定網站根域名")
 23     private String domain;
 24     private String charset = "utf8";
 25 
 26     /**
 27      * 每次爬取頁面時的等待時間
 28      */
 29     @Max(value = 5000, message = "線程間隔時間最大隻能指定爲5000毫秒")
 30     @Min(value = 1000, message = "線程間隔時間最小隻能指定爲1000毫秒")
 31     private int sleepTime = 1000;
 32 
 33     /**
 34      * 抓取失敗時重試的次數
 35      */
 36     @Max(value = 5, message = "抓取失敗時最多隻能重試5次")
 37     @Min(value = 1, message = "抓取失敗時最少只能重試1次")
 38     private int retryTimes = 2;
 39 
 40     /**
 41      * 線程個數
 42      */
 43     @Max(value = 5, message = "最多隻能開啓5個線程(線程數量越多越耗性能)")
 44     @Min(value = 1, message = "至少要開啓1個線程")
 45     private int threadCount = 1;
 46 
 47     /**
 48      * 抓取入口地址
 49      */
 50 //    @NotEmpty(message = "必須指定待抓取的網址")
 51     private String[] entryUrls;
 52 
 53     /**
 54      * 退出方式{1:等待時間(waitTime必填),2:抓取到的url數量(urlCount必填)}
 55      */
 56     private int exitWay = 1;
 57     /**
 58      * 單位:秒
 59      */
 60     private int waitTime = 60;
 61     private int urlCount = 100;
 62 
 63     private List<Cookie> cookies = new ArrayList<>();
 64     private Map<String, String> headers = new HashMap<>();
 65     private String ua = "Mozilla/5.0 (ozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36";
 66 
 67     private String uid;
 68     private Integer totalPage;
 69 
 70     /* 保留字段,針對ajax渲染的頁面 */
 71     private Boolean ajaxRequest = false;
 72     /* 是否轉存圖片 */
 73     private boolean convertImg = false;
 74 
 75     public String getUid() {
 76         return uid;
 77     }
 78 
 79     public BaseModel setUid(String uid) {
 80         this.uid = uid;
 81         return this;
 82     }
 83 
 84     public Integer getTotalPage() {
 85         return totalPage;
 86     }
 87 
 88     public BaseModel setTotalPage(Integer totalPage) {
 89         this.totalPage = totalPage;
 90         return this;
 91     }
 92 
 93     public BaseModel setTitleRegex(String titleRegex) {
 94         this.titleRegex = titleRegex;
 95         return this;
 96     }
 97 
 98     public BaseModel setContentRegex(String contentRegex) {
 99         this.contentRegex = contentRegex;
100         return this;
101     }
102 
103     public BaseModel setReleaseDateRegex(String releaseDateRegex) {
104         this.releaseDateRegex = releaseDateRegex;
105         return this;
106     }
107 
108     public BaseModel setAuthorRegex(String authorRegex) {
109         this.authorRegex = authorRegex;
110         return this;
111     }
112 
113     public BaseModel setTargetLinksRegex(String targetLinksRegex) {
114         this.targetLinksRegex = targetLinksRegex;
115         return this;
116     }
117 
118     public BaseModel setTagRegex(String tagRegex) {
119         this.tagRegex = tagRegex;
120         return this;
121     }
122 
123     public BaseModel setKeywordsRegex(String keywordsRegex) {
124         this.keywordsRegex = keywordsRegex;
125         return this;
126     }
127 
128     public BaseModel setDescriptionRegex(String descriptionRegex) {
129         this.descriptionRegex = descriptionRegex;
130         return this;
131     }
132 
133     public BaseModel setDomain(String domain) {
134         this.domain = domain;
135         return this;
136     }
137 
138     public BaseModel setCharset(String charset) {
139         this.charset = charset;
140         return this;
141     }
142 
143     public BaseModel setSleepTime(int sleepTime) {
144         this.sleepTime = sleepTime;
145         return this;
146     }
147 
148     public BaseModel setRetryTimes(int retryTimes) {
149         this.retryTimes = retryTimes;
150         return this;
151     }
152 
153     public BaseModel setThreadCount(int threadCount) {
154         this.threadCount = threadCount;
155         return this;
156     }
157 
158     public BaseModel setEntryUrls(String[] entryUrls) {
159         this.entryUrls = entryUrls;
160         return this;
161     }
162 
163     public BaseModel setEntryUrls(String entryUrls) {
164         if (StringUtils.isNotEmpty(entryUrls)) {
165             this.entryUrls = entryUrls.split("\r\n");
166         }
167         return this;
168     }
169 
170     public BaseModel setExitWay(int exitWay) {
171         this.exitWay = exitWay;
172         return this;
173     }
174 
175     public BaseModel setWaitTime(int waitTime) {
176         this.waitTime = waitTime;
177         return this;
178     }
179 
180     public BaseModel setHeader(String key, String value) {
181         Map<String, String> headers = this.getHeaders();
182         headers.put(key, value);
183         return this;
184     }
185 
186     public BaseModel setHeader(String headersStr) {
187         if (StringUtils.isNotEmpty(headersStr)) {
188             String[] headerArr = headersStr.split("\r\n");
189             for (String s : headerArr) {
190                 String[] header = s.split("=");
191                 setHeader(header[0], header[1]);
192             }
193         }
194         return this;
195     }
196 
197     public BaseModel setCookie(String domain, String key, String value) {
198         List<Cookie> cookies = this.getCookies();
199         cookies.add(new Cookie(domain, key, value));
200         return this;
201     }
202 
203     public BaseModel setCookie(String cookiesStr) {
204         if (StringUtils.isNotEmpty(cookiesStr)) {
205             List<Cookie> cookies = this.getCookies();
206             String[] cookieArr = cookiesStr.split(";");
207             for (String aCookieArr : cookieArr) {
208                 String[] cookieNode = aCookieArr.split("=");
209                 if (cookieNode.length <= 1) {
210                     continue;
211                 }
212                 cookies.add(new Cookie(cookieNode[0].trim(), cookieNode[1].trim()));
213             }
214         }
215         return this;
216     }
217 
218     public BaseModel setAjaxRequest(boolean ajaxRequest) {
219         this.ajaxRequest = ajaxRequest;
220         return this;
221     }
222 }

如上方代碼中所示,咱們抽取出了基本的抓取規則和針對不一樣平臺設置的網站屬性(domain、cookies和headers等)。

  第三步,編寫解析器

由於「博客遷移功能」目前只涉及到頁面的解析、抽取,因此,咱們只須要實現webMagic的PageProcessor接口便可。這裏有個關鍵點須要注意:隨着網絡技術的發展,如今先後端分離的網站愈來愈多,而先後端分離的網站基本經過ajax渲染頁面。這種狀況下,httpClient獲取到的頁面內容只是js渲染前的html,所以按照常規的解析方式,是解析不到這部份內容的,所以咱們須要針對普通的html頁面和js渲染的頁面分別提供解析器。本文主要講解針對普通html的解析方式,至於針對js渲染的頁面的解析,之後會另行寫文介紹。

 1 /**
 2  * 統一對頁面進行解析處理
 3  *
 4  * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 5  * @version 1.0
 6  * @website https://www.zhyd.me
 7  * @date 2018/7/31 17:37
 8  */
 9 @Slf4j
10 public class BaseProcessor implements PageProcessor {
11     private static BaseModel model;
12 
13     BaseProcessor() {
14     }
15 
16     BaseProcessor(BaseModel m) {
17         model = m;
18     }
19 
20     @Override
21     public void process(Page page) {
22         Processor processor = new HtmlProcessor();
23         if (model.getAjaxRequest()) {
24             processor = new JsonProcessor();
25         }
26         processor.process(page, model);
27 
28     }
29 
30     @Override
31     public Site getSite() {
32         Site site = Site.me()
33                 .setCharset(model.getCharset())
34                 .setDomain(model.getDomain())
35                 .setSleepTime(model.getSleepTime())
36                 .setRetryTimes(model.getRetryTimes());
37 
38         //添加抓包獲取的cookie信息
39         List<Cookie> cookies = model.getCookies();
40         if (CollectionUtils.isNotEmpty(cookies)) {
41             for (Cookie cookie : cookies) {
42                 if (StringUtils.isEmpty(cookie.getDomain())) {
43                     site.addCookie(cookie.getName(), cookie.getValue());
44                     continue;
45                 }
46                 site.addCookie(cookie.getDomain(), cookie.getName(), cookie.getValue());
47             }
48         }
49         //添加請求頭,有些網站會根據請求頭判斷該請求是由瀏覽器發起仍是由爬蟲發起的
50         Map<String, String> headers = model.getHeaders();
51         if (MapUtils.isNotEmpty(headers)) {
52             Set<Map.Entry<String, String>> entrySet = headers.entrySet();
53             for (Map.Entry<String, String> entry : entrySet) {
54                 site.addHeader(entry.getKey(), entry.getValue());
55             }
56         }
57         return site;
58     }
59 }

Processor.java接口,只提供一個process方法供實際的解析器實現

 1 /**
 2  * 頁面解析接口
 3  *
 4  * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 5  * @version 1.0
 6  * @website https://www.zhyd.me
 7  * @date 2018/7/31 17:37
 8  */
 9 public interface Processor {
10     void process(Page page, BaseModel model);
11 }

HtmlProcessor.java

 1 /**
 2  * 解析處理普通的Html網頁
 3  *
 4  * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 5  * @version 1.0
 6  * @website https://www.zhyd.me
 7  * @date 2018/7/31 17:37
 8  */
 9 public class HtmlProcessor implements Processor {
10 
11     @Override
12     public void process(Page page, BaseModel model) {
13         Html pageHtml = page.getHtml();
14         String title = pageHtml.xpath(model.getTitleRegex()).get();
15         String source = page.getRequest().getUrl();
16         if (!StringUtils.isEmpty(title) && !"null".equals(title) && !Arrays.asList(model.getEntryUrls()).contains(source)) {
17             page.putField("title", title);
18             page.putField("source", source);
19             page.putField("releaseDate", pageHtml.xpath(model.getReleaseDateRegex()).get());
20             page.putField("author", pageHtml.xpath(model.getAuthorRegex()).get());
21             page.putField("content", pageHtml.xpath(model.getContentRegex()).get());
22             page.putField("tags", pageHtml.xpath(model.getTagRegex()).all());
23             page.putField("description", pageHtml.xpath(model.getDescriptionRegex()).get());
24             page.putField("keywords", pageHtml.xpath(model.getKeywordsRegex()).get());
25         }
26         page.addTargetRequests(page.getHtml().links().regex(model.getTargetLinksRegex()).all());
27     }
28 }

JsonProcessor.java

 1 /**
 2  * 解析處理Ajax渲染的頁面(待完善)
 3  *
 4  * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 5  * @version 1.0
 6  * @website https://www.zhyd.me
 7  * @date 2018/7/31 17:37
 8  */
 9 public class JsonProcessor implements Processor {
10     @Override
11     public void process(Page page, BaseModel model) {
12         String rawText = page.getRawText();
13         String title = new JsonPathSelector(model.getTitleRegex()).select(rawText);
14         if (!StringUtils.isEmpty(title) && !"null".equals(title)) {
15             page.putField("title", title);
16             page.putField("releaseDate", new JsonPathSelector(model.getReleaseDateRegex()).select(rawText));
17             page.putField("author", new JsonPathSelector(model.getAuthorRegex()).select(rawText));
18             page.putField("content", new JsonPathSelector(model.getContentRegex()).select(rawText));
19             page.putField("source", page.getRequest().getUrl());
20         }
21         page.addTargetRequests(page.getHtml().links().regex(model.getTargetLinksRegex()).all());
22     }
23 }
View Code

  第四步,定義爬蟲的入口類

此步很少作解釋,就是最基本啓動爬蟲,而後經過自定義Pipeline對數據進行組裝

 1 /**
 2  * 爬蟲入口
 3  *
 4  * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 5  * @version 1.0
 6  * @website https://www.zhyd.me
 7  * @date 2018/7/23 10:38
 8  */
 9 @Slf4j
10 public class ArticleSpiderProcessor extends BaseProcessor implements BaseSpider<Article> {
11 
12     private BaseModel model;
13     private PrintWriter writer;
14     private ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
15 
16     private ArticleSpiderProcessor() {
17     }
18 
19     public ArticleSpiderProcessor(BaseModel model, PrintWriter writer) {
20         super(model);
21         this.model = model;
22         this.writer = writer;
23     }
24 
25     public ArticleSpiderProcessor(BaseModel model) {
26         super(model);
27         this.model = model;
28     }
29 
30     /**
31      * 運行爬蟲並返回結果
32      *
33      * @return
34      */
35     @Override
36     public List<Article> run() {
37         List<String> errors = validateModel(model);
38         if (CollectionUtils.isNotEmpty(errors)) {
39             WriterUtil.writer2Html(writer, "校驗不經過!請依據下方提示,檢查輸入參數是否正確......");
40             for (String error : errors) {
41                 WriterUtil.writer2Html(writer, ">> " + error);
42             }
43             return null;
44         }
45 
46         List<Article> articles = new LinkedList<>();
47 
48         WriterUtil.writer2Html(writer, ">> 爬蟲初始化完成,共需抓取 " + model.getTotalPage() + " 頁數據...");
49 
50         Spider spider = Spider.create(new ArticleSpiderProcessor())
51                 .addUrl(model.getEntryUrls())
52                 .addPipeline((resultItems, task) -> {
53                     Map<String, Object> map = resultItems.getAll();
54                     String title = String.valueOf(map.get("title"));
55                     if (StringUtils.isEmpty(title) || "null".equals(title)) {
56                         return;
57                     }
58                     String content = String.valueOf(map.get("content"));
59                     String source = String.valueOf(map.get("source"));
60                     String releaseDate = String.valueOf(map.get("releaseDate"));
61                     String author = String.valueOf(map.get("author"));
62                     String description = String.valueOf(map.get("description"));
63                     description = StringUtils.isNotEmpty(description) ? description.replaceAll("\r\n| ", "")
64                             : content.length() > 100 ? content.substring(0, 100) : content;
65                     String keywords = String.valueOf(map.get("keywords"));
66                     keywords = StringUtils.isNotEmpty(keywords) && !"null".equals(keywords) ? keywords.replaceAll(" +|,", ",").replaceAll(",,", ",") : null;
67                     List<String> tags = (List<String>) map.get("tags");
68                     log.info(String.format(">> 正在抓取 -- %s -- %s -- %s -- %s", source, title, releaseDate, author));
69                     WriterUtil.writer2Html(writer, String.format(">> 正在抓取 -- <a href=\"%s\" target=\"_blank\">%s</a> -- %s -- %s", source, title, releaseDate, author));
70                     articles.add(new Article(title, content, author, releaseDate, source, description, keywords, tags));
71                 })
72                 .thread(model.getThreadCount());
73         // 啓動爬蟲
74         spider.run();
75         return articles;
76     }
77 
78     private <T> List<String> validateModel(T t) {
79         Validator validator = factory.getValidator();
80         Set<ConstraintViolation<T>> constraintViolations = validator.validate(t);
81 
82         List<String> messageList = new ArrayList<>();
83         for (ConstraintViolation<T> constraintViolation : constraintViolations) {
84             messageList.add(constraintViolation.getMessage());
85         }
86         return messageList;
87     }
88 }
View Code

  第五步,提取html規則,運行測試。

個人博客園爲例,爬蟲的通常以文章列表頁做爲入口頁面,本文示例爲:https://www.cnblogs.com/zhangyadong/,而後咱們須要手動提取文章相關內容的抓取規則(OneBlog中主要使用Xsoup-XPath解析器,使用方式參考連接)。以推薦一款自研的Java版開源博客系統OneBlog一文爲例

如圖所示,須要抽取的一共爲六部分:

  1. 文章標題
  2. 文章正文內容
  3. 文章標籤
  4. 文章發佈日期
  5. 文章做者
  6. 待抽取的其餘文章列表

 經過f12查看頁面結構,以下

整理相關規則以下:

  1. 標題:"//a[@id=cb_post_title_url]/html()"
  2. 文章正文:"//div[@id=cnblogs_post_body]/html()"
  3. 標籤:"//div[@id=EntryTag]/a/html()"
  4. 發佈日期:"//span[@id=post-date]/html()"
  5. 做者:"//div[@class=postDesc]/a[1]/html()"
  6. 待抽取的其餘文章連接:".*www\\.cnblogs\\.com/zhangyadong/p/[\\w\\d]+\\.html"

注:「待抽取的其餘文章連接」就是根據這篇文章的連接抽取出的規則

到這一步爲止,基本的文章信息抽取規則就以獲取完畢,接下來就跑一下測試

 1 @Test
 2 public void cnblogSpiderTest() {
 3     BaseSpider<Article> spider = new ArticleSpiderProcessor(new CnblogModel().setUid("zhangyadong")
 4             .setTotalPage(1)
 5             .setDomain("www.cnblogs.com")
 6             .setTitleRegex("//a[@id=cb_post_title_url]/html()")
 7             .setAuthorRegex("//div[@class=postDesc]/a[1]/html()")
 8             .setReleaseDateRegex("//span[@id=post-date]/html()")
 9             .setContentRegex("//div[@id=cnblogs_post_body]/html()")
10             .setTagRegex("//div[@id=EntryTag]/a/html()")
11             .setTargetLinksRegex(".*www\\.cnblogs\\.com/zhangyadong/p/[\\w\\d]+\\.html")
12             .setHeader("Host", "www.cnblogs.com")
13             .setHeader("Referer", "https://www.cnblogs.com/"));
14     spider.run();
15 }

  Console控制檯打印數據

2018-09-12 11:50:49 [us.codecraft.webmagic.Spider:306] INFO  - Spider www.cnblogs.com started!
2018-09-12 11:50:51 [com.zyd.blog.spider.processor.ArticleSpiderProcessor:89] INFO  - >> 正在抓取 -- https://www.cnblogs.com/zhangyadong/p/oneblog.html -- 推薦一款自研的Java版開源博客系統OneBlog -- 2018-09-11 09:53 -- HandsomeBoy丶
2018-09-12 11:50:52 [us.codecraft.webmagic.Spider:338] INFO  - Spider www.cnblogs.com closed! 2 pages downloaded.

 如圖,文章已成功被抓取,剩下的,無非就是要麼保存到文件中,要麼持久化到數據庫裏。OneBlog中是直接保存到了數據庫裏。

關於文章圖片轉存

  爲何要添加「文章轉存」功能?那是由於一些網站對本站內的靜態資源作了「防盜鏈」,而所謂的「防盜鏈」說簡單點就是:個人東西別人不能用,得須要我受權纔可。這樣作的好處就是,不會讓本身的勞動成果白白給別人作了嫁衣。那麼,針對這一特性,若是在「文章搬運」時,原文圖片未經處理就原封不動的保存下來,以開源博客這篇文章爲例,可能就會碰到以下狀況:

  如上圖,有一些圖片沒法顯示,在控制檯中能夠看到這些圖片全是報錯403,也就是未受權,也就是所謂的被原站作了「防盜鏈」!這個時候,咱們在抓取文章時就須要將原文的圖片所有轉存到本身服務器上,如此一來就解決了「被防盜鏈」的問題。

  針對這一問題,OneBlog中則是經過正則表達式,將全部img標籤的src裏的網絡文件下載下來後轉存到七牛雲中。簡單代碼以下:

 1 private static final Pattern PATTERN = Pattern.compile("<img[^>]+src\\s*=\\s*['\"]([^'\"]+)['\"][^>]*>");
 2 private String parseImgForHtml(String html, String qiniuBasePath, PrintWriter writer) {
 3     if (StringUtils.isEmpty(html)) {
 4         return null;
 5     }
 6     Matcher m = PATTERN.matcher(html);
 7     Set<String> imgUrlSet = new HashSet<>();
 8     while (m.find()) {
 9         String imgUrl = m.group(1);
10         imgUrlSet.add(imgUrl);
11     }
12     if (!CollectionUtils.isEmpty(imgUrlSet)) {
13         WriterUtil.writer2Html(writer, "  > 開始轉存圖片到七牛雲...");
14         for (String imgUrl : imgUrlSet) {
15             String qiniuImgPath = ImageDownloadUtil.convertToQiniu(imgUrl);
16             if (StringUtils.isEmpty(qiniuImgPath)) {
17                 WriterUtil.writer2Html(writer, "  >> 圖片轉存失敗,請確保七牛雲以配置完畢!請查看控制檯詳細錯誤信息...");
18                 continue;
19             }
20             html = html.replaceAll(imgUrl, qiniuBasePath + qiniuImgPath);
21             WriterUtil.writer2Html(writer, String.format("  >> <a href=\"%s\" target=\"_blank\">原圖片</a> convert to <a href=\"%s\" target=\"_blank\">七牛雲</a>...", imgUrl, qiniuImgPath));
22         }
23     }
24     return html;
25 }

  ImageDownloadUtil.convertToQiniu方法以下

 1 /**
 2  * 將網絡圖片轉存到七牛雲
 3  *
 4  * @param imgUrl 網絡圖片地址
 5  */
 6 public static String convertToQiniu(String imgUrl) {
 7     log.debug("download img >> %s", imgUrl);
 8     String qiniuImgPath = null;
 9     try (InputStream is = getInputStreamByUrl(checkUrl(imgUrl));
10          ByteArrayOutputStream outStream = new ByteArrayOutputStream();) {
11         byte[] buffer = new byte[1024];
12         int len = 0;
13         while ((len = is.read(buffer)) != -1) {
14             outStream.write(buffer, 0, len);
15         }
16         qiniuImgPath = QiniuApi.getInstance()
17                 .withFileName("temp." + getSuffixByUrl(imgUrl), QiniuUploadType.SIMPLE)
18                 .upload(outStream.toByteArray());
19     } catch (Exception e) {
20         log.error("Error.", e);
21     }
22     return qiniuImgPath;
23 }

(注:以上代碼只是簡單示例了一下核心代碼,具體代碼請參考個人開源博客:OneBlog

總結

  看完了我上面的介紹,你應該能夠發現,其實技術實現起來,並無太大的難點。主要重難點無非就一個:如何編寫提取html內容的規則。規則一旦肯定了,剩下的無非就是粘貼複製就能完成的代碼而已。

最後聲明

  • 本工具開發初衷只是用來遷移 本身的文章 所用,所以不可用該工具惡意竊取他人勞動成果
  • 因不聽勸阻,使用該工具惡意竊取他們勞動成果而形成的一切不良後果,本人表示:堅定不背鍋!
  • 若是該工具很差用,大家絕對不能打我!
  • 有問題、建議,請留言,或者去gitee上提Issues

        最後打個廣告,若是你以爲這篇文章對你有用,能夠關注個人技術公衆號:碼一碼,你的關注和轉發是對我最大的支持,O(∩_∩)O

相關文章
相關標籤/搜索