docs: Python 脚本实现爬取疫情数据并用 Markdown 预览

This commit is contained in:
杭城小刘
2020-04-11 02:42:38 +08:00
parent 32d90b27fc
commit 62c3544f10
6 changed files with 403 additions and 10 deletions

View File

@@ -1861,7 +1861,7 @@ iOS 网络现状是由4层组成的最底层的 BSD Sockets、SecureTransport
#### 2.1 NSURLProtocol 监控 App 网络请求
#### 2.1 NSURLProtocol 监控 App 网络请求<a name="network-2.1"></a>
NSURLProtocol 作为上层接口,使用较为简单,但 NSURLProtocol 属于 URL Loading System 体系中。应用协议的支持程度有限,支持 FTP、HTTP、HTTPS 等几个应用层协议,对于其他的协议则无法监控,存在一定的局限性。如果监控底层网络库 CFNetwork 则没有这个限制。
@@ -2341,9 +2341,9 @@ API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0))
#### 2.2 骚操作篇
#### 2.2 骚操作篇 <a name="network-2.2"></a>
在 2.1 分析到了 NSURLSessionTaskMetrics 由于兼容性问题,对于网络监控来说似乎不太完美,但是自后在搜资料的时候看到了一篇[文章](https://www.jianshu.com/p/1c34147030d1)。文章在分析 WebView 的网络监控的时候分析 Webkit 源码的时候发现了下面代码
文章上面 [2.1 ](#network-2.1)分析到了 NSURLSessionTaskMetrics 由于兼容性问题,对于网络监控来说似乎不太完美,但是自后在搜资料的时候看到了一篇[文章](https://www.jianshu.com/p/1c34147030d1)。文章在分析 WebView 的网络监控的时候分析 Webkit 源码的时候发现了下面代码
```objective-c
#if !HAVE(TIMINGDATAOPTIONS)
@@ -2441,9 +2441,9 @@ NSURLSession 在 iOS9 之前使用 `_setCollectsTimingData:` 就可以使用 Tim
在 iOS 中 AOP 的实现是基于 Runtime 机制目前由3种方式Method Swizzling、NSProxy、FishHook主要用用于 hook c 代码)。
2.1 讨论了满足大多数的需求的场景NSURLProtocol 监控了 NSURLConnection、NSURLSession 的网络请求自身代理后可以发起网络请求并得到诸如请求开始时间、请求结束时间、header 信息等,但是无法得到非常详细的网络性能数据,比如 DNS 开始解析时间、DNS 解析用了多久、reponse 开始返回的时间、返回了多久等。 iOS10 之后 NSURLSessionTaskDelegate 增加了一个代理方法 `- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));`,可以获取到精确的各项网络数据。但是具有兼容性。2.2 讨论了一个 Webkit 源码中得到的信息,通过私有方法 `_setCollectsTimingData:` 、`_timingData` 可以获取到 TimingData。
文章上面 [2.1 ](#network-2.1)讨论了满足大多数的需求的场景NSURLProtocol 监控了 NSURLConnection、NSURLSession 的网络请求自身代理后可以发起网络请求并得到诸如请求开始时间、请求结束时间、header 信息等,但是无法得到非常详细的网络性能数据,比如 DNS 开始解析时间、DNS 解析用了多久、reponse 开始返回的时间、返回了多久等。 iOS10 之后 NSURLSessionTaskDelegate 增加了一个代理方法 `- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));`,可以获取到精确的各项网络数据。但是具有兼容性。文章上面 [2.2 ](#network-2.2)讨论了 Webkit 源码中得到的信息,通过私有方法 `_setCollectsTimingData:` 、`_timingData` 可以获取到 TimingData。
但是如果需要监全部的网络请求就不能满足需求了,查阅资料后发现了阿里百川有 APM 的解决方案,对于网络监控需要做如下的 hook
但是如果需要监全部的网络请求就不能满足需求了,查阅资料后发现了阿里百川有 APM 的解决方案,于是有了方案3对于网络监控需要做如下的处理
![network hook](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-04-network_monitor.png)
@@ -2546,7 +2546,292 @@ void printResponseData (CFDataRef responseData) {
}
```
Method Swizzling 方法替换需要知道类名,我们在开发阶段使用 NSURLConnection、NSURLSession 都需要指定代理对象,且代理对象在该阶段没有办法确定,所以在此处的思路是在 NSURLConnection、NSURLSession 设置代理的时候替换掉代理对象,
我们知道 NSURLSession、NSURLConnection、CFNetwork 的使用都需要调用一堆方法进行设置然后需要设置代理对象,实现代理方法。所以针对这种情况进行监控首先想到的是使用 runtime hook 掉方法层级。但是针对设置的代理对象的代理方法没办法 hook因为不知道代理对象是哪个类。所以想办法可以 hook 设置代理对象这个步骤,将代理对象替换成我们设计好的某个类,然后让这个类去实现 NSURLConnection、NSURLSession、CFNetwork 相关的代理方法。然后在这些方法的内部都去调用一下原代理对象的方法实现。所以我们的需求得以满足,我们在相应的方法里面可以拿到监控数据,比如请求开始时间、结束时间、状态码、内容大小等。
具体步骤及其关键代码如下,以 NSURLConnection 举例
- 因为要 Hook 挺多地方,所以写一个 method swizzling 的工具类
```objective-c
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (hook)
/**
hook对象方法
@param originalSelector 需要hook的原始对象方法
@param swizzledSelector 需要替换的对象方法
*/
+ (void)apm_swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector;
/**
hook类方法
@param originalSelector 需要hook的原始类方法
@param swizzledSelector 需要替换的类方法
*/
+ (void)apm_swizzleClassMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector;
@end
NS_ASSUME_NONNULL_END
+ (void)apm_swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
{
class_swizzleInstanceMethod(self, originalSelector, swizzledSelector);
}
+ (void)apm_swizzleClassMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
{
//类方法实际上是储存在类对象的类(即元类)中,即类方法相当于元类的实例方法,所以只需要把元类传入,其他逻辑和交互实例方法一样。
Class class2 = object_getClass(self);
class_swizzleInstanceMethod(class2, originalSelector, swizzledSelector);
}
void class_swizzleInstanceMethod(Class class, SEL originalSEL, SEL replacementSEL)
{
Method originMethod = class_getInstanceMethod(class, originalSEL);
Method replaceMethod = class_getInstanceMethod(class, replacementSEL);
if(class_addMethod(class, originalSEL, method_getImplementation(replaceMethod),method_getTypeEncoding(replaceMethod)))
{
class_replaceMethod(class,replacementSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
}else {
method_exchangeImplementations(originMethod, replaceMethod);
}
}
```
- 建立一个继承自 NSProxy 抽象类的类,实现相应方法。
```objective-c
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
// 为 NSURLConnection、NSURLSession、CFNetwork 代理设置代理转发
@interface NetworkDelegateProxy : NSProxy
+ (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate;
@end
NS_ASSUME_NONNULL_END
// .m
@interface NetworkDelegateProxy () {
id _originalTarget;
id _NewDelegate;
}
@end
@implementation NetworkDelegateProxy
#pragma mark - life cycle
+ (instancetype)sharedInstance {
static NetworkDelegateProxy *_sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [NetworkDelegateProxy alloc];
});
return _sharedInstance;
}
#pragma mark - public Method
+ (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate
{
NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance];
instance->_originalTarget = originalTarget;
instance->_NewDelegate = newDelegate;
return instance;
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
if ([_originalTarget respondsToSelector:invocation.selector]) {
[invocation invokeWithTarget:_originalTarget];
[((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation];
}
}
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [_originalTarget methodSignatureForSelector:sel];
}
@end
```
- 创建一个对象,实现 NSURLConnection、NSURLSession、NSIuputStream 代理方法
```objective-c
// NetworkImplementor.m
#pragma mark-NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"%s", __func__);
}
- (nullable NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(nullable NSURLResponse *)response {
NSLog(@"%s", __func__);
return request;
}
#pragma mark-NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSLog(@"%s", __func__);
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSLog(@"%s", __func__);
}
- (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten
totalBytesWritten:(NSInteger)totalBytesWritten
totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite {
NSLog(@"%s", __func__);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"%s", __func__);
}
#pragma mark-NSURLConnectionDownloadDelegate
- (void)connection:(NSURLConnection *)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes {
NSLog(@"%s", __func__);
}
- (void)connectionDidResumeDownloading:(NSURLConnection *)connection totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes {
NSLog(@"%s", __func__);
}
- (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *) destinationURL {
NSLog(@"%s", __func__);
}
// 根据需求自己去写需要监控的数据项
```
- 给 NSURLConnection 添加 Category专门设置 hook 代理对象、hook NSURLConnection 对象方法
```objective-c
// NSURLConnection+Monitor.m
@implementation NSURLConnection (Monitor)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@autoreleasepool {
[[self class] apm_swizzleMethod:@selector(apm_initWithRequest:delegate:) swizzledSelector:@selector(initWithRequest: delegate:)];
}
});
}
- (_Nonnull instancetype)apm_initWithRequest:(NSURLRequest *)request delegate:(nullable id)delegate
{
/*
1. 在设置 Delegate 的时候替换 delegate。
2. 因为要在每个代理方法里面,监控数据,所以需要将代理方法都 hook 下
3. 在原代理方法执行的时候,让新的代理对象里面,去执行方法的转发,
*/
NSString *traceId = @"traceId";
NSMutableURLRequest *rq = [request mutableCopy];
NSString *preTraceId = [request.allHTTPHeaderFields valueForKey:@"head_key_traceid"];
if (preTraceId) {
// 调用 hook 之前的初始化方法,返回 NSURLConnection
return [self apm_initWithRequest:rq delegate:delegate];
} else {
[rq setValue:traceId forHTTPHeaderField:@"head_key_traceid"];
NSURLSessionAndConnectionImplementor *mockDelegate = [NSURLSessionAndConnectionImplementor new];
[self registerDelegateMethod:@"connection:didFailWithError:" originalDelegate:delegate newDelegate:mockDelegate flag:"v@:@@"];
[self registerDelegateMethod:@"connection:didReceiveResponse:" originalDelegate:delegate newDelegate:mockDelegate flag:"v@:@@"];
[self registerDelegateMethod:@"connection:didReceiveData:" originalDelegate:delegate newDelegate:mockDelegate flag:"v@:@@"];
[self registerDelegateMethod:@"connection:didFailWithError:" originalDelegate:delegate newDelegate:mockDelegate flag:"v@:@@"];
[self registerDelegateMethod:@"connectionDidFinishLoading:" originalDelegate:delegate newDelegate:mockDelegate flag:"v@:@"];
[self registerDelegateMethod:@"connection:willSendRequest:redirectResponse:" originalDelegate:delegate newDelegate:mockDelegate flag:"@@:@@"];
delegate = [NetworkDelegateProxy setProxyForObject:delegate withNewDelegate:mockDelegate];
// 调用 hook 之前的初始化方法,返回 NSURLConnection
return [self apm_initWithRequest:rq delegate:delegate];
}
}
- (void)registerDelegateMethod:(NSString *)methodName originalDelegate:(id<NSURLConnectionDelegate>)originalDelegate newDelegate:(NSURLSessionAndConnectionImplementor *)newDelegate flag:(const char *)flag
{
if ([originalDelegate respondsToSelector:NSSelectorFromString(methodName)]) {
IMP originalMethodImp = class_getMethodImplementation([originalDelegate class], NSSelectorFromString(methodName));
IMP newMethodImp = class_getMethodImplementation([newDelegate class], NSSelectorFromString(methodName));
if (originalMethodImp != newMethodImp) {
[newDelegate registerSelector: methodName];
NSLog(@"");
}
} else {
class_addMethod([originalDelegate class], NSSelectorFromString(methodName), class_getMethodImplementation([newDelegate class], NSSelectorFromString(methodName)), flag);
}
}
@end
```
这样下来就是可以监控到网络信息了,然后将数据交给数据上报 SDK按照下发的数据上报策略去上报数据。
顺道说一句,上面针对 NSURLConnection、NSURLSession、NSInputStream 代理对象的 hook 之后,利用 NSProxy 实现代理对象方法的转发,有另一种方法可以实现,那就是 **isa swizzling**。
- Method swizzling 原理
```objective-c
struct old_method {
SEL method_name;
char *method_types;
IMP method_imp;
};
```
![method swizzling](./../assets/2020-04-09-methodSwizzling.png)
method swizzling 改进版如下
```objective-c
Method originalMethod = class_getInstanceMethod(aClass, aSEL);
IMP originalIMP = method_getImplementation(originalMethod);
char *cd = method_getTypeEncoding(originalMethod);
IMP newIMP = imp_implementationWithBlock(^(id self) {
void (*tmp)(id self, SEL _cmd) = originalIMP;
tmp(self, aSEL);
});
class_replaceMethod(aClass, aSEL, newIMP, cd);
```
- isa swizzling
```objective-c
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
```
![method swizzling](./../assets/2020-04-09-methodSwizzling.png)
@@ -2556,7 +2841,7 @@ Method Swizzling 方法替换需要知道类名,我们在开发阶段使用 NS
#### 2.4 监控 App 常见网络请求
#### 2.4 监控 App 常见网络请求<a name="categoryNameRules"></a>
本着成本的原因,由于现在大多数的项目的网络能力都是通过 [AFNetworking](https://github.com/AFNetworking/AFNetworking) 完成的,所以本文的网络监控可以快速完成。

View File

@@ -407,10 +407,10 @@ if ([cls respondsToSelector:sel]) {
6. 网络请求完成后,通过 NetworkProtocolClient 将请求结果返回给 WKWebView。
### 5. 拦截 WebView 内 Ajax 请求
其实上述的方法也是可行,不过使用私有 API 的方式不是很推荐,一般在穷途末路的时候才选择私有 API所以另一种思路是 hook Web 端的 ajax 请求。在执行 hook 后的 ajax 请求的时候将 ajax 的请求相关信息请求方式、header、body 等)以 messageHandler 的方式告诉 Native然后起到监控的效果。
参考: https://www.jianshu.com/p/7337ac624b8ehttps://github.com/wendux/Ajax-hook

106
Chapter3 - Server/3.9.md Normal file
View File

@@ -0,0 +1,106 @@
# 爬取疫情数据并用 Markdown 预览
> 周五不困,无聊写了一个 Python 脚本,功能很简单:获取新浪关于各个国家疫情数据,并写入 md 文件并预览,定时去获取数据,有新数据则生成新的 markdown 内容拼接在文件最后。
## 一、 代码
由于功能和代码都很简单,直接上代码
```Python
# -*-coding:utf8-*-
import re,requests,json,pprint,time
import os
pattern=re.compile(r'^try{sinajp_15844213244528328543098388435\((.*?)\);}catch\(e\){};')
lasttimes='00:00:00'
while True:
res=requests.get('https://gwpre.sina.cn/ncp/foreign?_=1584421324452&callback=sinajp_15844213244528328543098388435')
match=pattern.search(res.text)
if match:
obj=json.loads(match.group(1))
resultObj=obj['result']
times=resultObj['times'] # 截止时间
timesMatch=re.search(r'截至(\d{2})月(\d{2})日(\d{2})时(\d{2})分',times)
if timesMatch:
times=timesMatch.group(1)+''+timesMatch.group(2)+''+timesMatch.group(3)+':'+timesMatch.group(4)
if times==lasttimes:
continue
else:
lasttimes=times
totalObj=resultObj['total']
certain=totalObj['certain'] # 累计确诊
die=totalObj['die'] # 死亡
recure=totalObj['recure'] # 治愈
certain_inc=totalObj['certain_inc'] # 确诊增加
die_inc=totalObj['die_inc'] # 死亡增加
recure_inc=totalObj['recure_inc'] # 治愈增加
# 各国数据列表
worldlistArr=resultObj['worldlist']
worldlistArr.sort(key=lambda x: int(x.get('conNum','0')),reverse=True)
fo=open('./coronavirus.md','a')
fo.writelines('\n# '+times+'\n')
fo.writelines('感染国家总数:'+str(len(worldlistArr))+'\n')
fo.writelines('```\n累计确诊:'+certain.rjust(10,' ')+' 较昨日:'+certain_inc+'\n'+'累计死亡:'+die.rjust(10,' ')+' 较昨日:'+die_inc+'\n'+'累计治愈:'+recure.rjust(10,' ')+' 较昨日:'+recure_inc+'\n```\n')
fo.writelines('|国家|新增确诊|累计确诊|新增死亡|累计死亡|累计治愈|'+'\n')
fo.writelines('|:--:|---:|---:|---:|---:|---:|'+'\n')
top15=worldlistArr[:15]
pattient_countrys=['澳大利亚','加拿大','巴西','印度','丹麦','越南','新加坡','俄罗斯','塞尔维亚','巴基斯坦',]
pattient=[c for c in worldlistArr if c['name'] in pattient_countrys]
for countryObj in top15:
name=countryObj['name'] # 国家
if name=='中国':
continue
conadd=countryObj['conadd'] # 新增确诊
conNum=countryObj['conNum'] # 累计确诊
deathadd=countryObj['deathadd'] # 新增死亡
deathNum=countryObj['deathNum'] # 累计死亡
cureNum=countryObj['cureNum'] # 累计治愈
fo.writelines('|'+name+'|'+conadd+'|'+conNum+'|'+deathadd+'|'+deathNum+'|'+cureNum+'|\n')
fo.writelines('\n特别关心'+'\n')
fo.writelines('|国家|新增确诊|累计确诊|新增死亡|累计死亡|累计治愈|'+'\n')
fo.writelines('|:--:|---:|---:|---:|---:|---:|'+'\n')
for countryObj in pattient:
name=countryObj['name'] # 国家
conadd=countryObj['conadd'] # 新增确诊
conNum=countryObj['conNum'] # 累计确诊
deathadd=countryObj['deathadd'] # 新增死亡
deathNum=countryObj['deathNum'] # 累计死亡
cureNum=countryObj['cureNum'] # 累计治愈
fo.writelines('|'+name+'|'+conadd+'|'+conNum+'|'+deathadd+'|'+deathNum+'|'+cureNum+'|\n')
fo.close()
# 用 Markdown IDE 打开 .md 文件进行预览
os.system('open -a "/Applications/Typora.app" ./coronavirus.md')
for i in range(1,61):
time.sleep(10)
print(i*10)2
```
## 二、 如何使用
1. 安装 requests
```shell
pip3 install requests
```
2. 修改 Markdown 的打开方式。由于我电脑安装 Marodown 编辑器是 `Typora`,所以脚本是 `open -a "/Applications/Typora.app" ./coronavirus.md`。修改这里的 `***.app` 为自己的 ide
3. 终端运行即可
```shell
python3 coronavirus.py
```

View File

@@ -11,3 +11,4 @@
* [6、YAML](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.6.md)
* [7、Node单元测试](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.7.md)
* [8、数据安全反爬虫之「防重放」策略](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.8.md)
* [9、爬取疫情数据并用 Markdown 预览](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.9.md)

View File

@@ -138,6 +138,7 @@
* [6、YAML](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.6.md)
* [7、Node单元测试](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.7.md)
* [8、数据安全反爬虫之「防重放」策略](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.8.md)
* [9、爬取疫情数据并用 Markdown 预览](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.9.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB