由于工作需要,开始做起了网络爬虫
以爬取小红书为例
创建chromeDriver
// 创建浏览器
public void createDriver() {
// 开启浏览器后需要访问的地址
String XIAOHOGNSHU_URL = "https://www.xiaohongshu.com/explore";
// 这里我是使用了ip代理模式,因为同一个ip地址频繁请求一个网页会出发反爬虫机制
HttpProxy httpProxy= new HttpProxy();
try {
//获取最优代理ip
httpProxy=httpProxyService.getBestProxyIp();
Assert.isNotNull(httpProxy, BaseCodeUtil.OBJECT_NOT_EXIST);
}catch (Exception e) {
// 生成5个代理ip
httpProxyService.createProxy(5);
httpProxy=httpProxyService.getBestProxyIp();
Assert.isNotNull(httpProxy, BaseCodeUtil.OBJECT_NOT_EXIST);
}
// 过期时间
String expireTimeStr = httpProxy.getExpire_time();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Long expireTime = 0L;
try {
// 格式化,计算ip到期时间
expireTime =simpleDateFormat.parse(expireTimeStr).getTime() - System.currentTimeMillis();
}catch (Exception e) {
logger.error(e.getMessage(),e);
}
String ip = httpProxy.getIp();
String ipAndPort = httpProxy.getIp() + ":" + httpProxy.getPort();
WebDriver driver=null;
// 获取当前服务器系统
String osName=System.getProperty("os.name");
if(osName.contains("Windows")) {
String webDirverHome = "E:\\code\\chromedriver.exe";
System.setProperty("webdriver.chrome.driver", webDirverHome);
ChromeOptions chromeOptions = new ChromeOptions();
// 启用所有类型的日志并收集所有日志
LoggingPreferences logPrefs = new LoggingPreferences();
logPrefs.enable(LogType.PERFORMANCE, Level.ALL);
logPrefs.enable(LogType.BROWSER, Level.ALL);
chromeOptions.setCapability(CapabilityType.LOGGING_PREFS, logPrefs);
// 不加载图片
chromeOptions.addArguments("blink-settings=imagesEnabled=false");
// 代理ip
chromeOptions.addArguments("--proxy-server=http://" + ipAndPort);
// 生成浏览器
driver = new ChromeDriver(chromeOptions);
}else {
/* linux */
ChromeOptions options = new ChromeOptions();
// 禁用沙盒
options.addArguments("no-sandbox");
// 禁止加载图片
options.addArguments("blink-settings=imagesEnabled=false");
options.addArguments("--whitelisted-ips=\"\"");
// 代理ip
options.addArguments("--proxy-server=http://" + ipAndPort);
// 无界面模式 在Linux中一定是不能唤起浏览器的(很重要)
options.setHeadless(Boolean.TRUE);
driver = new ChromeDriver(options);
}
// 发起请求
driver.get(XIAOHOGNSHU_URL);
// 在页面加载时,寻找 note-wrapper 元素,若在10秒钟未找到,抛出异常
new WebDriverWait(driver, 10).until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".note-wrapper")));
// driver.manage().timeouts().implicitlyWait(1, TimeUnit.SECONDS);
// 获取当前浏览器的信息
logger.info("Title:" + driver.getTitle());
logger.info("currentUrl:" + driver.getCurrentUrl());
// 这里我是把生成的浏览器放在一个BlockingQueue中进行管理
logger.info(">>>>>>>>>>queue:"+queue.size()+">>>>>>>>");
// 这里是把生成的浏览器放在DelayQueue中进行定时删除ip到期的浏览器(ip到期后不可用,浏览器也会失效)
logger.info(">>>>>>>>>>delayQueue:"+delayQueue.size()+">>>>>>>>");
HashMap<String, Object> driverMap = new HashMap<String,Object>();
String name = UUID.randomUUID().toString();
driverMap.put("name", name);
driverMap.put("driver", driver);
driverMap.put("ip", ip);
driverMap.put("failNum", 0);
logger.info("################浏览器信息#####################");
logger.info("name:" + name + ";ip:" + ip + ";failNum:" + 0);
queue.offer(driverMap);
WebDriverTask webDriverTask = new WebDriverTask(name,expireTime);
delayQueue.offer(webDriverTask);
logger.info("<<<<<<<<<<queue:"+queue.size()+"<<<<<<<<");
logger.info("<<<<<<<<<<delayQueue:"+delayQueue.size()+"<<<<<<<<");
}
创建浏览器是在项目初始化的时候进行的,我这里是直接创建了5个浏览器放在容器中,并定时的去更新浏览器
private int webDriverNum = 5;
public void initDriver() {
// 循环创建浏览器
for(int i=0;i<webDriverNum;i++) {
System.out.println("========initDriver 第" + (i + 1) + "个===================");
try {
createDriver();
}catch (Exception e) {
logger.error(e.getMessage(),e);
logger.error("第" + (i + 1) + "个浏览器启动失败");
}
}
// 再启动一个线程进行轮询工作
new Thread(new Runnable() {
@Override
public void run() {
while(delayQueue.size() > 0) {
try {
// 当delayQueue中的浏览器时间到期后会取出来,否则将阻塞在这里
WebDriverTask webDriverTask = delayQueue.take();
String name = webDriverTask.getName();
// 然后用轮询的方式一个一个进行名字比较,true 则销毁并创建新的浏览器,false 则重新塞回队列中
for(int i = 0; i < queue.size(); i++) {
Map<String, Object> driverMap = queue.take();
String driverName = (String) driverMap.get("name");
if(StringUtils.equals(driverName, name)) {
//销毁
WebDriver driver = (WebDriver)driverMap.get("driver");
driver.quit();
//新建浏览器并塞入队列
createDriver();
}else {
//塞回队列
queue.put(driverMap);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
我之前用的是 @PostConstruct 进行初始化的,后来可能是生命周期初始化顺序有点问题,后来改成在 Listerner中使用 SpringContextHolder.getBean(ChromeDriverService.class).initDriver(); 的方法在程序启动的时候开启5个浏览器
// 项目关闭的时候,记得结束掉开启的浏览器,释放资源
@PreDestroy
public void destoryDriver() {
logger.info("=======销毁前,关闭webDriver======");
while(queue.size()>0) {
logger.info(">>>>>>>>>>销毁前,webDriver_count:"+queue.size()+">>>>>>>>");
Map<String, Object> driverMap = queue.poll();
WebDriver driver = (WebDriver)driverMap.get("driver");
driver.quit();
delayQueue.clear();
logger.info("<<<<<<<<<<销毁后,webDriver_count:"+queue.size()+"<<<<<<<<");
}
}
请求小红书
public UserKocAuth xiaohongshuAuthentication(String homePageUrl) {
String chromeDriver =null;
Map<String, Object> driverMap = null;
UserKocAuth userKocAuth = new UserKocAuth();
try {
long before = System.currentTimeMillis();
System.out.println("无头浏览器开始,before:" + before);
// 获取浏览器
driverMap = chromeDriverService.getDriver();
// 发起请求
chromeDriver = doChromeDriver(homePageUrl,driverMap);
long after = System.currentTimeMillis();
System.out.println("无头浏览器结束,after:" + after);
System.out.println("无头浏览器花费时间,all:" + (after - before));
// 用来解析浏览器返回的HTML页面(放在之后一篇再讲Jsoup)
Document doc = Jsoup.parse(chromeDriver);
logger.info(doc.text());
Elements cardElement=doc.getElementsByClass("card-info").get(0).getElementsByClass("info-number");
logger.info("关注:"+cardElement.get(0).text());
logger.info("粉丝:"+cardElement.get(1).text());
logger.info("获赞与收藏:"+cardElement.get(2).text());
String attentionStr = cardElement.get(0).text();
String fansStr = cardElement.get(1).text();
String likeNumStr = cardElement.get(2).text();
Integer attention = numberEndWithCN(attentionStr);
Integer fansNum = numberEndWithCN(fansStr);
Integer likeNum = numberEndWithCN(likeNumStr);
Element element = doc.getElementsByClass("author-container").first().getElementsByClass("left").first();
String face = element.select("img").first().attr("src");
logger.info("头像:" + face);
String name = doc.getElementsByClass("user-name").get(0).getElementsByClass("name-detail").text();
logger.info("名字:" + name);
// 这里我是写了一个方法,切割url链接,获取url中的id
Map<String, String> paramsMap = GetParamsMapFromUrlUtil.urlSplit(homePageUrl);
String platformId = paramsMap.get("appuid");
userKocAuth.setPlatformId(platformId);
userKocAuth.setFansNum(fansNum);
userKocAuth.setAttention(attention);
userKocAuth.setLikeNum(likeNum);
userKocAuth.setPlatformFace(face);
userKocAuth.setPlatformName(name);
}catch (Exception e) {
logger.error(e.getMessage(),e);
int failNum = (Integer)driverMap.get("failNum");
driverMap.put("failNum", ++failNum);
throw new IllegalParameterException(ErrorCodeUtil.AUTHENTICATION_EXCEPTION);
}finally {
// 归还浏览器
chromeDriverService.returnDriver(driverMap);
}
return userKocAuth;
}
具体的chromeDriver请求方法
public String doChromeDriver(String url, Map<String,Object> driverMap) {
String pageSource = null;
Long chromeDriver = System.currentTimeMillis();
WebDriver driver = (WebDriver)driverMap.get("driver");
Long chromeDriverOver = System.currentTimeMillis();
logger.info("结束唤起浏览器,开始渲染页面,chromeDriverOver:" + chromeDriverOver);
logger.info("唤起页面一共花费,all:" + (chromeDriverOver - chromeDriver));
// 发起请求
driver.get(url);
long urlOver = System.currentTimeMillis();
logger.info("获取url,urlOver:" + urlOver);
logger.info("获取url共花费,all:" + (urlOver - chromeDriverOver));
// 页面加载直到页面获取到 author-container 元素,10秒后若没找到该元素,则抛出异常
new WebDriverWait(driver, 10).until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".author-container")));
pageSource = driver.getPageSource();
logger.info("获取结果pageSource:" + pageSource);
return pageSource;
}
小结
1、每次创建浏览器的时候我都会去Redis中取一个ip,如果没有ip,我会创建5个ip放在Redis中,问题就在于:如果有一个ip过期了,程序中就会从剩下的四个ip中去取一个,这样一来,创建的5个浏览器中一定会有两个浏览器共用同一个ip,循序下去5个浏览器就会使用容一个ip,这样一来就违背了我们的初衷,并且当这个ip过期之后,5个浏览器就同时崩溃,且再次创建浏览器的时候,只会创建一个浏览器。所以需要定时去刷新ip的数量和浏览器的数量
2、在项目启动时我是另外开了一个线程去做一个死循环的轮询,定时查看过期ip,当有ip过期,则关闭该浏览器并重新启动一个新ip的浏览器
3、结束项目的时候一定要关闭浏览器释放资源,不然会循序下去会占用CPU大量的资源