乱序拼图验证的识别并还原-puzzle-captcha
一、前言
乱序拼图验证是一种较少见的验证码防御,市面上更多的是拖动滑块,被完美攻克的有不少,都在行为轨迹上下足了功夫,本文不讨论轨迹模拟范畴,就只针对拼图还原进行研究。
找一个市面比较普及的顶像乱序拼图进行验证,它号称的防御能力4星
,用户体验3星
,通过研究发现,它的还原程度相当高,思路也很简单,下面一步步的讲解还原过程。
二、环境准备
1.依赖
- 采集模拟
selenium
- 特征匹配
python
+opencv
2.安装环境
!pip install setuptools
!pip install selenium
!pip install numpy Matplotlib
!pip install opencv-python
3.chormedriver 下载
找到对应浏览器版本+系统平台
的driver
后,macOS
建议存放到 /usr/local/bin
!wget https://npm.taobao.org/mirrors/chromedriver/95.0.4638.69/chromedriver_mac64.zip
三、采集样本
引入依赖库,使用 webdriver
打开官方网站的产品演示页面
import os
import cv2
import time
import urllib.request
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
创建下载样本的代码,主要流程是打开官网的demo页后,截图并保存
# 采集代码
class CrackPuzzleCaptcha():
# 初始化webdriver
def init(self):
self.url = 'https://www.dingxiang-inc.com/business/captcha'
chrome_options = webdriver.ChromeOptions()
# chrome_options.add_argument("--start-maximized")
chrome_options.add_experimental_option("excludeSwitches", ["ignore-certificate-errors","enable-automation"]) # 设置为开发者模式
path = r'/usr/local/bin/chromedriver' #macOS
# path = r'D:\Anaconda3\chromedriver.exe' #windows
self.browser = webdriver.Chrome(executable_path=path,chrome_options=chrome_options)
#设置显示等待时间
self.wait = WebDriverWait(self.browser, 20)
self.browser.get(self.url)
# 打开验证码demo页面,并强制元素在浏览器可视区域
def openTest(self):
time.sleep(1)
self.browser.execute_script('setTimeout(function(){document.querySelector("body > div.wrapper-main > div.wrapper.wrapper-content > div > div.captcha-intro > div.captcha-intro-header > div > div > ul > li.item-8").click();},0)')
self.browser.execute_script('setTimeout(function(){document.querySelector("body > div.wrapper-main > div.wrapper.wrapper-content > div > div.captcha-intro > div.captcha-intro-body > div > div.captcha-intro-demo").scrollIntoView();},0)')
time.sleep(1)
# 找到原图,webp格式,直接下载保存
def download(self):
onebtn = self.browser.find_element_by_css_selector('#dx_captcha_oneclick_bar-logo_2 > span')
ActionChains(self.browser).move_to_element(onebtn).perform()
time.sleep(1)
#下载webp
img_url = self.browser.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-top-left_3 > img').get_attribute("src")
img_address = "test.png" # 样本文件
response = urllib.request.urlopen(img_url)
img = response.read()
with open(img_address, 'wb') as f:
f.write(img)
print('已保存', img_address)
return self.browser
def crack(self):
pass
开始采集
crack = CrackPuzzleCaptcha()
crack.init()
crack.openTest()
browser2 = crack.download()
已保存 test.png
四、调研结果
- 关键1:显示的拼图的原图就是
已经乱序
的状态 - 关键2:原图是一个整体,那么获取原图
切割并编号
,能得到与拼图过程一致的结果 - 关键3:拼图只需要做
1
次换位即可,2x2
的矩阵,可以对[1,2,3,4]
进行排列组合,得到所有的拼接结果
五、分析过程
1.辅助函数
定义辅助函数,方便获取参数
# 显示图形
def show_images(images: list , title = '') -> None:
if title!='':
print(title)
n: int = len(images)
f = plt.figure()
for i in range(n):
f.add_subplot(1, n, i + 1)
plt.imshow(images[i])
plt.show(block=True)
# 获取图像的基本信息
def getSize(p):
sum_rows = p.shape[0]
sum_cols = p.shape[1]
channels = p.shape[2]
return sum_rows,sum_cols,channels
2.图像切割
# 输入样本
file = 'test.png'
img = cv2.imread(file)
sum_rows,sum_cols,channels = getSize(img)
part_rows,part_cols = round(sum_rows/2),round(sum_cols/2)
print('样本图 高度、宽度、通道',sum_rows,sum_cols,channels)
print('四图切分,求原图中心位置',part_rows,part_cols)
part1 = img[0:part_rows, 0:part_cols]
part2 = img[0:part_rows, part_cols:sum_cols]
part3 = img[part_rows:sum_rows, 0:part_cols]
part4 = img[part_rows:sum_rows, part_cols:sum_cols]
print('切割为4个小块的 W/H/C 信息,并四图编号:左上=1,右上=2,左下=3,右下=4\n',getSize(part1),getSize(part2),getSize(part3),getSize(part4))
show_images([img],'原图')
show_images([part1,part2],'切割图')
show_images([part3,part4])
样本图 高度、宽度、通道 150 300 3
四图切分,求原图中心位置 75 150
切割为4个小块的 W/H/C 信息,并四图编号:左上=1,右上=2,左下=3,右下=4
(75, 150, 3) (75, 150, 3) (75, 150, 3) (75, 150, 3)
原图
切割图
完成切割后,还需要重组合并4个图像,用于匹配最佳结果
3.图像拼接
# 拼接函数
def merge(sum_rows,sum_cols,channels,p1,p2,p3,p4):
final_matrix = np.zeros((sum_rows, sum_cols,channels), np.uint8)
part_rows,part_cols = round(sum_rows/2),round(sum_cols/2)
final_matrix[0:part_rows, 0:part_cols] = p1
final_matrix[0:part_rows, part_cols:sum_cols] = p2
final_matrix[part_rows:sum_rows, 0:part_cols] = p3
final_matrix[part_rows:sum_rows, part_cols:sum_cols] = p4
return final_matrix
从编号上来看,应该将 [1,2,3,4]
还原成 [4,2,3,1]
就是正确的图,测试下还原效果
# 还原图
f = merge(sum_rows,sum_cols,channels,part4,part2,part3,part1)
show_images([f],'还原图 [4,2,3,1]')
还原图 [4,2,3,1]
4.排列组合
已知 python 实现排列组合非常方便,测试代码如下
import itertools
# 对应拼图的4个块的编号
puzzle_list = [
"1:左上","2:右下",
"3:左下","4:右下"
]
result = itertools.permutations(puzzle_list,4)
cnt=0
for x in result:
cnt+=1
print(x)
print('共',cnt,'种组合')
('1:左上', '2:右下', '3:左下', '4:右下')
('1:左上', '2:右下', '4:右下', '3:左下')
('1:左上', '3:左下', '2:右下', '4:右下')
('1:左上', '3:左下', '4:右下', '2:右下')
('1:左上', '4:右下', '2:右下', '3:左下')
('1:左上', '4:右下', '3:左下', '2:右下')
('2:右下', '1:左上', '3:左下', '4:右下')
('2:右下', '1:左上', '4:右下', '3:左下')
('2:右下', '3:左下', '1:左上', '4:右下')
('2:右下', '3:左下', '4:右下', '1:左上')
('2:右下', '4:右下', '1:左上', '3:左下')
('2:右下', '4:右下', '3:左下', '1:左上')
('3:左下', '1:左上', '2:右下', '4:右下')
('3:左下', '1:左上', '4:右下', '2:右下')
('3:左下', '2:右下', '1:左上', '4:右下')
('3:左下', '2:右下', '4:右下', '1:左上')
('3:左下', '4:右下', '1:左上', '2:右下')
('3:左下', '4:右下', '2:右下', '1:左上')
('4:右下', '1:左上', '2:右下', '3:左下')
('4:右下', '1:左上', '3:左下', '2:右下')
('4:右下', '2:右下', '1:左上', '3:左下')
('4:右下', '2:右下', '3:左下', '1:左上')
('4:右下', '3:左下', '1:左上', '2:右下')
('4:右下', '3:左下', '2:右下', '1:左上')
共 24 种组合
5.特征提取
采用 merge 函数,对切割的小图进行组合还原
后,转换为灰度图
并提取轮廓
。
# 还原图
f = merge(sum_rows,sum_cols,channels,part1,part2,part3,part4)
show_images([f],'还原图[1,2,3,4]')
# 灰度
gray = cv2.cvtColor(f, cv2.COLOR_BGRA2GRAY)
show_images([gray],'灰度')
# 提取轮廓
edges = cv2.Canny(gray, 35, 80, apertureSize=3)
show_images([edges],'提取轮廓')
还原图[1,2,3,4]
灰度
提取轮廓
再测试一种新的组合,看看轮廓特征[1,3,2,4]和原始的轮廓特征[4,2,3,1]
f = merge(sum_rows,sum_cols,channels,part1,part3,part2,part4)
gray = cv2.cvtColor(f, cv2.COLOR_BGRA2GRAY)
edges = cv2.Canny(gray, 35, 80, apertureSize=3)
show_images([edges],'提取轮廓')
f = merge(sum_rows,sum_cols,channels,part1,part2,part3,part4)
gray = cv2.cvtColor(f, cv2.COLOR_BGRA2GRAY)
edges = cv2.Canny(gray, 35, 80, apertureSize=3)
show_images([edges],'提取轮廓')
# 正确的
f = merge(sum_rows,sum_cols,channels,part4,part2,part3,part1)
gray = cv2.cvtColor(f, cv2.COLOR_BGRA2GRAY)
edges = cv2.Canny(gray, 35, 80, apertureSize=3)
show_images([edges],'正确的-提取轮廓')
提取轮廓
提取轮廓
正确的-提取轮廓
通过提取轮廓,可以看到拼接结果的明显的线条
,错误的图至少存在一条x轴或y轴的线
,而拼接成功的基本没有(线段位置或长度及线条数量可以决定正确率,需要多调整参数并筛选
)。
这是因为原图有明显的过渡色
,它是为了用户体验而设计,方便人们使用它的时候,能够‘容易
’的区分,并找出正确的拼图位置。
f = merge(sum_rows,sum_cols,channels,part1,part2,part3,part4)
show_images([f],'背景渐变色')
show_images([part3,part2,part1,part4],'切割后')
f = merge(sum_rows,sum_cols,channels,part1,part2,part3,part4)
lf = f.copy()
cv2.line(lf, (0, 75), (300, 75), (0, 0, 255), 2)
cv2.line(lf, (150, 0), (150, 150), (0, 0, 255), 2)
show_images([lf],'乱序,渐变色成为了‘十字’特征线')
背景渐变色
切割后
乱序,渐变色成为了‘十字’特征线
6.特征匹配
特征已知后,现在剩下的就是对特征进行检测
,可以计算 x/2,y/2 十字架
的色差,也可以用 opencv 的直线提取
,测试代码如下:
f = merge(sum_rows,sum_cols,channels,part1,part2,part3,part4)
gray = cv2.cvtColor(f, cv2.COLOR_BGRA2GRAY)
edges = cv2.Canny(gray, 35, 80, apertureSize=3)
show_images([edges],'提取轮廓')
lines = cv2.HoughLinesP(edges,0.01,np.pi/360,60,minLineLength=50,maxLineGap=10)
if lines is None:
print('没找到线条')
else:
lf = f.copy()
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(lf, (x1, y1), (x2, y2), (0, 0, 255), 2)
show_images([lf])
提取轮廓
尝试正确的组合 [4,2,3,1]
f = merge(sum_rows,sum_cols,channels,part4,part2,part3,part1)
gray = cv2.cvtColor(f, cv2.COLOR_BGRA2GRAY)
edges = cv2.Canny(gray, 35, 80, apertureSize=3)
show_images([edges],'提取轮廓')
lines = cv2.HoughLinesP(edges,0.01,np.pi/360,60,minLineLength=50,maxLineGap=10)
if lines is None:
print('没找到线条')
else:
lf = f.copy()
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(lf, (x1, y1), (x2, y2), (0, 0, 255), 2)
show_images([lf])
提取轮廓
没找到线条
7.匹配过程
import itertools
print('原图顺序')
print(1,2)
print(3,4)
show_images([img])
# 按编号,将切割的图放入list做排列组合
list1 = [
[1,part1],
[2,part2],
[3,part3],
[4,part4]
]
result = itertools.permutations(list1,4)
idx =1
finded = False
finalResult = []
for x in result:
# 排列组合合并图像
f = merge(sum_rows,sum_cols,channels,x[0][1],x[1][1],x[2][1],x[3][1])
# 图像特征提取
gray = cv2.cvtColor(f, cv2.COLOR_BGRA2GRAY)
edges = cv2.Canny(gray, 35, 80, apertureSize=3)
# 直线匹配
lines = cv2.HoughLinesP(edges,0.01,np.pi/360,60,minLineLength=50,maxLineGap=10)
if lines is None:
print('还原图像')
show_images([f])
show_images([gray])
show_images([edges])
print('正确顺序')
print(x[0][0],x[1][0])
print(x[2][0],x[3][0])
print('完成!!')
finded = True
finalResult =[x[0][0],x[1][0],x[2][0],x[3][0]] #获取最终排列正确的结果
break
else:
print(idx, '排列:' , x[0][0],x[1][0],x[2][0],x[3][0] , '线:', len(lines))
lf = f.copy()
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(lf, (x1, y1), (x2, y2), (0, 0, 255), 2)
# show_images([lf])
pass
idx+=1
print('测试次数',idx,'最终状态',finded,finalResult)
原图顺序
1 2
3 4
1 排列: 1 2 3 4 线: 4
2 排列: 1 2 4 3 线: 5
3 排列: 1 3 2 4 线: 4
4 排列: 1 3 4 2 线: 2
5 排列: 1 4 2 3 线: 3
6 排列: 1 4 3 2 线: 4
7 排列: 2 1 3 4 线: 3
8 排列: 2 1 4 3 线: 5
9 排列: 2 3 1 4 线: 3
10 排列: 2 3 4 1 线: 3
11 排列: 2 4 1 3 线: 1
12 排列: 2 4 3 1 线: 1
13 排列: 3 1 2 4 线: 2
14 排列: 3 1 4 2 线: 2
15 排列: 3 2 1 4 线: 3
16 排列: 3 2 4 1 线: 3
17 排列: 3 4 1 2 线: 5
18 排列: 3 4 2 1 线: 3
19 排列: 4 1 2 3 线: 4
20 排列: 4 1 3 2 线: 3
21 排列: 4 2 1 3 线: 2
还原图像
正确顺序
4 2
3 1
完成!
测试次数 22 最终状态 True [4, 2, 3, 1]
8.提取结果
再看看如何这种拼图,如果要交换位置
的组合有12种
list1 = [1,2,3,4]
result = itertools.permutations(list1,2)
idx=0
for x in result:
idx+=1
print(idx,x)
1 (1, 2)
2 (1, 3)
3 (1, 4)
4 (2, 1)
5 (2, 3)
6 (2, 4)
7 (3, 1)
8 (3, 2)
9 (3, 4)
10 (4, 1)
11 (4, 2)
12 (4, 3)
#交换函数
def change_check(a,b):
diffs = []
if len(a)!=len(b):
return diffs
for i in range(len(a)):
if a[i]!=b[i]:
diffs.append(b[i])
return diffs
ab = change_check([1,2,3,4],finalResult)
print('原始',[1,2,3,4])
print('最终',finalResult)
print('要交换的位置',ab)
原始 [1, 2, 3, 4]
最终 [4, 2, 3, 1]
要交换的位置 [4, 1]
将‘交换的位置
’换算成小图中心
的偏移坐标
,采用查表法
#大图尺寸
pwidth = 150
pheight = 75
#小图xy中心点 = 大图wh 1/4
px = round(pwidth/2)
py = round(pheight/2)
#创建坐标表
offset_points = [
[px,py],[px+pwidth,py],
[px,py+pheight],[px+pwidth,py+pheight]
]
print(offset_points)
print(ab)
#通过结果作为索引,拿到坐标表索引的坐标
drag_start = offset_points[ ab[0] -1 ]
drag_end = offset_points[ ab[1] -1 ]
print('起点偏移坐标',drag_start,'终点偏移坐标',drag_end)
[[75, 38], [225, 38], [75, 113], [225, 113]]
[4, 1]
起点偏移坐标 [225, 113] 终点偏移坐标 [75, 38]
9.模拟操作
至此,已经完成了拼图还原的分析所有过程,下面采用另一种简单的方法,move_to_element
方法,内置的拖动 dom-a 到 dom-b
位置,测试下结果
# 模拟聚焦按钮,让拼图显示出来
onebtn = browser2.find_element_by_css_selector('#dx_captcha_oneclick_bar-logo_2 > span')
ActionChains(browser2).move_to_element(onebtn).perform()
time.sleep(1)
获取最终结果
ab = change_check([1,2,3,4],finalResult)
print(ab)
[4, 1]
找到网页拼图的dom元素,存储下来用于操作并交换拼图
d1 = browser2.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-top-left_3 > div')
d2 = browser2.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-top-right_3 > div')
d3 = browser2.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-bottom-left_3 > div')
d4 = browser2.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-bottom-right_3 > div')
drag_elements = [d1,d2,d3,d4]
<ipython-input-22-61fb3f895e04>:1: DeprecationWarning: find_element_by_* commands are deprecated. Please use find_element() instead
d1 = browser2.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-top-left_3 > div')
<ipython-input-22-61fb3f895e04>:2: DeprecationWarning: find_element_by_* commands are deprecated. Please use find_element() instead
d2 = browser2.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-top-right_3 > div')
<ipython-input-22-61fb3f895e04>:3: DeprecationWarning: find_element_by_* commands are deprecated. Please use find_element() instead
d3 = browser2.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-bottom-left_3 > div')
<ipython-input-22-61fb3f895e04>:4: DeprecationWarning: find_element_by_* commands are deprecated. Please use find_element() instead
d4 = browser2.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-bottom-right_3 > div')
找出要拖动的2个dom,并交付给 webdriver
drag_start = drag_elements[ ab[0] -1 ]
drag_end = drag_elements[ ab[1] -1 ]
print('drag_start',drag_start, 'drag_end',drag_end)
drag_start <selenium.webdriver.remote.webelement.WebElement (session="1d7d691bd509cd03cd8b1483da2056ea", element="8439005e-eb70-4b02-856e-eebbe2526d6d")> drag_end <selenium.webdriver.remote.webelement.WebElement (session="1d7d691bd509cd03cd8b1483da2056ea", element="f9239df5-9aa3-43ae-a6af-afacf81eb670")>
ActionChains(browser2).drag_and_drop(drag_start,drag_end).perform()
# browser2.close()
简单拖一下,目标网站认可了,但它判定是有问题的,又弹出一种新的验证码出来,看来仅仅能够识别还原正确拼图还只是开端,如何伪造一个让其认可的运行环境,又是一个新的技术研究领域,值得与各位共同学习与分享交流。
六、终
边学边做,如有错误
之处敬请指出,谢谢!
项目地址:https://github.com/suifei/puzzle-captcha
乱序拼图验证的识别并还原-puzzle-captcha的更多相关文章
- 疯狂位图之——位图生成12GB无重复随机乱序大整数集
上一篇讲述了用位图实现无重复数据的排序,排序算法一下就写好了,想弄个大点数据测试一下,因为小数据在内存中快排已经很快. 一.生成的数据集要求 1.数据为0--2147483647(2^31-1)范围内 ...
- 主键乱序插入对Innodb性能的影响
主键乱序插入对Innodb性能的影响 在平时的mysql文档学习中我们经常会看到这么一句话: MySQL tries to leave space so that future inserts do ...
- 关于乱序(shuffle)与随机采样(sample)的一点探究
最近一个月的时间,基本上都在加班加点的写业务,在写代码的时候,也遇到了一个有趣的问题,值得记录一下. 简单来说,需求是从一个字典(python dict)中随机选出K个满足条件的key.代码如下(py ...
- Wireshark理解TCP乱序重组和HTTP解析渲染
TCP数据传输过程 TCP乱序重组原理 HTTP解析渲染 TCP乱序重组 TCP具有乱序重组的功能.(1)TCP具有缓冲区(2)TCP报文具有序列号所以,对于你说的问题,一种常见的处理方式是:TCP会 ...
- tf.train.batch的偶尔乱序问题
tf.train.batch的偶尔乱序问题 觉得有用的话,欢迎一起讨论相互学习~Follow Me tf.train.batch的偶尔乱序问题 我们在通过tf.Reader读取文件后,都需要用batc ...
- 乱序优化与GCC的bug
以下内容来自搜狗实验室技术交流文档,搜狐公司研发中心版权所有,仅供技术交流 摘要 --------- 乱序优化是现代编译器非常重要的特性,本文介绍了什么是乱序优化,以及由此引发的一个bug,希 ...
- mysql select 无order by 默认排序 出现乱序的问题
原文:mysql select 无order by 默认排序 出现乱序的问题 版权声明:感谢您的阅读,转载请联系博主QQ3410146603. https://blog.csdn.net/newMan ...
- wireshark和tcpdump抓包TCP乱序和重传怎么办?PCAP TCP排序工具分享
点击上方↑↑↑蓝字[协议分析与还原]关注我们 " 介绍TCP排序方法,分享一个Windows版的TCP排序工具." 在分析协议的过程中,不可避免地需要抓包. 无论抓包条件如何优越, ...
- memory barrier 内存屏障 编译器导致的乱序
小结: 1. 很多时候,编译器和 CPU 引起内存乱序访问不会带来什么问题,但一些特殊情况下,程序逻辑的正确性依赖于内存访问顺序,这时候内存乱序访问会带来逻辑上的错误, 2. https://gith ...
随机推荐
- Luogu P1297 [国家集训队]单选错位 | 概率与期望
题目链接 题解: 单独考虑每一道题目对答案的贡献. 设$g_i$表示gx在第$i$道题目的答案是否正确(1表示正确,0表示不正确),则$P(g_i=1)$表示gx在第$i$道题目的答案正确的概率. 我 ...
- java 垃圾回收及内存分配策略
一.在垃圾收集器对堆进行回收前,首先需要判断对象是否"存活",对已经"死去"的对象进行回收 判断对象是否存活:引用计数法和可达性分析法 引用计数法:给对象添加一 ...
- SpringCloud升级之路2020.0.x版-29.Spring Cloud OpenFeign 的解析(1)
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在使用云原生的很多微服务中,比较小规模的可能直接依靠云服务中的负载均衡器进行内部域名与服务 ...
- GDB 调试技巧(不断更新中......)
一.break到不同类的同名函数 方法: 在函数前面加类名以及作用域运算符 eg : break A::func //break 到类A的func函数 程序如下: //gdb_test.cpp #in ...
- Spring一套全通4—持久层整合
百知教育 - Spring系列课程 - 持久层整合 第一章.持久层整合 1.Spring框架为什么要与持久层技术进行整合 1. JavaEE开发需要持久层进行数据库的访问操作. 2. JDBC Hib ...
- js运算符 及 运算符优先级
「运算符」是用于实现赋值.比较和执行算数运算等功能的符号.常用运算符分类如下符号 算数运算符 递增和递减运算符 比较运算符 逻辑运算符 赋值运算符 算数运算符 运算符 描述 案例 + 加 10+20= ...
- 🏆【Alibaba中间件技术系列】「RocketMQ技术专题」帮你梳理RocketMQ或Kafka的选择理由以及二者PK
前提背景 大家都知道,市面上有许多开源的MQ,例如,RocketMQ.Kafka.RabbitMQ等等,现在Pulsar也开始发光,今天我们谈谈笔者最常用的RocketMQ和Kafka,想必大家早就知 ...
- Effective C++ 总结笔记(二)
二.构造/析构/赋值运算 05.了解C++默默编写并调用那些函数 如果自己不声明, 编译器就会暗自为class创建一个default构造函数.一个copy构造函数.一个copy assignment操 ...
- 偷天换日,用JavaAgent欺骗你的JVM
原创:微信公众号 码农参上(ID:CODER_SANJYOU),欢迎分享,转载请保留出处. 熟悉Spring的小伙伴们应该都对aop比较了解,面向切面编程允许我们在目标方法的前后织入想要执行的逻辑,而 ...
- R数据分析:跟随top期刊手把手教你做一个临床预测模型
临床预测模型也是大家比较感兴趣的,今天就带着大家看一篇临床预测模型的文章,并且用一个例子给大家过一遍做法. 这篇文章来自护理领域顶级期刊的文章,文章名在下面 Ballesta-Castillejos ...