爬虫基础-1
其次,关于_token
,新开浏览器(无痕)会发现这个值是会变化的
思路:每次请求前先访问登陆界面获得token,再和email、password一起请求。这是一个公共的过程,封装成 env.py。
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 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 52 53 54 55 """ 环境设置 """ import requestsimport reclass Env (object ): ip_each = 30 login_data = { "email" : "邮箱" , "password" : "密码" , "_token" : "" } proxy_api_url = "api_url xxx &getnum=" + str (ip_each) def __init__ (self ): self.headers = { "User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/89.0.4389.90 Safari/537.36 " } self.login_url = "http://www.glidedsky.com/login" self.session = requests.session() def login (self ): response = self.session.get(self.login_url, headers=self.headers) self.login_data["_token" ] = re.search('name="_token" value="(.*?)"' , response.text).group(1 ) self.session.post(self.login_url, data=self.login_data, headers=self.headers) return self.session """ 实时获取代理ip: 某宝找的1块测试, 1s最多获取200次api 这里获取60 感觉前面的速度快些 ['ip1:port1', 'ip2:port2'] 过期后用的华益云 ip数量计费 """ def get_proxy (self ): response = requests.get(self.proxy_api_url, self.headers) return response.text.split("\r\n" )if __name__ == '__main__' : env = Env() print (env.get_proxy())
第一题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 """ 爬虫基础1 """ from env import Envfrom lxml import etree env = Env() session = env.login() url = "http://www.glidedsky.com/level/web/crawler-basic-1" response = session.get(url, headers=env.headers) html = etree.HTML(response.text) div_list = html.xpath('//div[@class="col-md-1"]' ) total = 0 for div in div_list: total += int (div.xpath('normalize-space(./text())' ))print (f"结果是: {total} " )
爬虫基础-2 登陆同上,这次需要翻页,这就需要循环获取下一页的链接。
每一页获取数据方式同爬虫基础1
,只是xpath语法有差别。
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 26 27 28 29 30 31 32 33 34 35 36 37 """ 爬虫基础2 """ from env import Envfrom lxml import etreeimport re env = Env() session = env.login() total = 0 curr_page_url = "http://www.glidedsky.com/level/web/crawler-basic-2?page=1" while True : response = session.get(curr_page_url, headers=env.headers) html = etree.HTML(response.text) div_list = html.xpath('//div[@class="card-body"]//div[@class="col-md-1"]' ) for div in div_list: total += int (div.xpath('normalize-space(./text())' )) cur_page_num = re.search('page=(\\d+)' , curr_page_url).group(1 ) print (f"前{cur_page_num} 页数字和为: {total} " ) next_page_btn = html.xpath('//ul[@class="pagination"]/li[last()]' )[0 ] if next_page_btn.xpath('contains(@class, "disabled")' ): break else : curr_page_url = next_page_btn.xpath('./a/@href' )[0 ]print (f"结果是: {total} " )
IP屏蔽-1 先用奇怪的方法达到修改ip的目的,再进去查看下网页结构,把xpath先行记录一遍,后续有错再改。(代理获取网页内容也可)
结果是:页面结构和爬虫基础2相同。
现在最重要的就是如何获取1000多个可用的代理ip了,我们去某宝随便找一个便宜的高匿ip
。
在env.py的Env类中添加如下成员方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ip_each = 30 proxy_api_url = "api_url" + str (ip_each) """ 实时获取代理ip: 某宝找的1块测试, 1s最多获取200次api 这里获取60 感觉前面的速度快些 ['ip1:port1', 'ip2:port2'] 过期后用的华益云 ip数量计费 """ def get_proxy (self ): response = requests.get(self.proxy_api_url, self.headers) return response.text.split("\r\n" )
现在将上个爬虫基础2
的代码进行改造,添加每次更换一个代理进行请求。
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 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 52 53 54 55 56 57 """ ip反爬1 结构和爬虫基础相同 """ from env import Envfrom lxml import etreeimport reimport time env = Env() session = env.login() total = 0 curr_page = 1 proxy_index = 0 proxy_list = env.get_proxy()while True : try : response = session.get(f"http://www.glidedsky.com/level/web/crawler-ip-block-1?page={curr_page} " , headers=env.headers, timeout=0.3 , proxies={"http" : proxy_list[proxy_index]}) if 403 == response.status_code: raise Exception(f"该代理已经用过: {proxy_list[proxy_index]} " ) except Exception as e: proxy_index += 1 if proxy_index >= env.ip_each: proxy_list = env.get_proxy() proxy_index = 0 continue html = etree.HTML(response.text) div_list = html.xpath('//div[@class="card-body"]//div[@class="col-md-1"]' ) print ("-" *40 ) for div in div_list: total += int (div.xpath('normalize-space(./text())' )) print (f"前{curr_page} 页数字和为: {total} " ) if curr_page >= 1000 : break curr_page += 1 print (f"结果是: {total} " )
IP屏蔽-2 因为每次请求都是用的新代理ip,代码同 ip屏蔽1
题。
结果(多循环了一次,curr_page >1000 之前没加上 >=,对结果没影响):
字体反爬-1 看过题并分析过html代码的应该都清除,每次刷新源码中的数字都是变化的,而实际显示出的数字都没变化。 有心的同学应该比较过每次刷新网页后的base64串的值,它们都是不同的,说明base64串在这里承载了字体转化的功能。
所以我们需要对每次的base64串进行分析,在这里了解到了能将base64转化为ttf
文件,再利用代码转为xml
文件做分析。
我们在浏览器
内(方便后期对比确认结果),获取base64串,用代码将其转为 ttf 和 xml 文件。
数据采集过程
进入页面后F12,搜索base64
,进入界面复制值。
代码验证base64的不唯一性 并 转化ttf、xmlttf_test.py
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 """ 关于转码 https://blog.csdn.net/yishengzhiai005/article/details/80045042 python2中进行Base64编码和解码 python3不太一样:因为3.x中字符都为unicode编码,而b64encode函数的参数为byte类型,所以必须先转码。 测试base64转ttf、分析ttf、xml文件 """ from env import Envimport reimport base64from fontTools.ttLib import TTFont env = Env() env.login() session = env.session url = "http://www.glidedsky.com/level/web/crawler-font-puzzle-1" response = session.get(url, headers=env.headers) base64_str = "自己补全" data = base64.b64decode(base64_str.encode())with open ('font-puzzle-1.ttf' , 'wb' ) as f: f.write(data) font = TTFont('font-puzzle-1.ttf' ) font.saveXML('font-puzzle-1.xml' )
第一次获取base64串
将base64串赋值到代码中变量 base64_str
,运行生成 ttf 和 xml。 将ttf用fontstore链接 (或fontcreator)打开,
fontstore打开如下图:
fontcreator打开如下:
我们再打开 xml 进行分析,搜索code
相关内容,发现就只有以下2处有效数据(和数字相关的数据)。
结合xml中上面2图的映射关系
和fontstore中的
将数据整理成表格:
纸面分析完了,进行浏览器内容比较:
比较浏览器显示:说明字体文件(ttf/xml)中的cmap中的name值 唯一对应一个实际显示 。因为表格中,”zero” –> 8,”one” –> 5 ……,只是我在图中用了数字表示。
第二次获取base64串
重开一个无痕浏览器,进行同样访问,获得一个新base64串,用代码(ttf_test.py)重新生成ttf和xml文件,fontstore重新 打开ttf文件。
现在先就可以不用再分析xml文件内容了,经过第一次观察,直接看fontstore结果
即可,结果是一样的。
重要结论:name对应code一直都没有改变的。 所以,源码改变,但是实际显示内容不变,肯定是 code --> 实际显示
这一过程的映射发生了变化。正好GlyphOrder映射有所改变。
下面是新一轮统计的表格:
“7”,…..。
现在靠第二次纸面分析的结果,我们推测:
推测正确 。
思考
现在我们能从xml中获取name --> code
的映射(这是不变的),那么 从 name --> 实际显示
的映射怎么办呢?
从第二次测试中能得出:实际显示的改变就和GlyphOrder
的变化有关(即ID和name的映射的改变)。
再加上实际显示的结果,我们比较2次测试,表格结果的区别:
从xml文件中和"code"
有关的部分就只有GlyphOrder
和cmap
,cma的name、code我们已经用了,也用了GlyphOrder中的name,还有GlyphOrder中的ID规律我们没考虑到。
我们观察上面2图,很轻松就能发现ID-1
实现
所以代码逻辑也有了:请求获取base64,转为tff再转为xml,lxml解析,(能构建cmap中 name --> code
,但我们只关心name-->ID
),再构建GlyphOrder中 name --> ID
的映射。得到了name,name其实就是浏览器源码中提起出来的数字,转化为ID,再减一,就成了真实数据了 。但是name中 'zero'
需要变为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 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 from env import Envimport reimport base64from fontTools.ttLib import TTFontfrom lxml import etreeimport io english_map = { '0' : 'zero' , '1' : 'one' , '2' : 'two' , '3' : 'three' , '4' : 'four' , '5' : 'five' , '6' : 'six' , '7' : 'seven' , '8' : 'eight' , '9' : 'nine' }""" 将源码中的字符串转化为真实的数字 src_str: 如'231' --> 567 '23' --> 56 '1' --> 7 digit_map: 源码数字 --> 真实数字 的映射 {"zero": see_num1, "one": see_num2, ...} 返回: 真实数字 """ def parse_srcstr_realnum (src_str, digit_map ): return int ('' .join(str (digit_map[english_map[ch]]) for ch in src_str))""" 获取从源码中的数字到见到的数字的映射 返回: 当前页响应, 当前页的数字映射 """ def get_response_and_digitmap (page ): url = f"http://www.glidedsky.com/level/web/crawler-font-puzzle-1?page={page} " response = session.get(url, headers=env.headers) base64_str = re.search('base64,(.*?)[)]' , response.text).group(1 ) data = base64.b64decode(base64_str.encode()) fio = io.BytesIO(data) font = TTFont(fio) glyph_order = font.getGlyphOrder() digit_map = {} for glyph_name in glyph_order: digit_map[glyph_name] = str (font.getGlyphID(glyph_name) - 1 ) print ('源码数字 -> 可见数字: ' , digit_map) return digit_map, response""" 获取当前页真实的数字和, 一次请求一页, 一页内的数字 映射不会变的 page: 第几页数据 """ def get_page_sum (page ): digit_map, response = get_response_and_digitmap(page) html = etree.HTML(response.text) div_list = html.xpath('//div[@class="card-body"]//div[@class="col-md-1"]' ) page_total = 0 for div in div_list: s = str (div.xpath('normalize-space(./text())' )) page_total += parse_srcstr_realnum(s, digit_map) return page_total env = Env() env.login() session = env.session total = 0 for page_num in range (1 , 1001 ): total += get_page_sum(page_num) print (f'前 {page_num} 页和为 {total} ' )
字体反爬-2 原理同字体反爬1
,但是现在从浏览器获取的内容不是数字了,而是汉字,并且xml中camp的name值也并没有像 “zero”、”one”的提示了,所以之前存在的 english_map 现在需要由我们自己构建了,原来english_map是 “1” -> “zero” 的映射(这我们是熟知的),但现在我们需要 “源码中汉字” -> “name” 的映射。
测试get请求获取的内容,编码前和编码后的内容,并生成ttf和xml
用浏览器能得到base64和显示情况,分析 “源码汉字” –> “实际显示” 的过程。
测试代码如下(test_ttf.py):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import base64from fontTools.ttLib import TTFont base64_str = "浏览器源码里复制" data = base64.b64decode(base64_str.encode())with open ('font-puzzle-2.ttf' , 'wb' ) as f: f.write(data) font = TTFont('font-puzzle-2.ttf' ) font.saveXML('font-puzzle-2.xml' )
用fontstore打开ttf文件,只分析 0 - 9。
浏览器中实际显示为“零”的情况下:
对应源码汉字为:“钡”,经过站长工具计算,“钡”的unicode编码为:”\u94a1”
我们再在xml文件中搜索“钡”的unicode编码,结合xml中cmap的内容,我们直接搜索”uni94a1”,总共2个结果。
在浏览器中实际显示为“①”的情况下,
而“成”字对应的unicode编码为“\u6210”,参照cmap中name的值,我们在xml中搜索“uni6210”。
根据以上2次测试,并根据字体反爬1
的结果,很明显能看出
1 2 name = "uni" + "源码汉字的unicode编码 去除\u" 根据这个name对应的ID,再减1,结果就是实际的数字。
代码逻辑
先根据GlyphOrder构建 {“name1”: “id”, “name2”, “id2”…} 的字典,之后再获取到”源码汉字”,将汉字进行切割后,取每一位的unicode码(就是name),以unicode码为键,从字典中取id,id再减1就是用户看到的数字。
代码
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 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 from env import Envimport reimport base64from fontTools.ttLib import TTFontfrom lxml import etreeimport io""" 将源码中的汉字转化为真实的数字 src_str: 如'随成成' --> 211 uni_map: 源码汉字 --> 真实数字 的映射 {"成": 1, "随": 2, ...} 返回: 真实数字 """ def parse_srcstr_realnum (src_str, uni_map ): arr = [] for ch in src_str: uni = ch.encode('unicode-escape' ).decode() code = uni.replace('\\u' , '' ) arr.append(uni_map[code]) return int ('' .join(arr))""" 获取从源码中的汉字到见到的数字的映射 返回: 当前页响应, name(unicode去除'uni')->真实数字 的映射 如, (response, {'7bd9': '0', '9716': '1', ...}) """ def get_response_and_unimap (page ): url = f"http://www.glidedsky.com/level/web/crawler-font-puzzle-2?page={page} " response = session.get(url, headers=env.headers) base64_str = re.search('base64,(.*?)[)]' , response.text).group(1 ) data = base64.b64decode(base64_str.encode()) fio = io.BytesIO(data) font = TTFont(fio) glyph_order = font.getGlyphOrder() name_id_map = {} for glyph_name in glyph_order: name_id_map[str (glyph_name).replace('uni' , '' ).lower()] = str (font.getGlyphID(glyph_name) - 1 ) print ('源码汉字unicode码 -> 可见数字: ' , name_id_map) return name_id_map, response""" 获取当前页真实的数字和, 一次请求一页, 一页内的数字 映射不会变的 page: 第几页数据 """ def get_page_sum (page ): uni_map, response = get_response_and_unimap(page) html = etree.HTML(response.text) div_list = html.xpath('//div[@class="card-body"]//div[@class="col-md-1"]' ) page_total = 0 for div in div_list: s = str (div.xpath('normalize-space(./text())' )) page_total += parse_srcstr_realnum(s, uni_map) return page_total env = Env() env.login() session = env.session total = 0 for page_num in range (1 , 1001 ): total += get_page_sum(page_num) print (f'前 {page_num} 页和为 {total} ' )
上面的思路及代码其实有误,会在从 uni_map 中取值的时候会报错(KeyError)。
修改代码为如下,进行调试:
①:出错位置添加try…except
1 2 3 4 5 6 7 8 9 10 11 12 def parse_srcstr_realnum (src_str, uni_map ): arr = [] for ch in src_str: uni = ch.encode('unicode-escape' ).decode() code = uni.replace('\\u' , '' ) try : arr.append(uni_map[code]) except Exception as e: print (e) return int ('' .join(arr))
②:获取response之后,我们添加将base64写入ttf和xml的过程。(目的是保证出错时我们能查看xml,分析为什么没有对应的unicode编码的name值)
1 2 3 4 5 6 7 8 9 10 11 12 13 data = base64.b64decode(base64_str.encode()) with open ('./font-puzzle-test2/font-puzzle-2.ttf' , 'wb' ) as f: f.write(data) font = TTFont('./font-puzzle-test2/font-puzzle-2.ttf' ) font.saveXML('./font-puzzle-test2/font-puzzle-2.xml' ) fio = io.BytesIO(data) font = TTFont(fio) glyph_order = font.getGlyphOrder()
开始调试:
这时数据已经刷新到ttf、xml文件中了,我们进xml文件进行查看:
最后需要特别注意的是:
修改后的代码
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 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 from env import Envimport reimport base64from fontTools.ttLib import TTFontfrom lxml import etreeimport io""" 将源码中的汉字转化为真实的数字 src_str: 如'随成成' --> 211 uni_map: 源码汉字 --> 真实数字 的映射 {"成": 1, "随": 2, ...} 返回: 真实数字 """ def parse_srcstr_realnum (src_str, uni_map ): arr = [] for ch in src_str: uni = ch.encode('unicode-escape' ).decode() code = uni.replace('\\u' , '' ) arr.append(uni_map[code]) return int ('' .join(arr))""" 获取从源码中的汉字到见到的数字的映射 返回: 当前页响应, name(unicode去除'uni')->真实数字 的映射 如, (response, {'7bd9': '0', '9716': '1', ...}) """ def get_response_and_unimap (page ): url = f"http://www.glidedsky.com/level/web/crawler-font-puzzle-2?page={page} " response = session.get(url, headers=env.headers) base64_str = re.search('base64,(.*?)[)]' , response.text).group(1 ) data = base64.b64decode(base64_str.encode()) fio = io.BytesIO(data) font = TTFont(fio) glyph_order = font.getGlyphOrder() cmap = font.getBestCmap() name_id_map = {} for glyph_name in glyph_order: name_id_map[glyph_name] = str (font.getGlyphID(glyph_name) - 1 ) code_id_map = {} for code in cmap: code_id_map[hex (code).replace('0x' , '' )] = name_id_map[cmap[code]] print ('源码汉字unicode码 -> 可见数字: ' , code_id_map) return code_id_map, response""" 获取当前页真实的数字和, 一次请求一页, 一页内的数字 映射不会变的 page: 第几页数据 """ def get_page_sum (page ): uni_map, response = get_response_and_unimap(page) html = etree.HTML(response.text) div_list = html.xpath('//div[@class="card-body"]//div[@class="col-md-1"]' ) page_total = 0 for div in div_list: s = str (div.xpath('normalize-space(./text())' )) page_total += parse_srcstr_realnum(s, uni_map) return page_total env = Env() env.login() session = env.session total = 0 for page_num in range (1 , 1001 ): total += get_page_sum(page_num) print (f'前 {page_num} 页和为 {total} ' )
CSS反爬-1 每次刷新class都会重置,挑其中一次分析,每次分析的时候的css都要匹配本次显示的页面。
以下分析有错!①可以选择不看,直接看错误分析 + 重新讨论。②选择看错误的分类讨论,看下如何犯错的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 分类讨论(取了少数几个例子): ## div.col-md-1 内只有一个div(::before) 直接获取content即为具体内容 .Lg20BXy { float:left } .Lg20BXy:before { content:"256" } .Lg20BXy { letter-spacing:0.4em } ## div.col-md-1 内有4个div 有1个div的class为透明 如源码中为 9 6 7 2 实际显示为 276 9: .mk0GYspY { float:left } .mk0GYspY { width:1em } .mk0GYspY { margin-right:-1em } .mk0GYspY { opacity:0 } 6: .kXr1Xjjpn { position:relative } .kXr1Xjjpn { float:left } .kXr1Xjjpn { width:1em } .kXr1Xjjpn { left:2em } 7: .Miuk2yGt { float:left } .Miuk2yGt { width:1em } 2: .sX3iaG { position:relative } .sX3iaG { float:left } .sX3iaG { width:1em } .sX3iaG { left:-2em } 规律:顺序遍历, left:2em > 0 就在''末尾插入, left:-2em < 0 就在 ''开头插入, 不存在'left:'和'opacity:0', 就直接保持''末尾插入 ## div.col-md-1 内有3个div(正常情况) 如源码中为 2 9 4 实际显示为294, 这种直接提取即可 2: .pRwyq22qBP { float:left } .pRwyq22qBP { width:1em } 9: .fyM23XHiCE { float:left } .fyM23XHiCE { width:1em } 4: .BTYOy24xKA { float:left } .BTYOy24xKA { width:1em } 规律: 按照第二种情况即可(相当于第二种情况中: 所有数字都是 7 ) ## div.col-md-1 内有2个div 有一个div**包含数字** 但会设置完全透明,另一个div使用::before添加content, 这个div的情况就和第1种情况相同了
综上,①和④可以合并为,div.col-md-1内部只要有div.xpath(‘./div/text()’)为空值,那么这个div内部就必定是使用了伪元素选择器,我们就能直接获取content。
②和③也可以合并:②不管透明的那个元素后,就变为了③这种情况了。
至于生成的style直接从源码的<style>中获取即可,用正则验证具体的值存不存在。
之前考虑有错😭,例子太片面,下意识认为是“子绝父相”,以倍数获取自生位置,实际错误很大!!!!!!。
看以下例子:
现在才认识到相对定位的问题。
现在开始重新分析:
首先得认识到,div包含四个子div和包含三个子div是相同的,区别仅是它存在margin-right
和opacity
的区别(个人觉得等同于:display: none;),只不过得注意 这个透明元素的位置,本题中一直都是处于div的第一个,所以不会影响到之后三个div个顺序排列 。
还发现有的div,有position: relative,有的div没有。
这是都有的:
这是存在没有的:
现在可以看出:div.col-md-1的孩子div的顺序就是原始顺序,这个顺序加上position: relative; left: xem 之后,就变为了新顺序,当然,这个过程中,不存在position: relative 样式的就保留原位置。
孩子为4个div的同理,第一个div因为是隐形的,当作没看见即可,同样也当3个div处理,但是 ,在隐形元素之后位置的元素得位置得提前一个。(如果它之前有2个隐形元素,那么它的index也得提前2,这才是最初排除隐形元素之后,该元素应该在的位置)。
伪元素选择器情况:
补充 : 过程中报错,之后还发现了值div.col-md-1的孩子存在2个的情况,都是有效数字,请注意。
修正代码
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 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 """ css反爬-1 """ from env import Envimport refrom lxml import etree env = Env() env.login() session = env.session""" 判断一个元素是否为透明元素 clazz: 该div的class style: 页面总style层叠样式表 """ def is_opacity (clazz, style ): opacity = re.search(clazz + ' \\{ opacity:0 \\}' , style) return opacity is not None """ 解析 div.col-md-1 这个div, 返回真实数据(的字符串形式) """ def parse_div (div, style, test_text ): children = div.xpath('./div' ) for index, child in enumerate (children): if len (child.xpath('./text()' )) == 0 : return re.search(child.attrib['class' ] + ':before \\{ content:"(\\d+)" \\}' , style).group(1 ) opacity_count = 0 res = [-1 , -1 , -1 , -1 ] for index, child in enumerate (children): clazz = child.attrib['class' ] if is_opacity(clazz, style): opacity_count += 1 continue relative = re.search(clazz + ' \\{ position:relative \\}' , style) val = child.xpath('./text()' )[0 ] if relative is None : res[index - opacity_count] = val else : offset = re.search(clazz + ' \\{ left:(-?\\d)em \\}' , style).group(1 ) res[index + int (offset) - opacity_count] = val r = '' .join([i for i in res if i != -1 ]) return r""" 获取每一页数字和 """ def get_page_sum (page_num ): url = f"http://www.glidedsky.com/level/web/crawler-css-puzzle-1?page={page_num} " response = session.get(url, headers=env.headers) html = etree.HTML(response.text) div_list = html.xpath('//div[@class="card-body"]//div[@class="col-md-1"]' ) style = html.xpath('normalize-space(//style/text())' ) page_total = 0 for div in div_list: page_total += int (parse_div(div, style, response.text)) return page_total total = 0 for page_num in range (1 , 1001 ): total += get_page_sum(page_num) print (f'前 {page_num} 页和为 {total} ' )
雪碧图反爬-1 页面结构全是 div.col-md-1
中包含2/3个div,class都是随机串 + ‘ sprite’, 而’sprite’这个样式的background-image: url()
包含base64串。
多刷新几次网页可以发现每次雪碧图中每个数字大小有差异,那么它background-position-x 的值就有一定差异,可以观察到同一个数字偏移的’x’值相同,并且因为原图片中数字都是从左到右从小到大排列的,我们可以获取当前页所有携带’sprite’样式的div,每一个div都有一个随机的class,如 ‘qaz’ 它的position-x为-12,’wsx’它的position-x为-23,’edc’的postion-x为0。
1 2 3 4 5 6 7 8 9 10 11 { "qaz" : -12 , "wsx" : -23 , "edc" : 0 } ↓[ ("qaz" , -12 ), ("wsx" , -23 ), ("edc" , 0 ) ] d_order = sorted(字典.items(), key=lambda x: x[ 1 ] , reverse=True) 或者 列表.sort(key=lambda x: x[ 1 ] , reverse=True) # 这个列表是上面 ↓ 的那个
将值进行顺序排列,得到
1 2 3 [ "edc" , "qaz" , "wsx" ] 或者[ ("edc" , 0 ), ("qaz" , -12 ), ("wsx" , -23 ) ]
那么接下来再顺序解析每个div时,就能根据它的class 拿到对应的数字 了。
顺序排列取索引为数字的思维出了点问题,因为之前只注意到了相同数字的 postion-x 是相同的,但是没注意到尽管 position-x 想通了,但是它们的 class 依旧不是同一个!!!
重新考虑如下结构(qaz 和rfv 肯定都代表一个数字,只是class不同):
1 2 3 4 5 6 { "qaz" : -12 , "wsx" : -23 , "edc" : 0 , "rfv" , -12 }
我们直接构建上述字注意 典即可。
这种搜索position-x逆序排列,有个巨大漏:
错误代码
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 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 """ 爬虫-雪碧图-1 """ from env import Envfrom lxml import etreeimport re env = Env() session = env.login()""" 源码中通过解析style, 返回 返回值1: { "qaz": "-12", "wsx": "0", "rfv": "-12", ...} 返回值2: [ "0", "-12" ] """ def get_clazzs_num_dict (style ): class_num_dict = re.findall('.([0-9a-zA-Z]+) { background-position-x:(-?\\d+)px }' , style) class_num_dict = dict (class_num_dict) offset_list = list (set (class_num_dict.values())) offset_list.sort(key=lambda item: int (item), reverse=True ) print (class_num_dict) print (offset_list) return class_num_dict, offset_list""" 获取每一页数字和 """ def get_page_sum (page_num ): url = f"http://www.glidedsky.com/level/web/crawler-sprite-image-1?page={page_num} " response = session.get(url, headers=env.headers) html = etree.HTML(response.text) div_list = html.xpath('//div[@class="card-body"]//div[@class="col-md-1"]' ) print ('-' *100 ) print (response.text) print ('-' *100 ) style = html.xpath('normalize-space(//style/text())' ) clazz_num_dict, offset_list = get_clazzs_num_dict(style) page_total = 0 for div_col in div_list: length = len (div_col) one = 0 for i in range (0 , length): clazz = div_col[i].attrib['class' ] clazz = clazz[:clazz.index(' ' )] offset = clazz_num_dict[clazz] one += offset_list.index(offset) * pow (10 , length-i-1 ) print (one) page_total += one return page_total total = 0 for page_num in range (1 , 2 ): total += get_page_sum(page_num) print (f'前 {page_num} 页和为 {total} ' )
所以说明:之前的思路全部有问题,参考网上解法后,发现还是得均匀切割图片划分横向10个坐标范围,分别表示[0 ,9],
这里能取巧就是将 |position-x| * 10 / 整个图片横向宽度
取整即为数字,注意取最接近的整数(如3.2取3, 3.8取4)
这个算式依旧存在问题,因为不同的单位数字还存在一个不同的宽度!!!!
以上这个方法是默认图片中一串数字划分比较整齐采用的估算形式,实际并不准确。
下面参考大佬(github搜索<!–swig41–>)的像素法,竖线扫描判断黑白,以此划分出每个数字坐标范围。
个人分析:
将断点形成列表(长度为11,包括开始、结尾),那么每顺序2个列表元素就能形成10个区间,那么真实的数字就和这个列表的索引产生联系了。
重点在于如何扫描分割:遍历横坐标,循环内遍历纵坐标,遍历纵坐标过程出现黑点即表示当前的横坐标就是数字的开始
(除0以外,因为 0 总是从 position-x: 0px 开始的)。
如此能得到10个position-x的值,列表追加一个图片宽度即为结果,就合成为总的包含10个区间的判定列表。
之前的划分方式有错,应该在0结束的时候就开始分隔,因为此时0已经不会再出现了。
结合页面分析:0偏移为 -7 时,
0偏移为 -8 时,就已经消失,0都已经消失了,也就不可能算在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 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 """ 爬虫-雪碧图-1 """ from env import Envfrom lxml import etreeimport refrom PIL import Imageimport base64import io env = Env() session = env.login()""" 生成分隔列表 如: [-1, 8, 19, 31, 43, 54, 66, 79, 93, 104, 117] """ def get_split_list (style ): split = [] base64_bytes = base64.b64decode(re.search('base64,(.*?)\"' , style).group(1 )) fio = io.BytesIO(base64_bytes) img = Image.open (fio) pixels = img.load() last_x_all_white = True for x in range (img.width): current_x_all_white = True for y in range (img.height): if sum (pixels[x, y][:3 ]) != 255 * 3 : current_x_all_white = False break if current_x_all_white: if not last_x_all_white: split.append(x) last_x_all_white = current_x_all_white split.insert(0 , -1 ) split[len (split) - 1 ] = img.width return split""" 源码中通过解析style, 构建 class -> offset 返回值结构: { "qaz": '-12', "wsx": '0', "rfv": '-12', ...} """ def get_clazzs_num_dict (style ): class_num_dict = dict (re.findall('.([0-9a-zA-Z]+) { background-position-x:(-?\\d+)px }' , style)) for _ in class_num_dict: class_num_dict[_] = abs (int (class_num_dict[_])) return class_num_dict""" 获取每一页数字和 """ def get_page_sum (page_num ): url = f"http://www.glidedsky.com/level/web/crawler-sprite-image-1?page={page_num} " response = session.get(url, headers=env.headers) html = etree.HTML(response.text) div_list = html.xpath('//div[@class="card-body"]//div[@class="col-md-1"]' ) style = html.xpath('normalize-space(//style/text())' ) class_num_dict = get_clazzs_num_dict(style) split_list = get_split_list(style) page_total = 0 for div_col in div_list: length = len (div_col) num = 0 for _ in range (0 , length): clazz = div_col[_].attrib['class' ] clazz = clazz[:clazz.index(' ' )] offset = class_num_dict[clazz] for index, split in enumerate (split_list): if offset < split: n = index - 1 break num += n * pow (10 , length - _ - 1 ) page_total += num return page_total total = 0 for page_num in range (1 , 1001 ): total += get_page_sum(page_num) print (f'前 {page_num} 页和为: {total} ' )
爬虫-验证码-1 分析:
滑动按钮应该右移的距离得到了,直接操作滑动按钮右移,即可完成该滑动验证。
需要注意,验证框是iframe
标签,需要 driver.switch_to.frame(frame_element) 操作。
1 2 3 4 5 driver.switch_to.frame(iframe_element) """操作frame外边的元素需要切换出去""" windows = driver.window_handles driver.switch_to.window(windows[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 26 27 28 29 30 31 32 33 34 35 36 37 第 0 列像素点差异为 39 第 1 列像素点差异为 40 第 2 列像素点差异为 39 第 3 列像素点差异为 40 第 4 列像素点差异为 39 第 5 列像素点差异为 39 第 6 列像素点差异为 36 第 7 列像素点差异为 26 第 8 列像素点差异为 25 第 9 列像素点差异为 37 . . . 第 204 列像素点差异为 23 第 205 列像素点差异为 22 第 206 列像素点差异为 23 第 207 列像素点差异为 51 第 208 列像素点差异为 49 第 209 列像素点差异为 39 第 210 列像素点差异为 41 第 211 列像素点差异为 40 第 212 列像素点差异为 40 第 213 列像素点差异为 38 第 214 列像素点差异为 39 . . . 第 399 列像素点差异为 81 第 400 列像素点差异为 61 第 401 列像素点差异为 16 第 402 列像素点差异为 17 第 403 列像素点差异为 16 第 404 列像素点差异为 17 第 405 列像素点差异为 16 第 406 列像素点差异为 17
说明整个图片都是做了混淆处理的!!!
判断缺口的策略:
最开始是打算到了某一列,像素差异个数剧增,并且在接下来10列中保持像素差异点超过100,那么判断这个剧增列就是凹陷图的开始。
但是后来发现,不大行!!图片的混淆做的太好了……
后来采用在rgb三个数离(255,255,255)相差都仅小于10,并且一列中有40(因为有时凹陷图左中部有缺口,没缺口的话达到50个应该没问题)个这样的像素点以上,判断这列为凹陷图的开始。
后来策略又换成了
后来采用在rgb三个数离(255,255,255)相差都大于50时,认定这就是凹陷块开始得位置!!!
确定selenium获取的webelement对象的坐标属性时,发现怎么都对不上号,所以才有接下来的测试selenium的location(x和y值):
调试:
form坐标为(298,212),但是在没调整下面这个设置之前总是对不上号!!!
注意眼观浏览器多宽的时候,一定要把win10显示设置为100%
调整之后,能对上号了。
接下来确保代码中测量的距离都是对的,在以上显示设置设置为100%!!!之后进行测量。
滑块位置:x偏移是36
下面是iframe的范围:(图中白色框是我用snipaste截出的 36*89
的矩形)
下面是图片的范围:
由此,我们得出,在这里得出的location,是相对于iframe本身的!!!
还记得之前分析的这个图?
但由于图片还有个透明四周,这个推理要发生变化。
滑块相对图片的偏移应该是:x + x2 - x3
x:图片相对iframe的x
x2:滑块图片的透明四周宽度(大概有11.5)
这里x2它原图是136的,现在浏览器中是68的,原图四周宽度为23,按比例计算,浏览器中四周宽度为:68*23-136=11.5
x3:大的验证图片相对iframe有个padding(大概有9.6)
所以,我们拿到滑块的location[‘x’]后直接减2即可。
绿线的距离(就是缺口相对于大图片的x位置):因为获取的链接的图片,分辨率是实际显示的2倍,所以得到绿线之间的距离还得除以2。
selenium存在点问题,个人修正,不然滑动块移动得很慢!!
1 C:\Users\L\AppData\Local\Programs\Python\Python37\Lib\site-packages\selenium\webdriver\common\actions
成功的标志:存在 show-success
的class的div出现。
代码
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 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 """ 爬虫-验证码-1 """ from selenium import webdriverfrom selenium.webdriver import ActionChainsfrom selenium.webdriver.support.wait import WebDriverWaitfrom selenium.webdriver.support import expected_conditions as ECfrom selenium.webdriver.common.by import Byfrom PIL import Imageimport requestsimport ioimport timeimport randomclass SeleniumEnv (object ): def __init__ (self ): self.driver_path = 'D:\\driver\\89.0\\chromedriver.exe' self.url = 'http://www.glidedsky.com/level/web/crawler-captcha-1?page=' self.driver = webdriver.Chrome(self.driver_path) self.wait = WebDriverWait(self.driver, 2 , 0.5 ) def login (self ): self.driver.get("http://www.glidedsky.com/login" ) self.driver.maximize_window() self.driver.find_element_by_id('email' ).send_keys('ls1229344939@163.com' ) self.driver.find_element_by_id('password' ).send_keys('lovenowhyly0' ) self.driver.find_element_by_class_name('btn-primary' ).click() time.sleep(1 ) def get_slide_button (self ): time.sleep(1 ) btn = self.driver.find_element_by_id('slideBlock' ) return btn def get_origin_cover_image (self ): time.sleep(1 ) slide_bg = self.driver.find_element_by_id('slideBg' ) cover_url = slide_bg.get_attribute('src' ) origin_url = cover_url.replace('img_index=1' , 'img_index=0' ) return origin_url, cover_url def point_diff (self, origin_pixel, cover_pixel, threshold ): return abs (origin_pixel[0 ] - cover_pixel[0 ]) > threshold \ and abs (origin_pixel[1 ] - cover_pixel[1 ]) > threshold \ and abs (origin_pixel[2 ] - cover_pixel[2 ]) > threshold def get_offset_cover (self ): origin_url, cover_url = self.get_origin_cover_image() origin_img = Image.open (io.BytesIO(requests.get(origin_url).content)) origin_pixels = origin_img.load() cover_img = Image.open (io.BytesIO(requests.get(cover_url).content)) cover_pixels = cover_img.load() width = cover_img.width height = cover_img.height threshold = 50 for x in range (350 , width): for y in range (0 , height): if self.point_diff(origin_pixels[x, y], cover_pixels[x, y], threshold): return x / 2 def get_offset_slider (self ): time.sleep(1 ) slide_block = self.driver.find_element_by_id('slideBlock' ) slider_x = int (slide_block.location['x' ]) - 2 return slider_x """ 验证码验证过程 """ def checkout (self, page_num ): try : time.sleep(1 ) self.driver.get(self.url + str (page_num)) time.sleep(1 ) iframe = self.driver.find_element_by_id('tcaptcha_iframe' ) self.driver.switch_to.frame(iframe) slide_btn = self.get_slide_button() slider_x = self.get_offset_slider() cover_x = self.get_offset_cover() move_x = cover_x - slider_x self.move_to_gap(slide_btn, self.get_track(move_x)) success = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'show-success' ))) return success is not None except Exception as e: print (e) return False def get_total (self ): windows = self.driver.window_handles self.driver.switch_to.window(windows[0 ]) div_list = self.driver.find_elements_by_xpath('//div[@class="card-body"]//div[@class="col-md-1"]' ) page_total = 0 for div in div_list: page_total += int (div.text) return page_total """ 根据偏移量获取移动轨迹 :param distance: 偏移量 :return: 移动轨迹 """ def get_track (self, distance ): track = [] current = 0 mid = distance * 4 / 5 t = 0.3 v = 0 while current < distance: if current < mid: a = 5 else : a = -3 v = v + a * t move = v * t + 1 / 2 * a * t * t current += move if current > distance: track.append(distance - current + move) break track.append(round (move)) v = v + a * t return track """ 拖动滑块到缺口处 :param slider: 滑块 :param track: 轨迹 :return: """ def move_to_gap (self, slider, track ): ActionChains(self.driver).click_and_hold(slider).perform() for x in track: ActionChains(self.driver).move_by_offset(xoffset=x, yoffset=0 ).perform() time.sleep(random.random()) ActionChains(self.driver).release().perform() env = SeleniumEnv() env.login() total = 0 page = 967 while True : sign = env.checkout(page) if not sign: fail_count = 1 print (f'第 {page} 页失败(1)!!!' ) while True : sign = env.checkout(page) if sign: print (f'第 {page} 页成功(n)!!!' ) break else : print (f'第 {page} 页失败(n) 又失败{fail_count} 次!!!' ) fail_count += 1 if fail_count > 3 : env.driver.quit() env = SeleniumEnv() env.login() fail_count = 0 else : print (f'第 {page} 页成功(1)!!!' ) time.sleep(1 ) total += env.get_total() print (f'前{page} 页和为 {total} ' ) page += 1 if page == 1001 : env.driver.quit() break """ 中间总有网络原因...有时连页面都打不开 实在加载不出来, 分了多次测, 大概测了10多次吧... 我手动操作都还能一直出现 "网络恍惚了一下"/"拼图块半路丢失" ??? [1, 272] => 948786 怎么服务器还能 internal server error(nginx) 啊... [273, 378) => 368900 T^T [378, 1000] => 2177686 3495372 怀疑是腾讯防水墙有一定的访问限制, 建议每过100页等个10分钟再爬 """
我靠,页面刷新不出来我能咋办啊?
chrome:
刷新出来了等自己手动操作也出现这种:
爬虫-JS加密1
分析
源码中的响应皆为空串:
结合页面响应分析:
数据应该就是异步加载的了,接下来就是模拟异步GET请求了。
url:
1 http://www.glidedsky.com/api/level/web/crawler-javascript-obfuscation-1/items?page=1&t=1617340189&sign=e7c6b4b333bd07e4753288ca683062e925703341
参数分析:
page是页数
t明显是秒级时间戳
sign就是加密身份验证串了,在源码及响应信息中search不到,并且每次都会变化,那么推测是本地js生成
。
调试sha1.js
文件:
下一步过后,会发现sign的值就是这个return语句返回的结果。
可以比较watch窗口中的值,这里只列举了前几个值,可以看到都是和sign相等的,可以说明这个函数就是生成sign的函数,返回值正好有40项!
1 2 3 4 5 6 7 8 9 t.prototype .hex = function ( ) { this .finalize (); var t = this .h0 , h = this .h1 , s = this .h2 , i = this .h3 , e = this .h4 ; return r[t >> 28 & 15 ] + r[t >> 24 & 15 ] + r[t >> 20 & 15 ] + r[t >> 16 & 15 ] + r[t >> 12 & 15 ] + r[t >> 8 & 15 ] + r[t >> 4 & 15 ] + r[15 & t] + r[h >> 28 & 15 ] + r[h >> 24 & 15 ] + r[h >> 20 & 15 ] + r[h >> 16 & 15 ] + r[h >> 12 & 15 ] + r[h >> 8 & 15 ] + r[h >> 4 & 15 ] + r[15 & h] + r[s >> 28 & 15 ] + r[s >> 24 & 15 ] + r[s >> 20 & 15 ] + r[s >> 16 & 15 ] + r[s >> 12 & 15 ] + r[s >> 8 & 15 ] + r[s >> 4 & 15 ] + r[15 & s] + r[i >> 28 & 15 ] + r[i >> 24 & 15 ] + r[i >> 20 & 15 ] + r[i >> 16 & 15 ] + r[i >> 12 & 15 ] + r[i >> 8 & 15 ] + r[i >> 4 & 15 ] + r[15 & i] + r[e >> 28 & 15 ] + r[e >> 24 & 15 ] + r[e >> 20 & 15 ] + r[e >> 16 & 15 ] + r[e >> 12 & 15 ] + r[e >> 8 & 15 ] + r[e >> 4 & 15 ] + r[15 & e] }
刷新,重新调试,看下一步执行什么:
接下来就是如何的到参数t
和参数sign
了。
1 2 3 4 5 6 7 8 9 10 11 let p = $('main .container' ).attr ('p' );let t = Math .floor (($('main .container' ).attr ('t' ) - 99 ) / 99 );let sign = sha1 ('Xr0Z-javascript-obfuscation-1' + t); $.get ('/api/level/web/crawler-javascript-obfuscation-1/items?page=' + p + '&t=' + t + '&sign=' + sign, function (data ) { const list = JSON .parse (data).items ; $('.col-md-1' ).each (function (index ) { if (list && index < list.length ) { $('.col-md-1' ).eq (index).text (list[index]) } }) })
t就是当前的秒级时间戳
sign得调用sha1的函数:参数是Xr0Z-javascript-obfuscation-1 ,调用方法是:将sha1.js下载下来,采用js2py,用python动态执行js代码。
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 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 52 53 """ js加密-1 """ from env import Envimport js2pyimport mathimport timeimport json env = Env() session = env.login() context = js2py.EvalJs()with open ('js-obfuscation-1-test/js/sha1.js' , 'r' , encoding='utf-8' ) as f: context.execute(f.read())""" 获得参数t和sign """ def get_t_and_sign (): context.t = math.floor(time.time()) js = ''' var sign = sha1('Xr0Z-javascript-obfuscation-1' + t); ''' context.execute(js) return context.t, context.sign total = 0 for page_num in range (1 , 1001 ): t, sign = get_t_and_sign() url = f'http://www.glidedsky.com/api/level/web/crawler-javascript-obfuscation-1/items?page={page_num} &t={t} &sign={sign} ' response = session.get(url, headers=env.headers) nums = json.loads(response.text)['items' ] total += sum (nums) print (f"前 {page_num} 页数字和为: {total} " )print (f"结果是: {total} " )
爬虫-验证码-2 页面访问好像没有内容了:http://www.glidedsky.com/level/web/crawler-captcha-2
只不过就算做,估计我也没法,猜测都是图形判断或者汉字点击了。
雪碧图反爬-2 试了下百度识图(每天能免费用很多次),发现依然会有识别出错的情况!!!!
找了个类似的代码,自己修改了部分,能实现训练并识别mnist手写数字:
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 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 """ tensorflow2.0实现手写汉字实现: http://blog.itpub.net/69978904/viewspace-2733646/ 保存模型: https://www.cnblogs.com/piaodoo/p/14124831.html 测试tensorflow2实现mnist手写汉字识别 """ import os os.environ['TF_CPP_MIN_LOG_LEVEL' ] = '2' from tensorflow import kerasfrom tensorflow.keras.layers import Flatten, Densefrom tensorflow.keras.datasets import mnistimport numpy as np (train_images, train_labels), (test_images, test_labels) = mnist.load_data() train_images, test_images = train_images / 255.0 , test_images / 255.0 """ 重新创建一个model """ def new_model (): model = keras.Sequential([ Flatten(input_shape=(28 , 28 )), Dense(128 , activation='relu' ), Dense(10 , activation='softmax' ) ]) model.compile (optimizer='adam' , loss='sparse_categorical_crossentropy' , metrics=['acc' ]) return model""" 已存在的model, 加载权重和偏置(方法一: 保存模型的权重和偏置) """ def load_weights_bias (model ): if os.path.exists('./tensorflow2_mnist_model/weights_bias_model/checkpoint' ): model.load_weights('./tensorflow2_mnist_model/weights_bias_model/my_model' ) else : print ('第一次保存权重和偏置吧? 当前模型文件不存在呐! ' ) return None """ 保存model的权重和偏置 """ def save_weight_bias (model ): model.save_weights('./tensorflow2_mnist_model/weights_bias_model/my_model' )""" 训练模型 """ def train_model (model ): model.fit(train_images, train_labels, epochs=1 )""" 保存整个模型(方法二: 直接保存整个模型) """ def save_all_model (model ): model.save('./tensorflow2_mnist_model/all_model/mnist_weights.h5' )""" 加载整个模型 """ def load_all_model (): if os.path.exists('./tensorflow2_mnist_model/all_model/mnist_weights.h5' ): return keras.models.load_model('./tensorflow2_mnist_model/all_model/mnist_weights.h5' ) else : print ('第一次保存整个模型吧? 当前模型文件不存在呐! ' ) return None model = new_model() reload_model = load_all_model()if reload_model is not None : model = reload_model train_model(model) save_all_model(model) test_loss, test_acc = model.evaluate(test_images, test_labels, verbose=2 )print ('Test acc:' , test_acc) predictions = model.predict(test_images)print ('predictions: ' , predictions)print ('预测值:' , np.argmax(predictions[0 ]))print ('真实值:' , test_labels[0 ])
先仿照mnist数据集,将glidedsky上的雪碧图分割成每个小的数字,作为训练集图片,之后再读取图片,构成mnist数据集的数据结构,如此达到最终能识别的目的。
思路 训练数据:
在线获取二维雪碧图,用ps切割大的二维雪碧图成小数字图(https://jingyan.baidu.com/article/9989c746fe0ffef649ecfe5d.html),并批量修改为18*18(**百度**:https://jingyan.baidu.com/article/9f7e7ec0ecf9676f2815540a.html,并且要求为文件名**第一个字母是数字**即可,如'1.12312z.jpg'、'3.(1)jpg'
只不过横向均匀切割还好,但是纵向不行,会打乱数据的完整性,所以最好用代码以像素为基准判断切割好,再用ps统一处理为18*18的训练集(这一步也能用代码完成,所以建议直接用代码下载图片数据集,手动更改文件名作为labels)。
现在只需,将所有18*18图片数据转化为类似mnist的数据结构进行返回:
images和labels数据分别是三维和一维Tensor。
然后就能用最上方的训练代码进行训练了。
后续再多弄点数据集。
代码实现切割出小图 横向切割,结果是每一行,没一行内再按照数字最小宽度切割,切出来的图片将分辨率调为18*18就得到了训练图。
批量重命名:左键加上 ctrl ,多选,选好了再右键重命名:
右键重命名,输入“1”,下图即为结果(很方便啊):
用以下代码多生成写训练集,并批量重命名,有耐心一些。
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 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 """ 下载二维雪碧图并实现分割 """ from env import Envimport randomimport refrom PIL import Imageimport base64import io env = Env() session = env.login()""" 获取二维雪碧图 """ def get_sprite (url ): response = session.get(url, headers=env.headers) base64_bytes = base64.b64decode(re.search('base64,(.*?)\"' , response.text).group(1 )) return Image.open (io.BytesIO(base64_bytes))""" 针对每一行的图片进行按列切割, 返回数字的最小宽限集合 如 [(1, 3), (5, 8), ...], 那么横向x 属于 [1, 3] 就是数字0的范围, 一起类推 """ def get_gray_interval (row_image ): interval_list = [] last_col_all_white = True num_start_x = 0 num_end_x = 0 pixels = row_image.load() for x in range (row_image.width): cur_col_all_white = True for y in range (row_image.height): if pixels[x, y][0 ] != 255 : cur_col_all_white = False if last_col_all_white and not cur_col_all_white: num_start_x = x if not last_col_all_white and cur_col_all_white: num_end_x = x interval_list.append((num_start_x, num_end_x)) last_col_all_white = cur_col_all_white return interval_list""" 切割为合理大小, 保存到train_img文件夹下, 作为训练集 """ def slice_image (image ): piece_height = image.height / 10 for i in range (10 ): y = i * piece_height row_image = image.crop((0 , y, image.width, y + piece_height)) interval_list = get_gray_interval(row_image) for interval in interval_list: num_image = row_image.crop((interval[0 ], 0 , interval[1 ], row_image.height)) num_image = num_image.resize((18 , 18 ), Image.ANTIALIAS) num_image.save(f'./train_img/{random.randint(11 , 1000 )} .png' ) url = 'http://www.glidedsky.com/level/web/crawler-sprite-image-2?page=1' image = get_sprite(url) slice_image(image)
暂时弄这么多训练集:
再弄这么多测试集:
开始训练 读取进去的文件名列表,一定记得shuffle一下。
多训练些数据集,如果对测试结果不满意再重新添加些训练集
即可。
= =,有些数字形状也太离谱了吧
编码 把base64串和预测结果记录,比较,将不同的数字
保存为训练集,再次训练。
把感觉判断不好的数字也一并加入训练集。
1、2、7、4、6、3是高频错点,都全部加入训练集。
反复重复以上对比操作、添加数据集重新训练操作。
这。。,训练集太少了吧? = =
再加点:
按这样每行10个区分起来会快速些。
接下来就是不断训练的过程了。
这个 6
就离谱
之后才意识到,直接获取第一页数据,因为数字固定,能直接获得真实数字和 small_image
的对应关系。
https://www.cnblogs.com/TurboWay/p/13678074.html
训练数据:
测试数据:
我靠,resize需要重新赋值,没重新赋值就保存了!全是每resize之前的图片,都得重新下载
新数据:
重新训练。
然后每页数据的和,请求次数在3次及其以上,并且有个数频率大于等于3/4,才算请求成功,否则重新请求当前页数据。
说明:后续添加训练数据到达42w,应该在10w都够了,我还以为训练出错,多弄了些数据
还有,即使accuracy达到0.98/0.97(测试集有10w),实际爬取时出错概率依旧特别高,一定要一个页面多请求几组,取高频出现的那个判定数据!!!
代码
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 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 """ 爬虫-雪碧图-2 """ import os os.environ['TF_CPP_MIN_LOG_LEVEL' ] = '2' from env import Envfrom lxml import etreeimport refrom PIL import Imageimport base64import ioimport numpy as npfrom tensorflow import kerasimport tensorflow as tf env = Env() session = env.login()""" 获取二维雪碧图(同 2.sprite2_base64img_download.py) context: 网页源码 """ def get_sprite (context ): base64_bytes = base64.b64decode(re.search('base64,(.*?)\"' , context).group(1 )) return Image.open (io.BytesIO(base64_bytes))""" 针对每一行的图片进行按列切割, 返回数字的最小宽限集合 如 [(1, 3), (5, 8), ...], 那么横向x 属于 [1, 3] 就是数字0的范围, 一起类推 (同 2.sprite2_base64img_download.py) """ def get_gray_interval (row_image ): interval_list = [] last_col_all_white = True num_start_x = 0 num_end_x = 0 pixels = row_image.load() for x in range (row_image.width): cur_col_all_white = True for y in range (row_image.height): if pixels[x, y][0 ] != 255 : cur_col_all_white = False if last_col_all_white and not cur_col_all_white: num_start_x = x if not last_col_all_white and cur_col_all_white: num_end_x = x interval_list.append((num_start_x, num_end_x)) last_col_all_white = cur_col_all_white return interval_list""" 将图片列表转为类似mnist数据结构的数据, 方便模型进行预测 return: [100*18*18] 三维矩阵Tensor 基本同 3.sprite2_train_test.py 中的 def get_train_data_like_mnist(train_img_path) """ def parse_images_like_mnist_data (small_images ): images = np.zeros([len (small_images), 18 , 18 ]) for index, small_image in enumerate (small_images): width = 18 height = 18 for x in range (0 , width): for y in range (0 , height): images[index][x][y] = small_image.getpixel((x, y))[0 ] / 255.0 return tf.convert_to_tensor(images, dtype=tf.float32)""" 预测数字 return: 返回数字列表(str形式, 后期方便拼接) """ def predict (small_images, model ): num_list = [] images_data = parse_images_like_mnist_data(small_images) predictions = model.predict(images_data) for i in range (0 , len (small_images)): num_list.append(np.argmax(predictions[i])) return num_list""" 根据每个单数字div的偏移和宽高从雪碧图中切割出小图, 利用模型识别数字 image: 二位雪碧图 context: 网页源码 return: (切割好的 single_bit_image集合, 一页共12个数每个数有几位数字) """ def slice_image (image, context ): single_bit_images = [] count_bit = [] html = etree.HTML(context) num_divs = html.xpath('//div[@class="col-md-1"]' ) style = html.xpath('//style/text()' )[0 ] for num_div in num_divs: count = 0 for single_bit_div in num_div: clazz = single_bit_div.attrib['class' ] clazz = clazz[:clazz.index(' ' )] offset_x = abs (int (re.search(clazz + ' \\{ background-position-x:(-?\\d+)px' , style).group(1 ))) offset_y = abs (int (re.search(clazz + ' \\{ background-position-y:(-?\\d+)px' , style).group(1 ))) width = abs (int (re.search(clazz + ' \\{ width:(\\d+)px' , style).group(1 ))) height = abs (int (re.search(clazz + ' \\{ height:(\\d+)px' , style).group(1 ))) single_bit_image = image.crop((offset_x, offset_y, offset_x + width, offset_y + height)) single_bit_image = single_bit_image.resize((18 , 18 ), Image.ANTIALIAS) single_bit_images.append(single_bit_image) count += 1 count_bit.append(count) return single_bit_images, count_bit""" 根据每一个小数字 和 每个大数字占的位数 的到大数字集合 """ def mix_num (single_num_list, count_bit ): index = 0 num_list = [] for bit in count_bit: num = 0 while bit: num += single_num_list[index] * (10 ** (bit - 1 )) index += 1 bit -= 1 num_list.append(num) return num_list""" 请求返回当前页数字集合 """ def get_num_list (page_num, model ): url = f'http://www.glidedsky.com/level/web/crawler-sprite-image-2?page={page_num} ' response = session.get(url, headers=env.headers) sprite_image = get_sprite(response.text) single_bit_images, count_bit = slice_image(sprite_image, response.text) num_list = predict(single_bit_images, model) num_list = mix_num(num_list, count_bit) return num_list""" 判断预测是否精确 """ def is_accurate (predicts ): if len (predicts) < 6 : return False predicts_set = set (predicts) statistics = {num: predicts.count(num) for num in predicts_set} statistics = sorted (statistics.items(), key=lambda x: x[1 ], reverse=True ) if len (statistics) == 1 or statistics[0 ][1 ] / statistics[1 ][1 ] > 4 : print (statistics) return statistics[0 ][0 ] else : return False if __name__ == '__main__' : total = 0 model_path = './sprite-image-2-test/glided_sky_model/glided_sky_model.h5' restored_model = keras.models.load_model(model_path) for i in range (1 , 1001 ): predicts = [] print ('-' * 100 ) accurate = is_accurate(predicts) count = 0 while not accurate: count += 1 num_list = get_num_list(i, restored_model) predicts.append(sum (num_list)) accurate = is_accurate(predicts) print (f'经过 {count} 轮计算, 结果才精确!!!' ) print (f'第 {i} 页数据为: {num_list} , 求和是 {accurate} ' ) total += accurate print (f'前 {i} 页, 数字和为 {total} ' ) print ('-' * 100 )""" page1 [337, 379, 263, 144, 131, 285, 381, 364, 291, 171, 110, 94] page1 + page2 + page3 = 2956 + 2907 + 2844 = 8707 错误答案1: 2725233 错误答案2: 2724715 还真是出错出在连续三次都判定为错误答案!!! 修改为6次以上才判定就好了(多判定几组数据) (正确答案: 27247^_^) 之前还怀疑训练42w组数据都能出错 (* ̄▽ ̄*) """