Python3+selenium+BaiduAI识别并下载花瓣网高颜值妹子图片
一、说明
1.1 背景说明
上周在“Python3使用百度人脸识别接口识别高颜值妹子图片”中自己说到在成功判断颜值后,下截图片并不是什么难点。
直观感觉上确实如此,你判断的这个url适不适合下载,适合我就去下不适合就不去下,这算什么难点呢。
但事实经常没有想象的那么简单,所以决定去验证一下。结果再次证实自己想简单了,程序的编写和调试花了一周的业余时间,好在总算完成了。
1.2 程序编写过程说明
我以花瓣网http://huaban.com/favorite/beauty/入手,首先确定从beauty这个页面提取形如http://huaban.com/pins/1715655164/的浏览页面(//a/@herf),在这类界面中才真正提取图片src(//img/@src),然后将src提交到百度AI人脸识别接口,如果返回的颜值达标就下载;如果不达标就跳过。
第一个遇到的问题是ImagesPipeline找不到PIL,那就安装PIL,但PyCharm中安装PIL提示没有与当前python匹配的版本;然后自接用conda安装,最后看到环境中的python3.6直接被降成了python2.7,程序报了很多错,百度发现PIL只支持到Python2.7,Python3要安装Pillow,重新配置环境安装Pillow这个问题算处理了。
第二个遇到的问题是ImagesPipeline下载报错,再百度网上说PIL处理压缩图片有问题;ImagesPipeline也就是针对收到的url下载图片面已,自己写个保存函数(程序中的save_image)这个问题也就处理了,但这是有打击的因为我断言的“下载图片Scrapy的ImagesPipeline是绝配”太自以为然了
第三个遇到的问题是花瓣网不是直接整个返回页面,而是使用ajax取回各种元素再构造出的页面;ajax网上看了一般说用selenium处理,好那就去研究
第四个遇到的问题在下拉几个页面后花瓣网要求登录;首先考虑的是借助scrapy的FormRequest.from_response()取回Set-Cookie然后加到selenium的Cookie中去,但后来又想我分明selenium自己可以登录为什么要再多用个FormRequest.from_response()来取Cookie这么麻烦,所以登录这个问题也用selenium处理了
第五个问题是要模拟人看完一页才下拉滚动条加载下一页,异步加载相当于在上一页的后面追加,这样造成的问题是当前抽取出来的浏览页面包含了之前已经加载的所有的浏览页面,由于没有了scrapy我们如何去避免这里的重复问题;这个问题我这里的处理办法是使用for i in range(5):模拟五次异步加载,然后使用viewed_page_count这个变量记录之前异步加载已抽取的浏览页面数量,本次遍历从image_view_page_urls[viewed_page_count]开始(这种处理办法正确的前提是Seletor抽取浏览页面是严格从头向尾抽取的,这我并不确定但应该来说很大可能是这样)
第六个遇到的问题是http://huaban.com/favorite/beauty/,这个界面因为要下拉滚动条异步加载下一页所以这个界面应该保存,而http://huaban.com/pins/1715655164/这些链接需要不断打开,selenium又不支持多选项卡只用selenium构造一个浏览器是不够的;这个问题的处理办法是构造一个浏览器不够,那就构造两个浏览器
第七个遇到的问题是程序跑了大半个小时识别了很多符合下载条件的图片但都还没有下载任何一张图片;这个问题思路是降低程序的复杂度,首先花瓣网是用瀑布流形式的就http://huaban.com/favorite/beauty/一个页面,并不需要爬行所以直接使用Spider而不使用CrawlSpider这样可以去掉Rule;但依然还是没有下载,追踪了半天也没弄清楚不进入下载的原因,又一想既然我不用爬行那Spider的在程序并没有启什么作用而且其导致了我不清楚为什么没有下载图片。所以没有开始下载图片这个问题以弃用scrapy进行处理(我的“下载图片Scrapy的ImagesPipeline是绝配”彻底被否定了)
第八个问题是出现了大量的重复图片;这个问题一是限制main_page只能抽取中间的那些href,二是限制view-page只能抽取右侧边栏的src,三是图片采用“颜值”-“下载日期”-“url的md5值”形式命名确保图片不会被重复下载。但是通过这样限制后发现程序运行一段时间后大量提示“图片已存在”,下载下来的图片却又很少,最后观察发现是firefox只能打开第一个view_page后再调用get请求下一个view_page时页面并没有成功刷新,这就使得每次获取的page_source其实都是每一个view_page的提取出来的url当然都重复。这个问题通过更换成chrome在一定程度上缓解,但并没能完全解决因为chrome在运行一段时间后会出现强制退出的情况,所以这个问题的根本原因可能是selenium对开两个浏览器支持不完善造成的。
当然就功能实现来说是有一些捷径可以走的,比如首选登录这个问题如果通过修改User-Agent伪装成手机浏览器是不需要登录的。再比如瀑布流网站异步加载直接找到ajax接口访问从返回的json内容中就可以提取接链接,或者调大limit一次就可以获取更多的返回结果并不需要真去下拉滚动条,又再或者干脆直接换个网站。但,我们就是要正面硬刚,奇技淫巧是不要的。
20180627更新:
浏览器运行一段时间后挂掉这个问题,首先想可能是浏览器失去焦点就失效所以将使用两个浏览器改为使用一个浏览器两个选项卡,但是使用选项卡一段时间后一样还是出错,怀疑是选项卡一段时间后就失效所以想打开view_page取完内容后就关闭但还是出错,怀疑是跨函数切换选项卡引发的问题将所有代码都放内一个函数还是出错。最后确定问题是:selenium一段时间(观察大多是5秒)不操作后浏览器就会失效。处理办法是在main_page方面将程序原先设想的五次下拉滚动条第次解析各自的view_page改为连续五次下拉滚动条一次性抽取解析所有view_page,在view_page处理方面将程序由原先设想的一个浏览器不断打开新view_page调整为每次都实例化一个浏览器去打开传来的view_page取得其实内后就将浏览器关闭。
程序修改基本到这就定型了,比较可惜由于selenium的bug没能将流程完全拟人化(当然也可能是我不知道该怎么配置)。然后感谢花瓣网对爬虫的宽容,感谢百度对识别请求的“来者不拒”。最后对于AI,单就百度人脸检测这一项来说还有不少问题,比如说有时人脸识不到又比如说人不是站立正面直面前方(侧脸、躺着等)评分偏差较大等;总的来说首先AI是一个很好的方向,然后AI比较难百度算中国AI的领导级别了但搞了这么几年产品可用性实用性也只能说差强人意,最后就是说对AI的看法计算机行业这么多年来很多概念都是资本的炒作很不幸AI也就包括在其中。AI恐怕是计算机行业技术最为密集的领域,百度都是些什么级别的人才?搞了多久?出了什么产品?有多大市场?对应的现在一堆搞AI的都是些什么人?学了多久?你能期望他们能搞出什么产品?你准备期望他们能占领什么市场?或者怎么定义AI,像我这里一样去调个别人的接口就是搞AI吗。就计算机行业而言政府重视企业投钱社会认可美滋滋没必要去说破,只希望输的人不要太惨。
当前程序的流程是:
login_in()----登录花瓣网
open_main_page()----手动重定向到http://huaban.com/favorite/beauty/
open_main_page()----使用for i in range(5):模拟用户4次下拉滚动条到底部
open_main_page()----从五次下拉滚动条后的页面抽取抽取出所有view_page交给get_img_url_page()获取图片的src
get_img_url_page()----将src传到BaiduFaceIdentify进行鉴别,如果评分超过50分就将src传到save_image()保存图片,如果不达标则跳过
save_image()----接收传过来的图片url,判断图片是否已存在如果未存在则下载保存
1.3 程序运行结果展示
运行输出:
下载图片(我们是在认真地探讨技术,图片这种东西长什么样我根本没注意,我这么说你相信的吧(>_<))
文件夹:
二、程序源代码
使用时五件事:
1) 下载安装firefox
2) 配置好python环境----python3.x、selenium、requests、lxml
3) 下载geckodriver(chrome下载chromedriver),将之与下面两个文件保存在同个文件夹下
4) huaban.py----找到self.browser_main_page.find_element_by_name('email').send_keys('youemail@qq.com')修改成自己花瓣网的用户名密码
5) BaiduFaceIdentify.py----找到将client_id赋值成自己的API Key,client_secret赋值成自己的Secret Key
github地址:https://github.com/PrettyUp/BaiduAI
2.1 主程序----huaban.py
import hashlib import os import re import time import logging import urllib.request from lxml import etree from selenium import webdriver from BaiduFaceIdentify import BaiduFaceIdentify class HuabanDownloader(): # 构造函数,实例化bfi、browser、pic_download_count def __init__(self): # 百度人脸检测实例 self.bfi = BaiduFaceIdentify() # 用于打开首页、登录花瓣并五次下拉滚动条到底部的浏览器 # 在调试时建议使用正常呈现界面的浏览器模式,但在程序发布时建议使用无头模式 # 这样可以加快速度、减小资源消耗及防止误关浏览器导致程序中止 # 无头模式和普通模式在功能和使用上毫无区别,只是在实例化时加入以下--headless和--disable-gpu两个参数 self.browser_options = webdriver.FirefoxOptions() self.browser_options.add_argument('--headless') self.browser_options.add_argument('--disable-gpu') self.browser = webdriver.Firefox(firefox_options=self.browser_options) # 设置浏览器页面加载超时时间,这里设置15秒 self.browser.set_page_load_timeout(15) # 类成员变量,用于保存共下载的图片张数 self.pic_download_count = 0 # 此函数用于登录花瓣网 def login_in(self, login_page_url): try: # 使用浏览器打开花瓣网,以准备登录 self.browser.get(login_page_url) except Exception: # 如果加载超时,直接中止加载未完成内容运行后续代码 self.browser.execute_script('window.stop()') time.sleep(1) # 找到登录按钮并点击,唤出登录对话框 self.browser.find_element_by_xpath('//a[@class="login bounce btn wbtn"]').click() # 找到用户名输入框,填写用户名(我贴上来时乱改的,运行时改成自己花瓣网的用户名) self.browser.find_element_by_name('email').send_keys('youremail@qq.com') # 找到密码输入框,填写密码(我贴上来时乱改的,运行时改成自己的密码) self.browser.find_element_by_name('password').send_keys('yourpasswd') # 找到登录按钮并点击登录 self.browser.find_element_by_css_selector('a.btn:nth-child(4)').click() time.sleep(2) # 此函数用于打开main_page,下拉五次滚动条,然后提取页面所有目标a标签的href def open_main_page(self,main_page_url): try: # 登录后会自动重定向到个人主页,我们手动重定向到main_page self.browser.get(main_page_url) except Exception: # 如果加载超时,直接中止加载未完成内容运行后续代码 self.browser.execute_script('window.stop()') # for模拟用户4次将浏览器滚动条拉到了底部,由于打开时直接展示一个版面,所以下拉4次后最终是五个版面 # 这设置在我电脑大概跑了两小时下载554张图片 for i in range(1,5): logging.warning('开始第'+ str(i) +'次下拉滚动条') # 浏览器执行js将滚动条拉到底部 self.browser.execute_script("window.scrollTo(0,document.body.scrollHeight)") time.sleep(2) # 获取当前浏览器界面的html源代码 content = self.browser.page_source # 使用lxml解析内容构建选择器 sel = etree.HTML(content) # 提取目标a标签的href属性值 image_view_page_urls = sel.xpath('//div[@id="waterfall"]//a[@class="img x layer-view loaded"]/@href') # 关闭浏览器,实际使用发现浏览器5秒以上没操作后面就没用了,所以不关留着后面也用不了 self.browser.quit() # 遍历浏览页面a标签的href属性值,也就是view_page for image_view_page_url in image_view_page_urls: # 匹配“pins/”+6位以上数值的url才是我们的目标view_page if re.search('pins/\d{6,}',image_view_page_url): logging.warning('view_page url格式匹配,即将进入:' + image_view_page_url) image_view_page_url_temp = 'http://huaban.com' + image_view_page_url self.get_img_url_from_view_page(image_view_page_url_temp) else: logging.warning('view_page url格式不匹配,将不进入:'+ image_view_page_url) # 此函数负责从view_page中抽取图片src,并将本次view_page的所有src传到百度识别接口,获取检测结果 def get_img_url_from_view_page(self, image_view_page_url): # 每次都实例化一个浏览器来打开传来的url browser_view_page_options = webdriver.FirefoxOptions() browser_view_page_options.add_argument('--headless') browser_view_page_options.add_argument('--disable-gpu') browser_view_page = webdriver.Firefox(firefox_options=browser_view_page_options) browser_view_page.set_page_load_timeout(5) try: # 打开url browser_view_page.get(image_view_page_url) except Exception: # 如果到时间还没加载完成那就终止还没完成的加载,直接进行后续步骤 browser_view_page.execute_script('window.stop()') # 获取当前浏览器界面的html源代码 content = browser_view_page.page_source # 使用lxml解析内容构建选择器 sel = etree.HTML(content) # 从view_page中抽取图片src img_urls = sel.xpath('//div[@id="board_pins_waterfall"]//img/@src') # 关闭浏览器,实际使用发现浏览器5秒以上没操作后面就没用了,所以不关留着后面也用不了 browser_view_page.quit() # 遍历当前view_page抽取到的图片src for img_url in img_urls: # 排除gif及确保图片不是网站相对图径 if 'gif' not in img_url and 'aicdn.com' in img_url: logging.warning('\r\nimg_url格式匹配,即将调用百度识别:http:' + img_url) img_url_tmp = 'http:' + img_url[:img_url.find('_')] try: # 调用百度识别接口进行识别,当然这个接口是我们自己封装的BaiduFaceIdentify类 beauty_value = self.bfi.parse_face_pic(img_url_tmp) except Exception: logging.error('百度识别遇到了一个错误:' + img_url_tmp) continue # 对返回的颜值进行判断,以决定如何处理图片 if beauty_value > 50.0: logging.warning('颜值' + str(beauty_value) +'达标,准备确认图片是否已存在:' + img_url_tmp) self.save_image(img_url_tmp, beauty_value) elif beauty_value == 1.0: logging.warning('不是妹子,将不保存该图片:' + img_url_tmp) elif beauty_value == 0.0: logging.warning('没有人脸,将不保存该图片:' + img_url_tmp) else: logging.warning('颜值' + str(beauty_value) +'不达标,将不保存该图片:' + img_url_tmp) else: logging.warning('\r\nimg_url格式不匹配,将不调用百度识别:http' + img_url) # 此函数用于将颜值达标的图片保存到当前路径的pic目录下 def save_image(self, img_url,beauty_value): # 图片名称使用“颜值”-“下载日期”-“url的md5值”形式 image_name = str(beauty_value) + '-' + time.strftime("%Y%m%d", time.localtime()) + '-' + hashlib.md5(img_url.encode()).hexdigest()+'.jpg' # 判断pic目录是否存在,不存在则先创建 if not os.path.exists('pic'): logging.warning('pic目录尚不存在,即将创建') os.mkdir('pic') # 判断图片是否之前已保存过 if not os.path.isfile('pic\\' + image_name): logging.warning('图片尚不存在,即将保存:'+ image_name) # 保存图片 urllib.request.urlretrieve(img_url, 'pic\\' + image_name) self.pic_download_count += 1 logging.warning('当前已保存'+ str(self.pic_download_count) + '张图片') else: logging.warning('图片已存在,将不保存:'+ image_name) if __name__ == '__main__': login_page_url = 'http://huaban.com/' main_page_url = 'http://huaban.com/favorite/beauty/' huaban_downloader = HuabanDownloader() huaban_downloader.login_in(login_page_url) huaban_downloader.open_main_page(main_page_url)
2.2 百度识别接口----BaiduFaceIdentify.py
import base64 import urllib import json import logging import requests class BaiduFaceIdentify(): #此函数用于获取access_token,返回access_token的值 #此函数被parse_face_pic调用 def get_access_token(self): client_id = 'KuLRFhTzX3zBFBSrbQBsl6Q5' #此变量赋值成自己API Key的值 client_secret = '8ahbIb2hEOePzXhehw9ZDL9kGvbzIHTV' #此变量赋值成自己Secret Key的值 auth_url = 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=' + client_id + '&client_secret=' + client_secret response_at = requests.get(auth_url) json_result = json.loads(response_at.text) access_token = json_result['access_token'] return access_token #此函数进行人脸识别,返回识别到的人脸列表 #此函数被parse_face_pic调用 def identify_faces(self,url_pic,url_fi): headers = { 'Content-Type' : 'application/json; charset=UTF-8' } # 因为提交URL让baidu去读取图片,总是返回图片下载错了 # 所以我们这里将读取URL指向的图片,将图片转成BASE64编码,改用提交BASE64编码而不是提交URL pic_obj = urllib.request.urlopen(url_pic) pic_base64 = base64.b64encode(pic_obj.read()) post_data = { # 'image': url_pic, # 'image_type' : 'URL', 'image': pic_base64, 'image_type': 'BASE64', 'face_field' : 'facetype,gender,age,beauty', #expression,faceshape,landmark,race,quality,glasses 'max_face_num': 1 } response_fi = requests.post(url_fi,headers=headers,data=post_data) json_fi_result = json.loads(response_fi.text) # 有些图片是没有人脸的,或者识别有问题,这个我们不细管直接捕获异常就返回空列表 try: # if json_fi_result['result'] is None: # return [] # else: return json_fi_result['result']['face_list'] except: return [] #下边的print也许是最直观,你最想要的 #print(json_fi_result['result']['face_list'][0]['age']) #print(json_fi_result['result']['face_list'][0]['beauty']) #此函数用于解析进行人脸图片,返回图片中人物颜值 #此函数调用get_access_token、identify_faces def parse_face_pic(self,url_pic): #调用get_access_token获取access_token access_token = self.get_access_token() url_fi = 'https://aip.baidubce.com/rest/2.0/face/v3/detect?access_token=' + access_token #调用identify_faces,获取人脸列表 json_faces = self.identify_faces(url_pic,url_fi) # 如果没有人脸,那么就以0.0为颜值评分返回 if len(json_faces) == 0: logging.warning('未识别到人脸') return 0.0 else: for json_face in json_faces: logging.debug('种类:'+json_face['face_type']['type']) logging.debug('性别:'+json_face['gender']['type']) logging.debug('年龄:'+str(json_face['age'])) logging.debug('颜值:'+str(json_face['beauty'])) # 如果识别到的不是妹子,也以1.0为颜值评分返回 # 如果识别到的是妹子,直接以值颜值返回 if json_face['gender']['type'] != 'female': logging.info('图片不是妹子') return 1.0 else: return json_face['beauty'] if __name__ == '__main__': #uil_pic赋值成自己要测试的图片的url地址 url_pic = 'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1357154930,886228461&fm=27&gp=0.jpg' bfi = BaiduFaceIdentify() bfi.parse_face_pic(url_pic)