Why I Needed Public-Only Data
The login wall was slowing me down
For most of last year I kept a browser tab pinned just to check what a handful of overseas accounts were posting. Every time the session expired I lost ten minutes to the login dance: reset password, approve the new device, solve the captcha that my own hand seemed to fail twice. It was the kind of friction that makes you stop checking things you actually needed to check.
I didn't need private data
What I actually wanted was boring. Post timestamps, a caption or two, whether a profile had quietly shifted its posting rhythm. None of that lives behind a login. The public page already showed it. I had been paying the login tax for data I could read for free, and I only noticed once the tab crashed on a Friday afternoon.
A watcher, not a scraper
I started calling it a watcher instead of a scraper, because the goal was never to hoover everything. I wanted small, repeatable check-ins on public profiles I already followed by hand. The difference matters: a scraper is anxious and greedy, a watcher is calm and specific. That reframe alone changed how I wrote the code.
The Architecture in 60 Lines
Fetching a public page
The whole fetch was a plain GET with a browser-like user agent. No cookies, no session object, no stored token. I treated every request as a stranger knocking once and leaving.
import requests, time
def fetch_profile(url):
headers = {"User-Agent": "Mozilla/5.0 (research)"}
r = requests.get(url, headers=headers, timeout=10)
return r.text
Parsing what matters
I resisted the urge to grab everything. I kept only the fields I could defend in a meeting: the handle, the most recent post text, and the timestamps. Anything I could not explain to a teammate got dropped at parse time.
from bs4 import BeautifulSoup
def parse(html):
soup = BeautifulSoup(html, "html.parser")
return {
"handle": soup.select_one("h1") and soup.select_one("h1").text.strip(),
"posts": [p.text.strip() for p in soup.select(".post")][:20],
}
Storing deltas in SQLite
Instead of dumping full snapshots, I stored the diff. A profile that posted nothing new this week writes nothing new to the database. That single choice kept the file small and turned "what changed this week" into one query instead of a diff I ran in my head.
import sqlite3
conn = sqlite3.connect("watch.db")
conn.execute(
"INSERT OR REPLACE INTO profiles(handle, snapshot) VALUES(?, ?)",
(handle, json.dumps(snapshot)),
)
Rate Limits and Being Polite
Sleeping between requests
I added a random sleep of three to eight seconds between profiles. It costs me nothing and it keeps the target servers happy. The moment you start firing requests in a tight loop, you stop being a curious reader and become a small nuisance, and the block that follows is your own fault.
Caching to avoid re-fetch
If I had already fetched a profile in the last six hours, I skipped it. A tiny in-memory dict was enough at my scale, which was maybe forty profiles a day. I never needed Redis for a problem this small, and pretending otherwise would have been its own kind of waste.
Handling missing fields
Public pages break. Sometimes the selector returns nothing because the layout changed overnight. I treated every missing field as "unknown" rather than crashing the run. A watcher that dies on the third profile is worse than one that quietly logs a blank and moves on.
What I Actually Learned
Public pages tell a calmer story
Once I stopped logging in, I stopped seeing the algorithmic feed. What remained was the profile itself, which is a much calmer and more honest signal than the home timeline I used to confuse for research. The noise I thought I needed turned out to be the thing I most needed to remove.
The tool I keep open
These days I keep something lighter running in a background tab. When I only want to glance at a public account without the login wall, a small read-only viewer like TikTok account viewer does the same job my script used to, without me maintaining a single line of it.
One small habit
The real win was never the code. It was deciding that checking competitors and scrolling my own feed are two different activities, and I only let one of them happen during work hours. The script just made the boundary easier to keep, and that was worth more than any dashboard I had subscribed to.
Top comments (0)