0%

记一次成功的购物app爬虫

与其说爬虫写的成功,不如说这个 app 实在太弱了 ^_^

之前阿妈在网上某 P2P 平台投资遇到资本家谎称破产卷款跑路留下漫天的债务。无奈阿妈为了追回本金,只能去和平台走清算流程,最后换回了等额的“金豆”(代金券)。这些代金券只能用于在某个 app 中购买一些丝毫没有性价比可言的产品。说实话我觉得挺惨的。

更惨的是,或许是因为清算的人实在太多了,app 里凡是智商税稍低一点的硬通货(比如手机、耳机和一些大牌家电),都属于一进货就被抢购一空的情况。而根据观察,每次进货的时间不固定,进货量又特别少。于是我就萌生了写个爬虫脚本挂机的想法,一有货就通知阿妈,彻底解放阿妈的双手。

常识告诉我所有和利益直接打交道的平台,一般都是网络攻防的重灾区。然而或许是这个平台真的没钱了或者只是因为太小众了,这个 app 简直弱爆了(说实话比上大 sso 还弱)。

探索

我不会移动端开发,也不会 Java。所以去做一个 app 上的爬虫可能全都是 from scratch 的。

但是常识告诉我,这个 app 大概率也只不过是个 web 而已,所以为了划归问题,获取 url 就成了当务之急。

apktool 和 classes-dex2jar:

首先想到的办法就是对 apk 拆包,直接分析源码拿到 url 。apktool 和 classes-dex2jar 就是两个经典的轻量级 apk 分析工具(shell 脚本)。根据后来的使用,前者似乎更倾向于把 apk 中的资源(音频、图标、视频等)分离出来,后者则只对 class.dex 做拆包得到源码。所以从理论上来说,后者更符合我的需求。

遗憾的是,并没有找到想要的 url 。究其原因,或许是这个 url 是运行时动态获取的,不保存在 apk 里。

抓包工具:

然后在 hecate 的提示下,我换了一种思路。既然反向工程很复杂,获取 url 又是外部动作,为何不简单的抓个包呢?后续选用的是 NetCapture 的抓包工具,可以直接在手机上进行抓包。原理异常简单,开一个系统代理,然后一切网络访问就都对 NetCapture 透明了。

于是我轻而易举的抓到了这个商城的 url,我现在能成功在浏览器上访问这个商城了。

爬取

然后,经过长达 10 分钟的探索,我发现这个商城简直太离谱了:

  • 居然没有任何异步加载和加密 script!
  • 居然没有任何令人头疼的证书和 token!
  • 居然不检查 header!
  • 居然不需要登录就能访问商城首页!
  • 居然不需要登录就能搜索特定关键字的商品!
  • 搜索功能的 post 的 response 是裸的 json!

爬虫脚本 10 分钟就给整好了,甚至没开 pycharm,文本编辑器写的。

剩下的就是做简单的异常处理,找个售罄的商品测试测试,找个未售罄的商品再测试测试。封装一下,加个邮件提醒,一气呵成。整个脚本从拿到 url 后一小时就搞定了。

部署

放本地太占地方,直接丢服务器上跑了。用 xftp 把文件传服务器上,然后 nohup 一下。

1
sudo nohup python3 main.py &
2
sudo ps -ef | grep main.py

然后就等着收邮件吧 QwQ,但愿阿妈能买到她心仪的商品 hhh

1
import requests
2
from time import sleep
3
from email.mime.text import MIMEText
4
from email.utils import formataddr
5
import smtplib
6
7
ENV_TEST = True
8
SEARCH_URL = 'the secret url'
9
JSON = {
10
    "xxx": "xxx",
11
    "keyword": None,
12
}
13
HEADER = {
14
    "User-Agent": "xxx mobile phone agent"
15
}
16
KEY_WORDS = ["key_word_1", "key_word_2"]
17
vis = [False for _ in range(len(KEY_WORDS))]
18
19
my_sender = ''  # 发件人邮箱账号
20
my_pass = ''  # 发件人邮箱口令
21
my_user = ''  # 收件人邮箱账号
22
23
24
def mail(title, content, coding_type, retry_flag=False):
25
    print('Mailing...', title)
26
    while True:
27
        try:
28
            msg = MIMEText(content, coding_type, 'utf-8')
29
            msg['From'] = formataddr(['someone', my_sender])
30
            msg['To'] = formataddr(['me', my_user])
31
            msg['Subject'] = title
32
            server = smtplib.SMTP_SSL('smtp.qq.com', 465)
33
            server.login(my_sender, my_pass)
34
            server.sendmail(my_sender, [my_user, ], msg.as_string())
35
            server.quit()
36
        except Exception as err:
37
            print(f"\n{err}")
38
            if not retry_flag:
39
                break
40
            sleep(5)
41
            continue
42
        return
43
44
45
def main_loop():
46
    try:
47
        for i in range(len(KEY_WORDS)):
48
            json = JSON
49
            json["keyword"] = KEY_WORDS[i]
50
            response = requests.post(SEARCH_URL, json=JSON, headers=HEADER, timeout=5)
51
            assert response.status_code == 200, f"status_code = {response.status_code}"
52
            result = eval(response.text.replace("null", "None").replace("false", "False").replace("true", "True"))
53
            result = result['result']['dataList']
54
            assert len(result) == 1, f"len(result) = {len(result)}"
55
            x = result[0]
56
            if x['inventory'] != 0 and not vis[i]:
57
                print()
58
                print("=" * 20)
59
                print(f"发现进货! {x['productName']}")
60
                print(f"进货数量: {x['inventory']}")
61
                print("=" * 20)
62
                mail(
63
                    title=f"进货{x['inventory']} {KEY_WORDS[i]}",
64
                    content=f"发现进货! {x['productName']} <br> 进货数量: {x['inventory']}",
65
                    coding_type="HTML",
66
                    retry_flag=True,
67
                )
68
                vis[i] = True
69
            elif x['inventory'] == 0 and vis[i]:
70
                print()
71
                print("=" * 20)
72
                print(f"发现售空! {x['productName']}")
73
                print("=" * 20)
74
                mail(
75
                    title=f"售空 {KEY_WORDS[i]}",
76
                    content=f"发现售空! {x['productName']}",
77
                    coding_type="HTML",
78
                    retry_flag=False,
79
                )
80
                vis[i] = False
81
            sleep(2)
82
    except Exception as err:
83
        print(f"\n{err}")
84
        return False
85
    return True
86
87
88
if __name__ == '__main__':
89
    if ENV_TEST:
90
        print("env test!")
91
        sleep(10)
92
        mail(
93
            title=f"脚本开始运行",
94
            content=f"脚本已经开始运行...",
95
            coding_type="HTML",
96
            retry_flag=True,
97
        )
98
    attempt_cnt = 0
99
    success_cnt = 0
100
    continuous_err_cnt = 0
101
    while True:
102
        success = main_loop()
103
        print(f"{f'x{attempt_cnt % 10}'[success]}", end="", flush=True)
104
        attempt_cnt += 1
105
        if attempt_cnt % 10 == 0:
106
            print()
107
        if attempt_cnt % 500 == 0:
108
            mail(
109
                title=f"脚本运行正常",
110
                content=f"attempt_cnt: {attempt_cnt} <br> success_cnt: {success_cnt}",
111
                coding_type="HTML",
112
                retry_flag=True,
113
            )
114
        if not success:
115
            continuous_err_cnt += 1
116
            if continuous_err_cnt >= 10:
117
                mail(
118
                    title=f"脚本异常退出",
119
                    content=f"attempt_cnt: {attempt_cnt} <br>  success_cnt: {success_cnt}",
120
                    coding_type="HTML",
121
                    retry_flag=True,
122
                )
123
                break
124
            sleep(3)
125
        else:
126
            success_cnt += 1
127
            continuous_err_cnt = 0
128
            sleep(1)