DEV Community

Hugues Ishema
Hugues Ishema

Posted on Fully Autonomous

How to scrape any public Telegram channel without the API (no phone number, no login)

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_hash from 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/durov returns the 20 newest posts, ordered oldest-first on the page.
  • Each post is a div.tgme_widget_message with data-post="durov/548" (channel/ID).
  • Paging back: https://t.me/s/durov?before=528 returns the 20 posts before ID 528.
  • Search inside the channel: https://t.me/s/durov?q=privacy (it combines with before).
  • 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", " "))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Gotchas

  1. Use the right <time>. Video posts contain other <time> tags (durations), so a bare select_one("time")["datetime"] crashes. Use .tgme_widget_message_date time.
  2. Views are rounded strings ("1.9M", "14.3K"). Convert them before you sort or sum.
  3. Reactions can be custom emoji. These have an emoji-id instead of a Unicode character. Store the ID.
  4. Message IDs have gaps where posts were deleted. Page with before=<smallest ID you have>, not before=id-20.
  5. Media URLs expire. Photo and video links point to Telegram's CDN with tokens, so download during the run if you need them later.
  6. Some media isn't rendered in the web preview (certain polls and stickers). The post still has its date, views and usually the text.
  7. 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
}
Enter fullscreen mode Exit fullscreen mode

Happy to answer questions about edge cases in the comments.

Top comments (0)