逆向爬虫36 Android网络请求常用模块和python编码规范

逆向爬虫36 Android网络请求常用模块和python编码规范

目的:汇总下Android网络请求相关模块的用法,方便后续逆向定位代码,和一些python的编码规范

先来回顾一下完成一次Web HTTP请求需要用到的知识。

  • 常见的Web HTTP请求分为GET和POST请求,在python中使用的是urllibrequests模块,在Android里用的是okhttpretrofit模块,其中okhttpretrofit的关系就和urllibrequests的关系一样,后者均是在前者的基础上做了二次封装,使其使用起来更加方便。
  • Web HTTP请求的数据包可以分为两种形式,form表单json字符串,在python中这两种形式是通过requests方法的参数来区分的,但在Android中的okhttp也会有相应提交两种形式的方法。
  • 当接收到Web数据包时,接收到的如果时json字符串的话,就需要对数据进行反序列化,python中requests模块帮我们完成了这个工作,但在Android中需要引入Gson模块来实现反序列化。
  • 在浏览器Web应用中,使用cookies文件保存用户的信息,在Android里使用xml文件实现类似的功能,因此还需要学习下Android操作xml文件的方法。

okhttp

使用okhttp之前要做如下配置

  • 引入,在build.gradle中 implementation “com.squareup.okhttp3:okhttp:4.9.1”

  • 配置,在AndroidManifest.xml中,application标签同级上方添加
    只能发送https请求。

  • 配置,在AndroidManifest.xml中,application标签内部添加 android:networkSecurityConfig=“@xml/network_security_config”

    在res目录下新建一个xml文件夹,里面新建一个network_security_config.xml文件,写入以下内容

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <!--禁用掉明文流量请求的检查-->
        <base-config cleartextTrafficPermitted="true" />
    </network-security-config>
    

form表单格式

  • 数据包以表单格式提交
user=apphao&pwd=123456&sign=用户名和密码的md5值
new Thread() {
    @Override
    public void run() {
        // 线程执行的内容
        OkHttpClient client = new OkHttpClient.Builder().build();
        FormBody form = new FormBody.Builder()
            .add("user", dataMap.get("username"))
            .add("pwd", dataMap.get("password"))
            .add("sign", dataMap.get("sign")).build();
        Request req = new Request.Builder().url("http://192.168.1.4:9999/login").post(form).build();
        Call call = client.newCall(req);
        try {
            Response res = call.execute();
            ResponseBody body = res.body();
            String dataString = body.string();
            Log.e("请求发送成功", dataString);
        } catch (IOException ex) {
            Log.e("Main", "网络请求异常");
        }
    }
}.start();

json格式

  • 数据包以json格式提交
{
	name:"apphao",
	pwd:123456,
	sign:用户名和密码的md5值
}
new Thread() {
    @Override
    public void run() {
        // 线程执行的内容
        OkHttpClient client = new OkHttpClient();
        JSONObject json = new JSONObject(dataMap);
        String jsonString = json.toString();
        RequestBody form = RequestBody.create(MediaType.parse("application/json;charset=utf-8"),jsonString);
        Request req = new Request.Builder().url("http://192.168.1.4:9999/login").post(form).build();
        Call call = client.newCall(req);
        try {
            Response res = call.execute();
            ResponseBody body = res.body();
            String dataString = body.string();
            Log.i("登录", dataString);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}.start();

两者区别在于

  • form表单格式使用的是FormBody构造请求体
  • json字符串格式使用JSONObjectRequestBody两者一起构造请求体

请求拦截器

有一些请求头信息是每一个HTTP请求都会携带的,这些公共的请求头信息在python的scrapy爬虫框架中,用中间件来实现,在Android App的okhttp中,用请求拦截器来实现,下面是拦截器简单的测试代码。

// 创建拦截器
Interceptor interceptor = new Interceptor() {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request().newBuilder().addHeader("ts", "123456789").addHeader("sign", "abcdef").build();
        Response response = chain.proceed(request);     // 请求前
        return response;    // 请求后
    }
};
new Thread() {
    @Override
    public void run() {
        // 增加请求拦截器
        OkHttpClient client = new OkHttpClient.Builder().addInterceptor(interceptor).build();
        FormBody form = new FormBody.Builder()
            .add("user", dataMap.get("username"))
            .add("pwd", dataMap.get("password"))
            .add("sign", dataMap.get("sign")).build();
        Request req = new Request.Builder().url("http://192.168.1.4:9999/login").post(form).build();
        Call call = client.newCall(req);
        try {
            Response res = call.execute();
            ResponseBody body = res.body();
            String dataString = body.string();
            Log.e("请求发送成功", dataString);

        } catch (IOException ex) {
            Log.e("Main", "网络请求异常");
        }
    }
}.start();

和前面没有拦截器的区别

  • 在创建okhttp前先创建了拦截器
  • 在创建okhttp时,添加拦截器

NO_PROXY

一些App为了防止抓包,会使用okhttp的NO_PROXY参数来禁止Android手机设置系统代理,遇到这类情况可以使用Drony App进行无代理抓包,也可以利用小黄鸟等App进行本地VPN带抓包,我的手机Android版本太高用不了Drony,用的小黄鸟。

关于无代理抓包可以参考:Python爬虫之对app无代理模式下的抓包分析,以及针对这种的反爬优化方案 - Eeyhan - 博客园 (cnblogs.com)

retrofit

使用retrofit之前要做如下配置

  • 引入,在build.gradle中 implementation “com.squareup.retrofit2:retrofit:2.9.0”

具体使用方法如下:

  • 写接口,声明网络请求

    package com.example.liyang;
    
    package com.example.liyang;
    
    import okhttp3.RequestBody;
    import okhttp3.ResponseBody;
    import retrofit2.Call;
    import retrofit2.http.Body;
    import retrofit2.http.Field;
    import retrofit2.http.FormUrlEncoded;
    import retrofit2.http.POST;
    import retrofit2.http.GET;
    import retrofit2.http.Query;
    
    public interface HttpReq {
        // 向/api/v1/post 发送POST请求,表单格式 name=xx&pwd=xxx
        @POST("/api/v1/post")
        @FormUrlEncoded
        Call<ResponseBody> postLogin(@Field("name") String userName, @Field("pwd") String password);
    
        // 向/api/v2/xxx 发送GET请求,表单格式 ?age=xxx
        @GET("/api/v2/xxx")
        Call<ResponseBody> getInfo(@Query("age") String age);
    
        // 向/post/users 发送POST请求 json字符串格式 {name:xxxx,age:123}
        @POST("/post/users")
        Call<ResponseBody> postLoginJson(@Body RequestBody body);
    }
    
    
  • 调用接口,给接口传参,发送请求

    new Thread() {
        @Override
        public void run() {
            Retrofit retrofit = new Retrofit.Builder().baseUrl("http://192.168.31.201:9999/").build();
            HttpReq httpRequest = retrofit.create(HttpReq.class);		
    		
            // http://192.168.31.201:9999/api/v1/post  
            // name=xx&pwd=xxx
            Call<ResponseBody> call = httpRequest.postLogin("wupeiqi", "666");
            try {
                ResponseBody responseBody = call.execute().body();
                String responseString = responseBody.string();
                Log.i("登录", responseString);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }.start();
    
    new Thread() {
        @Override
        public void run() {
            // http://192.168.31.201:9999/api/v2/xxx?age=123
            Retrofit retrofit = new Retrofit.Builder().baseUrl("http://192.168.31.201:9999/").build();
            HttpReq req = retrofit.create(HttpReq.class);
            Call<ResponseBody> call = req.getInfo("123");
            try {
                ResponseBody responseBody = call.execute().body();
                String responseString = responseBody.string();
                Log.e("Retrofit返回的结果", responseString);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }.start();
    
    new Thread() {
        @Override
        public void run() {
            Retrofit retrofit = new Retrofit.Builder().baseUrl("http://192.168.31.201:9999/").build();
            HttpReq httpRequest = retrofit.create(HttpReq.class);
    
            JSONObject json = new JSONObject(dataMap);
            String jsonString = json.toString();
            RequestBody form = RequestBody.create(MediaType.parse("application/json;charset=utf-8"),jsonString);
    		
            // http://192.168.31.201:9999/post/users  
            // {username:"root",password:"123456","sign":"xxxxdfsdfsdfsdfdfd"}
            Call<ResponseBody> call = httpRequest.postLoginJson(form);
            try {
                ResponseBody responseBody = call.execute().body();
                String responseString = responseBody.string();
                Log.i("登录", responseString);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }.start();
    

注意事项

  • retrofit是在okhttp上的进一步封装,特点是可以把请求的url分成几段,分别在不同的地方来指定,可以更灵活地配置url
  • 当遇到了用retrofit写的接口时,不再是去找实现它的类,而是去找调用它的类,拼接出请求的url和找到传递的参数
  • 代码比较冗长,重点关注定义接口的方式,决定了请求的样式,以及调用接口的方式,决定了传递的参数,两部分合起来决定请求的url

Gson

类似python中的json模块,用于序列化对象和反序列化json字符串的,使用Gson前需要引入implementation ‘com.google.code.gson:gson:2.8.6’

  • 序列化,对象 -> 字符串类型

    class HttpContext{
        public int code;
        public String message;
        
        public HttpContext(int code,String msg){
            this.code = code;
            this.message = msg;
        }
    }
    HttpContext obj = new HttpContext(1000,"成功");
    String dataString = new Gson().toJson(obj); // '{"code":1000,"Message":"成功"}'
    
  • 反序列化,字符串 -> 对象

    // JSON格式
    String dataString = "{\"status\": true, \"token\": \"fffk91234ksd\", \"name\": \"武沛齐\"}";
    class HttpResponse{
        public boolean status;
        public String token;
        public String name;
    }
    HttpResponse obj = new Gson().fromJson(dataString,HttpResponse.class);
    // obj.status  obj.name  obj.token
    
  • 如果反序列化时,存在字典嵌套

    String responseString = "{\"origin\": \"110.248.149.62\",\"url\": \"https://www.httpbin.org/post\",\"dataList\":[{\"id\":1,\"name\":\"一个小黑\"},{\"id\":2,\"name\":\"eric\"}]}";
    class Item {
        public int id;
        public String name;
    }
    public class HttpResponse {
        public String url;
        public String origin;
        public ArrayList<Item> dataList;
    }
    HttpResponse obj = new Gson().fromJson(dataString, HttpResponse.class);
    // obj.url  obj.origin
    Item objItem = obj.dataList.get(1);
    // objItem.name
    

保存到XML文件

保存到手机上:/data/data/com.example.liyang/shared_prefs/sp_city.xml

保存

SharedPreferences sp = getSharedPreferences("sp_city", MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString("token","111111");
editor.commit();

删除

SharedPreferences sp = getSharedPreferences("sp_city", MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.remove("token");
editor.commit();

读取

SharedPreferences sp = getSharedPreferences("sp_city", MODE_PRIVATE);
String token = sp.getString("token","");
  • 记住SharedPreferences对象是用来读写手机App本地的XML文件的
  • XML文件和Cookies很像,要么是本地算法生成的,要么是HTTP请求服务器返回的。

python编码规范

关于main

# 全部变量大写
DATA_LIST = []

def run():
    """ 业务逻辑都写在函数 """
    pass

if __name__ == '__main__':
    run()

文件操作

  • 一样一行读,减少内存消耗
# 建议
def run():
    with open("account.txt", mode='r', encoding='utf-8') as f:
        for line in f:
            print(line)

if __name__ == '__main__':
    run()
  • 利用生成器还可以减少代码缩进
# 最佳
def load_file_data():
    with open("account.txt", mode='r', encoding='utf-8') as f:
        for line in f:
            yield line.strip().split(",")  # ["alex","123"]

def run():
    gen_object = load_file_data()
    
    for use, pwd in gen_object:
        print(use, pwd)

if __name__ == '__main__':
    run()

代理IP+生成器

import requests

def register(proxy_object):
    proxy_dict = next(proxy_object)

    session = requests.Session()
    session.proxies = {
        "http": "{ip}:{port}".format(**proxy_dict),  # "59.63.107.231:19025"
        "https": "{ip}:{port}".format(**proxy_dict),  # "59.63.107.231:19025"
    }
    session.post("......")

def get_proxy_object():
    while True:
        res = requests.get("....?count=100")
        for item in res.json()['obj']: # 100
            # item = {"port":"19025","ip":"59.63.107.231"}
            yield item

def run():
    proxy_object = get_proxy_object()
	for i in range(1000000):
        register(proxy_object)

if __name__ == '__main__':
    run()

并发

需求:去注册10w个账号,并写入到文件。

import requests
from concurrent.futures import ThreadPoolExecutor
import threading
import os

def task(password):
    # 1.requests发请求,给你一个手机(卡商 )
    res = requests.get(".......")
    phone = res.json()['data']['phone']
    # 2.注册
    requests.post(
        url="...",
        data={
            'phone': phone,
            'password': password,
        }
    )
    # 3.文件名
    file_name = "xxxx-{}.txt".format(threading.current_thread().ident)
    with open(file_name, mode='a+', encoding='utf-8') as f:
        f.write(".....")

def run():
    password = "qwe123"
    pool = ThreadPoolExecutor(40)
    for i in range(100000):
        pool.submit(task, password)
    # 等待,等待40个线程吧100000个任务全部执行完毕(等待线程池中的任务执行完毕)
	pool.shutdown()
    # 获取目录下的所有文件合并
    for name in os.listdir("xxx/xxx/xx/dist"):
        pass

if __name__ == '__main__':
    run()

异常+重试

try:
    phone = requests.get('......卡商的API获取手机号').json()['data']
	for i in range(10):
        try:
    		v1 = requests.get() # 异常
            break
        except Exceptions as e:
            pass
	else:
        # for循环中的次数执行完,else中的代码就会执行
        print("请求异常了,请及时处理")
    v2 = request.post()
    v3 = request.post()
except Exceptions as e:
    pass
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值