perl获取AppAnnie数据

1 篇文章 0 订阅
1 篇文章 0 订阅

。。。。。。

需求:
获取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/&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不同账号分布式同时进行,有待研究。。。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值