Bladeren bron

Initial commit

main
Ashton Charbonneau 3 jaren geleden
commit
1586221887
2 gewijzigde bestanden met toevoegingen van 264 en 0 verwijderingen
  1. 64
    0
      README.md
  2. 200
    0
      rok-stat-grabber.py

+ 64
- 0
README.md Bestand weergeven

@@ -0,0 +1,64 @@
1
+# rok-stat-grabber
2
+
3
+The grabber iterates through entries in the individual power rankings and takes screenshots of each profile page (main profile page, the more info page, and the more info page with kills expanded). It will also copy the username into `usernames.txt`.
4
+
5
+Some profiles cannot be clicked. These are skipped, and a screenshot is saved along with the skipped rank.
6
+
7
+Each profile is given a timestamp that is common to the three screenshot files and the line in `usernames.txt`. The timestamp can be used to associate individual accounts.
8
+
9
+## Setup
10
+
11
+Connect to an Android device via ADB. If connecting to a physical device, I would recommend using [scrcpy](https://github.com/Genymobile/scrcpy) with the [--turn-screen-off](https://github.com/Genymobile/scrcpy#turn-screen-off) flag to help prevent the device from heating up. If using an emulator, follow instructions for that emulator to connect via ADB. Get the device serial with `adb devices`.
12
+
13
+This script uses [AdbClipboard](https://github.com/PRosenb/AdbClipboard) and the [accompanying app](https://play.google.com/store/apps/details?id=ch.pete.adbclipboard) to pull the clipboard containing the username. This only works until android 9. If running scrcpy, the clipboard grabs on the device are pushed to your computer, which could be annoying.
14
+
15
+If running the script script on multiple accounts in parallel, I would recommend using different project names to avoid collisions in in the timestamp. It's also likely prudent to overlap the rankings a bit if running in parallel.
16
+
17
+The script terminates if it is unable to access a profile three times in a row. Could cause issues if there is a large amount of migration; I would recommend occasionally checking progress.
18
+
19
+## Usage
20
+
21
+Open the individual rankings to either the top of the list (if starting with rank 1), or to the profile page of whatever rank you are starting with. Run with the needed command line options:
22
+
23
+- `--project` (required): what folder should the scraped files be saved in?
24
+- `--serial` (required): serial number of the device from `adb devices`.
25
+- `--count` (required): what rank should you stop at?
26
+- `--start`: what rank are you starting with? The script taps in a different spot for the first 4 ranks compared to others, and this defaults to rank 1.
27
+- `--verbose`: show progress output.
28
+
29
+## Examples
30
+
31
+Grab ranks 1-990 (start at top of list, or with profile of rank 1 open): `python rok-stat-grabber.py -p "KVK4_Pass5" -s "SERIAL123456" -c 990 -v`
32
+
33
+Grab ranks 801-990 (start with profile of rank 801 open): `python rok-stat-grabber.py -p "KVK1_MGE" -v -s "SERIAL123456" -c 990 -n 801`
34
+
35
+## Troubleshooting
36
+
37
+- Check
38
+	- ADB issues
39
+	- Device resolution
40
+	- Screen rotation
41
+	- Android version (for clipboard)
42
+- ??? (TODO)
43
+
44
+## Todo
45
+
46
+- **Currently only works on devices with 1440x2560 resolution**. Change coordinates from absolute values to percentages to support other sizes. Grab screen size via adb or command line.
47
+- Correct for screen rotation on devices. Currently uses the `ROTATE` variable (have used 0 and 90 on different devices).
48
+- Skip your own rank if the scraping account is included in top 1000.
49
+- Draw a box around skipped profiles in screenshot to make it easier to see them.
50
+- Support scraping the last ~990-1000 rankings.
51
+- Remove serial requirement if only a single device is connected, or choose from a list.
52
+- More checks to ensure you are on the correct page.
53
+- Better tesseract config.
54
+- Better logging and debug notes.
55
+- Check/catch errors.
56
+- Add screenshots to README.
57
+- Don't force the folder structure.
58
+- Make sleep time random or adjustable or something.
59
+- Replace AdbClipboard with something that works with newer versions of Android.
60
+- Better handling when quitting.
61
+- Compress the images (optipng run in parallel or something?).
62
+- Probably some usernames are problematic and should be escaped.
63
+- I think when it skips more than one profile in a row, the rank number in the screenshot of the skipped profile doesn't increment properly, but I might have already fixed that. Will check next time.
64
+- Support for iOS has tricky requirements (maybe requires jailbreak) but structure would be similar.

+ 200
- 0
rok-stat-grabber.py Bestand weergeven

@@ -0,0 +1,200 @@
1
+import argparse
2
+import re
3
+import io
4
+import os
5
+import sys
6
+import time
7
+from datetime import datetime
8
+from ppadb.client import Client as AdbClient
9
+from PIL import Image
10
+import pytesseract
11
+
12
+# ----
13
+# Program
14
+# ----
15
+
16
+PROGRAM_NAME = "RoK Scraper"
17
+PROGRAM_VERSION = "0.3"
18
+PROGRAM_DESCRIPTION = "This program controls devices via ADB to take screenshots in Rise of Kingdoms."
19
+
20
+# TODO:
21
+# - Skip your own ranking
22
+# - Correct for device rotation
23
+# - Better logic for tap_profile()
24
+# - Draw box around skipped profiles
25
+# - Support for the last ~995-1000
26
+# - Coordinates to percentages
27
+
28
+# ----
29
+# Arguments
30
+# ----
31
+
32
+parser = argparse.ArgumentParser(description = PROGRAM_DESCRIPTION)
33
+parser.add_argument("-p", "--project", help = "project name", required = True)
34
+parser.add_argument("-s", "--serial", help = "device serial", required = True)
35
+parser.add_argument("-v", "--verbose", help = "be verbose", default = False, action = "store_true")
36
+parser.add_argument("-c", "--count", help = "how many to scrape", required = True, type = int)
37
+parser.add_argument("-n", "--start", help = "what number to start scraping at", default = 1, type = int)
38
+parser.add_argument("--debug", help = "debug", default = False, action = "store_true")
39
+arguments = parser.parse_args()
40
+
41
+# ----
42
+# Coordinates
43
+# ----
44
+
45
+ROTATE = 0 # TODO: fix this; shouldn't be needed
46
+
47
+TAP_LOCATIONS = {
48
+    "1": (460, 460),
49
+    "2": (460, 624),
50
+    "3": (460, 780),
51
+    "4": (460, 940),
52
+    "Middle": (460, 980),
53
+    "Middle + 1": (460, 1136),
54
+    "Middle + 2": (460, 1300)
55
+}
56
+
57
+# ----
58
+# Functions
59
+# ----
60
+
61
+# TODO: pass device instead of image
62
+def read_string_from_image(image, x, y, x2, y2):
63
+    return pytesseract.image_to_string(image.crop((x, y, x2, y2)), config="--psm 6").strip().replace('\n', ' ').replace('\r', '').replace('\t', ' ')
64
+
65
+def get_clipboard(device):
66
+    raw = device.shell('am broadcast -n "ch.pete.adbclipboard/.ReadReceiver"')
67
+    dataMatcher = re.compile("^.*\n.*data=\"(.*)\"$", re.DOTALL)
68
+    dataMatch = dataMatcher.match(raw)
69
+    return dataMatch.group(1)
70
+
71
+def take_screenshot(device, path):
72
+    screencap = device.screencap()
73
+    image = Image.open(io.BytesIO(screencap)).rotate(ROTATE, expand=1)
74
+    image.save(path + ".png", "PNG")
75
+    return
76
+
77
+# TODO: this can be done better
78
+def get_datetime_string():
79
+    d = datetime.now()
80
+    return str(d.year).zfill(4) + str(d.month).zfill(2) + str(d.day).zfill(2) + str(d.hour).zfill(2) + str(d.minute).zfill(2) + str(d.second).zfill(2)
81
+
82
+def read_single_power_ranking(device, folder):
83
+    datetime = get_datetime_string()
84
+
85
+    # Screenshot profile
86
+    # ID, Username, Power, Kill Points
87
+    time.sleep(1)
88
+    take_screenshot(device, folder + datetime + "_profile")
89
+    
90
+    # Copy and save username
91
+    device.shell("input tap 1055 455")
92
+    time.sleep(1)
93
+    username = get_clipboard(device)
94
+    with open(projectFolder + "usernames.txt", 'a+', newline='', encoding='utf-8') as output_file:
95
+        output_file.write(datetime + "\t")
96
+        output_file.write(username)
97
+        output_file.write("\n")
98
+    if arguments.verbose: print(username, end = "", flush = True)
99
+    
100
+    # Click "More Info"
101
+    # Screenshot "More Info"
102
+    # Power, Kill Points, Highest Power, Victory, Defeat, Dead, Scout Times, Resources Gathered, Resource Assistance, Alliance Help Times
103
+    device.shell("input tap 620 1070")
104
+    time.sleep(2)
105
+    take_screenshot(device, folder + datetime + "_more")
106
+    
107
+    # Click "?"
108
+    # Screenshot Kills
109
+    # Kill Points, T1-5 Kills, T1-5 Points
110
+    device.shell("input tap 1725 250")
111
+    time.sleep(1)
112
+    take_screenshot(device, folder + datetime + "_kills")
113
+    
114
+    # Exit
115
+    if arguments.verbose: print("") # TODO: say "done" or something here if successful
116
+    device.shell("input tap 2230 85")
117
+    time.sleep(1)
118
+    device.shell("input tap 2185 160")
119
+    return
120
+
121
+# Tap to enter a profile, skipping ones that cannot be clicked
122
+def tap_profile(i, folder): # TODO: fix this absolute mess of a function
123
+    if i == 1:
124
+        if is_profile(TAP_LOCATIONS["1"], folder, i): return 1
125
+        elif is_profile(TAP_LOCATIONS["2"], folder, i + 1): return 2
126
+        elif is_profile(TAP_LOCATIONS["3"], folder, i + 2): return 3
127
+        elif is_profile(TAP_LOCATIONS["4"], folder, i + 3): return 4
128
+        else: sys.exit("Profile not found.")
129
+    elif i == 2:
130
+        if is_profile(TAP_LOCATIONS["2"], folder, i): return 1
131
+        elif is_profile(TAP_LOCATIONS["3"], folder, i + 1): return 2
132
+        elif is_profile(TAP_LOCATIONS["4"], folder, i + 2): return 3
133
+        else: sys.exit("Profile not found.")
134
+    elif i == 3:
135
+        if is_profile(TAP_LOCATIONS["3"], folder, i): return 1
136
+        elif is_profile(TAP_LOCATIONS["4"], folder, i + 1): return 2
137
+        else: sys.exit("Profile not found.")
138
+    elif i == 4:
139
+        if is_profile(TAP_LOCATIONS["4"], folder, i): return 1
140
+        else: sys.exit("Profile not found.")
141
+    else:
142
+        if is_profile(TAP_LOCATIONS["Middle"], folder, i): return 1
143
+        elif is_profile(TAP_LOCATIONS["Middle + 1"], folder, i + 1): return 2
144
+        elif is_profile(TAP_LOCATIONS["Middle + 2"], folder, i + 2): return 3
145
+        else: sys.exit("Profile not found.")
146
+
147
+# Tap and test if you are in a profile 
148
+def is_profile(coordinates, folder, i):
149
+    device.shell("input tap " + str(coordinates[0]) + " " + str(coordinates[1]))
150
+    time.sleep(1)
151
+    if read_string_from_image(Image.open(io.BytesIO(device.screencap())).rotate(ROTATE, expand=1), 1652, 1167, 1745, 1209) == "Mail":
152
+        return True
153
+    else:
154
+        device.shell("input tap " + str(coordinates[0]) + " " + str(coordinates[1]))
155
+        time.sleep(2)
156
+        if read_string_from_image(Image.open(io.BytesIO(device.screencap())).rotate(ROTATE, expand=1), 1652, 1167, 1745, 1209) == "Mail":
157
+            return True
158
+        if arguments.verbose: print("skipped rank " + str(i) + ". ", flush = True)
159
+        take_screenshot(device, folder + "skipped/" + get_datetime_string() + "_skipped_" + str(i))
160
+        return False
161
+
162
+# Read from the power rankings list
163
+def read_from_power_rankings(count, start, device, folder):
164
+    if arguments.verbose: print("Reading", count, "power rankings.")
165
+    time.sleep(1)
166
+
167
+    i = start
168
+    while i < (count + 1):
169
+        if arguments.verbose: print(str(i) + "/" + str(count) + ": ", end = "", flush = True)
170
+
171
+        # Scrape
172
+        i = i + tap_profile(i, folder)
173
+        read_single_power_ranking(device, folder)
174
+
175
+        # Rest
176
+        time.sleep(1)
177
+        if i%10 == 0: time.sleep(5)
178
+        if i%25 == 0: time.sleep(10)
179
+    return
180
+
181
+# ----
182
+# Main
183
+# ----
184
+
185
+if __name__ == '__main__':
186
+    # Connect to ADB device
187
+    client = AdbClient(host="127.0.0.1", port=5037)
188
+    device = client.device(arguments.serial)
189
+    print("Connected to", device.serial)
190
+
191
+    # Create project folder
192
+    projectFolder = "output/" + arguments.project + "/"
193
+    if not os.path.exists(projectFolder):
194
+        os.makedirs(projectFolder + "skipped/")
195
+
196
+    # Debug
197
+    if arguments.debug: take_screenshot(device, projectFolder + get_datetime_string() + "_debug")
198
+
199
+    # Scrape
200
+    read_from_power_rankings(arguments.count, arguments.start, device, projectFolder)

Laden…
Annuleren
Opslaan