feat: add crawl comparison command

This commit is contained in:
tiennm99 committed 2026-06-09 14:10:05 +07:00
1 parent 834ad56f65
commit fb9384d965
5 files changed
+184 -2

No files matched your search

+18 -2
View File
@@ -19,14 +19,30 @@ pip install -r requirements.txt
3. Create a new Telegram app at [https://my.telegram.org](https://my.telegram.org) and get the `api_id` and `api_hash`.
4. Create a free Redis database (e.g. [Upstash](https://upstash.com)) and copy its `rediss://` connection URL.
5. Copy `.env.example` to `.env` and fill in `API_ID`, `API_HASH`, `PHONE`, `GROUP_IDS`, and `REDIS_URL`.
6. Run the script:
6. Crawl configured groups:
```bash
python main.py
python crawl.py
```
The first run asks for the Telegram login code once, then stores the session in Redis. Any later run — on any device pointed at the same Redis — reuses that session and **does not** prompt again.
## Compare two crawls
Compare membership changes for one group between two saved crawls:
```bash
python compare.py <group_id> <time1> <time2>
```
If `time1` and `time2` are omitted, the command compares the latest two crawls for that group:
```bash
python compare.py <group_id>
```
The output lists members added in `time2` and removed since `time1`.
## Configuration
| Variable | Description |
+47
View File
@@ -60,3 +60,50 @@ def list_exports():
continue
records.sort(key=lambda r: (r.get('time', ''), r.get('group_id', 0)))
return records
def list_group_exports(group_id):
"""Return export records for one group, sorted by run time."""
group_id = str(group_id)
return [
record for record in list_exports()
if str(record.get('group_id')) == group_id
]
def get_group_export(group_id, run_time):
"""Return a group export at one run time, or None when missing."""
for record in list_group_exports(group_id):
if record.get('time') == run_time:
return record
return None
def latest_two_group_exports(group_id):
"""Return the latest two export records for a group."""
exports = list_group_exports(group_id)
if len(exports) < 2:
return None, None
return exports[-2], exports[-1]
def diff_group_members(before_record, after_record):
"""Compare two group export records by Telegram member id."""
before_members = _member_index(before_record)
after_members = _member_index(after_record)
before_ids = set(before_members)
after_ids = set(after_members)
added = [after_members[member_id] for member_id in sorted(after_ids - before_ids)]
removed = [before_members[member_id] for member_id in sorted(before_ids - after_ids)]
return added, removed
def _member_index(record):
members = {}
for member in record.get('members', []):
member_id = member.get('id')
if member_id is not None:
members[int(member_id)] = member
return members
+86
View File
@@ -0,0 +1,86 @@
import argparse
import sys
from common import diff_group_members, get_group_export, latest_two_group_exports
def parse_args():
parser = argparse.ArgumentParser(
description='Compare member changes between two crawls for one group.',
)
parser.add_argument('group_id', type=int, help='Telegram group id to compare')
parser.add_argument(
'times',
nargs='*',
help='Optional pair: time1 time2. When omitted, latest two crawls are used.',
)
args = parser.parse_args()
if len(args.times) not in (0, 2):
parser.error('provide both time1 and time2, or omit both to compare latest two crawls')
return args
def main():
args = parse_args()
if args.times:
before_time, after_time = args.times
before_record = get_group_export(args.group_id, before_time)
after_record = get_group_export(args.group_id, after_time)
if before_record is None:
print(f'export not found for group {args.group_id} at {before_time}', file=sys.stderr)
return 1
if after_record is None:
print(f'export not found for group {args.group_id} at {after_time}', file=sys.stderr)
return 1
else:
before_record, after_record = latest_two_group_exports(args.group_id)
if before_record is None or after_record is None:
print(f'need at least 2 exports for group {args.group_id}', file=sys.stderr)
return 1
print_summary(args.group_id, before_record, after_record)
return 0
def print_summary(group_id, before_record, after_record):
added, removed = diff_group_members(before_record, after_record)
title = after_record.get('title') or before_record.get('title') or ''
before_members = before_record.get('members', [])
after_members = after_record.get('members', [])
print(f'group: {title} ({group_id})' if title else f'group: {group_id}')
print(f'before: {before_record.get("time")} ({len(before_members)} members)')
print(f'after: {after_record.get("time")} ({len(after_members)} members)')
print()
print(f'added: {len(added)}')
for member in added:
print(f'+ {format_member(member)}')
print()
print(f'removed: {len(removed)}')
for member in removed:
print(f'- {format_member(member)}')
if not added and not removed:
print()
print('no membership changes.')
def format_member(member):
parts = [str(member.get('id'))]
username = member.get('username')
full_name = ' '.join(
part for part in [member.get('first_name'), member.get('last_name')]
if part
)
if username:
parts.append(f'@{username}')
if full_name:
parts.append(full_name)
return ' | '.join(parts)
if __name__ == '__main__':
raise SystemExit(main())
View File
File renamed without changes.
@@ -0,0 +1,33 @@
# Compare Group Export Diffs
## Context
- `common.py` stores and lists Redis export records.
- `crawl.py` exports groups and should not be reused for comparison because it logs into Telegram.
- `README.md` documents current usage and Redis record shape.
## Requirements
- Add command that receives `group_id`, optional `time1`, optional `time2`.
- If both times missing, compare latest 2 exports for that group.
- If one time missing, fail with clear usage error.
- Print members added and removed between two crawls.
- Keep Redis schema unchanged.
## Implementation
1. Add focused helpers in `common.py` to filter exports by group/time and compare members by Telegram `id`.
2. Add `compare.py` CLI with argparse.
3. Update `README.md` with command examples.
4. Run Python compile check.
## Success Criteria
- `python compare.py <group_id>` compares latest two times.
- `python compare.py <group_id> <time1> <time2>` compares explicit times.
- Missing export records produce readable errors.
- Existing export flow remains unchanged.
## Unresolved Questions
None.