前言
現在的技術博客(社區)越來越多,比如:imooc、spring4All、csdn、cnblogs或者iteye等,有很多朋友可能在這些網站上都發表過博文,當有一天我們想自己搞一個博客網站時就會發現好多東西已經寫過了,我們不可能再重新寫一遍,況且多個平台上都有自己發表的文章,也不可能挨個去各個平台ctrl c + ctrl v。鑒於此, 我在我的開源博客里新開發了一個“博客遷移”的功能,目前支持imooc、csdn、iteye和cnblogs,后期會適配更多站點。
功能介紹
如下視頻所示:
抓取展示:

功能特點
使用方便,抓取規則已內置,只需修改很少的配置就可運行。支持同步抓取文章標簽、description和keywords,支持轉存圖片文件。使用開源的國產爬蟲框架webMagic,方便擴展爬蟲功能。
使用教程
目前,該功能已內置了以下幾個平台(imooc、csdn、cnblogs和iteye),根據不同的平台,程序已默認了一套抓取規則,如下圖系列

cnblogs抓取規則:

使用時,只需要手動指定以下幾項配置即可

其他信息在選擇完博文平台后,程序會自動補充完整。圈中必填的幾項配置如下:
選擇博文平台:選擇待操作的博文平台(程序會自動生成對應平台的抓取規則)
自動轉存圖片:勾選時默認將文章中的圖片轉存到七牛雲中(需提前配置七牛雲)
文章分類:是指抓取的文章保存到本地數據庫中的文章分類
用戶ID:是指各平台中,登陸完成后的用戶ID,程序中已給出了對應獲取的方法
文章總頁數:是指待抓取的用戶所有文章的頁數
Cookie(非必填):只在必須需要登陸才能獲取數據時指定,獲取方式如程序中所示
在指定完博文平台、用戶ID和文章總頁數后,爬蟲的其他配置項就會自動補充完整,最后直接執行該程序即可。 注意:默認同步過來的文章為“草稿”狀態,主要是為了防止抓取的內容錯誤,而直接顯示到網站前台,造成不必要的麻煩。所以,需要手動確認無誤后修改發布狀態。另外,針對一些做了防盜鏈的網站,我們在使用“文章搬運工”時,還要勾選上“自動轉存圖片”,至於為何要這么做,在下面會有解釋。
關於“文章搬運工”功能的實現
“文章搬運工”功能聽起來覺得高大上,類似的比如CSDN和cnblogs里的“博客搬家”功能,其實實現起來很簡單。下面聽我道一道,你也可以輕松做出一個“博客搬家”功能!
“博客搬家”首先需要克服的問題無非就是:怎么從別人的頁面中提取出相關的文章信息后保存到自己的服務器中。說到頁面提取,可能很多同學不約而同的就想到了:爬蟲!沒錯,就是通過最基礎的網絡爬蟲就可實現,而OneBlog的文章搬運工功能就是基於爬蟲實現的。
OneBlog中選用了國產的優秀的開源爬蟲框架:webMagic。
WebMagic是一個簡單靈活的Java爬蟲框架。之所以選擇該框架,完全依賴於它的優秀特性:
- 完全模塊化的設計,強大的可擴展性。
- 核心簡單但是涵蓋爬蟲的全部流程,靈活而強大,也是學習爬蟲入門的好材料。
- 提供豐富的抽取頁面API。
- 無配置,但是可通過POJO+注解形式實現一個爬蟲。
- 支持多線程。
- 支持分布式。
- 支持爬取js動態渲染的頁面。
- 無框架依賴,可以靈活的嵌入到項目中去
關於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 }
第四步,定義爬蟲的入口類
此步不多做解釋,就是最基本啟動爬蟲,然后通過自定義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 }
第五步,提取html規則,運行測試。
以我的博客園為例,爬蟲的一般以文章列表頁作為入口頁面,本文示例為:https://www.cnblogs.com/zhangyadong/,然后我們需要手動提取文章相關內容的抓取規則(OneBlog中主要使用Xsoup-XPath解析器,使用方式參考鏈接)。以推薦一款自研的Java版開源博客系統OneBlog一文為例

如圖所示,需要抽取的一共為六部分:
- 文章標題
- 文章正文內容
- 文章標簽
- 文章發布日期
- 文章作者
- 待抽取的其他文章列表
通過f12查看頁面結構,如下

整理相關規則如下:
- 標題:"//a[@id=cb_post_title_url]/html()"
- 文章正文:"//div[@id=cnblogs_post_body]/html()"
- 標簽:"//div[@id=EntryTag]/a/html()"
- 發布日期:"//span[@id=post-date]/html()"
- 作者:"//div[@class=postDesc]/a[1]/html()"
- 待抽取的其他文章鏈接:".*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

