整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

JavaScript SDK 设计指南

JavaScript SDK 设计指南

文地址:http://sdk-design.js.org/

介绍

本指南为您介绍了在台式机和移动网络在不同的平台和浏览器( < 99.99 %我可能会跳过一些浏览器)开发的JavaScript SDK ,对于那些非浏览器开发的支持(硬件,嵌入式,节点/ IO JS )被排除在本文档之外,在未来予以考虑。

因为我没有找到一个关于设计JavaScript SDK的比较好的文档,所以我在这里收集并记下了我个人的经验。这份文档已经写了好几个月,有一点我们需要知道,JavaScript的SDK-设计不仅仅是设计SDK本身,这也是有关于开发者与设备浏览器中间的联系。我们写的越多,越会更多的思考我们真正关心的是不同平台和浏览器之间的性能和兼容问题。你可以根据情况自由的更改或者完全放弃我在文章里列出的建议。

什么是SDK

我知道它确实是很普通很常见。一般是一些软件工程师为特定的软件包、软件框架、硬件平台、操作系统等建立应用软件时的开发工具的集合。通常一个SDK包含一个或多个API,编程工具和档。

设计理念

这取决于你的SDK用来干什么的,但是它必须具备原生的,短,速度快,干净,可读可测试特性。用原生javascript写,不要用像Livescript, Coffeescript, Typescript和其它的编译语言。必须有更好的方法来编写自己的javascript原生代码比别人更快。请不要在你的SDK里用JQuery,除非它非常有必要。你可以使用其它的类似jQuery的库,譬如zetpo.js,用于DOM操作,如果你需要用到HTTP Ajax请求,可以使用另外一种轻量库像window.fetch。

每一次的SDK版本发布,确保它不仅适用于旧版本而且适应于未来的新版本。所以,记得为你的SDK写文档,代码要写注释,同时做好单元测试和用户场景测试。

适应范围

基于《Third-Party JavaScript》这本书。在何种情况下,你应该为你的应用设计一个JavaScript SDK?

  • 嵌入式组件 – 嵌入在出发布者的网页中的交互式应用程序(Disqus, Google Maps, Facebook Widget)。
  • 分析与数据 – 搜集网站访问者以及其与网站互动的数据信息。(GA, Flurry, Mixpanel)
  • web服务API封装 -对于发展与外部Web服务通信的客户端应用程序。(Facebook的图形API)

在什么情况下,我们应该在JavaScript环境中使用SDK呢?大家可以想想还有其它情没?

引入SDK

建议你采用异步加载脚本的方式。我们要优化网站的用户体验,所以不希望我们的SDK库阻塞其它主要进程。

异步加载

(function() {vars=document.createElement('script');s.type='text/javascript';s.async=true;s.src='http://xxx.com/sdk.js';varx=document.getElementsByTagName('script')[0];x.parentNode.insertBefore(s, x);})();**

在新的现代浏览器(chrome)你可以使用

<script asyncsrc="http://xxx.com/sdk.js"></script>

传统加载方法

<script type="text/javascript"src="http://xxx.com/sdk.js"></script>

对比:

下面是简单的图形显示异步加载和传统同步加载方式之间的区别

异步:

|----A-----|
 |-----B-----------|
 |-------C------|

同步:

|----A-----||-----B-----------||-------C------|

异步和延迟脚本执行解释

异步的问题

当你使用异步加载的时候,将会出现,页面中的函数无法正常调用SDK方法的情况。

<script>
 (function () {
 var s=document.createElement('script');
 s.type='text/javascript';
 s.async=true;
 s.src='http://xxx.com/sdk.js';
 var x=document.getElementsByTagName('script')[0];
 x.parentNode.insertBefore(s, x);
 })();
 // execute your script immediately hereSDKName('some arguments');
</script>

结果会报undefined错误,因为SDKName()在脚本加载之前执行了。所以我们应该使用点技巧让脚本正确执行。把事件保存在SDKName.q数组里,SDK初始化的时候执行SDKName.q。

<script>
 (function () {
 // add a queue event here
 SDKName=SDKName ||function () {
 (SDKName.q=SDKName.q|| []).push(arguments);
 };
 var s=document.createElement('script');
 s.type='text/javascript';
 s.async=true;
 s.src='http://xxx.com/sdk.js';
 var x=document.getElementsByTagName('script')[0];
 x.parentNode.insertBefore(s, x);
})();
 // execute your script immediately hereSDKName('some arguments');
 </script>

或者用 [ ].push

<script>
 (function () {
 // add a queue event here
 SDKName=window.SDKName|| (window.SDKName=[]);
 var s=document.createElement('script');
 s.type='text/javascript';
 s.async=true;
 s.src='http://xxx.com/sdk.js';
 var x=document.getElementsByTagName('script')[0];
 x.parentNode.insertBefore(s, x);
})();
// execute your script immediately hereSDKName.push(['some arguments']);
</script>

其他方式

还有其它不同方式加载脚本

Import in ES2015

import"your-sdk";

模块加载

这里有完整的源码和非常棒的教程. Loading JavaScript Modules

module('sdk.js',['sdk-track.js', 'sdk-beacon.js'],function(track, beacon) {
// sdk definitions, split into local and global/exported definitions// local definitions// exports
});
// you should contain this "module" method
(function () {
var modules={}; // private record of module data// modules are functions with additional informationfunctionmodule(name,imports,mod) {
// record module informationwindow.console.log('found module '+name);
modules[name]={name:name, imports: imports, mod: mod};
// trigger loading of import dependenciesfor (var imp in imports) loadModule(imports[imp]);
// check whether this was the last module to be loaded// in a given dependency grouploadedModule(name);
}
// function loadModule// function loadedModulewindow.module=module;
})();

SDK版本

避免使用自己的特例作为版本名称像

标识-v<时间戳>.js 标识-v<日期>.js 标识-v1-v2.js

它可能导致使用SDK的开发者很混乱不知道哪个是最新版本。

使用 Semantic Versioning (语义化版本规范)去定义SDK的版本号以”大.小.补丁”形式。

版本以v1.0.0 v1.5.0 v2.0.0的形式,会让使用者搜索跟踪日志文件更容易。

通常情况下,我们会有不同的方式去声明SDK的版本,这取决于具体针对的业务和设计。

使用查询字符串路径

http://xxx.com/sdk.js?v=1.0.0

使用文件夹命名

http://xxx.com/v1.0.0/sdk.js

使用主机名或者子域名

http://v1.xxx.com/sdk.js

为了以后版本的升级迭代,建议用stable unstable alpha latest experimental 版本。

http://xxx.com/sdk-stable.js
http://xxx.com/sdk-unstable.js
http://xxx.com/sdk-alpha.js
http://xxx.com/sdk-latest.js
http://xxx.com/sdk-experimental.js

更新日志文件

你应该注意到如果你升级你的SDK却没通知用户,用户不会知道。记得写更新日志来记录无论是主要、次要甚至bug修复等修改。这将是一个好的开发经验,我们能快速的跟踪到SDK某个API的修改。所以保持更新日志 – Keep a Changelog, Github Repo

每个版本的日志应该有:

[新增] 新功能.

[更新] 修改现有的更能

[废弃] 在即将发布的版本中删除某个功能.

[删除] 在这个版本中删除弃用的功能.

[修正] bug修复

[安全] 邀请用户对安全进行升级

命名空间

在你的SDK里只定义一个全局命名空间,并且不要用太过通用的名字,避免和其它类库名发生冲突。SDK的主体用(function () { … })()包裹。这种做法越来越普遍的应用于各种流行的javascript类库譬如jQuery,Node.js等等。这种创建私有的命名空间的技术很重要,有助于避免各种类库之间命名的冲突。

为了避免命名空间冲突

学习Google Analytics的做法,你可以通过改变 ga的值来定义你自己的命名空间。

(function(i,s,o,g,r,a,m) {i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o) [0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google- analytics.com/analytics.js','ga');

下面的是 openX的做法,支持通过给地址传递参数定义命名空间。

<script src="http://your_domain/sdk?namespace=yourcompany"></script>

存储机制

cookie

使用cookie就会面临复杂的作用域范围问题,而且涉及到子域和路径问题。

比如在路径 path=/下, cookie first=value1 在域名 http://github.com下, 另外一个 cookie second=value2 在域名 http://sub.github.com下

http://github.comhttp://sub.github.comfirst=value1??second=value2??

有个 cookie first=value1 在 http://github.com下, cookie second=value2 在 http://github.com/path1 另外一个 cookie third=value3 在 http://sub.github.com下,

http://github.comhttp://github.com/path1http://sub.github.comfirst=value1???second=value2???third=value3???

检查 Cookie 可读写

给定一个域 (默认当前主机域名), 检查cookie是否可读写。

var checkCookieWritable=function(domain) {
try {
 // Create cookie
 document.cookie='cookietest=1' + (domain ? '; domain=' + domain : '');
 var ret=document.cookie.indexOf('cookietest=') !=-1;
 // Delete cookie
 document.cookie='cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT' + (domain ? '; domain=' + domain : '');
 return ret;
} catch (e) {
 return false;
}
};

检查第三方 Cookie 可读写

检查第三方cookie仅仅通过客户端js是办不到的,需要服务器端配合。

写 读 删除 Cookie 代码

代码片段写/读/删除cookie的脚本。

var cookie={
write: function(name, value, days, domain, path) {
 var date=new Date();
 days=days || 730; // two years
 path=path || '/';
 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
 var expires='; expires=' + date.toGMTString();
 var cookieValue=name + '=' + value + expires + '; path=' + path;
 if (domain) {
 cookieValue +='; domain=' + domain;
 }
 document.cookie=cookieValue;
},
read: function(name) {
 var allCookie='' + document.cookie;
 var index=allCookie.indexOf(name);
 if (name===undefined || name==='' || index===-1) return '';
 var ind1=allCookie.indexOf(';', index);
 if (ind1==-1) ind1=allCookie.length;
 return unescape(allCookie.substring(index + name.length + 1, ind1));
},
remove: function(name) {
 if (this.read(name)) {
 this.write(name, '', -1, '/');
 }
}
};

Session

js写不了session,需要服务器端写。

一个页面的session会一直保存着只要浏览器是开着的即使页面重新加载。打开一个新页面会生成一个新的session。子窗口会和父窗口共享一个session。

LocalStorage

存储的数据没有时间限制。存储数据量大(至少5MB)并且信息不会传送到服务器。而且同一个域名从http和https访问localStorage是不共享的。你可以在你的网页上创建个iframe,然后用postMessage方法去传值到父页面。HOW TO?

检查 LocalStorage 可写

window.localStorage 并不是任何浏览器都支持,SDK在用之前要检查是否可用。

var testCanLocalStorage=function() {
var mod='modernizr';
 try {
 localStorage.setItem(mod, mod);
 localStorage.removeItem(mod);
 return true;
 } catch (e) {
 return false;
 }
};

SessionStorage

针对一个 session 的数据存储(当用户关闭浏览器窗口后,数据会被删除).

检查 SessionStorage 可写

var checkCanSessionStorage=function() {
var mod='modernizr';
try {
sessionStorage.setItem(mod, mod);
sessionStorage.removeItem(mod);
return true;
} catch (e) {
return false;
}
}

事件

在客户端浏览器有很多事件加载、卸载、绑定等会存在兼容问题。polyfills是个解决不同平台事件绑定的不错的解决方案。

Document Ready

确保整个页面完成加载了再执行SDK方法。

// handle IE8+
function ready (fn) {
if (document.readyState !='loading') {
 fn();
} else if (window.addEventListener) {
 // window.addEventListener('load', fn);
 window.addEventListener('DOMContentLoaded', fn);
} else {
 window.attachEvent('onreadystatechange', function() {
 if (document.readyState !='loading')
 fn();
 });
}
}

DOMContentLoaded - 所有DOM解析完会触发整个事件 不需要等到样式表、图片等加载完。

load 页面完整加载。

Message Event

这里是实现iframe和父页面之间的数据通信, 这里有文档 API documentation.

// in the iframe
parent.postMessage("Hello"); // string
//==========================================// in the iframe's parent
// Create IE + others compatible event handler
var eventMethod=window.addEventListener ? "addEventListener" : "attachEvent";
var eventer=window[eventMethod];
var messageEvent=eventMethod=="attachEvent" ? "onmessage" : "message";
// Listen to message from child window
eventer(messageEvent,function(e) {
// e.origin , check the message origin
console.log('parent received message!: ',e.data);
},false);

发送的数据是字符串, 对于使用更高级的json字符串. 不是所有的浏览器对支持 Structured Clone Algorithm on the parameter, (参数的结构化克隆)。

Orientation Change 横屏事件

检测设备横屏

window.addEventListener('orientationchange', fn);

获取旋转方向和角度

window.orientation; //=> 90, -90, 0

Screen portrait-primary(竖屏正方向), portrait-secondary(竖屏反方向), landscape-primary(横屏正方向), landscape-secondary (横屏反方向)(Experimental)

// https://developer.mozilla.org/en-US/docs/Web/API/Screen/orientation
var orientation=screen.orientation || screen.mozOrientation || screen.msOrientation;

Request

我们的SDK和服务器之间通信通过Ajax请求,因为我们知道我们可以使用jQuery的Ajax 方法。但是有更好的方案来实现它。

图片预加载

通过创建一个Image对象预加载一张图片。为了防止浏览器缓存记得加上时间戳。

(new Image()).src='http://xxxxx.com/collect?id=1111';

要注意通过GET方式传输参数最大长度是2048个字节(取决于不同的浏览器和服务器)。这里要做一些处理如果超过长度。

if (length > 2048) {
// do Multiple Post (form)
} else {
// do Image Beacon
}

你可能遇到问题在使用encodeURI 还是 encodeURIComponent的时候,最好理解它们的区别。 See below.

对于图像加载成功/错误回调

var img=new Image();
img.src='http://xxxxx.com/collect?id=1111';
img.onload=successCallback;
img.onerror=errorCallback;

单个 Post 请求

普通表单发送一个对应元素和值

var form=document.createElement('form');
var input=document.createElement('input');
form.style.display='none';
form.setAttribute('method', 'POST');
form.setAttribute('action', 'http://xxxx.com/track');
input.name='username';
input.value='attacker';
form.appendChild(input);
document.getElementsByTagName('body')[0].appendChild(form);
form.submit();

多个 Post 请求

服务通常比较复杂,需要通过POST方法发送更多数据。

function requestWithoutAjax( url, params, method ){
params=params || {};
method=method || "post";
// function to remove the iframe
var removeIframe=function( iframe ){
 iframe.parentElement.removeChild(iframe);
};
// make a iframe...
var iframe=document.createElement('iframe');
iframe.style.display='none';
iframe.onload=function(){
 var iframeDoc=this.contentWindow.document;
 // Make a invisible form
 var form=iframeDoc.createElement('form');
 form.method=method;
 form.action=url;
 iframeDoc.body.appendChild(form);
 // pass the parameters
 for( var name in params ){
 var input=iframeDoc.createElement('input');
 input.type='hidden';
 input.name=name;
 input.value=params[name];
 form.appendChild(input);
 }
 form.submit();
 // remove the iframe
 setTimeout( function(){
 removeIframe(iframe);
 }, 500);
};
document.body.appendChild(iframe);
}
requestWithoutAjax('url/to', { id: 2, price: 2.5, lastname: 'Gamez'});

Iframe

当你在需要在页面中生成内容时候,你可以通过iframe嵌入。

var iframe=document.createElement('iframe');
var body=document.getElementsByTagName('body')[0];
iframe.style.display='none';
iframe.src='http://xxxx.com/page';
iframe.onreadystatechange=function () {
if (iframe.readyState !=='complete') {
 return;
}
};
iframe.onload=loadCallback;
body.appendChild(iframe);

清除iframe的边框,内部margin值。

<iframe src="..."
 marginwidth="0"
 marginheight="0"
 hspace="0"
 vspace="0"
 frameborder="0"
 scrolling="no">
</iframe>

iframe中插入html

<iframe id="iframe"></iframe>
<script>
 var html_string="content <script>alert(location.href); </script>";
 document.getElementById('iframe').src="data:text/html;charset=utf-8," + escape(html_string);
 // alert data:text/html;charset=utf-8.....
 // access cookie get ERROR
 var doc=document.getElementById('iframe').contentWindow.document;
 doc.open();
 doc.write('<body>Test<script>alert(location.href);</script></body>');
 doc.close();
 // alert "top window url"
 var iframe=document.createElement('iframe');
 iframe.src='javascript:;\\\\'' + encodeURI('<html><body> <script>alert(location.href);</body></html>') + '\\\\'';
 // iframe.src='javascript:;"' + encodeURI((html_tag).replace(/\\\\"/g, '\\\\\\\\\\\\"')) + '"';
 document.body.appendChild(iframe);
 // alert "about:blank"
</script>

jsonp

这种情况下,你的服务器需要响应JavaScript 代码,并让浏览器执行它,仅仅通过js脚本链接。

(function () {
 var s=document.createElement('script');
 s.type='text/javascript';
 s.async=true;
 s.src='/yourscript? some=parameter&callback=jsonpCallback';
 var x=document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
 })();

关于jsonp你需要了解:

  • JSONP 只能通过GET请求。
  • JSONP 缺少错误处理机制, 意味着你不能检测代码是否404还是500等状态。
  • JSONP 请求是异步的。
  • 当心 CSRF 攻击。
  • 跨域通信。脚本响应端(服务器端)不需要关心CORS。

XMLHttpRequest

自己写XMLHttpRequest不是个好主意,因为你要浪费很多时间去做IE或者其它浏览器的兼容。这里提供一些现成的解决方案供大家参考:

1 - window.fetch - A window.fetch JavaScript polyfill.

2 - got - Simplified HTTP/HTTPS requests

3 - microjs - list of ajax lib

4 – more

Maximum Number of Connection

检查不同浏览器的最大连接数 browserscope

调试

模拟多个域

你不需要注册多个域名来模拟域,在本地搭建个虚拟服务器,绑定host的方式就可以:

$ sudo vim /etc/hosts

添加以下条目

#refer to localhost 
127.0.0.1 publisher.net
127.0.0.1 sdk.net

然后你就可以访问该页面http://publisher.net和http://sdk.net

Developer Tools

用浏览器自带的调试工具,Chrome Developer Tool 、Safari Developer Tools、Firebug都是不错的选择。

开发工具也简称为工具。

工具提供Web开发者深进入浏览器和Web应用程序的内部。使用工具来有效地追踪布局问题,将JavaScript打断点,并获得代码优化的建议。

控制台日志

用于测试和输出文本和其他一般的调试, 控制台日志可通过浏览器的API log()输出显示。有各种各样的方法和格式输出你的信息,了解更多API: Console API.

调试代理

代理在你调试SDK的很多时候都很有用。 修改cookies, headers, cache, 编辑 http request/response, SSL Proxying, ajax 调试等等。

这里推荐一些代理工具:

  • FiddlerCore
  • Charles
  • Cellist

BrowserSync

Browsersync能让浏览器实时、快速响应您的文件更改(html、js、css、sass、less等)并自动刷新页面。更重要的是 Browsersync可以同时在PC、平板、手机等设备下进项调试。它真的很有帮助如果你需要跨平台测试你的SDK)。

提示和小技巧

Console Logs Polyfill(Polyfilling 是由 RemySharp 提出的一个术语,它是用来描述复制缺少的 API 和API 功能的行为)

这不是一个真正的polyfill,只是保证在调用console.log API的时候不抛出错误。

if (typeof console==="undefined") { var f=function() {}; console={ log: f, debug: f, error: f, info: f };}

EncodeURI or EncodeURIComponent

理解三者的不同 escape()、encodeURI()、encodeURIComponent()

here.

记住使用 encodeURI()和encodeURIComponent()有11个字符不同。 它们是: # $ & + , / : ;=? @ more discussion。

你可能真的不需要JQuery

正如标题所说, 你可能真的不需要JQuery。如果你正在找一些公共的代码那下面这些会很有用:- AJAX EFFECTS, ELEMENTS, EVENTS, UTILS

你不需要 jQuery

Free yourself from the chains of jQuery by embracing and understanding the modern Web API and discovering various directed libraries to help you fill in the gaps.

http://blog.garstasio.com/you-dont-need-jquery/

有用的 Tips

Selecting Elements

DOM Manipulation

回调函数加载脚本

类似于 异步加载脚本 增加回调函数。

function loadScript(url, callback) { 
 var script=document.createElement('script'); 
 script.async=true; script.src=url; 
 var entry=document.getElementsByTagName('script')[0]; entry.parentNode.insertBefore(script, entry); 
 script.onload=script.onreadystatechange=function () { var rdyState=script.readyState; 
 if (!rdyState || /complete|loaded/.test(script.readyState)) { 
 callback(); // detach the event handler to avoid memory leaks in IE (http://mng.bz/W8fx) 
 script.onload=null;
 script.onreadystatechange=null; } };
}

执行一次函数

这里展示了如何实现函数只执行一次。

每当你想有一个只运行一次的函数。通常这些函数是以事件监听的方式,很难管理。当然如果很容易管理,你只需要删除监听事件,但是这是个理想的状态,很多时候你只需要允许一个函数执行一次。下面的代码可以实现:

// Copy from DWB
// http://davidwalsh.name/javascript-once
 function once(fn, context) { 
 var result; return function() { 
 if(fn) { 
 result=fn.apply(context || this, arguments); 
 fn=null;
 } 
 return result; };
}
// Usagevar 
 canOnlyFireOnce=once(function() { console.log('Fired!');});
 canOnlyFireOnce(); // "Fired!"canOnlyFireOnce(); // nada

获取样式

获取行间样式

<span id="black" style="color: black"> 
 This is black color span 
</span>
<script> document.getElementById('black').style.color; //=> black</script>

获取真正的样式

<style>
 #black { color: red !important;}
</style>
<span id="black" style="color: black">
 This is black color span 
</span>
<script> 
 document.getElementById('black').style.color; //=> black 
// real var black=document.getElementById('black'); 
 window.getComputedStyle(black, null).getPropertyValue('color'); //=> rgb(255, 0, 0)
</script>

ref:https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle

检测当前窗口

了解更多: here。

chrome中使用F12获取的nerwork中的网络请求,可以看到request headers,直接复制下来是一个字符串,可以手动修改为python requests可用的字典,但是费时费力,下面这段代码,可以直接把复制下来的字符串类型,转换为requests可用的python字典。

天要给大家介绍的是验证码的爬取和识别,不过只涉及到最简单的图形验证码,也是现在比较常见的一种类型。

运行平台:Windows

Python版本:Python3.6

IDE: Sublime Text

其他:Chrome浏览器

想要学习更多验证、破解网站验证码的知识点,关注小编后私信学习资料,领取相关学习资料

简述流程:

步骤1:简单介绍验证码

步骤2:爬取少量验证码图片

步骤3:介绍百度文字识别OCR

步骤4:识别爬取的验证码

步骤5:简单图像处理

目前,很多网站会采取各种各样的措施来反爬虫,验证码就是其中一种,比如当检测到访问频率过高时会弹出验证码让你输入,确认访问网站的不是机器人。但随着爬虫技术的发展,验证码的花样也越来越多,从最开始简单的几个数字或字母构成的图形验证码(也就是我们今天要涉及的)发展到需要点击倒立文字字母的、与文字相符合的图片的点触型验证码,需要滑动到合适位置的极验滑动验证码,以及计算题验证码等等,总之花样百出,让人头秃。验证码其他的相关知识大家可以看下这个网站:captcha.org

再来简单说下图形验证码吧,就像这张:

由字母和数字组成,再加上一些噪点,但为了防止被识别,简单的图形验证码现在也变得复杂,有的加了干扰线,有的加噪点,有的加上背景,字体扭曲、粘连、镂空、混用等等,甚至有时候人眼都难以识别,只能默默点击“看不清,再来一张”。

验证码难度的提高随之带来的就是识别的成本也需要提高,在接下来的识别过程中,我会先直接使用百度文字识别OCR,来测试识别准确度,再确认是否选择转灰度、二值化以及去干扰等图像操作优化识别率。

接下来我们就来爬取少量验证码图片存入文件。

首先打开Chrome浏览器,访问刚刚介绍的网站,里面有一个captcha图像样本链接:https://captcha.com/captcha-examples.html?cst=corg,网页里有60张不同类型的图形验证码,足够我们用来识别试验了。


直接来看代码吧:

import requests
import os
import time
from lxml import etree
def get_Page(url,headers):
 response=requests.get(url,headers=headers)
 if response.status_code==200:
 # print(response.text)
 return response.text
 return None
def parse_Page(html,headers):
 html_lxml=etree.HTML(html)
 datas=html_lxml.xpath('.//div[@class="captcha_images_left"]|.//div[@class="captcha_images_right"]')
 item={}
 # 创建保存验证码文件夹
 file='D:/******'
 if os.path.exists(file):
 os.chdir(file)
 else: 
 os.mkdir(file)
 os.chdir(file) 
 for data in datas:
 # 验证码名称
 name=data.xpath('.//h3')
 # print(len(name))
 # 验证码链接
 src=data.xpath('.//div/img/@src') 
 # print(len(src))
 count=0
 for i in range(len(name)):
 # 验证码图片文件名
 filename=name[i].text + '.jpg'
 img_url='https://captcha.com/' + src[i]
 response=requests.get(img_url,headers=headers)
 if response.status_code==200:
 image=response.content
 with open(filename,'wb') as f:
 f.write(image)
 count +=1
 print('保存第{}张验证码成功'.format(count))
 time.sleep(1)
def main():
 url='https://captcha.com/captcha-examples.html?cst=corg'
 headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36'}
 html=get_Page(url,headers)
 parse_Page(html,headers)
if __name__=='__main__':
 main()

仍然使用Xpath爬取,在右键检查图片时可以发现,网页分为两栏,如下图红框所示,根据class分为左右两栏,验证码分别位于两栏中。



datas=html_lxml.xpath('.//div[@class="captcha_images_left"]|.//div[@class="captcha_images_right"]')


这里我使用了Xpath中的路径选择,在路径表达式中使用“|”表示选取若干路径,例如这里表示的就是选取class为"captcha_images_left"或者"captcha_images_right"的区块。再来看下运行结果:



由于每爬取一张验证码图片都强制等待了1秒,最后这个运行时间确实让人绝望,看样子还是需要多线程来加快速度的,关于多进程多线程我们下次再说,这里我们先来看下爬取到的验证码图片。



图片到手了,接下来就是调用百度文字识别的OCR来识别这些图片了,在识别之前,先简单介绍一下百度OCR的使用方法,因为很多识别验证码的教程用的都是tesserocr库,所以一开始我也尝试过,安装过程中就遇到了很多坑,后来还是没有继续使用,而是选择了百度OCR来识别。百度OCR接口提供了自然场景下图片文字检测、定位、识别等功能。文字识别的结果可以用于翻译、搜索、验证码等代替用户输入的场景。另外还有其他视觉、语音技术方面的识别功能,大家可以直接阅读文档了解:百度OCR-API文档https://ai.baidu.com/docs#/OCR-API/top



使用百度OCR的话,首先注册用户,然后下载安装接口模块,直接终端输入pip install baidu-aip即可。然后创建文字识别应用,获取相关Appid,API Key以及Secret Key,需要了解一下的是百度AI每日提供50000次免费调用通用文字识别接口的使用次数,足够我们挥霍了。



然后就可以直接调用代码了。

from aip import AipOcr
# 你的 APPID AK SK 
APP_ID='你的 APP_ID '
API_KEY='你的API_KEY'
SECRET_KEY='你的SECRET_KEY'
client=AipOcr(APP_ID, API_KEY, SECRET_KEY)
# 读取图片 
def get_file_content(filePath):
 with open(filePath, 'rb') as fp:
 return fp.read()
image=get_file_content('test.jpg')
# 调用通用文字识别, 图片参数为本地图片 
result=client.basicGeneral(image)
# 定义参数变量 
options={
 # 定义图像方向
 'detect_direction' : 'true',
 # 识别语言类型,默认为'CHN_ENG'中英文混合
 'language_type' : 'CHN_ENG',
}
# 调用通用文字识别接口 
result=client.basicGeneral(image,options)
print(result)
for word in result['words_result']:
 print(word['words'])

这里我们识别的是这张图


可以看一下识别结果

上面是识别后直接输出的结果,下面是单独提取出来的文字部分。可以看到,除了破折号没有输出外,文字部分都全部正确输出了。这里我们使用的图片是jpg格式,文字识别传入的图像支持jpg/png/bmp格式,但在技术文档中有提到,使用jpg格式的图片上传会提高一定准确率,这也是我们爬取验证码时使用jpg格式保存的原因。

输出结果中,各字段分别代表:

  • log_id : 唯一的log id,用于定位问题
  • direction : 图像方向,传入参数时定义为true表示检测,0表示正向,1表示逆时针90度,2表示逆时针180度,3表示逆时针270度,-1表示未定义。
  • words_result_num : 识别的结果数,即word_result的元素个数
  • word_result : 定义和识别元素数组
  • words : 识别出的字符串
  • 还有一些非必选字段大家可以去文档里熟悉一下。

接下来,我们要做的,就是将我们之前爬取到的验证码用刚介绍的OCR来识别,看看究竟能不能得到正确结果。

from aip import AipOcr
import os
i=0
j=0
APP_ID='你的 APP_ID '
API_KEY='你的API_KEY'
SECRET_KEY='你的SECRET_KEY'
client=AipOcr(APP_ID, API_KEY, SECRET_KEY)
# 读取图片 
file_path='D:\******\验证码图片'
filenames=os.listdir(file_path)
# print(filenames)
for filename in filenames:
 # 将路径与文件名结合起来就是每个文件的完整路径
 info=os.path.join(file_path,filename)
 with open(info, 'rb') as fp:
 # 获取文件夹的路径 
 image=fp.read()
 # 调用通用文字识别, 图片参数为本地图片
 result=client.basicGeneral(image)
 # 定义参数变量 
 options={
 'detect_direction' : 'true',
 'language_type' : 'CHN_ENG',
 }
 # 调用通用文字识别接口 
 result=client.basicGeneral(image,options)
 # print(result)
 if result['words_result_num']==0:
 print(filename + ':' + '----')
 i +=1
 else:
 for word in result['words_result']: 
 print(filename + ' : ' +word['words'])
 j +=1
print('共识别验证码{}张'.format(i+j))
print('未识别出文本{}张'.format(i))
print('已识别出文本{}张'.format(j))

和识别图片一样,这里我们将文件夹验证码图片里的图片全部读取出来,依次让OCR识别,并依据“word_result_num”字段判断是否成功识别出文本,识别出文本则打印结果,未识别出来的用“----”代替,并结合文件名对应识别结果 。最后统计识别结果数量,再来看下识别结果。



看到结果,只能说Amazing!60张图片居然识别出了65张,并且还有27张为未识别出文本的,这不是我想要的结果~先来简单看下问题出在哪里,看到“Vertigo Captcha Image.jpg"这张图名出现了两次,怀疑是在识别过程中由于被干扰,所以识别成两行文字输出了,这样就很好解释为什么多出来5张验证码图片了。可是!为什么会有这么多未识别出文本呢,而且英文数字组成的验证码识别成中文了,看样子,不对验证码图片进行去干扰处理,仅靠OCR来识别的想法果然还是行不通啊。那么接下来我们便使用图像处理的方法来重新识别验证码吧。

还是介绍验证码时用的这张图




这张图也没能被识别出来,让人头秃。接下来就对这张图片进行一定处理,看能不能让OCR正确识别

from PIL import Image
filepath='D:\******\验证码图片\AncientMosaic Captcha Image.jpg'
image=Image.open(filepath)
# 传入'L'将图片转化为灰度图像
image=image.convert('L')
# 传入'1'将图片进行二值化处理
image=image.convert('1')
image.show()

这样子转化后再来看下图片变成什么样了?



确实有些不同了,赶紧拿去试试能不能识别,还是失败了~~继续修改

from PIL import Image
filepath='D:\******\验证码图片\AncientMosaic Captcha Image.bmp'
image=Image.open(filepath)
# 传入'L'将图片转化为灰度图像
image=image.convert('L')
# 传入'l'将图片进行二值化处理,默认二值化阈值为127
# 指定阈值进行转化
count=170
table=[]
for i in range(256):
 if i < count:
 table.append(0)
 else:
 table.append(1 )
image=image.point(table,'1')
image.show()

这里我将图片保存成了bmp模式,然后指定二值化的阈值,不指定的话默认为127,我们需要先转化原图为灰度图像,不能直接在原图上转化。然后将构成验证码的所需像素添加到一个table中,然后再使用point方法构建新的验证码图片。





现在已经识别到文字了,虽然我不知道为啥识别成了“珍”,分析之后发现是因为z我在设置参数设置了“language_type”为“CHN_ENG”,中英文混合模式,于是我修改成“ENG”英文类型,发现可以识别成字符了,但依然没有识别成功,尝试其他我所知道的方法后,我表示很无语,我决定继续尝试PIL库的其他方法试试。

# 找到边缘
image=image.filter(ImageFilter.FIND_EDGES)
# image.show()
# 边缘增强
image=image.filter(ImageFilter.EDGE_ENHANCE)
image.show()



还是不能正确识别,我决定换个验证码试试。。。。。。



我找了这张带有阴影的

from PIL import Image,ImageFilter
filepath='D:\******\验证码图片\CrossShadow2 Captcha Image.jpg'
image=Image.open(filepath)
# 传入'L'将图片转化为灰度图像
image=image.convert('L')
# 传入'l'将图片进行二值化处理,默认二值化阈值为127
# 指定阈值进行转化
count=230
table=[]
for i in range(256):
 if i < count:
 table.append(1)
 else:
 table.append(0)
image=image.point(table,'1')
image.show()

简单处理后,得到这样的图片:



识别结果为:



识别成功了,老泪纵横!!!看样子百度OCR还是可以识别出验证码的,不过识别率还是有点低,需要对图像进行一定处理,才能增加识别的准确率。不过百度OCR对规范文本的识别还是很准确的。

那么与其他验证码相比,究竟是什么让这个验证码更容易被OCR读懂呢?

  • 字母没有相互叠加在一起,在水平方向上也没有彼此交叉。也就是说,可以在每一个字 母外面画一个方框,而不会重叠在一起。
  • 图片没有背景色、线条或其他对 OCR 程序产生干扰的噪点。
  • 白色背景色与深色字母之间的对比度很高。

这样的验证码相对识别起来较容易,另外,像识别图片时的白底黑字就属于很标准的规范文本了,所以识别的准确度较高。至于更复杂的图形验证码,就需要更深的图像处理技术或者训练好的OCR来完成了,如果只是简单识别一个验证码的话,不如人工查看图片输入,更多一点的话,也可以交给打码平台来识别。