用java編寫基於selenium的方式抓取豆瓣讀書書籍內容

前言

  • 很久以前,生活中使用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 的使用示例,這裏不展開講,上面有源碼可以自己看,講一些印象深刻部分
  1. 關於瀏覽器可見的不生效問題
    我選擇的是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);
  1. 關於網頁中的圖片處理
    這個之前沒有弄過,想用傳統的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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章