-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathparser.py
More file actions
167 lines (133 loc) · 5.54 KB
/
Copy pathparser.py
File metadata and controls
167 lines (133 loc) · 5.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import os
import re
HARDCODED_IGNORE = {"following", "followers", "follow back", "follow"}
def load_ignorelist(path="ignorelist.txt"):
ignore = set(HARDCODED_IGNORE)
if os.path.exists(path):
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip().lower()
if line:
ignore.add(line)
return ignore
def load_aliases(path="aliases.txt"):
alias_map = {}
if not os.path.exists(path):
return alias_map
with open(path, encoding="utf-8") as f:
for group_id, line in enumerate(f):
line = line.strip()
if not line or line.startswith("#"):
continue
parts = [p.strip().lower() for p in line.split("=")]
for part in parts:
if part:
alias_map[part] = group_id
return alias_map
def is_username(line, min_length=2):
line = line.strip()
if not line or line.isdigit() or " " in line:
return False
if not re.search(r'[a-zA-Z0-9]', line):
return False
if len(line) < min_length:
return False
return bool(re.match(r'^[\w.\-]+$', line))
def is_display_name(line):
line = line.strip()
if not line or line.isdigit():
return False
if " " in line:
return True
return not bool(re.match(r'^[\w.\-]+$', line))
def parse_file(filepath, ignore_set, min_username_length=2):
"""Parse a follower/following export.
These exports list one account per "block": an optional display name
line, followed by the @username line, with blank line(s) separating each
account from the next. A block can also be a single line on its own if
the account has no display name (TikTok shows nothing on that line).
Parsing by these blank-line-delimited blocks (rather than guessing
line-by-line) avoids desync: a single empty-nickname account no longer
shifts every subsequent pairing by one line.
"""
with open(filepath, encoding="utf-8") as f:
raw_lines = [line.strip() for line in f.readlines()]
blocks = []
current = []
for line in raw_lines:
if not line:
if current:
blocks.append(current)
current = []
continue
if line.lower() in ignore_set:
continue
current.append(line)
if current:
blocks.append(current)
entries = []
for block in blocks:
if len(block) == 1:
line = block[0]
if is_username(line, min_username_length):
entries.append({"username": line.lower(), "display_name": None})
elif is_display_name(line):
entries.append({"username": None, "display_name": line})
continue
# 2+ lines: position is authoritative for this export format.
# The last line is the @username, everything before it is the
# display name (joined back together in case of stray extra lines).
display_name = " ".join(block[:-1])
candidate_username = block[-1]
if is_username(candidate_username, min_username_length):
entries.append({"username": candidate_username.lower(), "display_name": display_name})
else:
# Last line doesn't look like a username (unexpected shape) —
# keep the whole block as a display name rather than dropping it.
entries.append({"username": None, "display_name": " ".join(block)})
return entries
def get_platform_from_filename(filename):
name = os.path.splitext(os.path.basename(filename))[0].lower()
match = re.match(r'^([a-z]+)_(followers|following)(\d+)$', name)
if match:
return match.group(1), match.group(2), int(match.group(3))
return "unknown", "unknown", 0
def load_all_users(users_dir="data/users", ignore_set=None, min_username_length=2):
if ignore_set is None:
ignore_set = load_ignorelist()
all_users = {}
if not os.path.exists(users_dir):
print(f"users directory not found: {users_dir}")
return all_users
for user_folder in os.listdir(users_dir):
user_path = os.path.join(users_dir, user_folder)
if not os.path.isdir(user_path):
continue
all_users[user_folder] = {}
for filename in os.listdir(user_path):
if not filename.endswith(".txt"):
continue
filepath = os.path.join(user_path, filename)
platform, list_type, number = get_platform_from_filename(filename)
bucket_key = (platform, number)
if bucket_key not in all_users[user_folder]:
all_users[user_folder][bucket_key] = {
"platform": platform,
"followers": [],
"following": []
}
entries = parse_file(filepath, ignore_set, min_username_length)
if list_type == "followers":
all_users[user_folder][bucket_key]["followers"] = entries
elif list_type == "following":
all_users[user_folder][bucket_key]["following"] = entries
return all_users
def load_target(path="target.txt"):
if not os.path.exists(path):
return None
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#"):
return line.lower()
return None