Skip to content

About

Telegram channel scraper in Python: pull the post archive of public t.me channels into pandas with views, media type, outbound links, forwards and subscriber counts, saved as JSON or CSV. Python example for the Devil Scrapes Apify Actor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

1 Commit

Folders and files

Repository files navigation

Telegram Channel Scraper β€” Python example

Telegram Channel Scraper in Python: export public channel posts to pandas, JSON and CSV

Run on Apify Python MIT license CI

This repository shows how to pull a public Telegram channel's history into Python with the Telegram Channel Scraper. You list channel usernames, for example telegram (Telegram's own news channel), and the script hands back a dataset of posts plus one summary row per channel. Each post arrives as a typed record: ID, timestamp, text, views, media, links and forward source. Collection happens on Apify's servers, so there is no Telegram account, phone number, API ID or session file involved on your side. What lands locally is results.json and results.csv, shaped for a DataFrame from the first line.

Actor: Telegram Channel Scraper Β· Publisher: Devil Scrapes Β· Also in JavaScript: telegram-channel-scraper-javascript

πŸ”₯ What you get

  • Two row shapes in one dataset: a channel_meta row per channel (display title, subscriber total, about text) sits next to the post rows, and row_type tells them apart.
  • Posts with their context: each message keeps its numeric ID, UTC timestamp, plain text with line breaks, view total, media kind and link, the URLs it points to and the channel it was forwarded from.
  • Any username format: telegram, @telegram, t.me/telegram and https://t.me/s/telegram all resolve to the same channel, and repeats in the list are dropped.
  • Goes back in time: the Actor follows Telegram's older-posts cursor page by page until it reaches your per-channel cap or the cut-off date.
  • Dead channels do not kill the batch: a private, renamed or missing channel is logged and skipped, and the run summary counts how many were skipped.
  • Throttling is handled server-side: temporary errors are retried with growing pauses and a fresh proxy session, so a busy moment on Telegram's side rarely costs you a channel.
  • Metadata rows are free: only post rows trigger the per-result charge.

🎯 Use cases

Measure how far a channel's posts actually travel

Divide each post's view_count by the channel's subscriber_count and you get a reach ratio that is comparable across channels of very different sizes. A notebook with a dozen industry or government channels shows quickly which ones still get read and which ones have a large but sleeping audience.

Archive official announcements for later reference

Ministries, transport operators, exchanges and software vendors often publish status updates on Telegram before anywhere else. A nightly cron job with sinceDate set to yesterday keeps an append-only archive in Parquet or SQLite, with a permalink on every row for citation.

Trace where a story spreads

forwarded_from names the channel a post was copied from, and outbound_links lists the sites it points to. Group by those two columns and you have the raw material for a source graph, which is the first step in most OSINT and media research on Telegram.

Study posting rhythm and format

Bucket datetime by hour or weekday and split by media_type to see when a channel publishes and whether photo, video or text-only posts collect more views. It is a quick way to benchmark your own channel against peers before changing a content calendar.

⚑ Quick start

You need an Apify account and an API token from Console β†’ Settings β†’ Integrations. Apify's free plan includes monthly usage credit; this Actor's event prices are listed under Pricing below.

git clone https://github.com/devilscrapes/telegram-channel-scraper-python.git
cd telegram-channel-scraper-python
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export APIFY_TOKEN=your_token_here
python main.py                      # uses input.json
python main.py my-input.json --out data --max-charge 2

The script starts the Telegram Channel Scraper, waits for it to finish, prints a link to the run in Apify Console and saves every result to results.json and results.csv. It exits with code 1 and the run's status message if the run fails.

🧩 The core call

Below is everything the integration needs. apify-client starts the run, blocks until it ends and then iterates over the dataset in pages, so even a 5,000-post backfill streams without a memory spike.

from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("DevilScrapes/telegram-channel-scraper").call(run_input={'usernames': ['telegram'], 'maxPostsPerChannel': 10, 'proxyConfiguration': {'useApifyProxy': True}})
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

πŸ“₯ Input

input.json is posted to the Actor exactly as written. It asks for the ten newest posts of Telegram's official news channel, which costs a few cents and finishes quickly. For a deeper backfill raise maxPostsPerChannel, or add sinceDate (for example "2026-01-01"; a value without a time zone is read as UTC) to stop at a fixed day.

{
  "usernames": [
    "telegram"
  ],
  "maxPostsPerChannel": 10,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
Field Type Required Default Allowed values
usernames array yes ["durov"] β€”
maxPostsPerChannel integer no 100 β€”
sinceDate string no β€” β€”
proxyConfiguration object no {"useApifyProxy": true} β€”

πŸ“€ Output

The dataset mixes both row kinds, so filter on row_type first. Fields that belong to the other kind come back as null. View and subscriber numbers are expanded from Telegram's abbreviated display, so 3.47M becomes 3470000: precise to the digits Telegram shows, not to the single view. For videos, media_url is the preview image, not the video file. reply_count is always null because the public web view does not show it. Rows are written page by page while the Actor walks backward, so sort by datetime or post_id before any time-series work.

Field Type Always present
row_type string yes
channel string yes
post_id integer or null no
datetime string or null no
text string or null no
view_count integer or null no
media_type string or null no
media_url string or null no
outbound_links array or null no
forwarded_from string or null no
reply_count integer or null no
permalink string or null no
title string or null no
subscriber_count integer or null no
description string or null no

πŸ’° Pricing

Event Price (USD)
Actor start $0.2
Post row emitted $0.002

Prices as configured on 2026-08-20. The Actor page always shows the current price. Runs are billed to the Apify account that owns the token. Use --max-charge to cap what a single run can cost.

πŸ› οΈ Working with the data in Python

The pandas sketch below (pip install pandas) splits the two row kinds, computes a reach ratio per post, summarizes each channel, ranks the most-linked domains and shows median views by hour of day.

import json
from urllib.parse import urlparse

import pandas as pd

rows = pd.DataFrame(json.load(open("results.json", encoding="utf-8")))
meta = rows[rows["row_type"] == "channel_meta"].set_index("channel")
posts = rows[rows["row_type"] == "post"].copy()
posts["datetime"] = pd.to_datetime(posts["datetime"], utc=True)
posts = posts.sort_values("datetime")

subscribers = posts["channel"].map(meta["subscriber_count"])
posts["reach"] = posts["view_count"] / subscribers
posts["hour_utc"] = posts["datetime"].dt.hour

summary = posts.groupby("channel").agg(
    posts=("post_id", "count"),
    median_views=("view_count", "median"),
    median_reach=("reach", "median"),
    oldest=("datetime", "min"),
    newest=("datetime", "max"),
)
print(summary.to_string())

links = posts.explode("outbound_links")["outbound_links"].dropna()
print(links.map(lambda url: urlparse(url).netloc).value_counts().head(10).to_string())
print(posts.groupby("hour_utc")["view_count"].median().to_string())
posts.to_csv("posts_sorted.csv", index=False)

❓ FAQ

Is this the same as the Telegram Bot API or MTProto?

No. The Bot API only sees chats your bot was added to, and MTProto clients need a phone-verified account plus an API ID. This Actor reads the public t.me/s/ web view that Telegram serves to browsers for public channels, so it needs neither.

Which channels can it read?

Any channel that has a public username and shows posts at t.me/s/<name>. Private channels, invite-link-only channels and group chats do not have that page and are skipped with a warning.

How do I collect only new posts on a schedule?

Store the highest post_id per channel after each run and drop anything at or below it next time, or pass sinceDate with the date of the previous run. Post IDs grow with every new message in a channel, so they make a reliable high-water mark.

Are comments under posts included?

No. Discussion threads live in a linked group that the public channel page does not render, so neither comment text nor comment totals are available here.

🧯 Troubleshooting

  • Set APIFY_TOKEN first: export the token in the same shell, or put it in a local .env file your shell loads. Never commit it.
  • Run finished with FAILED or ABORTED: open the printed Console link; the run log says why. Input validation errors name the field.
  • Fewer rows than expected: a --max-charge cap also limits how many results can be charged. Raise the cap or lower the limits in your input.
  • Something looks wrong in the data: open an issue on the Actor page so the maintainers see it.

πŸ”— More from Devil Scrapes

Related Actors:


MIT licensed example code. The Actor itself runs on Apify and is maintained by Devil Scrapes. Not affiliated with Telegram or Telegram Messenger Inc..

About

Telegram channel scraper in Python: pull the post archive of public t.me channels into pandas with views, media type, outbound links, forwards and subscriber counts, saved as JSON or CSV. Python example for the Devil Scrapes Apify Actor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages