Activity History经历了三大改版以后,这一次是 App 活动历史的终章了。本文将把相关内容整合成一篇文章的同时删除过往文章,以免信息繁杂和重复。本文将介绍活动信息的获取的关键方法,并提供进阶优化的 HTML Demo,希望帮助大家把活动信息应用在 Workflow 中。使用的数据来自于 AppZapp,AppZapp 是一个记录 iOS 应用活动历史的社区,其最大的优势是包含完整的版本更新信息,十分适合制作图示的 Activity History 流程。

分析

AppZapp 记录 iOS 应用活动历史相对而言十分完整,可惜其网站并不支持中国地区,这导致了中区 App Store 的应用不能正常调用,不太适合使用中区账号的用户。通过仔细分析,意识到 AppZapp 客户端是支持中区 App Store 应用的,正好能够帮助我们解决这一问题。详细分析的过程就不再详述了,最终分析发现确实可以将其加以利用。制作 Activity History 流程的目的只有一个,就是补充 App Store 关于 App 价格动向的信息,但进阶优化的过程需要自行修改调试的地方较多,所以如果你并非乐意折腾,我强烈推荐你关注起源实验室

前期工作

如上所述,此处需要调用 AppZapp App 的数据,因此我们先来分析 AppZapp 客户端的数据包。

  1. 在 iPhone 或 iPad 运行 Surge、Replica 等应用,下面以 Replica 为例展示;
  2. 打开 AppZapp 客户端,并进入任意一个具体 App 页面;
  3. 转回 Replica,查找形如以下的数据包: AppZapp-1
  4. 分析数据包,从 Request 中获取 POST 的内容,提取device-uid
  5. 分析阶段结束,本阶段主要任务是获取个人的device-uid

数据处理

虽说分析的结果是 POST 的,但是通过不断地实验,最终发现请求 AppZapp 数据的方式可为 GET/POST,在这里我们将使用 GET 代替 POST 来获取数据。现 Workflow 已支持 POST 等系列 Web Actions,你亦可修改为使用 POST 方式来获取数据。分析后,得知具体 URL 为:

http://mobile.appzapp.net/Sevice/Mobile5.asmx/GetAppDetailsLogDevice?storeKey=[Region]&deviceuid=[device-uid]&langKey=zh-Hans-CN&appID=[ID]&isIpad=false

其中各参数的含义如下:

  • [storeKey] :商店地区,这里选用 CN
  • [eviceuid] :上一分析阶段提取的唯一设备 uid
  • [langKey] :返回内容的语言,这里选用汉字
  • [appID] :App 的唯一 ID,可从 App Store 等途径获取
  • [isIpad] :false 表示不搜索 iPad 应用

如果使用 GET 方式请求,返回的数据将不是正规的 JSON(实质是 XML),因此需要采用正则表达式来去除首尾不相关内容。

<string xmlns="http://tempuri.org/">
...
</string>

实际处理方法也是很简单的,只需要应用如下正则表达式 Match 即可:

\{.+\}

需要指出,Workflow 已无法正常传递 XML 文件内容,请在 Match Text 之前将文件格式转为文本文档格式 .txt,如下。AppZapp-2上述工作完成后,熟悉的 JSON 便可呈现。接下来的工作就是使用 Workflow 解析和处理 JSON,相信这些内容大家都比较熟悉,这里就不一一阐述制作的过程了,如有困难可以搜索关键词 Workflow JSON 查阅学习。

进阶优化

通常我们想到输出文本信息的途径就是 Show Alert,但是你会发现历史过多的时候,内容看起来很糟糕,列表显示冗长且文本居中导致信息杂乱。历史显示对比 - Before现改用另一种显示的方法,使用 CSS + JavaScript 写出具有筛选功能的 HTML Demo,通过 Show Web Page 显示最终内容。这里假设诸位已经自主完成了上述必要信息获取的工作,那么这里只需要结合给出的 HTML Demo 做对应的修改即可。HTML Demo 如下(文末附件给出 .txt 文件):

/***提前说明***/
/*[Date]表示获取的该条历史时间;[PriceFrom]表示获取的该条历史变化前价格(对应于 Price Increase 和 Price Drop 内容);*/
/*[PriceTo]表示获取的该条历史变化后价格(对应于 Price Increase 和 Price Drop 内容);[Version]表示获取的该条历史更新版本号(对应于 Upgrade  内容)。*/

/***以下内容用于循环处理每条活动历史信息***/
/*对应于 Price Increase 的内容*/
<div class="container" name="PriceInc">
<div class="box">
<div class="date">
<p name="Price_data_date">[Date]</p>
</div>
<div class="content">
<li style="width: 35%;"><span style="color: rgba(244,67,54,0.8);">Price Increase</span></li><li style="text-align: center;width: 65%;"><p>¥<span name="Price">[Price From]</span> → ¥<span name="Price">[Price To]</span></p></li>
</div>
</div>
<div class="border">
</div>
</div>

/*对应于 Price Drop 的内容*/
<div class="container" name="PriceDrop">
<div class="box">
<div class="date">
<p name="Price_data_date">[Date]</p>
</div>
<div class="content">
<li style="width: 35%;"><span style="color: rgba(76,175,80,0.8);">Price Drop</span></li><li style="text-align: center;width: 65%;"><p>¥<span name="Price">[Price From]</span> → ¥<span name="Price">[Price To]</span></p></li>
</div>
</div>
<div class="border">
</div>
</div>

/*对应于 Upgrade 的内容*/
<div class="container" name="Upgrade">
<div class="box">
<div class="date">
<p>[Date]</p>
</div>
<div class="content">
<li style="width: 35%;"><span style="color: rgba(237,108,0,0.8);">Upgrade</span></li><li style="text-align: center;width: 65%"><p>Version [Version]<p></li>
</div>
</div>
<div class="border">
</div>
</div>

*****************************************************************

/***提前说明***/
/*[App Icon]表示获取的 App 图标 URL;[App Name]表示获取的 App 主名;*/
/*[Current Price]表示获取的当前价格;[Results]表示处理后每条历史的集合。*/

/***以下内容用于最终完整 HTML 生成***/
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title>Activity History</title>
<style>
h1 {padding: 0.4em 0 0 0;text-align: center;font-size: 50pt;font-weight: 600;margin: 0;}
body {font-family: sans-serif;}
a {text-decoration: none;color: #444;}
p {margin: 1rem 0;}
img {float: left;border-radius: 35px;margin-left: 0.5em;box-shadow: 5px 5px 10px 5px #ddd;}
li {list-style: none;display: inline-block;}
.header {font-size: 25pt;display: block;margin: 1em 2em 2em 2em;}
.header p {text-align: center;}
.menu {margin: 0 0 2em 5%;}
.main {padding: 0 5%;}
.box {padding: 0 5%;}
.date {font-size: 20pt;padding-left: 0.8rem;border-left: 0.3rem solid #999;color: #444;}
.date p {margin: 1rem 0 0 0;}
.content {font-size: 25pt;padding-left: 5rem;}
.border {height: 3px;width: 95%;margin-left: auto;margin-right: auto;background-color: #ddd;background-image: repeating-linear-gradient(-45deg, #fff, #fff 4px, transparent 4px, transparent 10px);}
.container:last-of-type .border {display: none;}
.link {border-radius: 6px;height: 77px;line-height: 80px;display: inline-block;font-size: 20pt;border-bottom: 3px solid rgba(233,233,233,1);padding-left: 32px;padding-right: 32px;margin: 0.5em;background: rgba(233,233,233,1);background-size: 40px 39px;box-shadow: 0px 2px 8px 0px rgba(158, 158, 158, 0.5);transition: border-color 0.5s;-webkit-transition: border-color 0.5s;-moz-transition: border-color 0.5s;-ms-transition: border-color 0.5s;-o-transition: border-color 0.5s;}
.footer {margin: 5em 0}
</style>
<script type="text/javascript" src="http://cdn.staticfile.org/jquery/1.11.0/jquery.min.js"></script>
<script type="text/javascript" src="http://cdn.bootcss.com/highcharts/5.0.0/highcharts.js"></script>
<script language="javascript">
var $All_Price = new Array();

window.onload=function(){
$PriceInc=document.getElementsByName("PriceInc");
$PriceDrop=document.getElementsByName("PriceDrop");
$Upgrade=document.getElementsByName("Upgrade");
$Price=document.getElementsByName("Price");
$Price_data_date=document.getElementsByName("Price_data_date");
$Price_data_value=$Price;
$AllDOM=getElementsByClassName("container");
$New_date=$AllDOM[$AllDOM.length-1].getElementsByTagName("p")[0].innerHTML;
$PriceInc_flag=1;
$PriceDrop_flag=1;
$Upgrade_flag=1;
$Chart_flag=0;
document.getElementById("PriceInc").style.borderBottom="3px solid rgba(244,67,54,0.8)";
document.getElementById("PriceDrop").style.borderBottom="3px solid rgba(76,175,80,0.8)";
document.getElementById("Upgrade").style.borderBottom="3px solid rgba(237,108,0,0.8)";
Price();
if(document.getElementsByTagName("h1")[0].innerHTML.length>=13){
document.getElementsByTagName("h1")[0].style.fontSize="35pt";
document.getElementsByTagName("h1")[0].style.padding="0.8em 0 0 0";
document.getElementsByClassName("header")[0].getElementsByTagName('p')[0].style.margin="1em 0";
}}

function getElementsByClassName(n){
var classElements = [],allElements = document.getElementsByTagName('*');
for (var i=0; i< allElements.length; i++ ){
if (allElements[i].className == n ){
classElements[classElements.length] = allElements[i];
}}
return classElements;
}

function sortNumber(a,b){
return a - b
}

function borderDisplayReset(){
if($PriceInc.length>0) $PriceInc[$PriceInc.length-1].getElementsByTagName("div")[3].style.display="";
if($PriceDrop.length>0) $PriceDrop[$PriceDrop.length-1].getElementsByTagName("div")[3].style.display="";
if($Upgrade.length>0) $Upgrade[$Upgrade.length-1].getElementsByTagName("div")[3].style.display="";
}

function borderDisplaySet(){
for(var i=$AllDOM.length-1;i>=0;i--){
if($AllDOM[i].style.display!='none'){
$AllDOM[i].getElementsByTagName("div")[3].style.display="none";
break;
}}}

function menuDisplayToggle(){
if($Chart_flag){
$PriceInc_menu_cache=document.getElementById("PriceInc").style.borderBottom;
$PriceDrop_menu_cache=document.getElementById("PriceDrop").style.borderBottom;
$Upgrade_menu_cache=document.getElementById("Upgrade").style.borderBottom;
document.getElementById("PriceInc").style.borderBottom="";
document.getElementById("PriceDrop").style.borderBottom="";
document.getElementById("Upgrade").style.borderBottom="";
}else{
document.getElementById("PriceInc").style.borderBottom=$PriceInc_menu_cache;
document.getElementById("PriceDrop").style.borderBottom=$PriceDrop_menu_cache;
document.getElementById("Upgrade").style.borderBottom=$Upgrade_menu_cache;
}}

function PriceInc(){
if(!$Chart_flag){
borderDisplayReset();
if($PriceInc_flag){
document.getElementById("PriceInc").style.borderBottom="";
for(var i=0;i<$PriceInc.length;i++){
$PriceInc[i].style.display="none";
}
$PriceInc_flag=0;
}else{
document.getElementById("PriceInc").style.borderBottom="3px solid rgba(244,67,54,0.8)";
for(var i=0;i<$PriceInc.length;i++){
$PriceInc[i].style.display="";
}
$PriceInc_flag=1;
}
borderDisplaySet();
}}

function PriceDrop(){
if(!$Chart_flag){
borderDisplayReset();
if($PriceDrop_flag){
document.getElementById("PriceDrop").style.borderBottom="";
for(var i=0;i<$PriceDrop.length;i++){
$PriceDrop[i].style.display="none";
}
$PriceDrop_flag=0;
}else{
document.getElementById("PriceDrop").style.borderBottom="3px solid rgba(76,175,80,0.8)";
for(var i=0;i<$PriceDrop.length;i++){
$PriceDrop[i].style.display="";
}
$PriceDrop_flag=1;
}
borderDisplaySet();
}}

function Upgrade(){
if(!$Chart_flag){
borderDisplayReset();
if($Upgrade_flag){
document.getElementById("Upgrade").style.borderBottom="";
for(var i=0;i<$Upgrade.length;i++){
$Upgrade[i].style.display="none";
}
$Upgrade_flag=0;
}else{
document.getElementById("Upgrade").style.borderBottom="3px solid rgba(237,108,0,0.8)";
for(var i=0;i<$Upgrade.length;i++){
$Upgrade[i].style.display="";
}
$Upgrade_flag=1;
}
borderDisplaySet();
}}

function Price(){
for(var i=0;i<$Price.length;i++){
$All_Price[i]=$Price[i].innerHTML;
}
if($Price.length>0){
$All_Price.sort(sortNumber);
document.getElementById("Lowest").innerHTML=$All_Price[0];
document.getElementById("Highest").innerHTML=$All_Price[i-1];
}else{
document.getElementById("Lowest").innerHTML=document.getElementById("Current").innerHTML;
document.getElementById("Highest").innerHTML=document.getElementById("Current").innerHTML;
}}

function Chart(){
if($Chart_flag){
document.getElementById("chart").style.display="none";
document.getElementById("lists").style.display="";
document.getElementById("Chart").style.borderBottom="";
document.ontouchmove=function(){return true;}
$Chart_flag=0;
}else{
Highcharts.setOptions({
global: {
useUTC: false
}});

var chart;
$(function() {
chart = new Highcharts.Chart({
chart: {
renderTo: 'chart',
defaultSeriesType: 'line'
},
title: {
text: ''
},
xAxis: {
type: 'datetime',
tickPixelInterval: 150,
labels: {
style: {
fontSize: '25px'
}}
},
yAxis: {
title: '',
min: 0,
labels: {
style: {
fontSize: '25px'
}}
},
tooltip: {
formatter: function() {
return '<b>Price Details</b><br/>' + Highcharts.dateFormat('%Y-%m-%d', this.x) +'<br/>'+  '¥'+Highcharts.numberFormat(this.y, 0);
},
style: {
fontSize: '25px'
}},
legend: {
enabled: false
},
credits: {
enabled: false 
},
plotOptions: {
series: {
marker: {
enabled: false
}}},
series: [{
color: '#444',
lineWidth: 1.5,
data: (function() {
var data = [];
if($Price_data_date.length==0){
data.push({
x: Date.parse(new Date()), 
y: parseInt(document.getElementById("Current").innerHTML)
});
data.push({
x: Date.parse($New_date), 
y: parseInt(document.getElementById("Current").innerHTML)
});
}else{
data.push({
x: Date.parse(new Date()), 
y: parseInt($Price_data_value[1].innerHTML)
});
for(var i=0;i<$Price_data_date.length;i++){
data.push({
x: Date.parse(new Date($Price_data_date[i].innerHTML)), 
y: parseInt($Price_data_value[i*2+1].innerHTML)
});
data.push({
x: Date.parse(new Date($Price_data_date[i].innerHTML))+1000, 
y: parseInt($Price_data_value[i*2].innerHTML)
});}
data.push({
x: Date.parse($New_date), 
y: parseInt($Price_data_value[(i-1)*2].innerHTML)
});}
return data;
})()
}]
});});
document.getElementById("chart").style.display="";
document.getElementById("lists").style.display="none";
document.getElementById("Chart").style.borderBottom="3px solid #444";
document.ontouchmove=function(){return false;}
$Chart_flag=1;
}
menuDisplayToggle();
}
</script>
</head>

<body>
<div class="header">
<img src="[App Icon]" />
<h1>[App Name]</h1>
<p><span style="color: rgba(76,175,80,0.8);">L: ¥<span id="Lowest" style="padding: 0 20px 0 0;"></span></span><span style="color: rgba(244,67,54,0.8);">H: ¥<span id="Highest" style="padding: 0 20px 0 0;"></span></span>Current: ¥<span id="Current" style="padding: 0 20px 0 0;">[Current Price]</p>
</div>

<div class="main">

<div class="menu">
<span class="link" id="PriceDrop" style="color: rgba(76,175,80,0.8);" onclick="PriceDrop();">Price Drop</span>
<span class="link" id="PriceInc" style="color: rgba(244,67,54,0.8);" onclick="PriceInc();">Price Inc</span>
<span class="link" id="Upgrade" style="color: rgba(237,108,0,0.8);" onclick="Upgrade();">Upgrade</span>
<span class="link" id="Chart" style="color: #444;" onclick="Chart();">Chart</span>
</div>

<div id="chart" style="min-width: 90%;"></div>

<div id="lists">
[Results]
</div>
</div>

<div class="footer">
</div>

</body>
</html>

需要修改的内容已在相应位置做了标示(以[ ]形式标出),请仔细阅读给出代码。按照下图的步骤继续处理即可输出历史价格信息。HTML Demo这样的实现具有一大优点,即信息直观且完整,通过点选对应按钮即可筛选想要显示的内容(此处说明:通过点选按钮来组合筛选条件),满足可能想要阅读关于价格和版本动向不同信息组合或图表的需求。历史显示对比 - After

适配问题

本 HTML Demo 是通过 Plus 设备调试制作的,对于其他设备可能存在一定的布局问题,所以你可能需要进行以下适配工作:

  • 对于 App Icon 大小,你需要作如下处理:App Icon
  • 关于 App Name 过于冗长的问题,这里需要做特殊处理。大多数 App 为了增加被搜索的机会,采用「xxx - xxxxxxxx」的结构命名,然而命名方式不规范,有的用半角符号,有的用全角符号;有的加空格,有的直接没有空格。形形色色的命名结构分析起来有点费劲。下面给出较为完善的正则表达式,用以匹配 App 主名,基本解决 Title 过长的问题。

    /*用于匹配 App 主名的正则表达式*/
    ^[^-—-–]+(?=\s[-—-–::((])|^[^-—-–]+(?=[-—-–::((])|^.+
  • 关于 App Icon 布局问题,你需要调试margin-left数值:

    img {float: left;border-radius: 35px;margin-left: 0.5em;box-shadow: 5px 5px 10px 5px #ddd;}
  • 关于App Name 布局问题,你需要调试margin数值(建议复制搜索):

    /*大字号位于 style 段*/
    .header {font-size: 25pt;display: block;margin: 1em 2em 2em 2em;}
    /*小字号位于 Script 段的 onload 函数*/
    document.getElementsByClassName("header")[0].getElementsByTagName('p')[0].style.margin="1em 0";
  • 关于 Menu Button 布局问题,你需要调试margin数值:

    .menu {margin: 0 0 2em 5%;}

说明

  • 如前所述,此流程是对 App Store 历史价格信息的补充,因此应当选为 Action Extension 的方式运行。实际使用情景为:在 App Store 浏览了一款应用,想要了解它的价格动向,便可通过右上角分享按钮以 Run Workflow。
  • 给出 HTML 代码是鉴于这部分内容要求比较高,所以诸位只需要结合前面步骤自行做好的前期信息获取工作,修改本 HTML Demo 对应内容即可。
  • 鉴于 AppZapp 的特殊性,请不要直接分享你制作的流程,可以提供相应的分析和处理方法,或删除有关自己信息的内容。因为这其中包含了你的设备信息,虽然泄漏这些信息并没有任何潜在风险,但你更应该授人以渔。

附件

HTML Demo: Activity History


如有问题,欢迎留言或邮件咨询

  • « 上一篇:Workflow - QRCode Detect