如今的技術博客(社區)愈來愈多,好比:imooc、spring4All、csdn、cnblogs或者iteye等,有不少朋友可能在這些網站上都發表過博文,當有一天咱們想本身搞一個博客網站時就會發現好多東西已經寫過了,咱們不可能再從新寫一遍,何況多個平臺上都有本身發表的文章,也不可能挨個去各個平臺ctrl c + ctrl v。鑑於此, 我在個人開源博客裏新開發了一個「博客遷移」的功能,目前支持imooc、csdn、iteye和cnblogs,後期會適配更多站點。html
以下視頻所示:java
抓取展現:git
使用方便,抓取規則已內置,只需修改不多的配置就可運行。支持同步抓取文章標籤、description和keywords,支持轉存圖片文件。使用開源的國產爬蟲框架webMagic,方便擴展爬蟲功能。web
目前,該功能已內置瞭如下幾個平臺(imooc、csdn、cnblogs和iteye),根據不一樣的平臺,程序已默認了一套抓取規則,以下圖系列ajax
cnblogs抓取規則:正則表達式
使用時,只須要手動指定如下幾項配置便可spring
其餘信息在選擇完博文平臺後,程序會自動補充完整。圈中必填的幾項配置以下:數據庫
選擇博文平臺:選擇待操做的博文平臺(程序會自動生成對應平臺的抓取規則)後端
自動轉存圖片:勾選時默認將文章中的圖片轉存到七牛雲中(需提早配置七牛雲)瀏覽器
文章分類:是指抓取的文章保存到本地數據庫中的文章分類
用戶ID:是指各平臺中,登錄完成後的用戶ID,程序中已給出了對應獲取的方法
文章總頁數:是指待抓取的用戶全部文章的頁數
Cookie(非必填):只在必須須要登錄才能獲取數據時指定,獲取方式如程序中所示
在指定完博文平臺、用戶ID和文章總頁數後,爬蟲的其餘配置項就會自動補充完整,最後直接執行該程序便可。 注意:默認同步過來的文章爲「草稿」狀態,主要是爲了防止抓取的內容錯誤,而直接顯示到網站前臺,形成沒必要要的麻煩。因此,須要手動確認無誤後修改發佈狀態。另外,針對一些作了防盜鏈的網站,咱們在使用「文章搬運工」時,還要勾選上「自動轉存圖片」,至於爲什麼要這麼作,在下面會有解釋。
「文章搬運工」功能聽起來以爲高大上,相似的好比CSDN和cnblogs裏的「博客搬家」功能,其實實現起來很簡單。下面聽我道一道,你也能夠輕鬆作出一個「博客搬家」功能!
「博客搬家」首先須要克服的問題無非就是:怎麼從別人的頁面中提取出相關的文章信息後保存到本身的服務器中。說到頁面提取,可能不少同窗不約而同的就想到了:爬蟲!沒錯,就是經過最基礎的網絡爬蟲就可實現,而OneBlog的文章搬運工功能就是基於爬蟲實現的。
OneBlog中選用了國產的優秀的開源爬蟲框架:webMagic。
WebMagic是一個簡單靈活的Java爬蟲框架。之因此選擇該框架,徹底依賴於它的優秀特性:
關於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 }
以個人博客園爲例,爬蟲的通常以文章列表頁做爲入口頁面,本文示例爲:https://www.cnblogs.com/zhangyadong/,而後咱們須要手動提取文章相關內容的抓取規則(OneBlog中主要使用Xsoup-XPath解析器,使用方式參考連接)。以推薦一款自研的Java版開源博客系統OneBlog一文爲例
如圖所示,須要抽取的一共爲六部分:
經過f12查看頁面結構,以下
整理相關規則以下:
注:「待抽取的其餘文章連接」就是根據這篇文章的連接抽取出的規則
到這一步爲止,基本的文章信息抽取規則就以獲取完畢,接下來就跑一下測試
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內容的規則。規則一旦肯定了,剩下的無非就是粘貼複製就能完成的代碼而已。
最後打個廣告,若是你以爲這篇文章對你有用,能夠關注個人技術公衆號:碼一碼,你的關注和轉發是對我最大的支持,O(∩_∩)O