爬蟲+基於接口的網絡爬蟲
上一篇講了【java爬蟲】---爬蟲+jsoup輕鬆爬博客,該方式有個很大的侷限性,就是你通過jsoup爬蟲只適合爬靜態網頁,所以只能爬當前頁面的所有新聞。如果需要爬一個網站所有信息,就得通過接口,通過改變參數反覆調該網站的接口,爬到該網站的所有數據信息。
本博客以爬金色財經新聞信息爲對象,去爬取該網站從建站以來發表的所有新聞信息。下面會一步一步講解。這裏重點重點講思路,最後我會提供完整源碼。
第一步:找接口
你要獲得該網站所有新聞數據,第一步當然是獲得接口,通過接口來獲取所有信息。
F12-->Network-->all,找到接口:https://api.jinse.com/v4/information/listcatelogue_key=news&limit=23&information_id=56630&flag=down&version=9.9.9
對這三個參數做個說明:
limit=23 代表每次調用該接口返回23條數據。
information_id=56630 代表下面返回的23條數據是通過大於56630或者小於56630這個ID指來返回數據。
flag=down 代表向下翻頁 這裏也就是指ID小於56630的23條數據。
通過postMan測試
輸入:https://api.jinse.com/v4/information/list?catelogue_key=news&limit=2&information_id=0&flag=down&version=9.9.9(這裏返回兩條,id=0這裏代表最新的兩條數據)
返回json數據格式:
接口返回信息
第二步:通過定時任務開啓爬蟲工作
@Slf4j @Component public class SchedulePressTrigger { @Autowired private CrawlerJinSeLivePressService crawlerJinSeLivePressService; /** * 定時抓取金色財經的新聞 */ @Scheduled(initialDelay = 1000, fixedRate = 600 * 1000) public void doCrawlJinSeLivePress() { // log.info("開始抓取金色財經新聞, time:" + new Date()); try { crawlerJinSeLivePressService.start(); } catch (Exception e) { // log.error("本次抓取金色財經新聞異常", e); } // log.info("結束抓取金色財經新聞, time:" + new Date()); } }
第三步:主要實現類
/** * 抓取金色財經快訊 * @author xub * @since 2018/6/29 */ @Slf4j @Service public class CrawlerJinSeLivePressServiceImpl extends AbstractCrawlLivePressService implements CrawlerJinSeLivePressService { //這個參數代表每一次請求獲得多少個數據 private static final int PAGE_SIZE = 15; //這個是真正翻頁參數,每一次找id比它小的15個數據(有寫接口是通過page=1,2來進行翻頁所以比較好理解一點,其實它們性質一樣) private long bottomId; //這個這裏沒有用到,但是如果有數據層,就需要用到,這裏我只是把它答應到控制檯 @Autowired private LivePressService livePressService; //定時任務運行這個方法,doTask沒有被重寫,所有運行父類的方法 @Override public void start() { try { doTask(CoinPressConsts.CHAIN_FOR_LIVE_PRESS_DATA_URL_FORMAT); } catch (IOException e) { // log.error("抓取金色財經新聞異常", e); } } @Override protected List<PageListPress> crawlPage(int pageNum) throws IOException { // 最多抓取100頁,多抓取也沒有特別大的意思。 if (pageNum >= 100) { return Collections.emptyList(); } // 格式化翻頁參數(第一次bottomId爲0,第二次就是這次爬到的最小bottomId值) String requestUrl = String.format(CoinPressConsts.CHAIN_FOR_LIVE_PRESS_DATA_URL_FORMAT, PAGE_SIZE, bottomId); Response response = OkHttp.singleton().newCall( new Request.Builder().url(requestUrl).addHeader("referer", CoinPressConsts.CHAIN_FOR_LIVE_URL).get().build()) .execute(); if (response.isRedirect()) { // 如果請求發生了跳轉,說明請求不是原來的地址了,返回空數據。 return Collections.emptyList(); } //先獲得json數據格式 String responseText = response.body().string(); //在通過工具類進行數據賦值 JinSePressResult jinSepressResult = JsonUtils.objectFromJson(responseText, JinSePressResult.class); if (null == jinSepressResult) { // 反序列化失敗 System.out.println("抓取金色財經新聞列表反序列化異常"); return Collections.emptyList(); } // 取金色財經最小的記錄id,來進行翻頁 bottomId = jinSepressResult.getBottomId(); //這個是谷歌提供了guava包裏的工具類,Lists這個集合工具,對list集合操作做了些優化提升。 List<PageListPress> pageListPresss = Lists.newArrayListWithExpectedSize(PAGE_SIZE); for (JinSePressResult.DayData dayData : jinSepressResult.getList()) { JinSePressData data = dayData.getExtra(); //新聞發佈時間(時間戳格式)這裏可以來判斷只爬多久時間以內的新聞 long createTime = data.getPublishedAt() * 1000; Long timemill=System.currentTimeMillis(); // if (System.currentTimeMillis() - createTime > CoinPressConsts.MAX_CRAWLER_TIME) { // // 快訊過老了,放棄 // continue; // } SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String sd = sdf.format(new Date(createTime)); // 時間戳轉換成時間 Date newsCreateTime=new Date(); try { //獲得新聞發佈時間 newsCreateTime = sdf.parse(sd); } catch (ParseException e) { e.printStackTrace(); } //具體文章頁面路徑(這裏可以通過這個路徑+jsoup就可以爬新聞正文所有信息了) String href = data.getTopicUrl(); //新聞摘要 String summary = data.getSummary(); //新聞閱讀數量 String pressreadcount = data.getReadNumber(); //新聞標題 String title = dayData.getTitle(); pageListPresss.add(new PageListPress(href,title, Integer.parseInt(pressreadcount), newsCreateTime , summary)); } return pageListPresss; } }
AbstractCrawlLivePressService 類
public abstract class AbstractCrawlLivePressService { String url; public void doTask(String url) throws IOException { this.url = url; int pageNum = 1; //通過 while (true)會一直循環調取接口,直到數據爲空或者時間過老跳出循環 while (true) { List<PageListPress> newsList = crawlPage(pageNum++); // 抓取不到新的內容本次抓取結束 if (CollectionUtils.isEmpty(newsList)) { break; } //這裏並沒有把數據放到數據庫,而是直接從控制檯輸出 for (int i = newsList.size() - 1; i >= 0; i--) { PageListPress pageListNews = newsList.get(i); System.out.println(pageListNews.toString()); } } } //這個由具體實現類實現 protected abstract List<PageListPress> crawlPage(int pageNum) throws IOException; @Data @AllArgsConstructor @NoArgsConstructor public static class PageListPress { //新聞詳情頁面url private String href; //新聞標題 private String title; //新聞閱讀數量 private int readCounts; //新聞發佈時間 private Date createTime; //新聞摘要 private String summary; } }
JinSePressResult
/** *在創建對象的時候一定要分析好json格式的類型 *金色新聞的返回格式就是第一層有普通屬性和一個list集合 *在list集合中又有普通屬性和一個extra的對象。 */ @JsonIgnoreProperties(ignoreUnknown = true) @Data public class JinSePressResult { private int news; private int count; @JsonProperty("top_id") private long topId; @JsonProperty("bottom_id") private long bottomId; //list的名字也要和json數據的list名字一致,否則無用 private List<DayData> list; @Data @JsonIgnoreProperties(ignoreUnknown = true) public static class DayData { private String title; //這裏對象的屬性名extra也要和json的extra名字一致 private JinSePressData extra; @JsonProperty("topic_url") private String topicUrl; } }
這裏需要注意兩點
(1)在創建對象時一定要先搞清楚json格式類型是對象裏含有集合,或者集合中還有對象等等。
(2) 你可以只定義你需要的屬性字段,當你不能和json的屬性名一致但類型不一致。比如上面你改成 List extra 這個時候序列化就會失敗,因爲json的extra明顯是一個對象,而這邊接受的確實一個集合。關鍵是屬
性名一致 所以在賦值的時候就會報錯,序列化失敗。
第四步:看運行結果
這裏只是截取控制檯輸出的部分信息,通過這種方式可以獲得該網站的所有新聞信息。同時我們已經獲得具體新聞的URL,那麼我們就可以通過JSOup來獲取該新聞的所有具體信息。(完美)
第五步:數據庫去重思路
因爲你不可能每一次爬取玩數據都直接放到數據庫中,肯定要比較該條新聞數據庫中是否已經存在,不存在才放到數據庫中。思路如下:
(1)數據庫表中添加一個能辨別該新聞唯一的屬性字段,比如jinse+bottomId組成唯一屬性,或者該新聞具體頁面路徑URI組成唯一屬性。
(2)創建map<URL,Boolean>集合,通過URI看數據庫是否存在,有就<URL,true>,沒有<URL,flase>。
(3)在存儲之前通過map.get(URI)如果爲false則存儲數據庫中。
Git源碼
首先說明下,源碼本人是通過Idea測試運行通過,這裏用了lombok,你需要現在idea或者eclipse配置Lombok。
源碼地址:https://github.com/yudiandemingzi/panjiekou
想太多,做太少,中間的落差就是煩惱。想沒有煩惱,要麼別想,要麼多做。中校【9】