用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

猜你喜欢

转载自blog.csdn.net/vipshop_fin_dev/article/details/106862227