整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

滑块验证码识别、破解-技术详解(JAVA)

滑块验证码识别、破解-技术详解(JAVA)

滑块验证码是目前网络上使用最多的,也是体验相对来说比较好的一种验证码。但爬虫和反爬虫就像矛和盾一样的关系相互促进相互影响,互联网技术就是在这样的不断碰撞中向前发展。

结合我自己的个人工作经验,来聊聊滑块验证码,我们就拿京东登陆页面的滑块验证举例,进行详细分解学习。

滑块验证码样例

目标:通过算法找到需要滑动的滑块(下文一律叫切片区)距离背景目标区域(下文一律叫背景区)的距离,然后自动拖动完成拼接。


一、利用Chrome-F12的开发者工具来定位滑块验证码的请求地址:

1、在google浏览器中打开对应的网站,进入到滑块验证码页面

2、在验证码页面按F12,进入Network区

3、点击验证码右上角的换一张(图中标号为1),目的是捕获验证码的请求地址

4、在name区可以看到多个情况地址,找到其中的验证码请求地址,这里是g.html(图中标号为2)

5、在Headers表头可以看到对应此链接地址的情况地址,以及请求方式,这里是GET请求

(注:后期可以通过JS或者Java等模拟网站GET请求来获取验证码信息)

定位滑块验证码的请求地址


二、分析、查找"切片区"和"背景区"的对应图片数据信息:

1、点击开发者工具中的Response来查看请求的返回值

2、这里是一个JSON串格式,其中bg对应的值就是背景图片区域的base64字符串值,patch对应的值就是切片区base64字符串值.

切片区"和"背景区"数据信息

3、将这些base64字符串值转换成图片,我们看一下背景区和切片区字符串对应的具体图像:

(背景区)

(切片区)

    		//切片对应的base64String
    		String sliceImg="iVBORw0KGgoAAAANSUhEUgAAADIAAA.....";//内容太多省略,自己从浏览器中获取即可
    		//背景区对应的base64String
    		String bgImg="iVBORw0KGgoAAAANSUhE....";//内容太多省略,自己从浏览器中获取即可

       	//背景区
    		BufferedImage biBuffer=base64StringToImg(bgImg);
    		//切片区
    		BufferedImage sliceBuffer=base64StringToImg(sliceImg);
    		
       	//将图片输出到本地查看
    		ImageIO.write(biBuffer,
    				"png", new File("E:\\bgImg.png"));
    		ImageIO.write(sliceBuffer,
    				"png", new File("E:\\sliceImg.png"));


	/**
	 * base64字符串转存图片
	 * @param base64String base64字符串
	 * @return  BufferedImage
	 */
   public static BufferedImage base64StringToImg(final String base64String) {
       try {
           BASE64Decoder decoder=new BASE64Decoder();
           byte[] bytes=decoder.decodeBuffer(base64String);
           ByteArrayInputStream bais=new ByteArrayInputStream(bytes);
           return ImageIO.read(bais);
       } catch (final IOException ioe) {
           throw new UncheckedIOException(ioe);
       }
   }  

三、(重点,核心)利用orc模板匹配算法进行匹配,查找最相似区域,也就是我们的期望的坐标点:
废话不多说,直接上代码:

import static com.googlecode.javacv.cpp.opencv_core.CV_32FC1;
import static com.googlecode.javacv.cpp.opencv_core.cvCreateMat;
import static com.googlecode.javacv.cpp.opencv_core.cvMinMaxLoc;
import static com.googlecode.javacv.cpp.opencv_imgproc.CV_TM_CCOEFF_NORMED;
import static com.googlecode.javacv.cpp.opencv_imgproc.cvMatchTemplate;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
import com.googlecode.javacv.cpp.opencv_core;
import com.googlecode.javacv.cpp.opencv_core.CvMat;
import com.googlecode.javacv.cpp.opencv_core.CvSize;
import com.googlecode.javacv.cpp.opencv_core.IplImage;
import com.googlecode.javacv.cpp.opencv_imgproc;
import sun.misc.BASE64Decoder;

public class Test {

	public static void main(String[] args) throws IOException {
    //切片对应的base64String
    String sliceImg="iVBORw0KGgoAAAANSUhEUgAAADIAAA.....";//内容太多省略,自己从浏览器中获取即可
    //背景区对应的base64String
    String bgImg="iVBORw0KGgoAAAANSUhE....";//内容太多省略,自己从浏览器中获取即可

		// 背景区
		BufferedImage biBuffer=base64StringToImg(bgImg);
		// 切片区
		BufferedImage sliceBuffer=base64StringToImg(sliceImg);

		// 由于切片矩形区域存在透明区域,所以预处理将透明区域变成白色,方便后面对图片二值化处理。
		// (重点:如果这里不对透明区域预处理,切片预处理后将只有一种颜色导致匹配失败)
		int white=new Color(255, 255, 255).getRGB();
		for (int x=0; x < sliceBuffer.getWidth(); x++) {
			for (int y=0; y < sliceBuffer.getHeight(); y++) {
				if ((sliceBuffer.getRGB(x, y) >> 24)==0) {
					sliceBuffer.setRGB(x, y, white);
				}
			}
		}

		IplImage sourceImage=IplImage.createFrom(biBuffer);
		IplImage targetImage=IplImage.createFrom(sliceBuffer);
		CvMat sourceMat=sourceImage.asCvMat();
		CvMat targetMat=targetImage.asCvMat();

		// 模板匹配算法,根据目标图片在背景图片中查找相似的区域
		List<Rectangle> a=matchTemplateTest(sourceMat, targetMat);

		// 取第一个值,也就是匹配到的最相识的区域,可以定位目标坐标
		// 也是我们期望的坐标点
		Rectangle rec=a.get(0);

		// 下面是验证,将识别到的区域用红色矩形框标识出来,进行验证看是否正确
		Graphics g=biBuffer.getGraphics();

		// 画笔颜色
		g.setColor(Color.RED);

		// 矩形框(原点x坐标,原点y坐标,矩形的长,矩形的宽)
		g.drawRect(rec.x, rec.y, rec.width, rec.height);
		g.dispose();
    
    //输出到本地,验证区域查找是否正确
		FileOutputStream out=new FileOutputStream("d:\\checkImage.png");
		ImageIO.write(biBuffer, "png", out);
	}
	/**
	 * 模板匹配算法,根据目标图片在背景图片中查找相似的区域
	 * @param sourceMat 背景区域图片数组矩阵
	 * @param targetMat 切片目标区域图片数组矩阵
	 * @return 坐标点集合
	 */
	public static List<Rectangle> matchTemplateTest(CvMat sourceMat, CvMat targetMat) {
		List<Rectangle> rtn=new ArrayList<Rectangle>();

    //对图象进行单通道、二值化处理
		CvMat source=opencv_core.cvCreateMat(sourceMat.rows(), sourceMat.cols(), opencv_core.CV_8UC1);
		CvMat target=opencv_core.cvCreateMat(targetMat.rows(), targetMat.cols(), opencv_core.CV_8UC1);
		opencv_imgproc.cvCvtColor(sourceMat, source, opencv_imgproc.CV_BGR2GRAY);
		opencv_imgproc.cvCvtColor(targetMat, target, opencv_imgproc.CV_BGR2GRAY);

		CvSize targetSize=target.cvSize();
		CvSize sourceSize=source.cvSize();

		CvSize resultSize=new CvSize();
		resultSize.width(sourceSize.width() - targetSize.width() + 1);
		resultSize.height(sourceSize.height() - targetSize.height() + 1);

		CvMat result=cvCreateMat(resultSize.height(), resultSize.width(), CV_32FC1);
    
    //利用模板匹配算法进行查找
		cvMatchTemplate(source, target, result, CV_TM_CCOEFF_NORMED);
		opencv_core.CvPoint maxLoc=new opencv_core.CvPoint();
		opencv_core.CvPoint minLoc=new opencv_core.CvPoint();
		double[] minVal=new double[2];
		double[] maxVal=new double[2];
    
    //找出图片数据中最大值及最小值的数据
		cvMinMaxLoc(result, minVal, maxVal, minLoc, maxLoc, null);
		Rectangle rec=new Rectangle(maxLoc.x(), maxLoc.y(), target.cols(), target.rows());
    //将查找到的坐标按最优值顺序放入数组
		rtn.add(rec);
    
		source.release();
		target.release();
		result.release();
		opencv_core.cvReleaseMat(result);
		opencv_core.cvReleaseMat(source);
		opencv_core.cvReleaseMat(target);
		source=null;
		target=null;
		result=null;
		return rtn;
	}

我们看一下识别到的结果区域(红色矩形标识就是有系统自动识别出来的)霸气不霸气:

系统自动识别出来的区域坐标

四、根据第三步得到的移动坐标点进行坐标移动(这太小菜了,就不大篇幅在这里啰嗦了,可以使用你知道的任何技术进行模拟坐标移动),我用autoit进行举例;

//autoit代码块
//移动鼠标指针。
MouseMove ( x, y [, 速度] )

//参数说明:

x:要移动到的目标位置的 X 坐标。

y:要移动到的目标位置的 Y 坐标。

速度:鼠标移动速度,可设数值范围在 1(最快)和 100(最慢)之间。若设置速度为 0 则立即移动鼠标到指定位置。默认速度为 10。

ue可以通过插件来实现滑动验证码。下面是几种方法:

1 使用第三方滑动验证码库

可以使用第三方的滑动验证码库,例如'vue-verify-slide'、'vue-slide-verify'等,这些库已经实现了滑动验证码的逻辑,我们只需要将其作为插件引入即可。

优点:实现方便,无需自己编写逻辑代码。

缺点:依赖第三方库,如果第三方库更新不及时或存在漏洞,会影响到整个系统。

2 自己编写滑动验证码组件

可以自己编写滑动验证码组件,实现自定义的UI和逻辑。

优点:可以自由定制UI和逻辑。

缺点:需要编写大量的逻辑代码,工作量较大。

下面是一个自己编写的滑动验证码组件的示例:

<template>
  <div class="slider-verify">
    <div class="slider-bar" :style="{left: thumbLeft}">
      <div class="slider-thumb" @mousedown="onMouseDown"></div>
    </div>
    <div class="slider-mask"></div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isDragging: false, // 是否正在拖动滑块
      thumbLeft: 0, // 滑块左边距
      maxWidth: 280, // 滑块最大可移动距离
      dragStartX: 0, // 开始拖动时鼠标的x坐标
      dragOffsetX: 0, // 鼠标相对于滑块左边缘的偏移量
    };
  },
  methods: {
    onMouseDown(e) {
      this.isDragging=true;
      this.dragStartX=e.clientX;
      this.dragOffsetX=e.offsetX;
    },
    onMouseMove(e) {
      if (this.isDragging) {
        const distance=e.clientX - this.dragStartX;
        const thumbLeft=Math.min(Math.max(distance - this.dragOffsetX, 0), this.maxWidth);
        this.thumbLeft=`${thumbLeft}px`;
      }
    },
    onMouseUp(e) {
      this.isDragging=false;
      if (parseInt(this.thumbLeft)===this.maxWidth) {
        this.$emit('success');
      } else {
        this.thumbLeft='0px';
      }
    },
  },
  mounted() {
    window.addEventListener('mousemove', this.onMouseMove);
    window.addEventListener('mouseup', this.onMouseUp);
  },
  beforeDestroy() {
    window.removeEventListener('mousemove', this.onMouseMove);
    window.removeEventListener('mouseup', this.onMouseUp);
  },
};
</script>

<style scoped>
.slider-verify {
  position: relative;
  width: 300px;
  height: 40px;
  border-radius: 20px;
  overflow: hidden;
}
.slider-bar {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: #eee;
  transition: all 0.3s ease-out;
}
.slider-thumb {
  position: absolute;
  left: 0;
  top: 50%;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease-out;
}
.slider-mask {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 20px;
}
</style>

3 使用canvas实现滑动验证码

可以使用canvas绘制滑动验证码,将滑块拖动的距离作为验证依据。

优点:可以自由定制UI和逻辑,滑动效果更流畅。

缺点:需要对canvas有一定的了解,对性能有一定的影响。

下面是一个使用canvas实现的滑动验证码的示例:

<template>
  <div class="canvas-verify">
    <canvas ref="canvas" :width="canvasWidth" :height="canvasHeight" @mousedown="onMouseDown" @mousemove="onMouseMove" @mouseup="onMouseUp"></canvas>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isDragging: false, // 是否正在拖动滑块
      thumbLeft: 0, // 滑块左边距
      canvasWidth: 300, // canvas宽度
      canvasHeight: 150, // canvas高度
      maxWidth: 250, // 滑块最大可移动距离
      dragStartX: 0, // 开始拖动时鼠标的x坐标
      dragOffsetX: 0, // 鼠标相对于滑块左边缘的偏移量
      canvasContext: null, // canvas context
      imagePath: '', // 背景图路径
    };
  },
  methods: {
    onMouseDown(e) {
      if (this.isDragging) {
        return;
      }
      const rect=this.$refs.canvas.getBoundingClientRect();
      this.isDragging=true;
      this.dragStartX=e.clientX - rect.left;
      this.dragOffsetX=this.dragStartX - this.thumbLeft;
    },
    onMouseMove(e) {
      if (this.isDragging) {
        const rect=this.$refs.canvas.getBoundingClientRect();
        const distance=e.clientX - rect.left - this.dragOffsetX;
        const thumbLeft=Math.min(Math.max(distance, 0), this.maxWidth);
        this.thumbLeft=thumbLeft;
        this.draw();
      }
    },
    onMouseUp(e) {
      if (this.isDragging) {
        this.isDragging=false;
        if (this.thumbLeft===this.maxWidth) {
          this.$emit('success');
        } else {
          this.thumbLeft=0;
          this.draw();
        }
      }
    },
    draw() {
      this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
      // 绘制背景图
      const image=new Image();
      image.src=this.imagePath;
      image.onload=()=> {
        this.canvasContext.drawImage(image, 0, 0, this.canvasWidth, this.canvasHeight);
        // 绘制滑块
        this.canvasContext.fillStyle='#ccc';
        this.canvasContext.fillRect(this.thumbLeft, 50, 50, 50);
      };
    },
  },
  mounted() {
    // 获取canvas context
    this.canvasContext=this.$refs.canvas.getContext('2d');
    // 加载背景图
    this.imagePath='https://picsum.photos/300/150/?random';
    const image=new Image();
    image.src=this.imagePath;
    image.onload=()=> {
      this.draw();
    };
  },
  beforeUnmount() {
    this.canvasContext=null;
  },
};
</script>

<style scoped>
.canvas-verify {
  position: relative;
}
</style>

这个示例中,滑块使用了一个矩形代替,颜色为灰色。使用canvas实现滑动验证码需要对canvas有一定的了解,同时对性能也有一定的影响。但是可以自由定制UI和逻辑,实现更灵活。

以上是三种常见的实现滑动验证码的方法,每种方法都有其优点和缺点。使用CSS实现最简单,但是不太安全;使用canvas实现最灵活,但是需要对canvas有一定的了解;使用第三方库可以更快速地实现,但是需要依赖第三方库。具体使用哪种方法应该根据实际情况选择,权衡各种因素。

本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理。

作者:卡卡叮

PS:如有需要Python学习资料的小伙伴可以私信小编



这篇文章主要介绍了python模拟哔哩哔哩滑块登入验证的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

准备工具

  • pip3 install PIL
  • pip3 install opencv-python
  • pip3 install numpy

谷歌驱动

建议指定清华源下载速度会更快点

使用方法 :

pip3 install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple/opencv-python/

谷歌驱动

谷歌驱动下载链接 :http://npm.taobao.org/mirrors/chromedriver/



本篇文章采用的是cv2的Canny边缘检测算法进行图像识别匹配。

Canny边缘检测算法参考链接:https://www.jb51.net/article/185336.htm

具体使用的是Canny的matchTemplate方法进行模糊匹配,匹配方法用CV_TM_CCOEFF_NORMED归一化相关系数匹配。得出的max_loc就是匹配出来的位置信息。从而达到位置的距离。

难点

由于图像采用放大的效果匹配出的距离偏大,难以把真实距离,并存在误差。

由于哔哩哔哩滑块验证进一步采取做了措施,如果滑动时间过短,会导致验证登入失败。所以我这里采用变速的方法,在相同时间内滑动不同的距离。

误差的存在是必不可少的,有时会导致验证失败,这都是正常现象。

流程

1.实例化谷歌浏览器 ,并打开哔哩哔哩登入页面。

2.点击登陆,弹出滑动验证框。

3.全屏截图、后按照尺寸裁剪各两张。

5.模糊匹配两张图片,从而获取匹配结果以及位置信息 。

6.将位置信息与页面上的位移距离转化,并尽可能少的减少误差 。

7.变速的拖动滑块到指定位置,从而达到模拟登入。

效果图

代码实例

库安装好后,然后填写配置区域后即可运行。

from PIL import Image
from time import sleep
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import cv2
import numpy as np
import math
############ 配置区域 #########

zh='' #账号
pwd='' #密码
 # chromedriver的路径
chromedriver_path="C:\Program Files (x86)\Google\Chrome\Application\chromedriver.exe"

####### end #########

options=webdriver.ChromeOptions()
options.add_argument('--no-sandbox')
options.add_argument('--window-size=1020,720')
# options.add_argument('--start-maximized') # 浏览器窗口最大化
options.add_argument('--disable-gpu')
options.add_argument('--hide-scrollbars')
options.add_argument('test-type')
options.add_experimental_option("excludeSwitches", ["ignore-certificate-errors",
             "enable-automation"]) # 设置为开发者模式
driver=webdriver.Chrome(options=options, executable_path=chromedriver_path)
driver.get('https://passport.bilibili.com/login')

# 登入
def login():
 driver.find_element_by_id("login-username").send_keys(zh)
 driver.find_element_by_id("login-passwd").send_keys(pwd)
 driver.find_element_by_css_selector("#geetest-wrap > div > div.btn-box > a.btn.btn-login").click()
 print("点击登入")

# 整个图,跟滑块整个图
def screen(screenXpath):
 img=WebDriverWait(driver, 20).until(
  EC.visibility_of_element_located((By.XPATH, screenXpath))
 )
 driver.save_screenshot("allscreen.png") # 对整个浏览器页面进行截图
 left=img.location['x']+160 #往右
 top=img.location['y']+60 # 往下
 right=img.location['x'] + img.size['width']+230 # 往左
 bottom=img.location['y'] + img.size['height']+110 # 往上
 im=Image.open('allscreen.png')
 im=im.crop((left, top, right, bottom)) # 对浏览器截图进行裁剪
 im.save('1.png')
 print("截图完成1")
 screen_two(screenXpath)
 screen_th(screenXpath)
 matchImg('3.png','2.png')

# 滑块部分图
def screen_two(screenXpath):
 img=WebDriverWait(driver, 20).until(
  EC.visibility_of_element_located((By.XPATH, screenXpath))
 )
 left=img.location['x'] + 160
 top=img.location['y'] + 80
 right=img.location['x'] + img.size['width']-30
 bottom=img.location['y'] + img.size['height'] + 90
 im=Image.open('allscreen.png')
 im=im.crop((left, top, right, bottom)) # 对浏览器截图进行裁剪
 im.save('2.png')
 print("截图完成2")

# 滑块剩余部分图
def screen_th(screenXpath):
 img=WebDriverWait(driver, 20).until(
  EC.visibility_of_element_located((By.XPATH, screenXpath))
 )
 left=img.location['x'] + 220
 top=img.location['y'] + 60
 right=img.location['x'] + img.size['width']+230
 bottom=img.location['y'] + img.size['height'] +110
 im=Image.open('allscreen.png')
 im=im.crop((left, top, right, bottom)) # 对浏览器截图进行裁剪
 im.save('3.png')
 print("截图完成3")

#图形匹配
def matchImg(imgPath1,imgPath2):
 imgs=[]
 #展示
 sou_img1=cv2.imread(imgPath1)
 sou_img2=cv2.imread(imgPath2)
 # 最小阈值100,最大阈值500
 img1=cv2.imread(imgPath1, 0)
 blur1=cv2.GaussianBlur(img1, (3, 3), 0)
 canny1=cv2.Canny(blur1, 100, 500)
 cv2.imwrite('temp1.png', canny1)
 img2=cv2.imread(imgPath2, 0)
 blur2=cv2.GaussianBlur(img2, (3, 3), 0)
 canny2=cv2.Canny(blur2, 100, 500)
 cv2.imwrite('temp2.png', canny2)
 target=cv2.imread('temp1.png')
 template=cv2.imread('temp2.png')
 # 调整大小
 target_temp=cv2.resize(sou_img1, (350, 200))
 target_temp=cv2.copyMakeBorder(target_temp, 5, 5, 5, 5, cv2.BORDER_CONSTANT, value=[255, 255, 255])
 template_temp=cv2.resize(sou_img2, (200, 200))
 template_temp=cv2.copyMakeBorder(template_temp, 5, 5, 5, 5, cv2.BORDER_CONSTANT, value=[255, 255, 255])
 imgs.append(target_temp)
 imgs.append(template_temp)
 theight, twidth=template.shape[:2]
 # 匹配跟拼图
 result=cv2.matchTemplate(target, template, cv2.TM_CCOEFF_NORMED)
 cv2.normalize( result, result, 0, 1, cv2.NORM_MINMAX, -1 )
 min_val, max_val, min_loc, max_loc=cv2.minMaxLoc(result)
 # 画圈
 cv2.rectangle(target,max_loc,(max_loc[0]+twidth,max_loc[1]+theight),(0,0,255),2)
 target_temp_n=cv2.resize(target, (350, 200))
 target_temp_n=cv2.copyMakeBorder(target_temp_n, 5, 5, 5, 5, cv2.BORDER_CONSTANT, value=[255, 255, 255])
 imgs.append(target_temp_n)
 imstack=np.hstack(imgs)

 cv2.imshow('windows'+str(max_loc), imstack)
 cv2.waitKey(0)
 cv2.destroyAllWindows()

 # 计算距离
 print(max_loc)
 dis=str(max_loc).split()[0].split('(')[1].split(',')[0]
 x_dis=int(dis)+135
 t(x_dis)


#拖动滑块
def t(distances):
 draggable=driver.find_element_by_css_selector('div.geetest_slider.geetest_ready > div.geetest_slider_button')
 ActionChains(driver).click_and_hold(draggable).perform() #抓住
 print(driver.title)
 num=getNum(distances)
 sleep(3)
 for distance in range(1,int(num)):
  print('移动的步数: ',distance)
  ActionChains(driver).move_by_offset(xoffset=distance, yoffset=0).perform()
  sleep(0.25)
 ActionChains(driver).release().perform() #松开


# 计算步数
def getNum(distances):
 p=1+4*distances
 x1=(-1 + math.sqrt(p)) / 2
 x2=(-1 - math.sqrt(p)) / 2
 print(x1,x2)
 if x1>=0 and x2<0:
  return x1+2
 elif(x1<0 and x2>=0):
  return x2+2
 else:
  return x1+2

def main():
 login()
 sleep(5)
 screenXpath='/html/body/div[2]/div[2]/div[6]/div/div[1]/div[1]/div/a/div[1]/div/canvas[2]'
 screen(screenXpath)
 sleep(5)


if __name__=='__main__':
 main()

有能力的可以研究一下思路,然后写出更好的解决办法。