Xスクショ管理ツール
X(旧Twitter)の公開アカウントの投稿を、アドレスバー(投稿URL)ごと1件ずつスクリーンショットして貯め、あとから期間と重要度で選んで、PDFや提出用フォルダに書き出すWindowsのデスクトップアプリです。投稿を証拠として残し、提出できる形に整えるために作りました。撮影も保管も、すべて自分のパソコンの中で完結します。
このページは、その仕組みと、作るうえで判断したことの解説です。
全体の構成
Python製です。画面は標準の Tkinter、ブラウザ操作は Playwright、画像処理は Pillow だけで作っています。ファイルは役割ごとに4つに分けています。
| ファイル | 役割 |
|---|---|
screenshot_x_app.py |
画面(取得・仕分け・書き出しの3タブ)。作業は別スレッドで走らせ、キュー経由で画面に伝える |
screenshot_x_detail.py |
取得の本体。投稿を1件ずつ開き、ウィンドウを撮り、台帳(Archive)に記録する |
screenshot_x_export.py |
書き出しの本体。PDF・sidenote用JSON・提出用フォルダを作る |
screenshot_x.py |
共通部品(アカウント名の正規化、ブラウザ起動、日付のパース、タイムライン一覧の走査) |
画面を介さず、コマンドラインからも同じ処理を呼べます。log / should_stop / on_login_required / on_progress を引数で差し替えられる作りにしてあり、画面側は、それらをキューにつなぐだけです。
保存先はアカウント名から決まる
x_archive\
exampleuser\
posts\20260801_0912_2077xxxxxxxxxxxxxxxxx.png ← 投稿日_時刻_投稿ID
index.csv 一覧(人が見る用。毎回作り直す)
manifest.json 台帳(取得済みの記録・採点・メモ・履歴)
書き出し\
提出用_exampleuser_2026-08-01_2026-08-31\001_….png + 一覧.csv
毎回フォルダを選ばせないのは、別のアカウントのスクショが混ざる事故を防ぐためです。ファイル名に通し番号を使わず、投稿日時と投稿IDにしているのも同じ理由です。あとから古い期間を足しても、番号と日付の順序が食い違いません。投稿IDからは、投稿URLも復元できます。
アカウント名はそのままフォルダ名になるので、入力は normalize_account で検証しています。Xのユーザー名として有効な文字(英数字と_、15文字以内)だけを通し、https://x.com/user?s=20 や x.com/user のような貼り方も、先頭のユーザー名だけを取り出します。a/b や ../x はエラーです。
取得の流れ
取得は2段階です。まず投稿の一覧を集め、そのあと1件ずつ個別ページを開いて撮ります。 一覧のカードを撮るだけの版(screenshot_x.py)も残していますが、アドレスバーに投稿URLが写らないので、証拠としては個別ページを開く方式を使います。
一覧の収集:新しい順にしか辿れない
プロフィールのタイムラインは、新しい投稿から順にしか辿れません。そこで次のように走査します。
- 期間より新しい投稿は、数えずに通過する
- 期間より古い投稿が8件続いたら、そこで打ち切る
- 固定ポストと、日時が読めなかった投稿は、期間の判定を後段に任せる
タイムライン上の日時は「40m」「2h」「1月11日」のような表示テキストしかなく、概算にしかなりません。そのため、ここでは境界の投稿を広めに拾っておき、あとで個別ページの正確な日時で確かめ直します。
取りこぼしを防ぐスクロール量
Xのタイムラインは、画面外の投稿をDOMから外します(仮想化)。1回に大きくスクロールすると、一度も表示されないまま通り過ぎる投稿が出ます。証拠集めの道具としては、これは最も避けたい不具合です。
当初は1回4000pxずつ進めていましたが、画面の高さの7割だけ進めて、そのたびに走査する形に変えました。進みが遅くなる代わりに、見落としは起きにくくなります。
個別ページで確かめる
個別ページでは、投稿自身への日時リンクの aria-label(例:午後11:56 · 2026年9月2日)から、分まで正確な日時が取れます。これで期間を最終判定し、範囲外なら保存しません。取れた日時は、ファイル名と台帳にも使います。
アカウントの表示言語が英語だと、この表記は 11:56 PM · Sep 2, 2026 になります。日本語・英語の両方を読み、どちらも読めなければ <time datetime>(UTC)をローカル時刻に直して使います。日時が読めない投稿を黙って通さないのが目的です。
途中で止まっても続きから
- 1件撮るごとに台帳へ保存するので、中止やエラーのあとも、次回は未取得の分だけを続きから撮ります
- 読み込みに失敗した投稿は、台帳に入れず、次回の再試行に回します
- 読み込み失敗が3件続いたら、X側のブロックを疑って、そこで終了します
- 削除・非公開・凍結の投稿は、「見られない」と判別してスキップし、上の連続失敗には数えません。削除済みの投稿が続いただけで、全体が止まるのを避けるためです
ブラウザのウィンドウを撮る
Playwright の page.screenshot() は、ページの中身しか撮れません。アドレスバーが写らないので、投稿URLを示す証拠になりません。
そこで、ブラウザのウィンドウ本体を、Windows の PrintWindow で撮ります。画面そのものを切り抜く方式(ImageGrab)だと、ほかのウィンドウが重なった瞬間にそれが写り込みます。PrintWindow はウィンドウ本人に描かせる仕組みなので、実行中にほかの作業をしても、写り込みません。ただし、最小化すると撮れません。
撮る前に整えること
- ウィンドウを幅700px・画面の高さいっぱいに固定する。 Xは幅が狭いと右サイドバー(「関連性の高いアカウント」など)を出さず、左メニューもアイコンだけに畳みます。写るのは「アドレスバー+投稿の列」だけになります
- ウィンドウの特定。 CDP(
Browser.getWindowForTarget)で得た位置と、EnumWindowsで列挙したChrome_WidgetWin_1の矩形がぴったり一致するものを探します。見つからないときだけ、画面切り抜きにフォールバックします - 画面の拡大率。 Chromeが返す座標は拡大率(125%など)を掛ける前の値です。実ピクセルとの換算に
GetDeviceCapsで求めた倍率を使い、これを忘れるとウィンドウの右下が欠けます - 見えない縁。 Windowsのウィンドウには、リサイズ用の透明な余白(
SM_CXSIZEFRAME+SM_CXPADDEDBORDER)があります。切り抜くときに差し引かないと、左右と下に背景が写ります - 隠れても描画を止めない。 他のウィンドウに隠れるとChromeは描画を止めます。起動オプションで、このオクルージョン判定とバックグラウンド化を切っています
- 警告バーを出さない。 「サポートされていないコマンドライン フラグ」の黄色いバーがスクショに写らないよう、
--test-typeを付け、--no-sandboxを付けさせない設定にしています
返信まで1枚に繋ぐ
既定では、返信も含めて投稿ページを下までスクロールしながら撮り、1枚の縦長画像に繋げます。
Chrome にページ全体を一度に描かせる方法(captureBeyondViewport)も試しましたが、Xは画面外の要素をDOMから外すので、真っ白な画像になりました。そこで、1画面ずつ撮って、新しく現れた分だけを継ぎ足します。
継ぎ足しには、細かい処理がいくつも要ります。
- 先頭に戻してから始める。 返信への投稿は、元の投稿を上に出して自動でスクロールした状態で開きます。戻さないと上の部分が抜けます
- 動いた距離だけを切り出す。 スクロール前後の
scrollYの差を実ピクセルに換算し、新しく出た帯だけを足します - 左メニューとスクロールバーを消す。 どちらも画面に固定されているので、そのまま継ぐと同じものが何度も写ります。2枚目以降はページの背景色で塗りつぶします(ライト・ダーク・ディムのどれでも崩れないよう、背景色は実際のページから取ります)
- 「もっと見つける」以下を切る。 返信の下にXが出す、無関係な他人の投稿の推薦です。証拠には不要なので、見出しの少し上の無地の行を探して、そこで切ります。文字の途中で切らないためです
- 下端の余白を削る。 ページ末尾の大きな空白を落とします
- 上限は12画面。 返信が非常に多い投稿は、そこで打ち切り、ログに出します
加工をしていることを隠さない
この継ぎ足しは、画面に写ったとおりの1枚ではなく、加工を含む合成画像です。左メニューの塗りつぶし、推薦投稿の切り落とし、余白の削除が入ります。証拠として出す場合は、これを相手に説明できなければいけません。そのため、README に加工の内容を明記し、撮影時のSHA-256を台帳に残しています。1画面に収まる範囲だけを撮る設定(加工が入らない)にもできます。
台帳:何を取得済みかの唯一の記録
manifest.json が、取得済みの投稿、採点、メモ、取得履歴をすべて持つ唯一の台帳です。index.csv はそこから毎回作り直す、人が見るための一覧(Excelでそのまま開けるBOM付きUTF-8)で、手で直す前提にしていません。
1件ごとに、投稿日時・ファイル名・取得日時・SHA-256・重要度・メモを記録します。
台帳を壊さないために、次の点に気を配っています。
- 別名で書いてから置き換える。 1件撮るたびに保存するので、150件なら150回上書きします。直接上書きすると、途中の電源断やOneDriveの同期とぶつかった時に壊れます。
manifest.json.tmpに書いてからos.replaceで差し替えます - 読めない台帳は、空として扱わない。 読み込みに失敗したときに空のアーカイブを返すと、次の保存でそれが書き戻され、採点・メモ・ハッシュが全部消えます。例外にして止めます
- 取得中は採点を止める。 実行中の取得は、台帳の写しを自分で持って保存を繰り返します。同時に仕分け側から保存すると、古い写しで上書きして、撮影の記録か採点のどちらかが消えます。取得・書き出しの間は、採点を受け付けず、メモ欄も入力できなくします
仕分け:1枚ずつ見て、重要度とメモを付ける
撮ったスクショを1枚ずつ表示し、重要度(重要・普通・対象外)とメモを付ける画面です。キーの 1 / 2 / 3 で採点して、自動で次の1枚へ進みます。← → で前後に移動します。
- 返信まで撮った画像は縦に長いので、幅を合わせて表示し、縦スクロールで見ます。クリックすると原寸で開きます
- キーはウィンドウ全体に結び付けているため、入力欄にカーソルがあるときは受け付けません。別のタブで日付「2026-09-01」を打っただけで、表示していない投稿の採点が書き換わるのを防ぐためです
- 画像を開くときは
withで閉じ、表示中もPNGを掴みっぱなしにしません。掴んでいると、そのファイルを別のソフトから移動・削除できなくなります - 画像のファイルが無くなっていても、画面は固まらず、その旨を表示して、前後移動と採点は続けられます
- 保存に失敗したら、黙らず知らせます(
index.csvをExcelで開いたままだと書き込めません)
書き出し:原本は変えず、確かめてから作る
期間と重要度(すべて・普通以上・重要のみ)で選び、次の3つを作れます。
| 書き出すもの | 内容 |
|---|---|
| 1枚ごとに、出典(投稿日時・URL・取得日・重要度)とメモを添える | |
| sidenote用JSON | サイドノートで開くファイル。画像と注釈を入れた状態で作る |
| 提出用フォルダ | 001_… から連番を振ったコピー、ならびに 一覧.csv |
原本には一切手を触れません。 連番は書き出しの時に初めて振るので、何度書き出しても、別の期間を書き出しても、すでに引用した番号がずれません。
書き出す前にSHA-256を照合する
撮影時に台帳へ記録したSHA-256と、書き出す画像の実際のハッシュを照合します。不一致やファイルの欠落が1件でもあれば、書き出しを中止します。 改変・破損・移動のいずれかを疑うべき状態だからです。証拠として出すものを、確かめずに出さないための安全装置です。
縦長画像をページに切り分ける
返信まで撮った画像は、A4の1ページに収まりません。そのまま貼ると縮んで読めなくなるので、ページに収まる高さで切り分けて並べます(本文115mm幅・1枚あたり最大255mm)。
- 上限いっぱいで順に切ると、末尾に数ミリの切れ端が残り、「◯の続き」だけのページができます。残りの高さから毎回、必要な枚数を計算し直し、均等に分けます
- 切れ目は、目標位置の少し手前で、文字も罫線もない行を探して決めます。文字の途中で切らないためです。左メニューのボタンが同じ高さに並ぶだけで「無地の行がない」と判定されないよう、見る範囲は本文の列に絞ります
- 注釈は1枚目にだけ付け、2枚目以降は「◯ の続き」と表示します
PDFはブラウザの印刷機能で作る
HTMLを組み立て、Chrome(なければEdge)のヘッドレスで page.pdf() を呼びます。画像はファイルを直接参照するので、base64にせず、軽く作れます。一方、sidenote用JSONは、画像をbase64で埋め込みます(1ファイルを渡せば、そのまま開けるようにするためです)。1枚あたり約190KB、150枚で約31MBになります。
ログインとボット対策
- ログインは手動です。 初回だけ、開いたブラウザで自分でXにログインします。セッションは、このツール専用のプロファイル(
chrome_profile)に保存され、普段使いのChromeのプロファイルとは別物です。ログイン情報を入力する処理は持っていません - 画面を表示して動かす必要があります。 画面非表示(headless)で動かすと、X側のボット対策でブロックされます(2026年9月に確認)
- exe化した場合は、
__file__が実行のたびに変わる一時フォルダを指します。実行ファイル本体の場所を基準にして、プロファイルと設定を保存します - Chrome が入っていなければ、Edge で起動します。どちらも
Chrome_WidgetWin_1というウィンドウクラスなので、撮影方式は変わりません
chrome_profile にはログインセッションが入っています。公開すると、誰でもそのアカウントに入れてしまうので、リポジトリには絶対にコミットしません(.gitignore 済み)。
限界と使うときの注意
- Windows専用です。 ウィンドウの撮影に Win32 APIを使っています
- 単純リポスト(コメントなし)は取れません。 投稿リンクが元の投稿者のものになり、対象アカウント自身の投稿として検出できないためです
- Xの画面構成が変わると動かなくなることがあります。 投稿を特定するための安定した目印(
data-testid)が使えないので、リンクの形や表示テキストに頼っています - 日時の概算(一覧の段階)は誤差があります。 最終的な期間の判定は、個別ページの正確な日時で行います
- 1件あたり、体感で4〜5秒かかります。投稿が多いアカウントは時間がかかります
- 返信まで撮った画像は、合成を含む加工画像です(前節)
- ブラウザを自動操作するため、Xの利用規約に抵触したり、アカウントが制限されたりする可能性があります。自己責任で使ってください
- 取得した投稿の内容と使い道の責任は、利用者にあります
デザインシステム
掲載予定です。