perl获取AppAnnie数据

来源:互联网 发布:淘宝假授权书处罚 编辑:程序博客网 时间:2024/05/20 16:12

。。。。。。

需求:
获取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
    • 简单开始
      • html转义字符处理
    • 处理登陆
    • 获取chart数据
    • json utf8 unicode
    • Excel
    • Threads 节约时间
    • 细节和总结
    • Annie Api


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/&amp;/&/g;    $app_name =~ s/&#39;/'/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不同账号分布式同时进行,有待研究。。。

1 0