昔の画像を保存していたUSBメモリ(1GB)を、別のデータを入れてはフォーマットして、何度も使い回していた。あとになって、その昔の画像を取り出したくなった。
Claude Codeに「フォーマットしてしまったUSBから昔のデータを取り出せるか」と頼んで調べてもらった。専用の復元ソフトは入れず、PCにもともとあったPowerShellとPythonだけで作業した。
結論から書くと、取り出せた画像は36枚(うち1枚は下半分が欠けていた)。それより昔の画像は、あとから書き込んだデータで上書きされていて戻らなかった。フォーマットしただけなら戻る見込みはあるが、そのあと何度もデータを書き込んでいると、昔のものほど戻らない。
この記事では、USBをまるごとコピーしてから画像を切り出した手順と、使ったスクリプト、結果をまとめる。
まず:USBに何も書き込まない
フォーマットしたUSBから復元したいなら、最初に守ることは1つだけだ。そのUSBにファイルを保存しない、もう一度フォーマットしない。Windowsに「ドライブを修復しますか」と聞かれても応じない。書き込むたびに、残っている古いデータが上書きされていく。
私は、USBの中身をまるごと1つのファイル(イメージファイル)にしてPCのDドライブにコピーし、以後はそのコピーだけを使って作業した。USBには読み取りしかしていない。
フォーマットの種類で、戻る見込みが変わる
Windowsのフォーマットには「クイックフォーマット」と、そうでない通常のフォーマットがある。
- クイックフォーマット:ファイルの一覧(どこに何があるかの表)を作り直すだけ。データそのものはたいてい残っている
- 通常のフォーマット:Windows Vista以降は、ドライブ全体に0を書き込む。こうなるとデータは戻らない
通常のフォーマットで全体に0を書き込むことは、MicrosoftのChange in the behavior of the format command in Windows Vista and later versions - Microsoft Learnに書かれている。エクスプローラーのフォーマット画面で「クイック フォーマット」にチェックが入っていれば、0は書き込まれない。
私はどちらでフォーマットしたか覚えていなかった。ただ、コピーしたUSBの中身を調べると、全体の99.5%のセクターに0以外のデータが入っていた。少なくとも最後のフォーマットでは、全体を0で消してはいなかったことになる。
USBの状態
PowerShellで見たUSBの状態はこうだった。
- 容量:約1GB(1,039,859,712バイト)
- ファイルシステム:FAT
- 中身:Windowsが自動で作る
System Volume Informationだけで、空
確認に使ったコマンドはこれだ(FはUSBのドライブ文字)。
Get-Volume -DriveLetter F | Format-List DriveLetter,FileSystem,DriveType,Size,SizeRemaining Get-Partition -DriveLetter F | Format-List DiskNumber,Offset,Size
手順①:USBをまるごとイメージファイルにコピーする
USBを\\.\F:という形で開くと、ファイルの一覧を通さずに、USBの最初から最後までを直接読める。これをそのままファイルに書き出す。WindowsのCreateFileという機能を、読み取り専用で使っている。
次のスクリプトをPowerShellに貼り付けて実行する。最初の3行は自分の環境に合わせて書き換える。保存先は、USBとは別のドライブにする。
$drive = 'F'
$out = 'D:\usb_recovery\usb.img'
$start = 0
$end = (Get-Partition -DriveLetter $drive).Size
New-Item -ItemType Directory -Force -Path (Split-Path $out) | Out-Null
Add-Type -TypeDefinition @"
using System; using System.IO; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles;
public static class RawCopy {
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
public static extern SafeFileHandle CreateFile(string n, uint a, uint s, IntPtr sa, uint c, uint f, IntPtr t);
public static string Dump(string dev, string outp, long start, long end) {
var h = CreateFile(dev, 0x80000000, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);
if (h.IsInvalid) return "OPEN FAILED err=" + Marshal.GetLastWin32Error();
long pos = start; long bad = 0;
using (var fs = new FileStream(h, FileAccess.Read, 4096, false))
using (var o = File.Create(outp)) {
var buf = new byte[65536];
while (pos < end) {
int want = (int)Math.Min(buf.Length, end - pos);
bool ok = false;
for (int t = 0; t < 3 && !ok; t++) {
try { fs.Position = pos; int n = fs.Read(buf, 0, want); if (n <= 0) return "EOF at " + pos + " bad=" + bad; o.Write(buf, 0, n); pos += n; ok = true; }
catch (Exception e) {
if (e.Message.Contains("does not exist")) return "DEVICE LOST at " + pos + " bad=" + bad;
System.Threading.Thread.Sleep(200);
}
}
if (!ok) {
var sec = new byte[512];
for (int i = 0; i < want; i += 512) {
try { fs.Position = pos; fs.Read(sec, 0, 512); }
catch (Exception e) {
if (e.Message.Contains("does not exist")) return "DEVICE LOST at " + pos + " bad=" + bad;
Array.Clear(sec, 0, 512); bad++;
}
o.Write(sec, 0, 512); pos += 512;
}
}
}
}
return "OK end=" + pos + " badSectors=" + bad;
}
}
"@
[RawCopy]::Dump("\\.\$($drive):", $out, $start, $end)
スクリプトのポイントは次のとおり。
CreateFileの2つ目の値0x80000000は読み取り(GENERIC_READ)。USBには書き込まない- 64KBずつ読み、読めない時は3回までやり直す。それでもだめなら512バイトずつ読み、読めない部分は0で埋めて先へ進む(最後の
badSectorsがその数) - USBが抜けた時は、そこまで読んだ位置を
DEVICE LOST at 〇〇と表示して止まる
\\.\F:の形で開く方法は、CreateFileW 関数 (fileapi.h) - Microsoft Learnの「物理ディスクとボリューム」に書かれている。そこには管理者権限が必要とあるが、私のPCでは管理者として開いていないPowerShellでもUSBを読めた。OPEN FAILED err=5(アクセス拒否)と出る場合は、PowerShellを管理者として実行し直す。
途中でUSBが外れた時は、続きから読む
私の時は、約740MB(739,246,080バイト)まで読んだところでUSBがPCから見えなくなった。接触が悪かったのか、USBメモリ自体が弱っているのかは分からない。
USBを挿し直し、同じドライブ文字で認識されたのを確かめてから、続きだけを別のファイルに読んだ。上のスクリプトの最初の3行を次のように変えて、もう一度実行する($startには止まった位置の数字を入れる)。
$drive = 'F' $out = 'D:\usb_recovery\usb_part2.img' $start = 739246080
今度はOK end=1039859712 badSectors=0で最後まで読めた。読めなかった部分は0だった。2つのファイルは、コマンドプロンプトのcopy /bで1つにつなげる。
cd /d D:\usb_recovery copy /b usb.img + usb_part2.img usb_full.img
copy /bはファイルをバイナリのまま連結する(copy - Microsoft Learn)。できたusb_full.imgがUSBと同じ1,039,859,712バイトになっていれば、つなげられている。
手順②:イメージファイルからJPEGを探して切り出す
フォーマットでファイルの一覧は消えているので、ファイル名からは探せない。そこで、データの中身から画像を探す。JPEGは必ずFF D8 FFというバイトで始まり、FF D9で終わる。この並びを探して、間を1枚の画像として切り出す。「ファイルカービング」と呼ばれる方法だ。
ただ、JPEGの中にはサムネイル画像が入っていることがあり、最初に見つかったFF D9がサムネイルの終わりのこともある。なので、JPEGの構造(各ブロックの長さ)を順にたどって、本体の画像データの後ろにあるFF D9を探している。
使ったのはPython(3.14)と、画像を扱うライブラリのPillowだ。Pillowが入っていなければ、先に入れる(Basic Installation - Pillow)。
python -m pip install --upgrade Pillow
次の内容をcarve_jpeg.pyとして保存する。実際に使ったものを、読みやすく整えたものだ。
import re, struct, sys, os
from PIL import Image
img_path = sys.argv[1]
out_dir = sys.argv[2]
os.makedirs(out_dir, exist_ok=True)
data = open(img_path, 'rb').read()
def find_end(s):
# JPEGの構造をたどって、画像データの後ろの FF D9 を探す
p = s + 2
while p + 4 <= len(data):
if data[p] != 0xFF:
return None
m = data[p + 1]
if m == 0xFF:
p += 1
continue
if m in (0xD8, 0x01) or 0xD0 <= m <= 0xD7:
p += 2
continue
length = struct.unpack('>H', data[p + 2:p + 4])[0]
if m == 0xDA:
q = data.find(b'\xff\xd9', p + 2 + length)
return q + 2 if q >= 0 else None
p += 2 + length
return None
n = 0
for hit in re.finditer(rb'\xff\xd8\xff[\xe0\xe1\xdb\xee]', data):
s = hit.start()
e = find_end(s)
if not e:
print('skip', s)
continue
n += 1
f = os.path.join(out_dir, f'img_{n:03d}_off{s}.jpg')
with open(f, 'wb') as o:
o.write(data[s:e])
try:
with Image.open(f) as im:
im.load()
st = 'OK'
except Exception as ex:
st = 'NG ' + str(ex)[:50]
print(n, s, e - s, st)
print('found:', n)
実行するコマンドはこれだ。1つ目がイメージファイル、2つ目が画像の保存先になる。
cd /d D:\usb_recovery python carve_jpeg.py usb_full.img recovered
切り出した画像はimg_001_off193802240.jpgのような名前で保存される。offの後ろの数字は、USBの先頭から何バイト目にあったかだ。最後の列がOKなら、Pillowで画像として最後まで読み込めている。
このスクリプトはイメージファイルをまとめてメモリに読み込む。1GBのUSBなら問題ないが、数十GBのUSBやSDカードなら、後で書くPhotoRecを使う方がよい。
結果:戻ったのは36枚、昔の画像は残っていなかった
見つかったJPEGは36枚だった。PNG・GIFなど、ほかの画像の形式は見つからなかった。
| 項目 | 結果 |
|---|---|
| 見つかった画像 | JPEG 36枚(1枚あたり約0.3〜0.8MB) |
| 正常に見られた画像 | 35枚 |
| 一部が欠けた画像 | 1枚(上は表示されるが、下半分が灰色) |
| 画像があった場所 | USBの先頭から約190MB、約320MB、約335MBのあたりの3か所にまとまっていた |
| 約342MBより後ろ | 最後までランダムに見えるデータで埋まっていて、画像の始まり(FF D8 FF)は1つもなかった |
ファイル名や撮影日は戻らない。それらはフォーマットで消えた一覧の側に書かれていたからだ。また、1枚の画像がUSBの中でとびとびに保存されていると、このやり方ではつながらない。下半分が欠けた1枚は、おそらくこのためだと思う(確かめてはいない)。
約342MBより後ろに何が入っていたのかは分からなかった。圧縮ファイルや動画などの断片の可能性がある。いずれにしても、そこには画像ではないデータが書き込まれていた。
何度もフォーマットしたUSBで、昔の画像が戻らない理由
クイックフォーマットで消えるのは一覧だけで、データ本体は残る。しかし、そのあと新しいデータを書き込めば、その場所にあった古いデータは置き換わる。上書きされたデータは、どの復元ソフトでも戻せない。
私のUSBは、別のデータを入れてはフォーマットするのを何度も繰り返していた。今回戻った36枚は、最後のほうの使い方で残ったデータだと思う。もっと昔の画像は、その後に書き込んだデータで上書きされたと考えるのが自然だ。USBの中は99.5%がデータで埋まっていて、空いたまま残っている場所もほとんどなかった。
USBメモリの中には、PCからは見えない予備の領域もある。そこに古いデータが残っている可能性はゼロではないが、読むには業者がメモリチップを取り外して読む必要がある。私は試していない。
普通は無料のPhotoRecを使えばよい
今回はClaude Codeがその場でスクリプトを書いたので、ツールを入れずに済んだ。ただ、同じことをするなら、無料の復元ソフトPhotoRecを使う方が手軽だ。公式の説明(PhotoRec - CGSecurity)には次のように書かれている。
- ファイルシステムを無視して、データの中身から写真・動画・文書などを取り出す。フォーマットしたドライブにも使える
- 復元したいドライブは読み取り専用で扱う。取り出したファイルは、元と同じドライブに保存しない
- FAT・NTFS・exFATなどに対応し、Windowsで動く
- USBそのものだけでなく、ddで作ったイメージファイルにも使える
- オープンソース(GPL)で無料
やっている考え方は、今回のスクリプトと同じだ。PhotoRecはJPEG以外の多くの形式にも対応している。手順①で作ったusb_full.imgはそのままのコピー(ddの形式と同じ)なので、PhotoRecにかけることもできるはずだ。私はPhotoRecは試していない。
USBに無かった時に探す場所
USBから戻らなかった画像は、USB以外に残っていないか探す方が見込みがある。
- 画像を最初に保存した場所(デジカメ、スマホ、昔使っていたPC)
- Googleフォト、iCloud、OneDriveなどのクラウド
- 人に送ったことがあれば、メールやLINEの履歴
- 昔のPCやSDカード、外付けHDD。これらも同じ手順で調べられる
大事なデータは、USB1本に入れておくのではなく、別の場所にもコピーしておく。私はDドライブをNASにバックアップしている。そのやり方はRobocopyでローカルフォルダのデータをID/パスワード付きのNASにバックアップするに書いた。
まとめ
- フォーマットしたUSBから復元したいなら、まずUSBに何も書き込まない
- クイックフォーマットならデータ本体は残っていることが多い。Windows Vista以降の通常のフォーマットは全体に0を書き込むので戻らない
- USBを
\\.\F:で開いてまるごとイメージファイルにコピーし、そのコピーからFF D8 FF〜FF D9を探してJPEGを切り出した - 何度もフォーマットして使い回していた1GBのUSBで、戻ったのは36枚(1枚は下半分が欠けていた)。昔の画像は上書きされていて戻らなかった
- 同じことはPhotoRecでもできる。ファイル名や撮影日は戻らない

コメント