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分多个阶段,高阶的曲线更圆滑

贝塞尔曲线说明

贝塞尔曲线
Apple UIBezierPath

曲线的优化计算

1.移动到起始点 N1
2.当累计点到N2,N3,N4的时候,计算N2和N4的中点N3, 覆盖原来的N3点;
3.以N1为起始点对N3画一条二阶贝塞尔曲线,控制点为N2和N4
4.将N3做为起始点N1,N4作为第一个累计点N2,回到第一步

详细参考:Smooth Freehand Drawing

直线:取第一个点B和最后一个点E,画一条直线
矩形:取第一个点B和最后一个点E,范围内画一个矩形,需要注意起始点和终点的向量方向
椭圆:取第一个点B和最后一个点E,范围内画一个椭圆,需要注意起始点和终点的向量方向
点:当B和E的坐标相等时,就画一个点

动作,值a(1 正常绘图 2 撤销 3 恢复 4 清空等):
撤销:pop撤销栈最后一个临时图片并push恢复栈,渲染
恢复:pop恢复栈最后一个临时图片并push撤销栈,渲染
清空:清空所有栈及当前画板

点阵分片及重组

笔迹传输是按照每个笔迹采集的,如果一个笔迹的点阵数量过大,可以分片处理,NATS消息体里面会表示s和o表示分片的数量及序号,接收端需要收到全部分片后进行点阵d的数组重组及绘制。

时间戳

需要和视频流里面的时间戳消息做对比,小于视频流时间戳的时候才显示笔迹。

写在前面

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

构建流程如下:

  1. 新增或修改了文章,push到GitHub的source
  2. Travis CI 发现source改变后自动执行配置好的脚本,生成静态页面
  3. Travis 推送静态页面文件到VPS服务器
  4. Travis push静态页面到GitHub

GitHub 和 Travis 设置

因为之前已经建好Blog的仓库和GitHub Page的仓库了,这些网上都很多教程,并不复杂,就直接记录如何让Source的仓库和Travis关联的步骤:

  1. 进入GitHub的Setting页面

  2. 选择Developer settings

  3. 选择Personal access tokens,生成Travis需要的access token

  4. 填写一个token的描述,如hexoblog,选择repo,然后Generate token即可

  5. 这里要复制下生成的token(只允许看见一次),在Travis那边可以使用

  6. 打开 Travis 网站(https://travis-ci.org) ,org后缀的才是对GitHub Public仓库免费的,com的针对是付费的私有仓库的。使用GitHub账号登录。


    

  7. 网上很多教程说需要打开Build only if .travis.yml is present选项(.travis.yml存在才生成),但最新的设置好像没有这个选项了,这里也不会有影响,然后需要Add一个Token, 新增一个HEXO_TOKEN,ValuE那里填写刚刚GitHub复制的access token即可。
    iv和key是后面VPS那边自动生成的,这里可以先忽略。配置完以后就需要到博客仓库.travis.yml文件了。

本地配置

  1. 在本地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
    
  2. 提交.travis.yml文件到GitHub,这时候GitHub Page就会自动生成了。可以打开Travis查看对应的Current构建过程。首次提交的之后,发现生成的 redanula.github.io 是空白的,找了下原因是因为使用的next主题没有在Source仓库的hexo theme的目录,add & push对应的主题到Source仓库之后就成功了。接下来就是服务器的工作了。

VPS服务器配置

  1. 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
    
  2. 新增一个Git账号

    adduser git
    
  3. 赋予git用户sudo权限

    chmod 740 /etc/sudoers
    vim /etc/sudoers
    
    # User privilege specification 在root行后面增加
    git    ALL=(ALL:ALL) ALL
    
    # 保存退出后,修改回文件权限
    chmod 440 /etc/sudoers
    
  4. 配置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
    
  5. 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

前言

工作需要做了一个简易的、通过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.人脸识别接口文档

前言

基于安全的需求,要在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.csConfigure添加:

app.UseSecurityHeadersMiddleware(new SecurityHeadersBuilder().AddDefaultSecurePolicy());

也可以参考:ADDING HTTP HEADERS TO IMPROVE SECURITY IN AN ASP.NET MVC CORE APPLICATION,在 NuGet Package 里添加 NWebsec.AspNetCore.Middleware
然后在Startup.csConfigure添加:

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:
2018080101

参考

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云主机,如需要上传需要修改 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>

issue: dotnet/sdk/issues/795

之前写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

概念

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;
}

详细参考 http://mrpeak.cn/blog/ios-time/

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

业余时间做了个链家的爬虫,爬取数据写入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的方法还不太熟悉,用的都是简单粗暴的方法,后续再去细看了。

爬下来的数据: