DEV Community

Gio Rich
Gio Rich

Posted on Fully Autonomous

Scrape public Telegram channels with Python (no login, no API ID)

I built this actor; it's a paid tool on Apify with a free trial credit.

If you want posts from a public Telegram channel, the usual route is Telethon or Pyrogram. They work, but you need a Telegram account, a phone number, an API ID and a session file, and you're logging in as a user to do it. For reading public channels that's a lot of setup.

Every public channel with a web preview is also readable at t.me/s/<channel> in a normal browser. I'm an 18-year-old engineering student, and I built Telegram Channel Scraper on top of that page. This post shows how to call it, what the output looks like, and a small daily digest project.

How it works

t.me/s/<channel> returns server-rendered HTML with up to 20 posts per page and a ?before=N cursor to the older ones. The actor fetches those pages over plain HTTP (no headless browser), parses each message block with cheerio, and walks back through the history. In my tests, 250 posts took 7 seconds and 480 posts took 12 seconds.

Python example

pip install apify-client
Enter fullscreen mode Exit fullscreen mode
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("rel8ble/telegram-channel-scraper").call(run_input={
    "channels": ["durov", "bloomberg"],
    "maxPostsPerChannel": 100,
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["type"] == "post":
        print(item["date"], item["views"], item["text"][:80] if item["text"] else "")
Enter fullscreen mode Exit fullscreen mode

The dataset mixes two kinds of rows: type: "post" for each post and type: "channel" for one info row per channel. Filter on type.

Node example

import { ApifyClient } from "apify-client";

const client = new ApifyClient({ token: "<YOUR_APIFY_TOKEN>" });

const run = await client.actor("rel8ble/telegram-channel-scraper").call({
  channels: ["https://t.me/telegram"],
  onlyPostsNewerThan: "30 days",
  searchQuery: "update",
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
const posts = items.filter((i) => i.type === "post");
console.log(posts.map((p) => `${p.url} (${p.views} views)`));
Enter fullscreen mode Exit fullscreen mode

What comes back

A real post row from a run on 24 Sep 2026, trimmed (the CDN URL is shortened and I kept 2 of 8 reactions):

{
  "type": "post",
  "channel": "bloomberg",
  "channelId": "1488156064",
  "postId": 3225,
  "url": "https://t.me/bloomberg/3225",
  "date": "2026-09-22T03:26:17.000Z",
  "text": "Alibaba unveils new AI chip it says is China's most powerful and targets 20 gigawatts of data center capacity by 2032. https://bloom.bg/4ydroys ...",
  "views": 13400,
  "viewsText": "13.4K",
  "reactionsTotal": 173,
  "reactions": [
    { "emoji": "🔥", "isPaid": false, "count": 62 },
    { "emoji": "❤", "isPaid": false, "count": 33 }
  ],
  "mediaType": "photo",
  "photos": [{ "url": "https://cdn5.telesco.pe/file/...jpg" }],
  "links": ["https://bloom.bg/4ydroys"],
  "hashtags": [],
  "forwardedFrom": null,
  "edited": false
}
Enter fullscreen mode Exit fullscreen mode

And the channel row:

{
  "type": "channel",
  "username": "bloomberg",
  "title": "Bloomberg",
  "subscribers": 175000,
  "verified": true,
  "previewAvailable": true
}
Enter fullscreen mode Exit fullscreen mode

Posts also carry videos (with duration and thumbnail), files, polls with option percentages, forwarded-from and reply-to info when present.

Use case: a daily digest of new posts

Say you follow a handful of news or project channels and want one daily summary with the most-viewed posts, stored so you never see the same post twice.

import sqlite3
from apify_client import ApifyClient

CHANNELS = ["bloomberg", "durov", "telegram"]

db = sqlite3.connect("telegram.db")
db.execute("""CREATE TABLE IF NOT EXISTS posts (
    url TEXT PRIMARY KEY, channel TEXT, date TEXT, views INTEGER, text TEXT)""")

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("rel8ble/telegram-channel-scraper").call(run_input={
    "channels": CHANNELS,
    "onlyPostsNewerThan": "1 day",
    "includeChannelInfo": False,
})

new = []
for p in client.dataset(run["defaultDatasetId"]).iterate_items():
    if p["type"] != "post":
        continue
    cur = db.execute("INSERT OR IGNORE INTO posts VALUES (?,?,?,?,?)",
                     (p["url"], p["channel"], p["date"], p["views"], p.get("text") or ""))
    if cur.rowcount:
        new.append(p)
db.commit()

for p in sorted(new, key=lambda x: x["views"] or 0, reverse=True)[:10]:
    first_line = (p.get("text") or "").split("\n")[0][:100]
    print(f'{p["views"]:>8}  {p["channel"]:<10} {first_line}  {p["url"]}')
Enter fullscreen mode Exit fullscreen mode

Run it once a day. onlyPostsNewerThan: "1 day" makes the scraper stop as soon as posts get older than a day, so you only pay for new posts, and the url primary key handles any overlap between runs.

What it costs

$1.50 per 1,000 results. A result is a post or a channel-info row. Failed requests and duplicates aren't charged.

  • The digest above: if the three channels post ~30 times a day combined, that's ~900 posts a month ≈ $1.35/month
  • Archiving a 10,000-post channel history = $15
  • Apify's free plan gives $5 of monthly credit, about 3,300 posts

Limits

  • Public channels with a web preview only. No private channels, invite links (t.me/+...), groups or bots. Those return just a channel row with previewAvailable: false.
  • No comments. Discussion-group replies aren't in the web preview.
  • Counts are rounded the way Telegram shows them. 13.4K becomes 13400; the raw text is kept in viewsText.
  • Media URLs are signed CDN links that expire. Download files soon after the run if you need them.
  • Very large videos have no direct file URL (tooLargeForPreview: true), just the thumbnail, duration and post link.
  • Custom premium emoji reactions come back as a customEmojiId without the emoji character. The counts are still right.

If a channel returns 0 posts, the RUN_SUMMARY record in the key-value store says why. Anything else, the Issues tab on the actor page reaches me.

Telegram Channel Scraper on Apify


This article was drafted with AI and published by me.

Top comments (0)