前言
- 很久以前,生活中使用c#和java分別寫過網頁遊戲外掛,通過語言中內置的方式進行爬取數據,併發送新的指令,非常有意思,既能研究爬蟲相關技術又能實踐在休閒生活中。
- 後面也有在工作中,定時爬取中國天氣預報官方信息,提供給前端主頁顯示天氣信息。
需求
- 有朋友需要幫忙收集豆瓣讀書中的書籍信息,根據傳入的書叢地址,獲取書叢中的書籍的信息、封面,並存到excel.如果沒有這種自動工具,那麼朋友就需要幾個通宵才能完成,而且是非常非常重複的事,我一聽,我就想起了以前做過的,就答應下來了。
處理思路
-
雖然答應,但也不會像以前那樣說幹就幹,重複造輪子的事,沒有意義,第一個想到 ,現在這種爬蟲類的軟件或半自動的軟件很多,直接借用巨人的肩膀不是又快又好,結果發現爬是可以爬,但滿足不了需求。
-
研究了一些自動的或半自動的工具後(也需要懂一些html方面的選擇表達式的基礎,對於程序員來講,還是比較簡單的),發現數據是出來了,要把這樣的數據清洗成excel的格式,基本上都是需要人工操作,對於不懂技術的朋友來講,這是一件無法逾越的坑。特別還有封面這種圖片格式的需求。。。
-
查了一下資料,現在爬蟲類的,太多太多Python相關的資料了,不得感觸Python的發展迅猛。雖然我也會寫一些簡單的,但要抓取數據,清洗數據,生成EXCEL,用不熟悉的語言,會浪費大量的時間在語法研究上面。
-
所以看到 一個在多個語言都支持的組件
selenium,它有多個語言版本的組件,兼容不同的瀏覽器。這個東西非常符合我的胃口,既兼容多個語言,又兼容不同的瀏覽器。此組件的自身定位設計太妙了,很有生命力。 -
然後想直接找一下selenium的現成解析豆瓣讀書的java工程來改造,發生都已經不能解析目前的豆瓣讀書,看來豆瓣讀書也在不斷的迭代,之前格式已經變了,到了這一步,基本上不用想了,可以開始自己動手搭建全新工程
實踐
源代碼
首先,什麼都不說,先上源代碼地址,奉上github地址
https://github.com/riso-jay/riso/tree/master/riso-parent/riso-web-crawler
或下載打成zip的附件:https://download.csdn.net/download/vipshop_fin_dev/12536676
使用方式:
在/riso-parent/riso-web-crawler/jar 文件夾有打好包的jar及批處理(只要jdk是1.8以上就可以直接運行)
附上二種不同的豆瓣讀書的叢書來源頁,供測試
推薦列表頁 https://www.douban.com/doulist/1257960 下面是生成的效果
叢書列表頁https://book.douban.com/series/46192 下面是生成的效果
selenium
selenium 的使用示例,這裏不展開講,上面有源碼可以自己看,講一些印象深刻部分
- 關於瀏覽器可見的不生效問題
我選擇的是chrome的驅動,在使用瀏覽器驅動時,有些坑,網上沒有可用的解決方案。
這裏我發現一個很意思的地方,selenium我用的是3.9.1 版本,然後我按當前chrome瀏覽器版本( 75.0.3770.142)去選擇下載(75.0.3770.140),發生在selenium控制瀏覽器可見時,會不起作用,後面換了幾個版本,用(70.0.3538.97)是可以控制 瀏覽器可見不可見的。
附上參考代碼:
System.setProperty("webdriver.chrome.driver", chromedriverFilePath);
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless"); //無瀏覽器模式
WebDriver driver = new ChromeDriver(options);
- 關於網頁中的圖片處理
這個之前沒有弄過,想用傳統的java 用 URL或apache的httpclient的方式抓取圖片,熟悉的方式快速解決,忽視了豆瓣是https的協議,行不通了,發現selenium有對指定4軸截圖,通過WebElement可以簡單獲取,完美的解決圖片問題 ,還不限定對文件的格式及動態網頁一些特殊性。非常不錯的功能
附上參考代碼:
/**
* 通過webElement獲取圖片文件
* @param driver
* @param ele
* @return
*/
private File getPicFile(WebDriver driver, WebElement ele) throws IOException {
File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
// Get entire page screenshot
BufferedImage fullImg = ImageIO.read(screenshot);
// Get the location of element on the page
org.openqa.selenium.Point point = ele.getLocation();
// Get width and height of the element
int eleWidth = ele.getSize().getWidth();
int eleHeight = ele.getSize().getHeight();
// Crop the entire page screenshot to get only element screenshot
BufferedImage eleScreenshot = fullImg.getSubimage(point.getX(), point.getY(), eleWidth, eleHeight);
ImageIO.write(eleScreenshot, "jpg", screenshot);
return screenshot;
}
注意:
chrome官網是訪問不了,這裏有一個淘寶的倉庫可用: http://npm.taobao.org/mirrors/chromedriver/
設計模式
-
個人覺的,不管是大項目,還是小項目,優雅的代碼是讓人賞心悅目的,如果時間不夠,也不能將代碼的框架設計給省略 。有句俗話說的好,代碼往往有華麗的外表,卻有一個垃圾的內心。怎麼理解這句話呢:“你可以有很爛的代碼,但對外的接口一定要夠好。因爲你再爛,你也可以在後面迭代它,去修正它,但拋出來的接口已經不能隨便修改,很可能伴隨一世。”,這也就說明了對代碼不同層次的優化先處理方式,代碼的整體框架是很重要的。
-
所以我也非常重視設計模式,因爲設計模式是一個將抽象的解決方案落地的描述。這裏我主要使用了設計模式: 策略模式及一些基礎的面向對象手段繼承等方式(工廠和單例就 不用說了,雖然我沒有使用spring,但這個已經溶入骨子裏了)。來解決對於不同網頁的爬取解析不同邏輯的地方,這裏主要是處理豆瓣讀書詳情頁的上級多種來源(目前可以感知到 有叢書和推薦)
下面貼一下,工程結構及策略實現的關鍵代碼
策略接口:
/**
* 豆辨書解析接口
* @author: jie01.zhu
* @Date: 2020/6/15
*/
public interface DoubanBookService {
/**
* 是否需要處理
* @param url
* @return
*/
boolean isProcess(String url);
/**
* 獲取分頁個性化參數
* @param pageNum 分頁數量
* @return
*/
String getPageStr(int pageNum);
/**
* 通過列表首頁獲取書的集合信息
* @param driver
* @param url
* @return
*/
List<DoubanBook> getDoubanBookListByTopPage(WebDriver driver, String url);
}
豆瓣讀書推薦頁的策略實現
public class BookOfDoulistServiceImpl extends BaseDoubanBookService implements DoubanBookService {
private final String INDEX_STR = "https://www.douban.com/doulist/";
@Override
public boolean isProcess(String url) {
if (url.indexOf(INDEX_STR) == -1) {
return false;
}
return true;
}
/**
* doulist的分頁是按25的倍數翻頁的
* @param pageNum 分頁數量
* @return
*/
@Override
public String getPageStr(int pageNum) {
return "start=" + ((pageNum - 1) * 25);
}
@Override
public List<DoubanBook> getDoubanBookListByTopPage(WebDriver driver, String url) {
。。。後面省略推薦的具體代碼解析邏輯
豆瓣讀書書叢頁的策略實現
public class BookOfSeriesServiceImpl extends BaseDoubanBookService implements DoubanBookService {
private final String INDEX_STR = "https://book.douban.com/series/";
@Override
public boolean isProcess(String url) {
if (url.indexOf(INDEX_STR) == -1) {
return false;
}
return true;
}
/**
* Series 是按頁數簡單翻頁的
* @param pageNum 分頁數量
* @return
*/
@Override
public String getPageStr(int pageNum) {
return "page=" + pageNum;
}
@Override
public List<DoubanBook> getDoubanBookListByTopPage(WebDriver driver, String url) {
。。。後面省略推薦的具體代碼解析邏輯
通過這樣的拆分,我們用代碼分層拆分什麼是組件,什麼是前端入口,什麼是策略,基本上可以保證業務的變化不會對結構造成重大的結調和衝擊。
訪問入口
最後爲什麼要再說一下訪問入口,因爲我真的被 javax.swing給噁心到 了,下面的如此簡單的界面,寫起代碼非常麻煩。但從用戶的角度思考,它確實是最輕量級的。我不想使用網頁的方式,這樣太笨重了(需要再拉一個應用服務器,配合腳本彈出一個網頁)。
再看一下swing的流式界面開發代碼
GetWebBookApi getWebBookApi = new GetWebBookApi();
JFrame jf = new JFrame("豆瓣書單抓取");
jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 創建內容面板容器
JPanel jPanel = new JPanel();
// 創建分組佈局,並關聯容器
GroupLayout layout = new GroupLayout(jPanel);
// 設置容器的佈局
jPanel.setLayout(layout);
JLabel urlJLabel = null;
urlJLabel = new JLabel();
urlJLabel.setText("豆瓣叢書入口地址:");
urlJLabel.setFont(new Font(null, Font.PLAIN, 18)); // 設置字體,null 表示使用默認字體
// 創建文本框,指定可見列數爲8列
final JTextField urlTextField = new JTextField(50);
urlTextField.setText("https://book.douban.com/series/46192");
urlTextField.setFont(new Font(null, Font.PLAIN, 18));
JLabel chromedriverFilePathJLabel = new JLabel();
chromedriverFilePathJLabel.setText("chrome瀏覽器驅動文件地址:");
chromedriverFilePathJLabel.setFont(new Font(null, Font.PLAIN, 18)); // 設置字體,null 表示使用默認字體
jPanel.add(chromedriverFilePathJLabel);
final JTextField chromedriverFilePathTextField = new JTextField(50);
chromedriverFilePathTextField.setFont(new Font(null, Font.PLAIN, 18));
chromedriverFilePathTextField.setText("c:/chromedriver.exe");
jPanel.add(chromedriverFilePathTextField);
JLabel fileLocationPathJLabel = new JLabel();
fileLocationPathJLabel.setText("excel生成目件夾(必須存在):");
fileLocationPathJLabel.setFont(new Font(null, Font.PLAIN, 18)); // 設置字體,null 表示使用默認字體
jPanel.add(fileLocationPathJLabel);
final JTextField fileLocationPathTextField = new JTextField(50);
fileLocationPathTextField.setFont(new Font(null, Font.PLAIN, 18));
fileLocationPathTextField.setText("c:/");
jPanel.add(fileLocationPathTextField);
// 創建一個按鈕,點擊後獲取文本框中的文本
JButton btn = new JButton("抓取");
btn.setFont(new Font(null, Font.PLAIN, 18));
btn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("提交: " + urlTextField.getText());
try {
getWebBookApi.getWebStart(urlTextField.getText(), chromedriverFilePathTextField.getText(),
fileLocationPathTextField.getText());
} catch (Exception ex) {
log.error(ex.getMessage(), ex);
JOptionPane.showMessageDialog(jPanel, ex.getMessage(), "發生錯誤", JOptionPane.WARNING_MESSAGE);
} finally {
System.exit(0);
}
}
});
// 自動創建組件之間的間隙
layout.setAutoCreateGaps(true);
// 自動創建容器與觸到容器邊框的組件之間的間隙
layout.setAutoCreateContainerGaps(true);
/*
* 水平組(僅確定 X 軸方向的座標/排列方式)
*
* 水平串行: 水平排列(左右排列)
* 水平並行: 垂直排列(上下排列)
*/
// 水平並行(上下) btn01 和 btn02
GroupLayout.ParallelGroup hParalGroup01 = layout.createParallelGroup().addComponent(urlJLabel)
.addComponent(chromedriverFilePathJLabel).addComponent(fileLocationPathJLabel);
// 水平並行(上下)btn03 和 btn04
GroupLayout.ParallelGroup hParalGroup02 = layout.createParallelGroup().addComponent(urlTextField)
.addComponent(chromedriverFilePathTextField).addComponent(fileLocationPathTextField);
// 水平串行(左右)hParalGroup01 和 hParalGroup02
GroupLayout.SequentialGroup hSeqGroup = layout.createSequentialGroup().addGroup(hParalGroup01)
.addGroup(hParalGroup02);
// 水平並行(上下)hSeqGroup 和 btn05
GroupLayout.ParallelGroup hParalGroup = layout.createParallelGroup().addGroup(hSeqGroup)
.addComponent(btn, GroupLayout.Alignment.CENTER);
layout.setHorizontalGroup(hParalGroup); // 指定佈局的 水平組(水平座標)
/*
* 垂直組(僅確定 Y 軸方向的座標/排列方式)
*
* 垂直串行: 垂直排列(上下排列)
* 垂直並行: 水平排列(左右排列)
*/
// 垂直並行(左右)btn01 和 btn03
GroupLayout.ParallelGroup vParalGroup01 = layout.createParallelGroup().addComponent(urlJLabel)
.addComponent(urlTextField);
// 垂直並行(左右)btn02 和 btn04
GroupLayout.ParallelGroup vParalGroup02 = layout.createParallelGroup().addComponent(chromedriverFilePathJLabel)
.addComponent(chromedriverFilePathTextField);
// 垂直並行(左右)btn02 和 btn04
GroupLayout.ParallelGroup vParalGroup03 = layout.createParallelGroup().addComponent(fileLocationPathJLabel)
.addComponent(fileLocationPathTextField);
// 垂直串行(上下)vParalGroup01, vParalGroup02 和 btn05
GroupLayout.SequentialGroup vSeqGroup = layout.createSequentialGroup().addGroup(vParalGroup01)
.addGroup(vParalGroup02).addGroup(vParalGroup03).addComponent(btn);
layout.setVerticalGroup(vSeqGroup); // 指定佈局的 垂直組(垂直座標)
jf.setContentPane(jPanel);
jf.pack();
jf.setLocationRelativeTo(null);
jf.setVisible(true);
最後附上我之前寫的博文入口:
[1] https://blog.csdn.net/vipshop_fin_dev/article/details/89303458
[2] https://blog.csdn.net/vipshop_fin_dev/article/details/85323076
[3] https://blog.csdn.net/vipshop_fin_dev/article/details/85239099
[4] https://blog.csdn.net/vipshop_fin_dev/article/details/79618067
[5] https://blog.csdn.net/vipshop_fin_dev/article/details/79313432
朱傑
2020-06-19