一天,一个朋友给我发来一条链接https://ssr.163.com/cardmaker/#/,让我帮他看看怎么能获取到网页中所有的图片链接。我打开链接一看,页面的标题是阴阳师:百闻牌,下面有选择栏,再下边就是各种奇奇怪怪的看不懂的图片,我就问他这是什么呀?他说是一个游戏阴阳师里边的卡牌。怪不得我没听过,因为我不玩游戏,一个准程序猿不玩游戏一定有很多人不相信 ,但是确实如此,我从未玩过游戏 。
但是这并不影响我来分析网页得到图片,网页如下:
但是你右键查看网页源代码会发现源代码中无任何图片链接的信息,除了一堆HTML整体布局代码和极端JS,什么都没有,显然,图片是动态加载生成的,用常规的requests库是请求不到链接的,这个时候最简单也最直接的办法就是使用selenium模拟自动化来动态操作并抓取图片链接,很快就得到了所有图片链接。 代码如下:
import time
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.keys import Keys
driver = webdriver.Chrome()
driver.maximize_window()
driver.get('https://ssr.163.com/cardmaker/#/')
time.sleep(3)
while True:
try:
driver.find_element_by_class_name('load-more')
ActionChains(driver).key_down(Keys.END).perform()
time.sleep(1)
except:
break
# 通过xpath定位所有图片
imgs = driver.find_elements_by_xpath('/html/body/div[3]/div[4]/div/div[3]/ul/li/img')
print('共计有%d张卡牌' % len(imgs))
for img in imgs:
print('已获取到图片链接:', img.get_attribute('src'))
driver.quit()
测试效果如下:
阴阳师卡牌下载文字识别simple_test
但是一个项目不应该也不可能止于此,可以做一些更多的事,我可以使用请求到的链接来下载图片,再将其中的文字识别出来。虽然不知道文字具体是什么意思,是角色?还是技能?或者大招? 不管那么多了,先识别出来再说吧。
这个小项目不需要太多的配置,只需要安装两个Python库:
pip install selenium
命令安装,同时需要下载webdriver驱动,可以点击https://download.csdn.net/download/CUFEECR/12193208下载Google浏览器最新版对应版本,或点击http://chromedriver.storage.googleapis.com/index.html下载与Google对应版本,并(解压)放入Python对应安装路径下的Scripts目录下。pip install baidu-aip
安装即可。
除此之外,还需要在百度云https://login.bce.baidu.com/?redirect=http%3A%2F%2Fcloud.baidu.com%2Fcampaign%2FAnnualceremony-2020%2Findex.html创建自己的文字识别应用,示例如下:
最后可以在应用列表中看到:
即可获得AppID、API Key和Secret Key,后边会用到。
该项目的重点和难点有3个,分别是滚动加载所有图片、调用百度文字识别SDK定位角色、描述和技能的位置和实现多线程,下面一一进行讲解:
通常,网页在展示较多的内容时,一般不是直接在一个页面全部展示的,而是通过不同的方式分成不同的部分,常见的有3种: (1)分页 即将内容分到多页中,每页展示固定数量的内容,各页之间的网页结构类似,这类的网站如淘宝,如下:
阴阳师卡牌下载文字识别taobao_page
这类网页要实现获取到所有数据据,可以通过selenium模拟点击页码或者调整URL中与页数相关的参数实现。 通过selenium模拟点击的示例代码如下:
next_page = driver.find_element_by_class_name('//*[@id="mainsrp-pager"]/div/div/div/ul/li[8]/a/span[1]')
next_page.click()
通过URL中的参数实现示例如下:
url = 'https://s.taobao.com/search?q=Python&imgfile=&commend=all&ssid=s5-e&search_type=item&sourceId=tb.index&spm=a21bo.1000386.201856-taobao-item.1&ie=utf8&initiative_id=tbindexz_20170306&bcoffset=0&ntoffset=6&p4ppushleft=1%2C48&s={}'.format(44 * (i - 1)) # i为页数
(2)手动下滑并点击加载更多 这种方式是手动向下滚动加载,加载了一i的那个数量后需要点击加载更多或者类似的按钮,点击之后在同一网页继续向下加载,到了一定数量需要再次点击以加载更多…,如简书就是这种浏览方式:
这类网页要实现爬取所有数据或者尽可能多的数据需要模拟点击按钮以实现动态加载,所以需要使用selenium,示例如下:
while True:
try:
driver.find_element_by_xpath('load-more').click()
except:
break
这类的实现原理一般是通过循环实现,且一般要循环多次。 (3)手动下滑自动加载更多 这种方式不需要点击按钮,只需要一直向下滚动,到了页面底部会自动继续加载,一直循环,直到内容全部加载完毕,例如本项目的目标网站,动态加载如下:
阴阳师卡牌下载文字识别slide_load
此时已不再有按钮,所以不能通过点击按钮实现加载,有两种解决的方式:
js = "var q=document.documentElement.scrollTop=100000"
while True:
try:
driver.find_element_by_class_name('load-more')
driver.execute_script(js)
time.sleep(1)
except:
break
while True:
try:
driver.find_element_by_class_name('load-more')
ActionChains(driver).key_down(Keys.END).perform()
time.sleep(1)
except:
break
这两种方式都是用循环实现的,需要有一个退出循环的条件,否则会成为死循环。在该案例中,如果未加载到底部时,会出现下滑展示更多的提示,如下:
当加载到底部时,此提示消失,如下:
所以可用该元素的存在作为循环继续的条件,即该元素消失时,循环也就终止。
在利用百度文字识别模块进行文字识别的时候,因为不同位置的文字代表不同的信息,所以需要使用 通用文字识别(含位置信息版) 来得到不同位置文字的位置信息,用于判断文字信息所属的类型,每天可免费调用500次,对于本项目来说,也足够了。
一张卡牌的示意如下,我们要获取的信息包括已经标出来的3部分:
很显然,我们只能通过根据位置定位不同的文字来实现,因为识别出的文字并不是完全有序,且可能出现识别识别的文字,我们可以使用排除法来精确定位:
在识别的结果中,所有的数据都是以像素为单位给出的,以图片左上角为(0,0),向右为宽,向下为高,在对图片中不同类别文字信息的位置进行估计时,需要考虑到各种不同的情况,因为每张图片的文字情况可能不太一样,如下图:
显然,4张图片的文字就不太一样,有的没有描述,有的有描述,并且有的只有一行,有的有两行,有的有三行,并且有的左右下角有数字,有的没有。显然对于全部图片情况会更加复杂,需要对每类文字的位置区间进行准确判断,给出最合理的判断边界,因为一旦估计不合理,就可能存在对于这张图片合适的位置区间对于另一张图片就不再适合,因此可能导致将文字放错类别,而不能准确得出该图片的信息的后果。 挑选出最具代表性的图片,在像素的角度对每类文字的位置区间进行分析估计并经过多次测试后,得出了程序中的参数。
如果使用单一的线程效率肯定会很低,因此在实现的过程中使用了生成器,并且建立了线程池,但是这个项目需要注意的是线程数量不能随意指定,因为 通用文字识别(含位置信息版) 的请求有限制,如下:
QPS为2,即同一时间只能请求2次,也就限制了线程数只能为2,我也实验过超过2个线程,但是会报错,会提示QPS超限额。
import csv
import requests
import time
import os
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from concurrent.futures import ThreadPoolExecutor
from aip import AipOcr
# 定义失败数量对文字识别失败进行计数
fail_num = 0
# 定义百度api参数
APP_ID = '17076767'
API_KEY = 'Af3Rj5HALMz5AN8prSgwTH4m'
SECRET_KEY = '49lYvSCocGikjWTbYFuFuGzK9Eph8xqv'
# 定义文字识别参数
options = {
'detect_direction': 'true',
'language_type': 'CHN_ENG',
}
# 初始化AipOcr对象
aipOcr = AipOcr(APP_ID, API_KEY, SECRET_KEY)
# 定义请求头
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.116 Safari/537.36'
}
导入所需的所有库,包括爬取链接和下载图片所需的库selenium和requests和百度文字识别的AipOcr;同时定义整个程序需要使用的所有全局变量,主要是与百度OCR初始化相关的常量和请求头。
def slide_down_key(driver):
'''模拟按键模拟滚动到页面底部'''
while True:
try:
driver.find_element_by_class_name('load-more')
ActionChains(driver).key_down(Keys.END).perform()
time.sleep(1)
except:
break
def slide_down_js(driver):
'''执行JS模拟滚动到页面底部'''
js = "var q=document.documentElement.scrollTop=100000"
while True:
try:
driver.find_element_by_class_name('load-more')
driver.execute_script(js)
time.sleep(1)
except:
break
def get_img_src():
'''在页面中获取所有卡牌链接'''
driver = webdriver.Chrome()
driver.maximize_window()
driver.get('https://ssr.163.com/cardmaker/#/')
time.sleep(3)
slide_down_key(driver)
# slide_down_js(driver)
# 通过xpath定位所有图片
imgs = driver.find_elements_by_xpath('/html/body/div[3]/div[4]/div/div[3]/ul/li/img')
print('共计有%d张卡牌' % len(imgs))
for img in imgs:
print('已获取到图片链接:', img.get_attribute('src'))
yield img.get_attribute('src')
driver.quit()
使用webdriver模拟Chrome加载页面,并通过两种方式实现向下滚动到底部,从而获取到所有图片链接,并且不是一次返回,而是通过yield关键字构造生成器,边取边用。
def download_pic(index, url, writer):
'''下载图片'''
content = requests.get(url).content
file_name = url.split('/')[-1].split('.')[0]
print('当前正在下载第%d张图片:%s' % (index, file_name))
with open('Cards/%s.png' % file_name, 'wb') as f:
f.write(content)
time.sleep(0.5)
return file_name, index, writer
def identify_text(result):
'''调用百度aip识别文字并保存'''
global fail_num
time.sleep(0.5)
file_name = result.result()[0]
index = result.result()[1]
writer = result.result()[2]
print('当前正在识别第%d张图片:%s' % (index, file_name))
try:
with open('Cards/%s.png' % file_name, 'rb') as fp:
# 读取图片内容并用aip识别获取返回内容
pic_content = fp.read()
result = aipOcr.general(pic_content, options)
# 从结果中得到文字、对应图片区域的顶部位置和高度
words_result = [i['words'] for i in result['words_result']]
top_locations = [int(i['location']['top']) for i in result['words_result']]
height_locations = [int(i['location']['height']) for i in result['words_result']]
result_length = len(words_result)
# 初始化角色、技能和卡牌描述
if len(words_result[0]) >= 2:
role = words_result[0]
else:
role = words_result[1]
if len(words_result[-1]) >= 2:
skill = words_result[-1]
else:
skill = words_result[-2]
card_desp = ''
for i in range(result_length):
# 文字区域大于45为数字,过滤
if height_locations[i] >= 45:
continue
# 顶部位置小于340为无文字区域,跳过
if top_locations[i] <= 340:
continue
# 根据顶部和高度获取角色
if (top_locations[i] + height_locations[i]) <= 395:
role = words_result[i]
continue
# 根据顶部位置获取技能
if top_locations[i] >= 515:
skill = words_result[i]
continue
# 获取卡牌描述字符串并拼接
card_desp += words_result[i]
print('当前角色是:', role)
writer.writerow([role, card_desp, skill])
# 异常处理
except Exception as e:
print(e.args[0])
fail_num += 1
调用requests库获取图片内容并保存,再通过线程池的回调实现实现文字识别并保存到csv文件中。使用百度文字识别时,使用位置信息版从而可以根据位置判断不同的文字信息类型,经过排除和判断得到需要的3种类型的文字信息。并且使用异常处理机制,在识别时遇到异常时能够及时处理。
def main():
'''主函数'''
# 定义线程池,实现多线程
pool = ThreadPoolExecutor(2)
# 创建文件夹
if not os.path.exists('Cards'):
os.mkdir('Cards')
if os.path.exists('Card_Data.csv'):
os.remove('Card_Data.csv')
# 创建csv文件用于保存数据
with open('Card_Data.csv', 'a+', newline='', encoding='gb18030') as f:
writer = csv.writer(f, dialect="excel")
writer.writerow(['角色', '描述', '技能'])
for index, img_url in enumerate(get_img_src()):
# 向线程池中添加下载任务并在任务完成后回调实现文字识别
pool.submit(download_pic, index + 1, img_url, writer).add_done_callback(identify_text)
pool.shutdown(wait=True)
print('未成功识别文字的图片为:', fail_num)
if __name__ == '__main__':
'''执行主函数'''
print('******程序开始运行******')
start = time.time()
main()
time = int(time.time() - start)
print('******程序执行结束,共计用时%d分%d秒。******' % (time // 60, time % 60))
主函数中先判断并创建相应的文件(夹),并创建线程数为2的线程池,并循环将任务加入线程池且增加回调函数。同时对程序执行计时。
运行程序进行测试,如下:
阴阳师卡牌下载文字识别final_test
显然,效率还是比较不错的。 得到的数据截取部分如下:
如有需要,可点击https://download.csdn.net/download/CUFEECR/12258941下载进行测试。
报错说明:
如果在测试中遇到只是打印出word result但是并未返回识别出的文字并保存到csv文件中的情况,一般是由于500次含位置信息版文字识别的免费次数用完,这时需要换一个账号登录?重新创建应用或者等一天再测试,因为毕竟是免费的,百度也不可能无限量供应。一个避免这种情况的办法是测试的时候用较少数量来测试,一次不要将所有卡牌都识别,只需要在get_img_src()
中将for img in imgs
改为for img in imgs[:20]
或者别的数字即可。同时要注意最后打印出的失败次数,如果为0则很顺利,否则可能在文字识别部分出了问题,需要改进。