一塊兒看一下主流應用使用了哪些三方庫

背景

咱們在進行Android開發時每每會面臨技術選型的問題, 面對如此多的開源框架如何進行選擇、選擇的標準是什麼,這是一個值得思考的問題. 爲此我在後臺爬取了6000多個主流應用,逐個反編譯統計它們使用了哪些開源框架,所以作了一個款應用html

基本思路

首先咱們要有Apk才能夠進行分析,我選擇爬取酷安的應用數據(感受酷安比較好爬一點),將每一個應用的apk下載到本地,經過apktool進行反編譯,查看反編譯後的結果。雖然大部分應用都會進行混淆,可是涉及三方庫的包通常是不會進行混淆的,因此咱們只須要統計出代碼的目錄結構基本就能夠推敲出該應用使用了哪些三方庫。前端

使用pyspider爬取酷安數據

通常提到爬蟲咱們首先選擇Python,在GitHub上Python中star最多的爬蟲框架就是pyspider了,這是由國人開發的一個爬蟲框架,用起來還算方便。只是在windows上安裝不易,建議仍是在linux安裝,具體安裝方式這裏就很少介紹了,網上有不少教程。安裝以後的界面是這樣的java

直接點擊右邊的Create新建任務就能夠了python

咱們只須要在右邊寫代碼,保存以後在左邊點擊run就能夠查看執行結果 咱們先來看一下要爬取的對象linux

一共有653頁,每頁10個,一共6530個應用。爬取的就基本思路就是首先根據Url: https://www.coolapk.com/apk?p=1生成爬取的任務。在pyspider中經過self.crawl建立爬取任務,該方法有兩個參數,第一個爲要爬去的url,第二個爲回調函數。如爬取每頁數據的代碼爲

 @config(age=10 * 24 * 60 * 60)
    def index_page(self, response):
        url = 'https://www.coolapk.com/apk?p='
        # 從第1頁到653頁生成任務
        for i in range(1, 654):
            self.crawl(url + str(i), callback=self.list_page)

複製代碼

這樣爬蟲會自動訪問每頁的數據,在訪問成功以後回調list_page方法,在list_page方法中會提取該頁中每一個App的詳情頁對應的url,而後繼續生成抓取任務git

根據酷安App列表頁面的dom結構能夠看到咱們首先要找到 classapp_left_listdiv,該 diva標籤的 href值即爲App詳情頁對應的url,具體代碼以下

@config(priority=2)
    def list_page(self, response):
        # 從每一頁中打開App詳情頁面
        for each in response.doc('div[class="app_left_list"]').children('a').items():
            self.crawl(each.attr.href, callback=self.detail_page)

複製代碼

最後就是在App詳情頁面提取咱們須要的App的信息,而後將提取的信息保存到數據庫中,並根據提取到的apk連接下載該apk,實際測試中發現酷安在進行apk文件下載時是有session校驗的,因此下載時須要攜帶上session信息,因爲下載過程比較耗時,pyspider不支持這種耗時操做,因此咱們須要單獨開啓線程下載。github

對於稍微具有一點前端知識的同窗,而後查閱一下pyquery的用法,基本上提取咱們須要的信息就沒什麼大問題。算法

完整的爬取代碼以下shell

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
# Created on 2017-12-13 20:17:00
# Project: kuan

from pyspider.libs.base_handler import *
import requests
import _thread
import json


class Handler(BaseHandler):
    crawl_config = {
    }
    # bomb應用配置信息
    Bomb_Application_Id = 'bomb對應的Application Id'
    Bomb_Rest_Api_Key = 'bomb對應的Rest Api Key'

    headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)',
               'Referer': 'https://www.coolapk.com/apk/com.evernote'}

 @every(minutes=24 * 60)
    def on_start(self):
        self.crawl('https://www.coolapk.com/apk', callback=self.index_page)

 @config(age=10 * 24 * 60 * 60)
    def index_page(self, response):
        url = 'https://www.coolapk.com/apk?p='
        # 從第1頁到653頁生成任務
        for i in range(1, 654):
            self.crawl(url + str(i), callback=self.list_page)

 @config(priority=2)
    def list_page(self, response):
        # 從每一頁中打開App詳情頁面
        for each in response.doc('div[class="app_left_list"]').children('a').items():
            self.crawl(each.attr.href, callback=self.detail_page)

 @config(priority=2)
    def detail_page(self, response):
        url = response.url
        packageName = url[28:len(url)]
        imgUrl = list(response.doc('div[class="apk_topbar"]').items())[
            0].children('img').attr("src")
        scriptLine = list(response.doc('script').items())[
            2].text().split('\n')[2]
        apkUrl = scriptLine[36:len(scriptLine) - 2]
        appName = response.doc(
            'p[class="detail_app_title"]').text().split(" ")[0]
        desc = list(response.doc('div[class="apk_left_title_info"]').items())[
            0].html()
        left_info_list = list(response.doc(
            'p[class="apk_left_title_info"]').items())
        detail = left_info_list[len(left_info_list) - 1].html()
        # 獲取下載量
        apk_topba_message = response.doc('p[class="apk_topba_message"]').text()
        download_count = self.get_download_count(
            apk_topba_message.split('/')[1])
        cookie = 'SESSID=' + response.cookies['SESSID']
        _thread.start_new_thread(
            self.downloadFile, (apkUrl, packageName, cookie,))
        appInfo = {
            "url": url,
            "packageName": packageName,
            "name": appName,
            "detail": detail,
            "imgUrl": imgUrl,
            'downloadCount': download_count,
            "description": desc
        }
        self.saveAppInfo(appInfo)
        return appInfo

    def get_download_count(self, download_str):
        download_str = download_str.strip()
        if download_str.endswith('萬下載'):
            return float(download_str.split('萬下載')[0]) * 10000
        elif download_str.endswith('次下載'):
            return float(download_str.split('次下載')[0])
        elif download_str.endswith('下載'):
            return float(download_str.split('下載')[0])
        else:
            return 0

    def downloadFile(self, apkUrl, packageName, cookie):
        headers = self.headers
        headers['cookie'] = cookie
        r = requests.get(apkUrl, headers=self.headers,
                         allow_redirects=True, verify=False)
        # 保存下載的文件
        with open("/root/apk/" + packageName + ".apk", "wb") as f:
            f.write(r.content)

    # Bomb的惟一鍵不靠譜,每次保存以前先查詢是否存在,而後再進行更新或者保存
    def saveAppInfo(self, data):
        headers = {'X-Bmob-Application-Id': self.Bomb_Application_Id,
                   'X-Bmob-REST-API-Key': self.Bomb_Rest_Api_Key, 'Content-Type': 'application/json'}
        url = 'https://api.bmob.cn/1/classes/app_info'
        exitInfo = self.queryAppByPackageName(data['packageName'])
        if(len(exitInfo['results']) > 0):
            url = url + '/' + exitInfo['results'][0]['objectId']
            res = requests.put(url, headers=headers,
                               data=json.dumps(data), verify=False)
        else:
            res = requests.post(url, headers=headers,
                                data=json.dumps(data), verify=False)

    def queryAppByPackageName(self, packageName):
        headers = {'X-Bmob-Application-Id': self.Bomb_Application_Id,
                   'X-Bmob-REST-API-Key': self.Bomb_Rest_Api_Key, 'Content-Type': 'application/json'}
        url = 'https://api.bmob.cn/1/cloudQuery'
        bql = 'select * from app_info where packageName=?'
        values = '[\'' + packageName + '\']'
        data = {'bql': bql, 'values': values}

        url = url + '?bql=' + bql + '&values=' + values
        res = requests.get(url, headers=headers, verify=False)
        return json.loads(res.text)

複製代碼

使用Apktool反編譯apk文件

apk文件下載完成以後咱們就可使用apktool進行反編譯了。基本命令是java -jar apktool_2.3.0.jar d xxx.apk -o destDir -f。這裏我使用的apktool版本爲2.3.0。數據庫

具體作法是依次反編譯每一個apk文件,通常狀況下apk反編譯以後的文件目錄大體包含如下內容

第一個文件就不解釋了,作Android開發的同窗都知道。值得注意的是Apk的版本信息沒有在AndroidManifest文件中,而是在apktool.yml文件中,這個文件裏面包含不少apk有價值的信息。另外一個值得咱們關注的是smali文件夾,若是apk進行了分包可能還會出現smali_class二、smali_class3之類的文件夾。咱們分析該app引用了哪些三方庫主要看smali下的文件目錄結構是什麼樣的。雖然這種方式並不徹底準確,可是也能涵蓋絕大部分三方庫。

具體代碼以下

#!/usr/bin/env python
# -*- coding:utf-8 -*-


from __future__ import print_function

import requests
import json
import yaml
import os
import subprocess
import sys
import zipfile
from xml.dom import minidom
import threadpool
import shutil

apktool = "apktool_2.3.0.jar"
headers = {'X-Bmob-Application-Id': 'bomb對應的Application Id',
           'X-Bmob-REST-API-Key': 'bomb對應的Rest Api Key', 'Content-Type': 'application/json'}


def sh(command):
    print(command)
    p = subprocess.Popen(command, shell=True,
                         stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    print(p.stdout.read())


def decompileApk(f):
        # fix windows path
    if ":\\" in f and not ":\\\\" in f:
        f = f.replace("\\", "\\\\")
    dexes = []
    jars = []
    if f.endswith(".apk"):
        package_name = f[0:len(f) - 4]
        tempDir = os.path.splitext(f)[0]
        sh("java -jar %s d %s -o %s -f" % (apktool, f, tempDir))
        if os.path.isdir(os.path.join(tempDir, 'smali_classes2')):
            sh("cp -rf smali_classes2/* smali/")
        jarDir = os.path.join(tempDir, 'smali')
        if os.path.exists(jarDir):
            packageList = []
            getPackageName(jarDir, jarDir, packageList)
            packageList = cleanPackageName(packageList)
            savePackageList(packageList, package_name)
            sh('sed -i 1d %s' % (tempDir + '/apktool.yml'))
            versionInfo = getVersionInfo(tempDir + '/apktool.yml')
            saveApkInfo(package_name,
                        versionInfo['versionCode'], versionInfo['versionName'])
        shutil.rmtree(tempDir)
    print("Done")


def mapFunc(package):
    return package.replace('/', '.')


def cleanPackageName(packageList):
    return list(map(mapFunc, packageList))


def getVersionInfo(file):
    f = open(file)
    y = yaml.load(f)
    return y['versionInfo']


def getPackageName(root, dir, packageList):
    files = [f for f in os.listdir(
        dir) if os.path.isfile(os.path.join(dir, f))]
    if len(files) > 0 and root != dir:
        if len(dir.split(root + '/')) > 1:
            packageList.append(dir.split(root + '/')[1])
        else:
            print('error root:%s dir:%s' % (root, dir))
    elif len([f for f in os.listdir(dir) if len(f) > 1]) == 0:
        if len(dir.split(root + '/')) > 1:
            packageList.append(dir.split(root + '/')[1])
        else:
            print('error root:%s dir:%s' % (root, dir))
    else:
        for file in [f for f in os.listdir(dir) if os.path.isdir(os.path.join(dir, f))]:
            if len(file) > 1:
                getPackageName(root, os.path.join(dir, file), packageList)


def packageToRequest(package):
    return {'method': 'POST', 'path': '/1/classes/lib_info', 'body': {'packageName': package}}


def savePackageList(packageList, apk_id):
    url = 'https://api.bmob.cn/1/batch'
    i = 0
    while i < len(packageList):
        subList = packageList[i:i + 50]
        params = {}
        params['requests'] = list(
            map(packageToRequest, subList))
        res = saveDataToBomb(url, params)
        saveLibApkRelation(subList, apk_id)
        i += 50


def lib_id_to_request(lib_id):
    return {'method': 'POST', 'path': '/1/classes/r_apk_lib', 'body': {'libPackageName': lib_id}}


def saveLibApkRelation(lib_id_list, apk_id):
    url = 'https://api.bmob.cn/1/batch'
    params = {}
    params['requests'] = list(
        map(lib_id_to_request, lib_id_list))
    for req in params['requests']:
        req['body']['apkPackageName'] = apk_id
    res = saveDataToBomb(url, params)


def saveApkInfo(packageName, versionCode, versionName):
    data = {"packageName": packageName,
            "versionCode": versionCode, "versionName": versionName}
    url = 'https://api.bmob.cn/1/classes/apk_info'
    oldInfo = json.loads(queryDataFromBomb(url, data))
    if len(oldInfo['results']) > 0:
        print('%s is exits' % {str(data)})
    else:
        saveDataToBomb(url, data)


def saveDataToBomb(url, data):
    res = requests.post(url, headers=headers,
                        data=json.dumps(data), verify=False)
    return res


def queryDataFromBomb(url, data):
    print('%s ?where=%s' %
          (url, json.dumps(data)))
    res = requests.get('%s?where=%s' %
                       (url, json.dumps(data)),  headers=headers, verify=False)
    return res.text


if __name__ == "__main__":
    f = sys.argv[1]
    if os.path.isdir(f):
        pool = threadpool.ThreadPool(1)
        name_list = os.listdir(f)
        # 單線程運行
        for name in name_list:
            decompileApk(name)
        # 多線程運行
        # myrequets = threadpool.makeRequests(decompileApk, name_list)
        # [pool.putRequest(req) for req in myrequets]
        # pool.wait()
        print('All Finished')
    else:
        print('參數必須爲一個目錄')

複製代碼

從實際分析結果來看,目前的分析算法還有不少問題,統計出來的包名和咱們實際使用的三方庫不能徹底匹配,有時會把子包名統計進去。因此只能靠你們經驗還判斷每一個包名對應的是哪一個三方庫了。

App展現統計結果

最後將上面抓取和分析的結果以App的形式展現出來,相比上兩步而言這個是最簡單的了。目前主要提供兩個維度的展現,一是按照酷安上的下載量展現App信息,在App詳情中展現該app下統計出來的包信息;另外一個維度是按照庫被引用的次數展現,詳情頁面中展現哪些應用中包含這個庫。功能比較簡單因此就很少解釋了,直接放代碼地址:github.com/dumingxin/A…,歡迎你們star、提issue,或者有更好的想法一塊兒來實現。

App目前已經發布在酷安市場,下載地址爲:www.coolapk.com/apk/172597

二維碼:

總結

從開始着手準備,到最終完成第一個版本的功能大概兩週時間,因爲沒有正經學習過python,因此python相關代碼寫的可能不太規範,僅供你們參考。

目前實際下載下來的apk文件只有5000+,還有1000多沒有下載下來。apk反編譯還在進行,目前已經分析了2000+,因此統計結果可能還會不斷變化

感謝

https://www.coolapk.com/ 感謝酷安提供的數據(手動滑稽)

https://github.com/binux/pyspider 感謝pyspider讓我一個新手也能夠爬數據

https://github.com/tp7309/AndroidOneKeyDecompiler 感謝做者提供python反編譯apk的思路

相關文章
相關標籤/搜索