。。。。。。
需求:
获取https://www.appannie.com/apps/ios/top/united-states/games/?device=iphone 排行榜数据,并得到每个game的大小和语言(从排行榜点击对应game名字进入详细页面)以及分类(详细页面中点击左侧history)。
本blog主要用来记录coding中遇到的一些问题,相关知识点如下:
LWP(Library for www in Perl )
UserAgent and Cookies
perl正则表达式
utf8 and Json
threads 多线程处理
Spreadsheet 写入Excel
配置文件等小细节
目录
用 [TOC]
来生成目录:
before start
对perl的了解并不多,写过几个文件操作的脚本来简化工作流程,这次主要是为了帮中国好室友提升工作效率,同时提升对perl的驾驭能力,有目的的学习才是最有效率的!
这篇blog的所有code都可以在我的github页面找到https://github.com/playscforever/my_perl_study/blob/master/final/login_appannie_final.pl
简单开始
刚开始处于一无所知的状态,直接google如何利用perl获取网页内容:
use strict;
use warnings;
use LWP::Simple qw(get);
my $html = get( "https://www.appannie.com/apps/ios/top/united-states/games/?device=iphone" );
print $html;
bingo~ , 突然感觉好像成功了一半(虽然还没开始),需要的内容都出来了,接下来就要用正则来提取自己需要的信息,这里贴一下我学习perl和正则的两个教程,虽然大多数问题都是用google和百度来解决的,看看教程对perl了解个大概还是有必要的:
http://shouce.jb51.net/perl/index.html
http://qntm.org/files/perl/perl_cn.html简单分析$html里面的内容之后,写出正则提取需要的game名字和对应详细页面的url:
while($html =~ m/span title=\"(.*)\" class=\"oneline-info title-info\">\s*<a href=\"\/(.*)\">/g) {
$app_name = $1; # 这里的$1对应第一个括号(.*)
$app_url = $2;
}
参数g (global)可以匹配所有符合要求的字符串,当遇到符合要求的字符串之后便进入while循环中,其实最开始用到的方法是把$html split成一行一行的,然后逐行分析,后来发现有global参数才改成上面这种简答的形式。
html转义字符处理
有些标题里面会有&这样的特殊字符,提取出来之后就变成了 & amp; (没有空格),直接用替换简单处理
$app_name =~ s/&/&/g;
$app_name =~ s/'/'/g;
处理登陆
详细页面的url 需要稍微拼凑一下,还是蛮简单的
my $innerHtml = $ua->get("https://www.appannie.com/".$app_url);
好了,问题来了,这特么是一个需要登陆才能访问的页面啊我勒个擦!
ok,这里卡了一个星期 ,好了细节不描述了,直接贴代码说问题吧
#这里用到了UserAgent来模拟浏览器
my $ua = LWP::UserAgent->new();
#生成一个自动保存的cookie
my $cookie_jar = HTTP::Cookies->new(
file => "testcookies.txt",
autosave => 1,
);
$ua->cookie_jar( $cookie_jar );
$ua->agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36");
#先获取一下登陆页面
my $res = $ua->get("https://www.appannie.com/account/login/", Referer => "https://www.appannie.com/");
#这里是重点啊,在firefox或者chrom中登陆,然后观察传递的数据,会发现除了用户名和密码是必须要传递的以外,还有个token也是需要传递的,否则死也登陆不上去啊!这个token可以在header中用正则获取,然后一起post给服务器。
my $c = $res->header('set-cookie' );
$c =~ m{csrftoken=(\w+);};
my $token = $1;
$res = $ua->post("https://www.appannie.com/account/login/",
Content => [
csrfmiddlewaretoken=>$token ,
next => "/",
#这里需要替换成自己的用户名和密码,注意双引号中有些特殊字符是转义的,比如@
username => $cfg{username},
password=> $cfg{password},
]
);
print $res->content;
搞定了登陆就成功了一大半了,不同网站登陆方式都不太一样,按照总监的话来说,AppAnnie这里有点猥琐,一般网站都不会这么麻烦。这里附加一个美团登陆的例子:https://github.com/playscforever/my_perl_study/blob/master/login_success_meituan.pl 不需要用户名和密码,用浏览器登陆的时候看一下传递的token 复制过来就好,操作频繁会有验证码出现。
接着再用正则提取一下size 和 language
#这里的? 是惰性匹配,尽可能少的匹配
my($size,$empty,$language) = ($innerHtml->content =~ m/Size:<\/b>(.*?)<\/p><p><b>(.*?)<\/b>(.*?)<\/p>/);
获取chart数据
接下来要做的事情就是进入history页面获取游戏的类型~ 在get到history页面的内容之后,发现并没有想要的数据, 进入firefox开发者工具,查看页面发送的请求,会发现chart数据是进入history页面之后通过ajax获取的,好了 根据ajax的url来获取对应的数据吧!!(还是需要拼凑一下url)
my $res = $ua->get("https://www.appannie.com/".$tempUrl."rank-chart");
too young too simple… 这样理所当然是获取不到任何数据的,得到回应是 ajax only,也就是说服务器判断你的请求不是ajax请求,OK,继续查看当前请求发送了哪些数据(firefox按f12然后点击网络,进入对应页面即可),然后模仿登陆添加一下:
my $res = $ua->get("https://www.appannie.com/".$tempUrl."rank-chart",
Accept => "application/json, text/plain, */*",
#对应请求的类型
'X-Requested-With' =>"XMLHttpRequest",
'X-NewRelic-ID' =>"VwcPUFJXGwEBUlJSDgc=",
);
这样就能得到类似于这样的json数据(省略头尾) :
{“category”: {“name”: “\u8d5b\u8f66\u6e38\u620f (\u6e38\u620f)”, “id”: 7013}
json utf8 unicode
接下来就要解析一下json并处理一下unicode到中文的转换问题,关于中文乱码的问题,在前面加上use utf8::all;基本就ok 了 ,不然就encode decode什么的。
my $chart = $res->content;
#这一步是unicode转换
$chart =~ s/\\u([0-9a-fA-F]{4})/pack("U",hex($1))/eg;
my $json = new JSON;
#解析json数据
my $obj = $json->decode($chart);
my $type = "";
for (my $var = 0; $var <= $#{$obj->{data}}; $var++) {
#把每个data的name都拿出来
$type = $type.$obj->{data}->[$var]->{category}->{name};
}
#下面贴一下json数据
'{"meta": {"end": "2015-08-22", "vertical": "apps", "countries": "US", "f": null,
"app_id": "648668184", "start": "2015-07-24", "market": "ios"},
"data": [{"category": {"name": "\\u52a8\\u4f5c\\u6e38\\u620f (\\u6e38\\u620f)", "id": 7001},
"country": {"code": "US", "name": "\\u7f8e\\u56fd"}, "ranks": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 91, 3, 1, 0]}, {"category": {"name": "\\u6240\\u6709\\u7c7b\\u522b",
"id": 36}, "country": {"code": "US", "name": "\\u7f8e\\u56fd"}, "ranks":
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 984, 10, 1, 0]},
{"category": {"name": "\\u8d5b\\u8f66\\u6e38\\u620f (\\u6e38\\u620f)", "id": 7013}, "country":
{"code": "US", "name": "\\u7f8e\\u56fd"}, "ranks": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 27, 1, 1, 0]}, {"category": {"name": "\\u6e38\\u620f", "id": 6014}, "country":
{"code": "US", "name": "\\u7f8e\\u56fd"}, "ranks": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 301, 3, 1, 0]}], "events": ["", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "Initial release", "", "", "", ""],
"success": true}';
Excel
excel的处理,这里用到的是 Spreadsheet::WriteExcel,还算是比较简单吧,需要注意的就是不能操作一个已经打开的excel,并且不能在程序中打开excel之后很久再操作,否则无法写入。(有一次用了代理IP,结果访问网络要很久,然后得到的数据死活写不进去excel,后来花了1个多小时才发现原来是这个问题)
sub writeExcel{
# 打开之后立刻读写,不要停留很长时间
my $workbook = new Spreadsheet::WriteExcel( $cfg{outfilename} );
my $worksheet = $workbook->add_worksheet( $cfg{firstsheetname} );
# 这里的data是全局的
print Dumper \@data;
foreach my $i (0 .. $#data){
my @values = split(/\,\,\,/,$data[$i]);
foreach my $j (0 .. $#values){
#excel左上角第一个是【0,0】
$worksheet->write($i,$j,$values[$j]);
}
}
}
Threads 节约时间
因为AppAnnie是外国的站点,访问起来有点慢,一个网站打开要好几秒,想要获取100个数据就要十几分钟了,后来发现perl是可以实现多线程,然后果断加了进来:
#需要用到的模块
use threads;
use threads::shared;
。。。
# 需要调用的函数名,后面是函数对应的参数,这句话放在循环体中,把所有需要访问的url都加入threads里面
threads->create('getInnerData',$app_name,$app_url,$count/3);
。。。
#然后再取出所有的thread,开始同步执行
foreach ( threads->list() ){
$_->join( );
}
#。。。。 然后再getInnerData里面判断threads结束的方法:
writeExcel() unless threads->list(threads::running);
很不辛的是,加了threads之后,ip被安妮给封掉了,不过发封邮件,第二天差不多就解封了。
另外就是,thread访问的数据必须是shared的,否则就会出错
my @data : shared;
share(@data);
细节和总结
懒得写了,最后还是失败了,不过学到了很多东西,主要是加深了对perl的理解,正则也有进步,网络相关的只是也复习了一下,各方面都还是有待提高的,路漫漫啊,继续努力吧骚年!!!
一直都懒得写blog,总是把需要记住的东西写在云笔记,但是很少去总结去复习,以后还是要多写写blog,练练文笔的同时也可以梳理一下自己所学的东西。
还是提一下配置文件吧,我的配置文件比较简单,每行都是写成 key = value的形式,这样解析和访问起来也简单,直接正则加hash:
open I,"<yzj.cfg" or print("配置文件丢失 yzj.cfg 必须放在一起\n");
# cfg中存放yzj.cfg中的键值对 等号周围空格可有可无
my %cfg = map{m/(\w+?)\s*=\s*(.+)/} <I>;
close I;
Annie Api !!!!!!
在经历一连串失利之后(主要是需要登录才能访问的页面检测太严格了),上网查了一下发现安妮居然是有提供Api的,https://support.appannie.com/hc/en-us/categories/200261564
不过悲剧的是,这个api只能对自己的account和app才有用,卧槽这不是坑爹吗!!!
所以最后的结论是,获取排行榜的名单很容易,想要获取具体app/game的内容,那就只能用非常慢的速度去爬了,或者考虑用不同ip不同账号分布式同时进行,有待研究。。。