There are two usual ways to read a Telegram channel from code, and both are annoying:
- Bot API: a bot only sees posts from channels where it's an admin, so it can't read someone else's channel.
-
MTProto (Telethon / Pyrogram): works, but needs an
api_id/api_hashfrom my.telegram.org, which is tied to your phone number. You also manage a logged-in session, and risk your account if you scrape hard.
There's a third way that most people miss: every public channel has a web preview at https://t.me/s/<channel>. It's plain HTML, needs no login or account, and contains the full post text, date, view count, reactions, photos, videos and link previews.
Here's how it works, a 30-line Python scraper, and the gotchas I ran into.
How the preview works
-
https://t.me/s/durovreturns the 20 newest posts, ordered oldest-first on the page. - Each post is a
div.tgme_widget_messagewithdata-post="durov/548"(channel/ID). -
Paging back:
https://t.me/s/durov?before=528returns the 20 posts before ID 528. -
Search inside the channel:
https://t.me/s/durov?q=privacy(it combines withbefore). -
Channel stats: the right column (
.tgme_channel_info) has the title, description, subscriber count ("10.5M subscribers") and photo/video/link counts. -
Private channels, groups, bots and users have no preview. The request redirects to
t.me/<name>instead of returning posts.
Useful selectors inside a post:
| Field | Selector |
|---|---|
| Text |
.tgme_widget_message_text (keep the <br> as newlines) |
| Date | .tgme_widget_message_date time[datetime] |
| Views |
.tgme_widget_message_views ("1.9M") |
| Reactions |
.tgme_reaction (emoji or custom-emoji ID plus count) |
| Photos |
.tgme_widget_message_photo_wrap (URL in background-image) |
| Videos | .tgme_widget_message_video_player video[src] |
| Link preview |
.tgme_widget_message_link_preview (title, description, image) |
| Forwarded from | .tgme_widget_message_forwarded_from_name |
A minimal scraper (Python)
import time
import requests
from bs4 import BeautifulSoup
def channel_posts(channel, limit=50):
"""Newest-first posts from a public Telegram channel's web preview (t.me/s/<channel>)."""
posts, before = [], None
while len(posts) < limit:
url = f"https://t.me/s/{channel}" + (f"?before={before}" if before else "")
html = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=30).text
soup = BeautifulSoup(html, "html.parser")
page = soup.select(".tgme_widget_message[data-post]")
if not page:
break
for m in reversed(page): # the page is oldest-first
text = m.select_one(".tgme_widget_message_text")
views = m.select_one(".tgme_widget_message_views")
posts.append({
"id": int(m["data-post"].split("/")[1]),
"date": m.select_one(".tgme_widget_message_date time")["datetime"],
"text": text.get_text("\n", strip=True) if text else "",
"views": views.get_text(strip=True) if views else None,
})
before = min(p["id"] for p in posts)
time.sleep(1) # be polite
return posts[:limit]
for p in channel_posts("durov", limit=25)[:3]:
print(p["id"], p["date"], p["views"], p["text"][:60].replace("\n", " "))
Output today:
548 2026-09-11T16:04:02+00:00 1.9M 🤝 Telegram has become the sponsor of Codeforces — the larges
547 2026-09-06T17:40:39+00:00 2.81M 💸 In just one month, Telegram has awarded over $2,222,000 to
546 2026-08-31T16:30:15+00:00 3.18M 🏳️ Gram Wallet in Telegram is ready and is now accessible to
Gotchas
-
Use the right
<time>. Video posts contain other<time>tags (durations), so a bareselect_one("time")["datetime"]crashes. Use.tgme_widget_message_date time. - Views are rounded strings ("1.9M", "14.3K"). Convert them before you sort or sum.
-
Reactions can be custom emoji. These have an
emoji-idinstead of a Unicode character. Store the ID. -
Message IDs have gaps where posts were deleted. Page with
before=<smallest ID you have>, notbefore=id-20. - Media URLs expire. Photo and video links point to Telegram's CDN with tokens, so download during the run if you need them later.
- Some media isn't rendered in the web preview (certain polls and stickers). The post still has its date, views and usually the text.
- Stop by date, not by count, when you only need recent posts. Check each post's date and break as soon as you pass your cutoff, instead of paging the whole history.
If you don't want to maintain it
Disclosure: I built this. The Telegram Channel Scraper on Apify does all of the above and returns clean JSON or CSV:
- views as numbers, reactions, photos, videos, documents, link previews, forwards, replies, polls, hashtags and mentions;
- channel stats;
- search inside channels, date filters, and a monitoring mode that returns only new posts since the last run.
It costs $0.50 per 1,000 posts, with no login and no proxy needed.
{
"channels": ["durov", "https://t.me/telegram"],
"maxMessagesPerChannel": 500,
"dateFrom": "2026-01-01",
"onlyNewMessages": false
}
Happy to answer questions about edge cases in the comments.
Top comments (0)