iOS白板同步绘制方案调研
Apple Pencil绘制原理
- Apple Pencil 可以额外采集到 altitude(高度角)、azimuth(方位角)、force(压感值)
- 关于force压感值(3D Touch):force乘以一个压感系数sensitivity(自定义),计算一个两点之前的线宽linewidth(force * sensitivity),来实现压力不同线宽不同功能
- 关于altitude角度:一般用来绘画阴影部分,如角度小于30°的时候,可以模拟素描阴影的绘制
- 关于合并:Apple Pencil的捕获频率为240Hz,UIKit一般捕获触摸的频率为60Hz,这意味着Apple Pencil可以捕捉到更多的点,UIKit会把Apple Pencil捕获的多余的点合并到最后一个触摸点的UITouch对象里。
- 关于预估:iOS9以后UITouch可以实时返回预估的笔迹
以上的场景,使用force、altitude的计算、或处理合并的所有点均会增加绘制过程中的点阵数据包的大小,同时绘制的系数也需要调优减少可能造成的锯齿,如果是互动白板传输数据的场景(如腾讯在线课堂、网易白板等),都没有使用压感和角度的功能。如果是本地高精度的绘画应用,均是可以实现的。具体是否需要可以根据后续需求来定。后续会针对这一块(使用force、altitude、合并、预估笔迹)做预研和压力测试是否可行,增大笔迹包流量下的性能、消息丢失统计、体验、延时等的性价比综合对比分析
详情见:Handling Input from Apple Pencil
iOS端白板绘制实现方案
调研了目前网上白板的实现方式,基于不考虑压感值和角度的采集的情况下,都是基于UIResponder实现的,考虑优先实现一个画板的基础功能(撤销、恢复、橡皮等),实现方案有以下几种:
1.UIBezierPath➕drawRect实现
优点:实现简单
缺点:实现撤销、恢复等功能需要重绘调用drawRect,在累计笔迹大量的情况下,频繁刷新drawRect会导致内存过大
2.Quartz2D➕drawRect实现
优点:UIBezierPath其实是Quartz2D的封装,实现简单
缺点:因为重写了drawRect的方法,和方案一同样有笔迹过多的情况下内存过大的问题
3.UIBezierPath➕CAShapeLayer实现
优点:撤销、恢复等功能实现简单,内存小
缺点:橡皮实现较为复杂,需要注意优化计算量
4.OpenGLES实现
优点:实现自由度最高,性能理论可以调到最优
缺点:开发复杂,学习成本高,所有功能(橡皮、撤销、恢复、矩形等)需要采坑实现及调优
参考了大部分源码,一个高性能的白板基本采用方案三 UIBezierPath➕CAShapeLayer 实现
画板绘制流程
采集
每一个笔迹在开始、结束、移动的过程中有起始点B、终点E及移动点阵M1,M2,…,MN存在,iPad端绘制的同时会采集这些点阵通过NATS传输供学生端绘制,采集的通用格式为:
{
"n":1, // 序号 no
"c":1, // 笔画颜色 color
"l":1, // 笔画宽度 line
"s":0, // 分片数量 sub number
"o":0, // 分片序号 sub order
"e":0, // 画线类型0默认 1直线 2椭圆 3矩形 4橡皮擦等
"a":0, // 操作类型action: 1 正常绘图 2 撤销 3 恢复 4 清空等
"t":10000, // 时间戳 timestamp
"w":1024, // 画板高度
"h":768, // 画板宽度
"d":[{"x":10.5,"y":10.5},{"x":20.5,"y":20.5}] // x,y为坐标
}
其中,d的数组实际就是
[B,M1,M2,...,MN,E]
针对每个笔迹,归类为以下绘制方式,值为e(0默认 1直线 2椭圆 3矩形 4橡皮擦):
默认笔画(曲线):对相邻的点做贝塞尔曲线(BezierPath),BezierPath分多个阶段,高阶的曲线更圆滑
贝塞尔曲线说明
曲线的优化计算
1.移动到起始点 N1
2.当累计点到N2,N3,N4的时候,计算N2和N4的中点N3, 覆盖原来的N3点;
3.以N1为起始点对N3画一条二阶贝塞尔曲线,控制点为N2和N4
4.将N3做为起始点N1,N4作为第一个累计点N2,回到第一步
直线:取第一个点B和最后一个点E,画一条直线
矩形:取第一个点B和最后一个点E,范围内画一个矩形,需要注意起始点和终点的向量方向
椭圆:取第一个点B和最后一个点E,范围内画一个椭圆,需要注意起始点和终点的向量方向
点:当B和E的坐标相等时,就画一个点
动作,值a(1 正常绘图 2 撤销 3 恢复 4 清空等):
撤销:pop撤销栈最后一个临时图片并push恢复栈,渲染
恢复:pop恢复栈最后一个临时图片并push撤销栈,渲染
清空:清空所有栈及当前画板
点阵分片及重组
笔迹传输是按照每个笔迹采集的,如果一个笔迹的点阵数量过大,可以分片处理,NATS消息体里面会表示s和o表示分片的数量及序号,接收端需要收到全部分片后进行点阵d的数组重组及绘制。
时间戳
需要和视频流里面的时间戳消息做对比,小于视频流时间戳的时候才显示笔迹。
Travis CI 持续部署Hexo博客到GitHub Page和VPS服务器
写在前面
N久之前在GitHub Page上部署了Hexo,本地生成文章的时候需要
hexo g&&hexo d
等步骤才能上传到GitHub,最近又把博客折腾到了VPS上,手动deploy到VPS的同时又不能更新GitHub上,因此搜索了一下解决方案,发现可以实现git flow push到source后,使用Travis CI 持续集成自动deploy到GitHub Page,同时推送到VPS服务器。
构建流程
基本准备:
- GitHub Source 即博客源码仓库,以下称为Source
- GitHub Page 静态页仓库
- 配置好Nginx,git等环境的VPS
构建流程如下:
- 新增或修改了文章,push到GitHub的source
- Travis CI 发现source改变后自动执行配置好的脚本,生成静态页面
- Travis 推送静态页面文件到VPS服务器
- Travis push静态页面到GitHub
GitHub 和 Travis 设置
因为之前已经建好Blog的仓库和GitHub Page的仓库了,这些网上都很多教程,并不复杂,就直接记录如何让Source的仓库和Travis关联的步骤:
进入GitHub的Setting页面
选择Developer settings
选择Personal access tokens,生成Travis需要的access token
填写一个token的描述,如hexoblog,选择repo,然后Generate token即可
这里要复制下生成的token(只允许看见一次),在Travis那边可以使用
打开 Travis 网站(https://travis-ci.org) ,org后缀的才是对GitHub Public仓库免费的,com的针对是付费的私有仓库的。使用GitHub账号登录。
- 网上很多教程说需要打开
Build only if .travis.yml is present
选项(.travis.yml存在才生成),但最新的设置好像没有这个选项了,这里也不会有影响,然后需要Add一个Token, 新增一个HEXO_TOKEN,ValuE那里填写刚刚GitHub复制的access token即可。
iv和key是后面VPS那边自动生成的,这里可以先忽略。配置完以后就需要到博客仓库.travis.yml
文件了。
本地配置
在本地Blog根目录下新建.travis.yml文件,配置如下:
其中github.com/redanula/redanula.github.com.git
为GitHub Page的仓库,HEXO_TOKEN
为Travis刚刚Travis配置的token的标识。language: node_js node_js: stable cache: apt: true directories: - node_modules before_install: - export TZ='Asia/Shanghai' install: - npm install script: - hexo clean - hexo g && gulp after_script: - git clone https://${GH_REF} .deploy_git - cd .deploy_git - git checkout master - cd ../ - mv .deploy_git/.git/ ./public/ - cd ./public - git config user.name "redanula" - git config user.email "yanglangjing@hotmail.com" - git add . - git commit -m "Travis CI Auto Builder at `date +"%Y-%m-%d %H:%M"`" - git push --force --quiet "https://${HEXO_TOKEN}@${GH_REF}" master:master branches: only: - master env: global: - GH_REF: github.com/redanula/redanula.github.com.git notifications: email: - yanglangjing@hotmail.com on_success: change on_failure: always
提交
.travis.yml
文件到GitHub,这时候GitHub Page就会自动生成了。可以打开Travis查看对应的Current构建过程。首次提交的之后,发现生成的 redanula.github.io 是空白的,找了下原因是因为使用的next主题没有在Source仓库的hexo theme的目录,add & push对应的主题到Source仓库之后就成功了。接下来就是服务器的工作了。
VPS服务器配置
ssh登录VPS服务器,安装Travis服务
先安装ruby 如果提示如下:
current directory: /var/lib/gems/2.3.0/gems/ffi-1.9.25/ext/ffi_c
/usr/bin/ruby2.3 -r ./siteconf20180821-29696-1onq3fa.rb extconf.rb
mkmf.rb can’t find header files for ruby at /usr/lib/ruby/include/ruby.h
需要安装对应的ruby包:# 如果是在centos等系统下面,执行命令: yum install ruby-devel # 如果是在Ubuntu等系统下面,执行命令: apt-get install ruby-dev # 安装travis命令行工具,gem指令需要先安装ruby gem install travis
新增一个Git账号
adduser git
赋予git用户sudo权限
chmod 740 /etc/sudoers vim /etc/sudoers # User privilege specification 在root行后面增加 git ALL=(ALL:ALL) ALL # 保存退出后,修改回文件权限 chmod 440 /etc/sudoers
配置SSH
# 切换到git用户下 su git # 生成ssh密钥对 注意密码要为空,Travis自动过程中才不会被输入密码步骤卡住,生成后目录 /home/git/.ssh/id_rsa ssh-keygen -t rsa # 设置.ssh目录为700 chmod 700 ~/.ssh/ # 设置.ssh目录下的文件为600 chmod 600 ~/.ssh/* # 切换到.ssh/目录 cd .ssh/ # 将公钥内容添加到authorized_keys cat id_rsa.pub >> authorized_keys
Travis配置
# 在home目录下拉取Source仓库 cd /home git clone 你的仓库.git # cd到仓库根目录 # 登录github帐号 travis login --auto # 生成加密公钥文件id_rsa.enc并自动增加解密行到.travis.yml文件;-r 指向GitHub的Source仓库 travis encrypt-file ~/.ssh/id_rsa --add -r redanula/blog-hexo-source
修改.travis.yml,在before_install钩子后面添加权限处理:
before_install: - chmod 600 ~/.ssh/id_rsa - echo -e "Host 【配置名】\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config
修改.travis.yml,在after_success钩子后面添加推送行为:
#/var/www/blog为博客目录 after_script: - rsync -rv --delete -e 'ssh -o stricthostkeychecking=no -p 对应ssh端口' public/ git@对应的IP或域名:/var/www/blog
这里注意
~/.ssh/id_rsa
路径需要去掉travis命令自动添加的转义 \ 这里的key和iv文件就是上文图中Travis后台自动出现的两个值openssl aes-256-cbc -K $encrypted_598a070685f0_key -iv $encrypted_598a070685f0_iv -in id_rsa.enc -out ~/.ssh/id_rsa -d
推送.travis.yml文件到Source(也可以vps先推送上去本地拉下来做上述几步的修改)
git add ./ git commit -m "Travis CI" git push
接下来就可以随意只管提交文章,Travis会自动持续集成到GitHub Page 和 服务器了:)。
Done. Your build exited with 0.
参考文章
Hexo - 使用 Travis CI 自動佈署 Blog
使用 Travis 自动部署 Hexo 到 Github 与 自己的服务器
用TravisCI持续集成自动部署Hexo博客的个人实践
使用travis-ci自动部署hexo博客
Deploying Pharo builds from Travis over ssh
Travis-CI自动化测试并部署至自己的CentOS服务器
Hexo搭建个人博客并使用Git部署到VPS
iOS腾讯云人脸识别
前言
工作需要做了一个简易的、通过API第三方人脸识别、人脸对比分析的App Demo,对比了一下各家提供的服务、SDK,最后选择了腾讯云的智能图像服务。
调用API的流程为:
1.鉴权签名
2.调用人脸识别、人脸对比等API。
这里主要在iOS上模拟生成鉴权签名识别的流程,实际生产环境应该是服务器生成鉴权签名,APP调用接口识别。
鉴权签名
云端开通服务后的Key:
#define kAPPID @"your APPID"
#define kSecretId @"your SecretId"
#define kSecretKey @"your SecretKey"
拼接签名串:
- (NSString *)getSign{
NSDate* date = [NSDate dateWithTimeIntervalSinceNow:0];//获取当前时间0秒后的时间
NSDate* edate = [NSDate dateWithTimeIntervalSinceNow:60*60*24*30];//一个月
NSTimeInterval time=[date timeIntervalSince1970];// *1000 是精确到毫秒,不乘就是精确到秒
NSTimeInterval etime=[edate timeIntervalSince1970];// *1000 是精确到毫秒,不乘就是精确到秒
NSString *timeString = [NSString stringWithFormat:@"%.0f", time];
NSString *etimeString = [NSString stringWithFormat:@"%.0f", etime];
int ranInt = arc4random() %100000;
NSString *ranString = [NSString stringWithFormat:@"%d", ranInt];
NSString *abketr = [NSString stringWithFormat:@"a=%@&b=%@&k=%@&e=%@&t=%@&r=%@",
kAPPID,
@"tencentyun",
kSecretId,
etimeString,
timeString,
ranString
];
NSString *signString = [self HmacSha1:kSecretKey data:abketr];
return signString;
}
HMAC-SHA1 算法加密,注意最后需要拼接签名串到NSMutableData末尾:
//HmacSHA1加密;
- (NSString *)HmacSha1:(NSString *)key data:(NSString *)data
{
const char *cKey = [key cStringUsingEncoding:NSASCIIStringEncoding];
const char *cData = [data cStringUsingEncoding:NSASCIIStringEncoding];
unsigned char cHMAC[CC_SHA1_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA1, cKey, strlen(cKey), cData, strlen(cData), cHMAC);
NSData *HMAC = [[NSData alloc] initWithBytes:cHMAC length:sizeof(cHMAC)];
NSData *dData = [[NSData alloc] initWithBytes:cData length:strlen(cData)];
NSMutableData *mData = [[NSMutableData alloc] init];
[mData appendData:HMAC];
[mData appendData:dData];
NSString *hash = [mData base64EncodedStringWithOptions:0];//将加密结果进行一次BASE64编码。
return hash;
}
如果是SHA256,可以:
unsigned char cHMAC[CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, cKey, strlen(cKey), cData, strlen(cData), cHMAC);
人脸检测
通过生成的签名添加到请求头authorization里面,请求如下:
NSString *apiString = [NSString stringWithFormat:@"%@%@",@"https://recognition.image.myqcloud.com",@"/face/detect"];
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager setRequestSerializer:[AFHTTPRequestSerializer serializer]];
[manager.requestSerializer setValue:@"recognition.image.myqcloud.com" forHTTPHeaderField:@"host"];
[manager.requestSerializer setValue:[self getSign] forHTTPHeaderField:@"authorization"];
[manager POST:apiString parameters:params constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
[formData appendPartWithFileData:imageData name:@"image" fileName:@"image.jpg" mimeType:@"image/jpg"];
} progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
block(responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
block(nil);
}];
其他接口可以查看:
1.腾讯云鉴权签名
2.人脸识别接口文档
.Net Core 添加 HTTP Headers
前言
基于安全的需求,要在API(基于.Net Core)交互的过程中除了特有的鉴权外,还添加额外的Header。
Strict-Transport-Security
1.概述
HTTP Strict Transport Security is an excellent feature to support on your site and strengthens your implementation of TLS by getting the User Agent to enforce the use of HTTPS. Recommended value strict-transport-security: max-age=31536000; includeSubDomains.
HTTP Strict Transport Security(HTTP严格安全传输) 通常简称为HSTS,要求浏览器只能通过HTTPS访问当前资源,而不是HTTP。浏览器会自动把所有尝试使用HTTP的请求自动替换为HTTPS请求,在一定范围内(如钓鱼网站等)防止中间人攻击。
2.示例
Strict-Transport-Security: max-age=31536000
//includeSubDomains适用于该网站的所有子域名
Strict-Transport-Security: max-age=31536000; includeSubDomains
//Google维护 首次浏览器加载后可以预加载到缓存,需要向Google申请
Strict-Transport-Security: max-age=31536000; preload
Public-Key-Pins
1.概述
HTTP Public Key Pinning protects your site from MiTM attacks using rogue X.509 certificates. By whitelisting only the identities that the browser should trust, your users are protected in the event a certificate authority is compromised.
HTTP公钥固定(又称HTTP公钥钉扎,英语:HTTP Public Key Pinning,缩写HPKP)是HTTPS网站防止攻击者利用数字证书认证机构(CA)错误签发的证书进行中间人攻击的一种安全机制,用于预防CA遭受入侵或其他会造成CA签发未授权证书的情况。
2.示例
/*
pin-sha256 即证书指纹,允许出现多次(实际上最少应该指定两个);
max-age 和 includeSubdomains (可选) 与HSTS一致;
report-uri(可选) 用来指定验证失败时的上报地址,格式和含义跟 CSP(Content Security Policy)中的同名字段一致;
*/
Public-Key-Pins: pin-sha256="base64=="; max-age=expireTime [; includeSubdomains][; report-uri="reportURI"]
X-Frame-Options
1.概述
X-Frame-Options tells the browser whether you want to allow your site to be framed or not. By preventing a browser from framing your site you can defend against attacks like clickjacking. Recommended value x-frame-options: SAMEORIGIN.
X-Frame-Options是用来给浏览器指示允许一个页面可否在 frame,iframe 或者 object 标签中展现的标记。避免了点击劫持 (clickjacking) 的攻击。
2.示例
在IIS中配置如下:
<system.webServer>
...
<httpProtocol>
<customHeaders>
<add name="X-Frame-Options" value="SAMEORIGIN" />
</customHeaders>
</httpProtocol>
...
</system.webServer>
X-Content-Type-Options
1.概述
X-Content-Type-Options stops a browser from trying to MIME-sniff the content type and forces it to stick with the declared content-type. The only valid value for this header is X-Content-Type-Options: nosniff
通常浏览器会根据Content-Type字段来区分类型,X-Content-Type-Options用来禁用浏览器的 MIME 类型嗅探。
2.示例
X-Content-Type-Options: nosniff
Referrer-Policy
1.概述
Referrer Policy is a new header that allows a site to control how much information the browser includes with navigations away from a document and should be set by all sites.
HTTP来源地址(referer,或HTTP referer)是HTTP表头的一个字段,用来表示从哪儿链接到目前的网页,采用的格式是URL。换句话说,借着HTTP来源地址,目前的网页可以检查访客从哪里而来,这也常被用来对付伪造的跨网站请求。Referrer维基百科
2.示例
Referrer-Policy: no-referrer
Referrer-Policy: no-referrer-when-downgrade
Referrer-Policy: origin
Referrer-Policy: origin-when-cross-origin
Referrer-Policy: same-origin
Referrer-Policy: strict-origin
Referrer-Policy: strict-origin-when-cross-origin
Referrer-Policy: unsafe-url
同源、跨域等策略详细参考:同源策略
Content-Security-Policy
1.概述
Content Security Policy is an effective measure to protect your site from XSS attacks. By whitelisting sources of approved content, you can prevent the browser from loading malicious assets.
Content Security Policy,网页安全政策,缩写 CSP。CSP可以添加来源白名单,防止XSS攻击,更进一步可以通过report-uri
记录异常行为。
2.示例
Content-Security-Policy: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;
X-XSS-Protection
1.概述
X-XSS-Protection sets the configuration for the cross-site scripting filter built into most browsers. Recommended value “X-XSS-Protection: 1; mode=block”.
X-XSS-Protection 是浏览器默认开启的,防止XSS的保护机制。可以为不支持 CSP 的旧版浏览器的用户提供保护。
2.示例
//关闭
X-XSS-Protection: 0
//默认开始,清除页面
X-XSS-Protection: 1
//开启,不加载
X-XSS-Protection: 1; mode=block
//CSP 通过`report-uri`记录异常行为
X-XSS-Protection: 1; report=<reporting-uri>
.Net Core的 Header 添加
了解以上Header后,对应就要添加.Net Core的Header了,详细参考:How to add default security headers in ASP.NET Core using custom middleware
构建对应的SecurityHeadersMiddleware,然后在Startup.cs
的Configure
添加:
app.UseSecurityHeadersMiddleware(new SecurityHeadersBuilder().AddDefaultSecurePolicy());
也可以参考:ADDING HTTP HEADERS TO IMPROVE SECURITY IN AN ASP.NET MVC CORE APPLICATION,在 NuGet Package 里添加 NWebsec.AspNetCore.Middleware
然后在Startup.cs
的Configure
添加:
app.UseHsts(hsts => hsts.MaxAge(365).IncludeSubdomains());
app.UseXContentTypeOptions();
app.UseReferrerPolicy(opts => opts.NoReferrer());
app.UseXXssProtection(options => options.EnabledWithBlockMode());
app.UseXfo(options => options.Deny());
app.UseCsp(opts => opts
.BlockAllMixedContent()
.StyleSources(s => s.Self())
.StyleSources(s => s.UnsafeInline())
.FontSources(s => s.Self())
.FormActions(s => s.Self())
.FrameAncestors(s => s.Self())
.ImageSources(s => s.Self())
.ScriptSources(s => s.Self())
);
上述文章还推荐了一个在线扫描Header的网站:https://securityheaders.com/ 可以试试网站的安全评分。
按上述添加完后的响应的Header,Done:
参考
HTTP Strict Transport Security
HTTP Public Key Pinning 介绍
X-Frame-Options 响应头
Reducing MIME type security risks)
Content Security Policy 入门教程
X-Frame-Options 响应头
swagger生成xml文件上传到azure云主机
swagger xml文件默认不上传到azure云主机,如需要上传需要修改 csproj 文件(Asp.Net Core环境)。添加如下配置:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<Target Name="IncludeDocFile" BeforeTargets="PrepareForPublish">
<ItemGroup Condition=" '$(DocumentationFile)' != '' ">
<_DocumentationFile Include="$(DocumentationFile)" />
<ContentWithTargetPath Include="@(_DocumentationFile->'%(FullPath)')"
RelativePath="%(_DocumentationFile.Identity)"
TargetPath="%(_DocumentationFile.Filename)%(_DocumentationFile.Extension)"
CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
</Target>
SQL--数据导入
之前写API及处理后台数据的时候写了一些脚本,记录一下。
快捷数据表操作的存储过程脚本。eg: exec ‘Z_InitData’,’目标表T1’,’源表T2’
Create PROCEDURE Z_InitData
@tbname nvarchar(100),
@fromtbname nvarchar(100)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
declare @sql nvarchar(max)
declare @col nvarchar(max)
set @sql = 'select @col = stuff((select '',''+name from syscolumns where id=object_id( '''+ @tbname +''') order by name for xml path('''')),1,1,'''')'
exec sp_executesql @sql,N'@col nvarchar(max) output',@col output
set @sql = ' truncate table '+ @tbname +';'
set @sql = @sql + ' set IDENTITY_INSERT '+ @tbname +' on ;'
set @sql = @sql + ' insert into '+ @tbname + '('+ @col +') select '+@col+ ' from ' + @fromtbname
set @sql = @sql + '; set IDENTITY_INSERT '+ @tbname +' off;'
print(@sql)
exec(@sql)
END
打印表字段,方便insert。eg: exec ‘Z_Print’,’目标表T1’
ALTER PROCEDURE Z_Print
@tbname nvarchar(100) ='TTT'
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
declare @sql nvarchar(max)
declare @col nvarchar(max)
set @sql = 'select @col = stuff((select '',''+name from syscolumns where id=object_id( '''+ @tbname +''') order by name for xml path('''')),1,1,'''')'
exec sp_executesql @sql,N'@col nvarchar(max) output',@col output
set @sql = ' insert into '+ @tbname + '('+ @col +') '
print(@sql)
END
iOS本地时间与同步
概念
GMT(Greenwich Mean Time)格林尼治时间
UTC(Coordinated Universal Time )原子钟标准时间
Unix time(以UTC 1970年1月1号 00:00:00为基准时间)
sysctl API
iOS系统上次设备重启时间(iOS9后不能使用)
#include <sys/sysctl.h>
- (long)bootTime
{
#define MIB_SIZE 2
int mib[MIB_SIZE];
size_t size;
struct timeval boottime;
mib[0] = CTL_KERN;
mib[1] = KERN_BOOTTIME;
size = sizeof(boottime);
if (sysctl(mib, MIB_SIZE, &boottime, &size, NULL, 0) != -1)
{
return boottime.tv_sec;
}
return 0;
}
时间校准
1、记录上一次请求的时候获取服务器时间serverTime,与本地时间lastLocalTime
2、需要计算的时候取出curLocalTime(即NSDate),计算与lastLocalTime的偏移量T
3、serverTime+T 就是当前的服务器时间
另外,计算运行时长,计算自上一次重启后的运行时间
//get system uptime since last boot
- (NSTimeInterval)uptime
{
struct timeval boottime;
int mib[2] = {CTL_KERN, KERN_BOOTTIME};
size_t size = sizeof(boottime);
struct timeval now;
struct timezone tz;
gettimeofday(&now, &tz);
double uptime = -1;
if (sysctl(mib, 2, &boottime, &size, NULL, 0) != -1 && boottime.tv_sec != 0)
{
uptime = now.tv_sec - boottime.tv_sec;
uptime += (double)(now.tv_usec - boottime.tv_usec) / 1000000.0;
}
return uptime;
}
iOS Tips--NSDictionary和NSArray 快捷计算函数
NSArray 有快捷的汇总公式,只要用 @函数.键路径 即可:
NSArray *array = [NSArray arrayWithObjects:@"1.0",@"2.0",nil];
CGFloat sum = [[array valueForKeyPath:@"@sum.floatValue"] floatValue];
CGFloat avg = [[array valueForKeyPath:@"@avg.floatValue"] floatValue];
CGFloat max =[[array valueForKeyPath:@"@max.floatValue"] floatValue];
CGFloat min =[[array valueForKeyPath:@"@min.floatValue"] floatValue];
如果汇总值是在NSDictionary里面,同样可以取值进行汇总:
NSArray *dic = [{@"KeyName1":@"1.0",@"KeyName2":@"abc"},{@"KeyName1":@"2.0",@"KeyName2":@"bcd"}];
CGFloat sum = [[[dic valueForKeyPath:@"KeyName1"] valueForKeyPath:@"@sum.floatValue"] floatValue];
CGFloat max = [[[dic valueForKeyPath:@"KeyName1"] valueForKeyPath:@"@max.floatValue"] floatValue];
以上快捷汇总实际是KVC的用法,更多KVC的用法参考
1.http://www.jianshu.com/p/b7dda8d49dc4
2.https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/KeyValueCoding.html
Python链家爬虫
业余时间做了个链家的爬虫,爬取数据写入sqlite,方便浏览和对比。
具体参考了冰蓝大牛的博客http://lanbing510.info/2016/03/15/Lianjia-Spider.html?utm_source=tuicool&utm_medium=referral, 根据链家最新的web样式做了修改(2017-03)爬在售的二手房的数据。
链家对短时间内同一个IP的流量有监控,所以如果用多线程去爬,太快可能会被要求输入验证码。试了以下用代理ip池去爬,因为可用的免费代理ip不多也不稳定,就放弃了。后面想数据爬的也不多,就加个延时模拟人为慢慢爬了,另外还有个问题可能是单位时间内Request太快可能解析不到数据,就把延时放在BeautifulSoup解析后面,并加了如果没有数据则重复5次请求的验证过程,才成功抓全数据:
time.sleep(np.random.rand()*3+3)
这里贴出关键的匹配代码,代码好粗糙,仅供发参考:)
def onsell_spider(mydb,url_page=u"http://gz.lianjia.com/ershoufang/pg1rs越秀/",area=u"越秀"):
# time.sleep(np.random.rand()*1)
print url_page
counts = 0
trytime = 0
while counts==0 & trytime<=5:
try:
req = urllib2.Request(url_page,headers=hds[random.randint(0,len(hds)-1)])
source_code = urllib2.urlopen(req,timeout=10).read()
plain_text=unicode(source_code)#,errors='ignore')
soup = BeautifulSoup(plain_text, "lxml")
except (urllib2.HTTPError, urllib2.URLError), e:
print e
exception_write('onsell_spider',url_page)
return
except Exception,e:
print e
exception_write('onsell_spider',url_page)
return
time.sleep(np.random.rand()*3+3)
cj_list=soup.findAll('div',{'class':'info clear'})
print len(cj_list)
counts = len(cj_list)
trytime = trytime + 1
for cj in cj_list:
info_dict={}
href=cj.find('a')
if not href:
continue
info_dict.update({u'链接':href.attrs['href']})
name=cj.find('a').text
info_dict.update({u'标题':name})
#href TEXT primary key UNIQUE, name TEXT, community TEXT, style TEXT, area TEXT, orientation TEXT,decoration TEXT,haslift TEXT,floor TEXT, year TEXT, bplace TEXT,splace TEXT, unit_price TEXT, total_price TEXT, subway TEXT, other TEXT
content=unicode(cj.find('div',{'class':'houseInfo'}).renderContents().strip())
info=re.match(r"<span .*></span><a .*>(.*)</a>(.*)", content)
# print info
if info:
info=info.groups()
info_dict.update({u'小区':info[0]})
str = info[1].strip().split('|')
# print str[1]
try:
info_dict.update({u'户型':str[1].strip()})
except Exception,e:
info_dict.update({u'户型':''})
try:
info_dict.update({u'面积':str[2].strip()})
except Exception,e:
info_dict.update({u'面积':''})
try:
info_dict.update({u'朝向':str[3].strip()})
except Exception,e:
info_dict.update({u'朝向':''})
try:
info_dict.update({u'装修':str[4].strip()})
except Exception,e:
info_dict.update({u'装修':''})
try:
info_dict.update({u'有无电梯':str[5].strip()})
except Exception,e:
info_dict.update({u'有无电梯':''})
content=unicode(cj.find('div',{'class':'positionInfo'}).renderContents().strip())
info=re.match(r"<span .*></span>(.*)\)(.*)<a .*>(.*)</a>", content)
if info:
info=info.groups()
# print info
info_dict.update({u'楼层':info[0]})
info_dict.update({u'建造时间':info[1]})
info_dict.update({u'大区域':area})
try:
info_dict.update({u'小区域':info[2]})
except Exception,e:
info_dict.update({u'小区域':info[2]})
content=cj.find('div',{'class':'unitPrice'}).find('span').text
if content:
info_dict.update({u'单价':content})
content=cj.find('div',{'class':'totalPrice'}).find('span').text
if content:
info_dict.update({u'总价':content})
content=cj.find('span',{'class':'subway'})
# print content
if content:
try:
info_dict.update({u'地铁':content.text})
except Exception,e:
info_dict.update({u'地铁':''})
content=cj.find('div',{'class':'followInfo'}).text
if content:
info_dict.update({u'其他':content})
command=sql_onsell_insert_command(info_dict)
mydb.execute(command,1)
def do_onsell_spider(mydb,area=u"越秀"):
url=u"http://gz.lianjia.com/ershoufang/pg%drs%s/" % (1,area)
try:
req = urllib2.Request(url,headers=hds[random.randint(0,len(hds)-1)])
source_code = urllib2.urlopen(req,timeout=10).read()
plain_text=unicode(source_code)#,errors='ignore')
soup = BeautifulSoup(plain_text, "lxml")
except (urllib2.HTTPError, urllib2.URLError), e:
print e
exception_write('do_onsell_spider',area)
return
except Exception,e:
print e
exception_write('do_onsell_spider',area)
return
time.sleep(np.random.rand()*1+1)
content=soup.find('div',{'class':'page-box house-lst-page-box'})
# print soup
if content:
d="d="+content.get('page-data')
exec(d)
total_pages=d['totalPage']
print total_pages
for i in range(total_pages):
time.sleep(np.random.rand()*1)
url_page=u"http://gz.lianjia.com/ershoufang/pg%drs%s/" % (i+1,area)
onsell_spider(mydb,url_page,area)
对BeautifulSoup的方法还不太熟悉,用的都是简单粗暴的方法,后续再去细看了。
爬下来的数据: