コンテンツにスキップ

Xスクショ管理ツール

X(旧Twitter)の公開アカウントの投稿を、アドレスバー(投稿URL)ごと1件ずつスクリーンショットして貯め、あとから期間と重要度で選んで、PDFや提出用フォルダに書き出すWindowsのデスクトップアプリです。投稿を証拠として残し、提出できる形に整えるために作りました。撮影も保管も、すべて自分のパソコンの中で完結します。

このページは、その仕組みと、作るうえで判断したことの解説です。

全体の構成

Xスクショ管理ツールの全体像
Xスクショ管理ツールの全体像

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つを作れます。

書き出すもの 内容
PDF 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の利用規約に抵触したり、アカウントが制限されたりする可能性があります。自己責任で使ってください
  • 取得した投稿の内容と使い道の責任は、利用者にあります

デザインシステム

掲載予定です。