
背景交代 在反爬圈子的一个大类,涉及的网站其实蛮多的,目前比较常被爬虫coder欺负的网站,猫眼影视,汽车之家,大众点评,58同城,天眼查......还是蛮多的,技术高手千千万,总有五花八门的反爬技术出现,对于爬虫coder来说,干!就完了,反正也996了~ 作为一个系列的文章,那免不了,依旧拿猫眼影视“学习”吧,为什么?因为它比较典型~ 猫眼影视 打开猫眼专业版,常规操作,谷歌浏览器,开发者工具,抓取DOM节点, https://piaofang.maoyan.com/?ver=normal 注意下图所有的数字位置,在DOM结构中,都是方块。 字体反爬扫盲 字体反爬,是一种常见的反爬技术,网站采用了自定义的字体文件,在浏览器上正常显示,但是爬虫抓取下来的数据要么就是乱码,要么就是变成其他字符。采用自定义字体文件是CSS3的新特性,熟悉前端的同学可能知道,就是font-face属性。 一些重要破解素材的收集 找到font-family属性,查看设置的内容,发现是cs字体,这明显是自定义字体了,在网页中检索cs。 在页面的HTML源码中找到了字体的定义 注意文件的开头是base64 表示文件进行过base64编码,需要进行解码,然后在保存成ttf字体文件 上述截图中有个woff格式 Web开放字体格式(Web Open Font Format,简称WOFF) 是一种网页所采用的字体格式标准。此字体格式发展于2009年,现在正由万维网联盟的Web字体工作小组标准化,以求成为推荐标准。此字体格式不但能够有效利用压缩来减少档案大小,并且不包含加密也不受DRM(数位著作权管理)限制。 解码操作 import base64 font_face = "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" print(len(font_face)) b = base64.b64decode(font_face) with open('font.ttf','wb') as f: f.write(b) 对于ttf文件的处理,有3种方式,第一种使用软件 FontCreator可以直接打开ttf文件,第二种使用Python第三方库fontTools,借用这个库也可以操作ttf文件,第三种使用百度的fontstore, http://fontstore.baidu.com/static/editor/index.html FontCreator 软件查找这个就比较简单了 你可以自行百度寻找,也可以直接打开我的百度网盘下载 链接: https://pan.baidu.com/s/1ZyWwk37hNeo0vIsTqdK2fg 提取码: kk2h 安装完毕,直接试用即可,也可以采用国家支持的和谐方法,进行和谐 查阅一下sources里面的html编码 数字进行比对 顺便把这个地方的编码对应关系记录一下,方便后续操作 'uniE481': '7', 'uniE0AA': '4', 'uniF71E': '9', 'uniE767': '1', 'uniE031': '5', 'uniE4BD': '2', 'uniF2AA': '3', 'uniE2E3': '6', 'uniE3C9': '8', 'uniEA65': '0' 数字比对完全没有问题3.69亿 开始编码破解字体反爬 有的网页嵌套了多套字体,增加了反爬的成本,届时自行研究即可 利用fontTools可以获取每一个字符对象,这个对象你可以简单的理解为保存着这个字符的形状信息。而且编码可以作为这个对象的id,具有一一对应的关系。类似猫眼电影,多套字体对应的字符的编码是变化的,但是字符的形状是不变的,也就是说这个对象是不变的。 通过fontTools进行解析字库文件 安装fonttools pip install fonttools fontTools库详解: https://darknode.in/font/font-tools-guide/ 基本使用 from fontTools.ttLib import TTFont font = TTFont('font.ttf') font.saveXML('01.xml') 打开 xml 文件 开头显示的是全部编码,注意这里的ID是编号,千万不要当成对应的数字 下面对应的是字体信息,计算机只需要知道黑白像素点即可 注意事项,写代码的时候需要注意一下 关于猫眼的字体反爬做个总结 在实操中,你会发现猫眼电影,每次刷新字符编码都是变化的,但是字体的对象,也就是像素点是一致的。 你可以通过第一次下载一个字体文件base_font.ttf,并把对应编码的记下来,当第二次刷新页面之后,重新抓取字体文件online_font.ttf ,对比两个字体文件中的对象信息,如果对象是一样的,那么就可以知道对应的数字了。 首次获取字体文件 # 本地已经下载好的字体处理 base_font = TTFont('font.ttf') #打开本地的ttf文件 base_uni_list = base_font.getGlyphOrder()[2:] # 获取所有编码,去除前2个,可查看前文图示 # 写出第一次字体文件的编码和对应字体 origin_dict = {'uniE481': '7', 'uniE0AA': '4', 'uniF71E': '9', 'uniE767': '1', 'uniE031': '5', 'uniE4BD': '2','uniF2AA': '3', 'uniE2E3': '6', 'uniE3C9': '8', 'uniEA65': '0'} 获取在线字体 # 获取刷新之后在线的字体 # 获取字体文件的base64编码 online_ttf_base64 = re.findall(r"base64,(.*)\) format", response)[0] online_base64_info = base64.b64decode(online_ttf_base64) with open('online_font.ttf', 'wb')as f: f.write(online_base64_info) online_font = TTFont('online_font.ttf') # 网上动态下载的字体文件。 online_uni_list = online_font.getGlyphOrder()[2:] for uni2 in online_uni_list: obj2 = online_font['glyf'][uni2] # 获取编码uni2在online_font.ttf中对应的对象 for uni1 in base_uni_list: obj1 = base_font['glyf'][uni1] # 获取编码uni1在base_font.ttf 中对应的对象 if obj1 == obj2: # 判断两个对象是否相等 dd = "&#x" + uni2[3:].lower() + ';' # 修改为Unicode编码格式 if dd in response: # 如果编码uni2的Unicode编码格式 在response中,替换成origin_dict中的数字。 response = response.replace(dd, origin_dict[uni1]) response的获取采用的是request模块 url = 'https://piaofang.maoyan.com/?ver=normal' headers = { 'User-Agent': '浏览器UA', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', } response = requests.get(url=url, headers=headers).content # 得到字节 charset = chardet.detect(response).get('encoding') # 得到编码格式 response = response.decode(charset, "ignore") # 解码得到字符串 运行结果展示 关注微信公众账号:非本科程序员,回复0409获取下载地址
学术搜索 学习理论的知识少不了去检索文献,好多文献为你的实操提供了合理的支撑,我所在的大学内网默认是有知网账户的,非常NICE 今天要完成的网站是 http://ac.scmor.com/ Google学术搜索是一个文献检索服务,目前主要是提供维普资讯、万方数据等几个学术文献资源库的检索服务。通过Google学术搜索只能够查找到这些学术资料的“报告、摘要及引用内容... 来源百度百科 我们的目标 获取现在访问的链接地址,当你使用谷歌浏览器的开发者工具抓取的时候,得到的是一个js加密函数 注意看上图2的位置,接下来,我们采用上篇博客的方式,去尝试获取visit函数的具体内容 我们要在所有的请求链接中去检索一个visit方法,注意步骤 双击方法名,进入 找到核心方法 function visit(url) { var newTab = window.open('about:blank'); if(Gword!='') url = strdecode(url); // var newTab = window.open(url); newTab.location.href = url; //newTab.location.reload(true); } 发现url在跳转前调用了一个strdecode函数,你只需要关注这个函数的实现就可以了 再次查看visit的调用函数,找到参数的生成方式为 onclick="visit(\'' + autourl[b] + '\')" 对autourl[b] 我们是可以直接用爬虫在HTML页面获取到的 function auto(b) { t = (tim - ts[b]) / 100; tt = t.toString().split('.'); if(tt.length==1) t = t.toString() + '.00'; else if(tt[1].length < 2) t = t.toString() + '0'; if (t > 4) document.getElementById("txt" + b).innerHTML = '<font color=red>连接超时!<\/font>'; else document.getElementById("txt" + b).innerHTML = 'takes ' + t + 's. <a href="javascript:;" class="ok" onclick="visit(\'' + autourl[b] + '\')"> 现在访问 <\/a>' } function visit(url) { var newTab = window.open('about:blank'); if(Gword!='') url = strdecode(url); // var newTab = window.open(url); newTab.location.href = url; //newTab.location.reload(true); } 参数分析 if(Gword!='') url = strdecode(url); 如果Gword为空,调用的是strdecode方法,查阅之后,发现相关代码也在下面 Gword 在上面的一张图片中我们也已经获取到了,可以向上看 strdecode函数分析 进行base64编码 通过Gword生成一个key 计算key的len 循环string然后将code生成,这个地方注意js里面的fromCharCode函数(Python里面的chr)和charCodeAt函数(Python里面的ord) //code function strdecode(string) { string = base64decode(string); key = Gword+'ok '; len = key.length; code = ''; for (i = 0; i < string.length; i++) { var k = i % len; code += String.fromCharCode(string.charCodeAt(i) ^ key.charCodeAt(k)) } return base64decode(code) } var base64DecodeChars = new Array(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1); function base64decode(str) { var c1, c2, c3, c4; var i, len, out; len = str.length; i = 0; out = ""; while (i < len) { do { c1 = base64DecodeChars[str.charCodeAt(i++) & 0xff] } while (i < len && c1 == -1); if (c1 == -1) break; do { c2 = base64DecodeChars[str.charCodeAt(i++) & 0xff] } while (i < len && c2 == -1); if (c2 == -1) break; out += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4)); do { c3 = str.charCodeAt(i++) & 0xff; if (c3 == 61) return out; c3 = base64DecodeChars[c3] } while (i < len && c3 == -1); if (c3 == -1) break; out += String.fromCharCode(((c2 & 0XF) << 4) | ((c3 & 0x3C) >> 2)); do { c4 = str.charCodeAt(i++) & 0xff; if (c4 == 61) return out; c4 = base64DecodeChars[c4] } while (i < len && c4 == -1); if (c4 == -1) break; out += String.fromCharCode(((c3 & 0x03) << 6) | c4) } return out } 这个地方有2个解决方案了 1是用Python重写编写相关逻辑 2通过Python调用JS直接实现 我们采用方案2 将 base64decode 复制到一个文件中,然后通过execjs进行调用 Python 执行JS库 execjs execjs可以在python中运行javascript代码 官网:https://pypi.org/project/PyExecJS/ 安装:pip install PyExecJS 可以切换清华源 安装成功之后在pycharm中引入一下,不出错误,表示运行成功 我们对JS进行编译 import execjs with open('scmor.js', 'r', encoding='utf-8') as f: js = f.read() ctx = execjs.compile(js) # 对JS进行编译 核心的方法 def decode(string): string = ctx.call('base64decode', string) # base64解码string参数,string参数上面获取到的autourls里面的值 key = " link@scmor.comok " # Gword的值+ 'ok ' key 在HTML页面中可以获取到 Len = len(key) # Gword长度 code = '' for i in range(0, len(string)): k = i % Len n = ord(str(string[i])) ^ ord(str(key[k])) code += chr(n) return ctx.call('base64decode', code) 运行结果展示 完整代码下载 关注微信公众账号:非本科程序员,回复0402获取下载地址
@[toc] 图片比对 昨天的博客已经将图片存储到了本地,今天要做的第一件事情,就是需要在两张图片中进行比对,将图片缺口定位出来 缺口图片 完整图片 计算缺口坐标 对比两张图片的所有RBG像素点,得到不一样像素点的x值,即要移动的距离 def get_distance(self,cut_image,full_image): # print(cut_image.size) threshold = 50 for i in range(0,cut_image.size[0]): for j in range(0,cut_image.size[1]): pixel1 = cut_image.getpixel((i, j)) pixel2 = full_image.getpixel((i, j)) res_R = abs(pixel1[0] - pixel2[0]) # 计算RGB差 res_G = abs(pixel1[1] - pixel2[1]) # 计算RGB差 res_B = abs(pixel1[2] - pixel2[2]) # 计算RGB差 if res_R > threshold and res_G > threshold and res_B > threshold: return i # 需要移动的距离 极验证对于用户行为检测是有专门的算法的,找到一篇比较老的文章 https://blog.csdn.net/ieternite/article/details/51483491 如果我们直接把上面算出来的缺口位置放到前面脚本里,你会发现即使移动的位置正确了,提示却是“怪物吃了饼图”,验证不通过。很显然,geetest识别出了这个动作并不是人的行为。这我们就需要去查看自然人滑动鼠标和我们代码实现的滑动在轨迹上有什么不同。 鼠标拖动滑块进行移动的时候,也是遵循人类行为的,这个地方,你可以参考文章 https://www.cnblogs.com/xiao-apple36/p/8878960.html 移动滑块 这部分和我们之前滑动验证码识别是一致的,通过selenium进行人行为实现 # 移动滑块 def start_move(self, distance): element = self.driver.find_element_by_xpath('//div[@class="gt_slider_knob gt_show"]') # 使用滑块的一半进行偏移设置 distance -= element.size.get('width') / 2 distance += 15 # 按下鼠标左键 ActionChains(self.driver).click_and_hold(element).perform() time.sleep(0.5) while distance > 0: if distance > 20: # 如果距离大于20,就让他移动快一点 span = random.randint(5, 8) else: # 快到缺口了,就移动慢一点 span = random.randint(2, 3) ActionChains(self.driver).move_by_offset(span, 0).perform() distance -= span time.sleep(random.randint(10, 50) / 100) ActionChains(self.driver).move_by_offset(distance, 1).perform() ActionChains(self.driver).release(on_element=element).perform() 运行效果,第一次验证失败了,等待7秒左右进行第二次验证,注意成功了。最后要调整的是验证失败,需要重复验证 验证失败 验证失败,在拖动的下面继续编写即可,属于正常的逻辑代码了 self.start_move(dis) # 如果出现错误 try: WebDriverWait(self.driver, 5).until( EC.element_to_be_clickable((By.XPATH, '//div[@class="gt_ajax_tip gt_error"]'))) print("验证失败") return except TimeoutException as e: pass # 判断是否验证成功 try: WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, '//div[@class="gt_ajax_tip gt_success"]'))) except TimeoutException: print("重新验证....") time.sleep(5) # 失败后递归执行拖动 self.analog_drag() else: print("验证成功") 写在后面 到此为止,极验证已经编写完毕,代码中还有很多地方需要进行调整 例如 element = self.driver.find_element_by_xpath('//div[@class="gt_slider_knob gt_show"]') 上面获取元素的方式,很容易导致目标元素没有捕获到,然后项目直接报错退出,所以需要进行完善 driver 需要及时的关闭,否则会在你的任务管理器中出现大量的chromedriver.exe 进程 极验证验证码破解方式基本遵循滑动验证码,核心内容在于两个图片的处理,希望你可以学习到。 扫码关注微信公众账号,回复0321获取验证码源码
验证码类型 今天要搞定的验证码属于现在使用非常多的验证码的一种类型---极验证滑动验证码,关于这个验证码的详细说明查阅他的官网,https://www.geetest.com/ 把验证码做到这个地步,必须点赞了。 官网最新效果 官方DEMO最新的效果如下,按照验证码的更新频率,基本博客看完,验证码也更新了,不过套路依旧是相同的,反爬只能增加爬虫编写的成本,并不能完全杜绝爬虫。 这类验证码,常规解决办法,模拟人为操作,图像比对,查找缺口,移动覆盖缺口。 找个用极验证的网站 今天看新闻,随意找了一下,虎嗅使用的是直接拖拽,没有用最新的点击+拖拽方式,可以直接看一下如何操作。 这种验证码除了打码平台以外,直接selenium搞起 拼接验证码图片 当你在谷歌浏览器使用F12进行查找元素的时候,随意的去缺口图片上面点击一下,在控制台DOM结构中出现如下代码,有前端经验的童鞋知道,这个使用的是背景局部显示技术,是可以通过这个拼接成一个。 注意两个地方: https://static.geetest.com/pictures/gt/8bc4cb7fa/8bc4cb7fa.webp 图片地址 background-position:后面的坐标 查阅图片之后,发现是一张碎掉的图片,你要做的第一步是将这个图片进行还原,我们通过selenium进行实现。这个地方需要先备注一下图片的尺寸,后面用size = 312x116 编写自动化代码 使用selenium执行的操作,模拟人的点击行为即可 最初,我们导入一些selenium的基本模块与方法 import time import re from selenium import webdriver from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.action_chains import ActionChains 基本模块的作用如下webdriver 核心驱动selenium.common.exceptions 异常类 TimeoutException 超时异常selenium.webdriver.common.by 按照什么方式进行元素的查找 例如 By.ID,By.ClassName,By.XPATHselenium.webdriver.support.wait 等待页面加载某些元素 from selenium.webdriver.support import expected_conditions 场景判断用的,一般和上面的等待加载元素一起使用selenium.webdriver.common.action_chains 鼠标执行的动作链 主方法测试入口 if __name__ == '__main__': h = Geek_Huxiu() h.run() 构造方法,实现对部分参数的初始化操作 def __init__(self): self.driver = webdriver.Chrome() self.driver.set_window_size(1366,768) webdriver.Chrome() 启动谷歌浏览器,这个地方需要你提前配置好chromedriver.exeset_window_size(1366,768) 初始化浏览器大小 核心run方法 def run(self): self.driver.get("https://www.huxiu.com/") # 打开浏览器 WebDriverWait(self.driver,10).until(EC.element_to_be_clickable((By.XPATH,'//*[@class="js-register"]'))) reg_element = self.driver.find_element_by_xpath('//*[@class="js-register"]') reg_element.click() WebDriverWait(self.driver,10).until(EC.element_to_be_clickable((By.XPATH,'//div[@class="gt_slider_knob gt_show"]'))) # 模拟拖动 self.analog_drag() WebDriverWait 方法 说明 driver: 传入WebDriver实例,即我们上例中的driver timeout: 超时时间,等待的最长时间(同时要考虑隐性等待时间) poll_frequency: 调用until或until_not中的方法的间隔时间,默认是0.5秒 ignored_exceptions: 忽略的异常,如果在调用until或until_not的过程中抛出这个元组中的异常, 则不中断代码,继续等待; 如果抛出的是这个元组外的异常,则中断代码,抛出异常。默认只有NoSuchElementException。 基本使用方法 WebDriverWait(driver, 超时时长, 调用频率, 忽略异常).until(可执行方法, 超时时返回的信息) 模拟拖动方法 def analog_drag(self): # 鼠标移动到拖动按钮,显示出拖动图片 element = self.driver.find_element_by_xpath('//div[@class="gt_slider_knob gt_show"]') ActionChains(self.driver).move_to_element(element).perform() time.sleep(3) # 刷新一下极验证图片 element = self.driver.find_element_by_xpath('//a[@class="gt_refresh_button"]') element.click() time.sleep(1) # 获取图片地址和位置坐标列表 cut_image_url,cut_location = self.get_image_url('//div[@class="gt_cut_bg_slice"]') print(cut_image_url) print(cut_location) 行为链 ActionChains(self.driver).move_to_element(element).perform() 模拟人移动鼠标到指定DOM元素 图片处理方法 def get_image_url(self,xpath): link = re.compile('background-image: url\("(.*?)"\); background-position: (.*?)px (.*?)px;') elements = self.driver.find_elements_by_xpath(xpath) image_url = None location = list() for element in elements: style = element.get_attribute('style') groups = link.search(style) url = groups[1] x_pos = groups[2] y_pos = groups[3] location.append((int(x_pos), int(y_pos))) if not image_url: image_url = url return image_url, location 使用正则表达式进行匹配的时候,需要将所有的DIV匹配出来 ,采用find_elements_by_xpath 方法,尤其注意elements WebElement 具备一些常用的方法和属性 size:返回元素尺寸 text :返回元素文本 get_attribute(name):获得属性值 is_dispalyed() :该元素是否用户可见 初步运行结果 拼接图 看下图,注意一些基本元素,拼接的图片由N个小矩形构成,分为上下两个部分,小矩形的宽度和高度为10x58 核心由上下两部分构成,每部分都是26个小矩形 因为,整体宽度为2610 = 260px ,整体高度为582=116px 但是,还记得博客开始的时候,你记录的那个宽度和高度么? 312x116 高度一致,但是宽度出现偏差 312-260 = 52px 52个像素去除以26个矩形,发现每个矩形差2px,这两个像素也就是下面我们拼接图片的重点了 def splicing_image(self,image_url,location): res = requests.get(image_url) file = BytesIO(res.content) img = Image.open(file) image_upper = [] image_down = [] for pos in location: if pos[1] == 0: # y值为0的坐标 属于图片上半部分,高度58 image_upper.append(img.crop((abs(pos[0]), 0, abs(pos[0]) + 10, 58))) else: # y值为58的坐标 属于图片上半部分,高度58 image_down.append(img.crop((abs(pos[0]), 58, abs(pos[0]) + 10, img.height))) # 画布的x轴偏移量 x_offset = 0 # 创建一张画布 new_img = Image.new("RGB", (260, img.height)) for img in image_upper: new_img.paste(img, (x_offset, 58)) x_offset += img.width x_offset = 0 for img in image_down: new_img.paste(img, (x_offset, 0)) x_offset += img.width return new_img 说明 requests.get(image_url) 下载图片到本地 BytesIO(res.content) 将字节转换成二进制文件流 Image.open(file) 获取图片 img.crop 裁切图片 left, upper, right, lower Image.new("RGB", (260, img.height)) 创建一个空白的图片,将图片序列中的元素,依次的拼接到里面 最终实现效果 图片存储到本地 # 将图片存储到本地 cut_image.save("cut.jpg") full_image.save("full.jpg") 好了,今天博客就先把图片处理到位,明天着手拼接部分。
滑动验证码介绍 本篇博客涉及到的验证码为滑动验证码,不同于极验证,本验证码难度略低,需要的将滑块拖动到矩形区域右侧即可完成。 这类验证码不常见了,官方介绍地址为:https://promotion.aliyun.com/ntms/act/captchaIntroAndDemo.html 使用起来肯定是非常安全的了,不是很好通过机器检测 如何判断验证码类型 这个验证码的标识一般比较明显,在页面源码中一般存在一个 nc.js 基本可以判定是阿里云的验证码了 <script type="text/javascript" src="//g.alicdn.com/sd/ncpc/nc.js?t=1552906749855"></script> 识别套路 截止到2019年3月18日,本验证码加入了大量的selenium关键字验证,所以单纯的模拟拖拽被反爬的概率满高的,你也知道一般情况爬虫具备时效性 不确保这种手段过一段时间还可以使用! 导入selenium必备的一些模块与方法 from selenium import webdriver from selenium.webdriver.support.wait import WebDriverWait # from selenium.webdriver.support import expected_conditions as EC # from selenium.webdriver.common.by import By from selenium.webdriver.chrome.options import Options from selenium.webdriver import ActionChains import time import random 在启动selenium之前必须要设置一个本机的代理,进行基本的反[反爬] 处理,很多爬虫在获取用户指纹的时候,都比较喜欢selenium,因为使用selenium模拟浏览器进行数据抓取,能够绕过客户JS加密,绕过爬虫检测,绕过签名机制 但是selenium越来越多的被各种网站进行了相关屏蔽,因为selenium在运行的时候会暴露出一些预定义的Javascript变量(特征字符串),例如"window.navigator.webdriver",在非selenium环境下其值为undefined,而在selenium环境下,其值为true 下图所示为selenium驱动下Chrome控制台打印出的值 细致的绕过去的方法,可能需要单独的一篇博客进行赘述了,这里我只对上面的参数进行屏蔽,使用到的是之前博客中涉及的mitmdump进行代理 https://docs.mitmproxy.org/stable/concepts-certificates/ mitmdump进行代理 技术参考来源:https://zhuanlan.zhihu.com/p/43581988 关于这个模块的基本使用,参考我前面的博客即可,这里核心使用了如下代码 indject_js_proxy.py from mitmproxy import ctx injected_javascript = ''' // overwrite the `languages` property to use a custom getter Object.defineProperty(navigator, "languages", { get: function() { return ["zh-CN","zh","zh-TW","en-US","en"]; } }); // Overwrite the `plugins` property to use a custom getter. Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5], }); // Pass the Webdriver test Object.defineProperty(navigator, 'webdriver', { get: () => false, }); // Pass the Chrome Test. // We can mock this in as much depth as we need for the test. window.navigator.chrome = { runtime: {}, // etc. }; // Pass the Permissions Test. const originalQuery = window.navigator.permissions.query; window.navigator.permissions.query = (parameters) => ( parameters.name === 'notifications' ? Promise.resolve({ state: Notification.permission }) : originalQuery(parameters) ); ''' def response(flow): # Only process 200 responses of HTML content. if not flow.response.status_code == 200: return # Inject a script tag containing the JavaScript. html = flow.response.text html = html.replace('<head>', '<head><script>%s</script>' % injected_javascript) flow.response.text = str(html) ctx.log.info('>>>> js代码插入成功 <<<<') # 只要url链接以target开头,则将网页内容替换为目前网址 # target = 'https://target-url.com' # if flow.url.startswith(target): # flow.response.text = flow.url 上述脚本放置任意目录,之后进行mitmdump的启动即可 C:\user>mitmdump -s indject_js_proxy.py Loading script indject_js_proxy.py Proxy server listening at http://*:8080 启动之后,通过webdriver访问 测试网站:https://intoli.com/blog/not-possible-to-block-chrome-headless/chrome-headless-test.html 如果webDriver是绿色,也说明代理起作用了 selenium爬取 接下来就是通过selenium进行一些模拟行为的操作了,这部分代码比较简单,编写的时候参考一下注释即可。 # 实例化一个启动参数对象 chrome_options = Options() # 添加启动参数 chrome_options.add_argument('--proxy-server=127.0.0.1:8080') # 将参数对象传入Chrome,则启动了一个设置了窗口大小的Chrome driver = webdriver.Chrome(chrome_options=chrome_options) 关键函数 def move_to_gap(tracks): driver.get("https://passport.zcool.com.cn/regPhone.do?appId=1006&cback=https://my.zcool.com.cn/focus/activity") # 找到滑块span need_move_span = driver.find_element_by_xpath('//*[@id="nc_1_n1t"]/span') # 模拟按住鼠标左键 ActionChains(driver).click_and_hold(need_move_span).perform() for x in tracks: # 模拟人的拖动轨迹 print(x) ActionChains(driver).move_by_offset(xoffset=x,yoffset=random.randint(1,3)).perform() time.sleep(1) ActionChains(driver).release().perform() # 释放左键 注意看到上述代码中有何核心的点 --- 拖拽距离的 列表tracks if __name__ == '__main__': move_to_gap(get_track(295)) 这个地方可以借鉴网上的方案即可 def get_track(distance): ''' 拿到移动轨迹,模仿人的滑动行为,先匀加速后匀减速 匀变速运动基本公式: ①v=v0+at ②s=v0t+(1/2)at² ③v²-v0²=2as :param distance: 需要移动的距离 :return: 存放每0.2秒移动的距离 ''' # 初速度 v=0 # 单位时间为0.2s来统计轨迹,轨迹即0.2内的位移 t=0.1 # 位移/轨迹列表,列表内的一个元素代表0.2s的位移 tracks=[] # 当前的位移 current=0 # 到达mid值开始减速 mid=distance * 4/5 distance += 10 # 先滑过一点,最后再反着滑动回来 while current < distance: if current < mid: # 加速度越小,单位时间的位移越小,模拟的轨迹就越多越详细 a = 2 # 加速运动 else: a = -3 # 减速运动 # 初速度 v0 = v # 0.2秒时间内的位移 s = v0*t+0.5*a*(t**2) # 当前的位置 current += s # 添加到轨迹列表 tracks.append(round(s)) # 速度已经达到v,该速度作为下次的初速度 v= v0+a*t # 反着滑动到大概准确位置 for i in range(3): tracks.append(-2) for i in range(4): tracks.append(-1) return tracks 代码注释已经添加好,可以自行查阅,临摹一下即可明白 最后开始进行尝试,实测中,发现可以自动拖动,但是,出现一个问题是最后被识别为机器,这个地方,我进行了多次的修改与调整,最终从代码层面发现实现确实有些复杂,所以改变策略,找一下chromedriver.exe是否有修改过的版本,中间去除了selenium的一些关键字,运气不错,被我找到了。 目前只有windows10版本和linux16.04版本gitee地址:https://gitee.com/bobozhangyx/java-crawler/tree/master/file/%E7%BC%96%E8%AF%91%E5%90%8E%E7%9A%84chromedriver 下载之后,替换你的 chromedriver.exe 再次运行,成功验证 欢迎关注「非本科程序员」 回复 【0411】获取本篇博客源码
2019年11月
2019年07月
2019年06月