アナログCPU:5108843109

ゲームと音楽とプログラミング(酒と女とロックンロールのノリで)

('ω') < イザユケエンジニャー

surfaceのSSDが突然死した話

2016年に購入し、ついこの間の修理交換を経て使い倒してきたSurface pro4ですが

ついに壊れました。

結論としては元通りにはなりませんでしたが、
他の人や今後の自分の参考になるかもしれないので色々残しておきます。

症状

電源を入れるとこんな感じになりWindowsが起動しない。

f:id:honey8823:20190131120506j:plain:w300

試したこと一覧

  • データを残して修復
  • 自力でデータをサルベージして本体を初期化
  • データ復旧業者に調査を依頼
  • 以前の修理交換時のデータ移行に使用した外付けHDDのデータ復旧
  • DVDに焼いたUbuntuを起動し、そこからファイルをサルベージ
  • データ削除を伴う初期化

まあ、成功したのは外付けのデータ復旧だけなんですけどね。

試したこと その1

まずは「データを残して修復」みたいな項目があったのでそれを試してみました。

「無理やったわ!なんも変更してないから勘弁してや」みたいなメッセージが表示されて終わり。

しゃーないな。

試したこと その2

次は「自力でデータをサルベージして本体を初期化」を目指しました。
コマンドプロンプトと外付けHDDでなんとかする作戦です。

コマンドプロンプトを起動しようとすると回復キーなるものが必要と言われたので調べてゲット。
スマホからMicrosoftアカウントで見れたのでスクショ撮って保存。

クソ長い回復キーを入力するとコマンドプロンプトが無事起動。
ところが…

まずCドライブへ移動しようとすると

cd /D c:

こんなエラーが。

The volume does not contain a recognized file system. Please make sure that all required file system drivers are loaded and that the volume is not corrupted.

ファイルシステムがないよ」だそう。
いやいやまさか…。

まあとにかくこの方法はダメそう。

ついでにチェックディスクをしてみると

chkdsk c:

The type of the file system is RAW. CHKDSK is not available for RAW drivers.

……
未フォーマットになってる?
そんなまさか…。

試したこと その3

ここで、「とにかくデータの復旧が第一」と考え、データ復旧業者を調べて調査を依頼しました。
Surfaceは分解が極めて困難なのは知ってたので、まあダメ元です。
とりあえず預けて1週間ほど待ったところ、「SSDの物理障害のためデータ復元・修理不可」との回答。
調査費用は不要というめちゃめちゃ親切な対応には感謝しつつ、端末を引き取って振り出しに戻りました。

本筋とは全く関係ないんですが、ここで実はこの端末がSurface pro5だったことが判明。
以前の故障交換時に5に変わった模様?

試したこと その4

セカンドオピニオンを求めて往生際悪く別の業者にも持ち込みの予約をしつつ、
改めて自力でなんとかできないかいろいろ調べている途中、ふと思いつきました。
「以前の修理交換時に外付けHDD経由でデータ移行したので、そちらを復旧すればよい」と。

そもそも外付け側に残してあったデータも多いですし、
移行時に外付けから移動してしまったデータさえ復旧すれば、交換前のデータはすべて揃うことになります。
しかも交換~故障までは約2か月。
その間の惜しいデータもいろいろありはしますが、これに成功すれば精神的ダメージはものすごく少ないです。
これ思いついてよかった。

しかも、データ復元ソフトってだいたい有料なので「失敗したら無駄じゃん」と思うのですが、
復元できるファイルを見る+少量の復元なら無料のソフトがありました。

ということで、正常に稼働しているPCを使って以下手順で復元を試みました。

  • EaseUSをインストール
  • ソフトを起動して外付けを接続、スキャン開始
    • 数時間かかりました…放置して寝た
  • スキャン完了後、必要なファイルを選んで復元開始
    • 数十分~1時間くらい?かかりました…放置して出勤した
    • 結構なサイズの復元を試みたので課金しました
  • 復元したファイルをチェックして問題なければ完了

全部チェックしたわけではありませんが、見た感じではほとんどのファイルが復元できたようです。
(どういう理屈なんだろう…)
ファイル名が変わっているものや何故か重複しているものもあるようで、改めて整理するのは骨が折れそうですが、もうそんなことどうでもいい。神か。

もうこれでデータ復旧関連の悩みはほぼ解決です。
予約していた業者にはキャンセルの連絡入れました。

試したこと その5

もちろん、本体交換後に新しく作ったデータや復元に失敗した数少ないデータをサルベージできればそれに越したことはありません。
ということでもう一つ試してみました。
「DVDに焼いたUbuntuを起動し、そこからファイルをサルベージする」という方法です。

参考
起動しないパソコンからUbuntuを利用してデータを救出 | データ復旧大図鑑 – 自分で解決!ファイル復元

用意するもの

  • DVD-R
  • ISOイメージをDVDに焼けるPC
  • UbuntuのISOイメージ(ダウンロードするだけ)
  • Surfaceにつなぐための外付けDVDドライブ
  • USBハブと外付けストレージ、もしくはネット環境など、ファイルを移せる用意
    • ひとつしかないUSBポートがDVDドライブに取られるため

手順

  • 正常動作するPCから、UbuntuのISOイメージをDVD-Rに焼く
  • 故障したSurfaceで、DVDのUbuntuを起動できるよう設定する
    • ボリュームの上ボタンを押しながら電源ボタンを押して起動(ボリュームの上ボタンはそのまましばらく押しっぱなし?)
    • UEFIBIOSみたいなの)が起動するので、以下設定を変更
      • Security > Secure Boot configuration をNoneにする
      • Boot configuration にて USB StorageのみONにする
  • USBから外付けDVDドライブをつないで再起動
  • Ubuntuが起動するのを待つ
    • USB経由だからか、そこそこの時間がかかりました
    • もし「インストール」か「お試し」かを尋ねられたら「お試し」を選ぶそうです(尋ねられませんでした)
  • 必要なファイルがあれば任意の方法でサルベージする

どうやっても目当てのファイルは一切見つからず。
ちなみにファイルを探すには以下サイトあたりを参考にするとよいかもしれません。

参考
UbuntuのHDD管理 その1 - HDDの管理方法 - kledgeb

試したこと その6

データ復旧はここまでで諦めました。
そりゃ元通りが理想ですが、復元業者に何万~何十万も払うほどではなくなったので。
外付けHDD内の削除済みデータを復元できただけで大収穫です。
ということで、次は本体。

再度普通に起動して「データ削除していいので完全初期化」をしようとしましたが、あっさり失敗。
以上。

今後どうするか

本体はまた修理交換に出すか、もしくは新しいsurfaceでも検討するか…と思い、
microsoftに電話相談するため、microsoftアカウント内でシリアルコードを確認したく開いてみると。

f:id:honey8823:20190131145053p:plain

…?
保証…期間内…?
どこから数えて1年なのかはさっぱりですが。
(そしてほんとにpro5だ…)

なんとWEB上で交換を申し込めてしまうので、さくっと申し込みました。
あとは前回の交換と同じ流れになりそうです。やったぜ大勝利。

<追記>交換完了です

無事に交換品を受け取れました。
f:id:honey8823:20190205100256p:plain
また保証期間がちょっと残ってる。もしかして3か月つけてくれてるのか。
ぼちぼちセットアップしていきます。

1/16 故障発覚・データ復旧業者に問い合わせ
1/19 データ復旧業者に本体持ち込み
1/26 復旧・修理不可との連絡をもらう
1/28 死亡宣告された本体を引き取り
1/29 外付けから削除したやつ復元すればいいじゃんと思いついて実行
1/30 サルベージ成功・うぶんつ作戦試して失敗・初期化も失敗
1/31 本体が保証期間内であることに気付いて交換申し込み
2/1 本体をmicrosoft宛に郵送
2/5 交換品が自宅に郵送されてきた

microsoftの対応早すぎワロタ
2/2~3が土日だし2/5の朝8時台に届いたので、2/4に受取~発送の処理全部やってくれたっぽい。

ということで

今回は「本体は無料で交換」「データはまあまあ復旧」というほどほどの結果に落ち着きました。
まだ交換はできていませんがとりあえずめでたしめでたし。

復旧にあたっては以下が幸運ポイントでした。

  • ごく最近本体の交換を行っていたこと
    • おかげで本体の保証期間内だった
    • 無に帰したデータは本体交換後~故障の期間の分なので、期間が短い分ダメージが少なかった
  • 本体交換にあたってデータ移行に外付けHDDを使っていたこと
    • クラウドを介して、とかだと消したデータの復旧は難しそう
  • しかもその外付けHDDは本体交換後は全く使っていなかったこと
    • データを繰り返し書き込んだり消したりしていると復旧が難しくなっていくので

逆に以下は反省すべきポイントです。

  • そもそもデータのバックアップはもう少しきちんと取っておくべき
  • 安易にデータ復旧業者に頼まない方が良かった
    • 今回は何も起きませんでしたし実際に依頼したところはごく普通でしたが、依頼後に調べてるとなかなか闇の深い業界だったようで…
      • 2軒目の業者がなかなか悪名高かった上に、キャンセル連絡に返答すらよこさないんですがこれは…
    • 結局どこの業者なら信頼できるのか、口コミ見ても大変分かりづらい業界だなーと思いました
    • あと復旧費用めちゃめちゃ高い

スペース文字を無視して検索する(力技)

※書き方はMySQL前提です

たまーに、DBに登録されているデータのスペース文字を無視して検索を行いたい場合があります。

速度を求める必要がある場合は、検索用カラムを別途用意しておくのがよいのですが、
そうでもない場合(手作業で一時的に検索したいだけ、など)は以下のようなクエリで全半角スペースを無視して検索できます。

SELECT * FROM `テーブル`
WHERE REPLACE(REPLACE(`カラム`, ' ', ''), ' ', '') = '検索ワード'

単純にREPLACE関数を用いて、
「「対象カラムの半角スペースを空文字に置換した文字列」に対して、さらに全角スペースを空文字に置換した文字列」
と比較しているだけです。
もちろん同様の方法でスペース以外の特定文字を無視することもできますし、LIKE検索で部分一致検索も可能です。

対象カラムにインデックスを張っている場合でも全く効きませんので、レコード数が多い場合は時間がかかる可能性があります。

要素の表示・非表示を切り替える

個人的に開発作業をするときは、
非表示用のクラスを定義しておいて、そのクラスを追加削除することで対応しています。

CSS ※AdminLTEなんかだと、同様の定義が元から含まれています

.hidden {
    display: none;
}

HTML

<div class="hidden">
  ここは表示されない
</div>
<div>
  ここは表示される
</div>

jQuery

// 対象の要素をobjとして…
var obj = $(" ... ");

// 非表示にする
obj.addClass("hidden");

// 表示する
obj.removeClass("hidden");

// 非表示・表示を切り替える
obj.toggleClass("hidden");

// 表示か非表示かの判定
if (obj.hasClass("hidden")) { /* 非表示である */ }
else { /* 非表示でない */ }


最初はこういうの↓を参考にしていたのですが、クラスの追加削除で済ませる方がシンプルだなと思いまして…。
jQueryで要素の表示・非表示を判定する方法: 小粋空間


まとめていて気付きましたが、
jQuery部分は共通関数にしてしまってもいいかもしれませんね。

function displayOn(obj){
    obj.addClass("hidden");
}
function displayOff(obj){
    obj.removeClass("hidden");
}

// 呼び出すときはこんな感じで済む
displayOn($(" ... "));

WinSCPとWinMergeを連携する

特に複数人で開発する際、FTPで直接ファイルをアップロードするとき、作業がぶつかって困ることがありました。
(つーか複数人でFTPでっていうのが…そもそも…うん…)

簡単にファイルの差分を比較する方法はないのかと思い調べてみたら、WinSCPWinMergeが連携できました。
やったぜ。

  • WinSCPWinMergeがなければインストールする
  • WinMergeのインストール先フルパスをメモする
  • WinSCPツールバーから、「オプション」>「環境設定」を開く
  • 開いたウインドウ内の「コマンド」から「ファイルの比較」を編集
  • コマンドを書き換える(※後述)
  • 必要に応じてショートカットキーを割り当てる
  • 「OK」で保存
  • ツールバー上の適当なところで右クリックし、「カスタムコマンド」を有効にする

以上。

書き換えるコマンドはこんな感じ。パスはWinMergeのものにします。

"C:\Program Files\WinMerge\WinMergeU.exe" "!^!" "!"

ローカル側とサーバー側でそれぞれファイルを選び、「ファイルの比較」を押すか設定したショートカットキーでWinMergeが起動します。
あとはWinMerge側で編集・保存することもできます。

参考(というか「詳細はこちら」レベル)
WinSCPとWinMergeを連携して差分アップデートを楽にする | PC ウェブログ

WinSCPでローカルとサーバーのフォルダの移動を同期する

WinSCPは左にローカル側フォルダ、右にサーバー側フォルダが表示されていますが、
デフォルトでは片方でフォルダ遷移してももう片方は動きません。

これを同期させたかったので調べてみた。

  • ログインして、左右のフォルダ階層を合わせる
  • 一旦接続を切る
  • 改めてログイン情報の設定画面を開き、「設定」ボタンを押す
  • 開いたウインドウ内の「環境」>「ディレクトリ」にある「ローカルとリモートのディレクトリ移動を同期する」にチェックを入れて「OK」
  • 設定を保存し再度ログイン

これで同時に動いてくれるようになります。
ただし、片方にしか存在しないフォルダに移動すると、ほとんどの場合同期が解除されてしまいます。
そうなったら最初からやり直し。階層を合わせてから接続を切り、もう一度設定し再ログインです。めんどくさい。

参考
WinSCP でフォルダ同期移動 - パソコンサポートの00H ~社長BLOG~

<追記>

もっと簡単な方法があったorz
f:id:honey8823:20190227100323p:plain

配列をクエリストリングに変換する

連想配列なオブジェクトは、「$.param(オブジェクト)」で簡単にクエリストリング化できます。すげーお手軽。

// listは配列
var list = {
    hoge: 123,
    fuga: 456,
    piyo: 789,
}
// listをクエリストリング化
var querystring = $.param(list);
 
// 表示( hoge=123&fuga=456&piyo=789 となる)
console.log(querystring);

デバッグ用に一時的にログファイル出力するやつ

別に一時的なログでなくとも同じようなもんですが、よく使うので…要するに自分用コピペメモです。


PHPで開発する際、デバッグで変数の中身を知りたいときはvar_dump等でお手軽に画面表示できますが、
諸々の理由で画面には出したくない場合もあります。

  • 稼働中の本番環境で確認したい
  • データが膨大なのでブラウザには表示したくない
  • ajaxで呼び出すのでそもそもvar_dump等ではダメ

など。

そういう場合はログファイルに放り込めばよいので、その書き方を。

<?
$dat = ... ; // 中身を確認したい変数

// >>> デバッグ用 >>>>>>>>>>
$logtext  = "================================\n";
$logtext .= date("Y-m-d H:i:s") . "\n";
$logtext .= var_export($dat, true) . "\n\n";
file_put_contents("ログファイルのフルパス", $logtext, FILE_APPEND);
// <<<<<<<<<<<<<<<<<<<<<<<<<

これで、日時と変数の内容がログファイルに追加されていきます。

var_exportは変数だけを渡すと画面に表示する関数ですが、第2引数にtrueを渡すことで表示は行わず、戻り値に文字列が得られるようになります。
PHP: var_export - Manual

また、file_put_contentsの第3引数のFILE_APPENDはログファイルに追記するための定数です。
追記ではなく上書きしてしまいたい場合は省略すればOK。
PHP: file_put_contents - Manual

アップロードされた画像をDBに保存する

作る機会があったのでメモ。
フォームのタイプとか、DBではなくファイルとして保存するとか、そういうアレンジは適当に。

手順

ざっくり分けると以下のような感じ。

  • 画像のアップロードフォームを作る
  • アップロードされた画像を加工する(リサイズなど)
  • 加工した画像をbase64化する
  • base64化したデータをDBに保存する
続きを読む

Surface Pro 4 を交換に出してみた

2016年2月に購入して以来、便利に使い倒していたSurfaceさんですが

今年に入ってから、時々画面がちらつくようになりました。

軽い時は「ちょっと一瞬ノイズが走ったように見えた」という感じ、
重い時は「画面全体がものすごいブレブレで表示内容の認識が困難」という感じ。

応急処置としては、本体を冷やすと直ることが多いです。

更新プログラムで解決できない問題だそうで、2018年5月頃から無償交換が始まりました。
https://support.microsoft.com/ja-jp/help/4230448/surface-pro-4-screen-flicker
Surface Pro 4に画面ちらつきの不具合、マイクロソフトが無償交換へ | マイナビニュース

以下の条件に該当すればリフレッシュ品との交換に応じてくれます。

  • Surface Pro 4を使っていて、ちらつきが発生する
  • 最新の状態にしても解決しない
  • 購入から3年以内である → 3年経過している場合は有償でなら対応してくれるようです

交換プロセスをざっくり書くと、以下のような感じ。

  • 問い合わせて交換受付の処理をしてもらう
  • 本体をMicrosoftに送付する
  • 5~8営業日程度で交換品が返送されてくる

「交換なのでデータなどをバックアップしておく必要がある」「数日間は手元から消える」というのがめんどくさいポイントですね。
症状も軽いし別にいっか…な人は冷やしながら使うのもアリだと思います。
わたしは開発作業してると症状がめちゃくちゃ重くなり本当に困るので今回交換に出すことにしました。
(レイヤー何十枚の高解像度絵を描いてても平気なことが多いのに、Apache起動してしばらく使うだけで、表示されているものが認識できないレベルに酷くなっていた…)

交換に応じてもらい、返ってきたものもしばらく使ってみたので、詳しい手順を残しておきます。
問い合わせしたのが2018年10月29日なのでその時点の情報ということで。

準備

問い合わせ前に、以下の準備を済ませておくとスムーズです。

  • シリアルIDを控えておく
    • 設定→システム→バージョン情報→システム情報 で確認可能
    • これさえ控えておけば、問い合わせ時は本体が手元になくてもOK
  • Microsoftアカウントのメールを確認できる状態にしておく
    • 問い合わせ時の通話中、本人確認のためにメールを受信して内容を確認する必要が出てきます
  • メモが取れるようにしておく(紙でもスマホでもPCでもいいので)

問い合わせ開始

完了までは20分程度見たほうがよいです。

電話で問い合わせます。番号は「 0120-54-2244 」。
自動の音声案内に従って操作…するのですが、真面目に聞いているとたどり着けませんでした。
問い合わせ内容によって番号を入力する形式で、「1→4→1」でOKです。

「ここに空メールするとサポート情報送るよ」とか「surfaceの交換はネットでも受け付けてるよ」とか言われますが、全部無視。
釣られてちょっと見てみたところ、今回のようなイレギュラーには対応していないようです。
最後の選択肢も、「購入から1年未満なら1、それ以上なら…」という質問ですが、問答無用で1でOKでした。
そうでなければ「有料の案内」か「フォーラムへの誘導」の二択になります。

イレギュラーにも対応してほしいっていうか、リコールみたいなもんなんだから専用の窓口用意していいレベルだと思うんだけど…。

手続きの流れ

オペレーターさんにつながるまでが一苦労でだいぶキレそうになりましたが(さすがMicrosoft)、
オペレーターさんはとても親切で分かりやすい説明をしてくれました(さすがMicrosoft)。

通話がつながったら
Surface Pro 4を使っていて、画面のちらつきが発生するので交換していただきたいのですが…」
で通じます。

順番はうろ覚えですがだいたい以下のような内容。

  • 名前と電話番号を聞かれる
  • 対象SurfaceのシリアルIDを伝え、交換対象であるかを判断してもらう
  • 本人確認(Microsoftアカウント宛にメールが来るので、本文中の確認番号を伝える)
  • 交換に際してのいろいろな説明を受ける
  • 受付番号を教えてもらう
  • 本体の送付方法や送付先を教えてもらう
  • Officeの再インストール方法を教えてもらう

受ける説明の多くは、電話後にもらえるメールに記載されています。
ただ、メールは100%届く保証があるわけではないので、最低限受付番号はメモしておきましょう。

受けた説明のまとめ

  • 本体(※1)を指定住所(※2)へゆうパック着払いで送ってね
    • 品名に受付番号を記載してね
  • 到着後5営業日後くらいに交換品を発送するよ
  • 交換だからデータは消えるよ
  • Officeも再インストールが必要だよ(※3)

※1
電話で明言されなかったような気がしますが、本体のみ。
キーボードやペンなどは不要です。
(もし誤って送った場合も返してくれるかとは思います。わたしはうっかりmicroSDカードを入れっぱなしにしていましたがご丁寧な梱包で返送いただきました)
いらない段ボールや緩衝材で梱包してやります。
これも明言されなかった気がしますが、壊れ物であることを書いておいた方がいいかも。
(例えば品名に「タブレット端末」追記するとか)

※2
メモしてね!と言われますがメールに記載されています。
 -------------------------------------------------------------
 〒143-0001 東京都大田区東海1-3-6 プロロジスパーク東京大田 3F
 Quantium Solutions(株) 気付 マイクロソフト サービス センター
 (電話番号欄には問い合わせの「0120-54-2244」でOK)
 -------------------------------------------------------------

※3
これは人によるかも。
自分の場合はMicrosoftアカウントにOfficeも紐づいていたので、
マイページからインストールするだけ、と言われました。
ちなみにインストール用に紹介された短縮URLは「 aka.ms/mya 」です。

返送された交換品を受け取ったら完了

10/29に申込み、11/7頃には受け取れる状態でした。
これをセットアップし、バックアップしておいたデータを入れなおして交換完了です。
元の端末で重たかった操作もいろいろ試してみましたが、画面のちらつきは発生せず。快適になりました。

後日談

運が悪かった。

回数を指定して文字列置換

文字列を置換するのはふつうstr_replace関数を使用しますが
PHP: str_replace - Manual
これは文中の特定文字をすべて置換することしかできません。

最初の1回だけ置換したいような場合はpreg_replaceで回数指定することができます。
第1~第3引数はいつも通りに「置換前のパターン」「置換後の文字列」「置換対象の文字列」で、
第4引数に回数を入れておくだけでOK。

<?php
// 置換前の文字列
$str  = "abcabcabc";

// 1回だけ置換
$str1 = preg_replace("/a/", "A", $str, 1); // Abcabcabc

// 2回だけ置換
$str2 = preg_replace("/a/", "A", $str, 2); // AbcAbcabc

先読み後読みと併用することで、「あるパターンより前の特定文字を1回だけ置換」「○文字目より後の特定文字を1回だけ置換」なんてことも。

<?php
// 置換前の文字列
$str  = "abcabcabc";

// あるパターンより前にある特定文字を1回だけ置換
// ※この例では存在しないパターンとなるので実際は置換されない
$str3 = preg_replace("/a(?=bcd)/", "A", $str, 1) // abcabcabc

// ○文字目より後の特定文字を1回だけ置換
$str4 = preg_replace("/(?<=.{3})a/", "A", $str, 1) // abcAbcabc

(先読み後読みについてはこちら)
正規表現の先読み・後読み - アナログCPU:5108843109

さらに、preg_replaceのパターンは配列でも指定できるので、配列指定かつ回数指定する場合の挙動はどうなってるんだろう…? と思い試してみました。

<?php
$str  = "abcabcabc";
var_dump( preg_replace(array("/a/", "/b/"), array("A", "B"), $str, 2) ); // ABcABcabc

$str = "aaabbbccc";
var_dump( preg_replace(array("/a/", "/b/"), array("A", "B"), $str, 2) ); // AAaBBbccc
var_dump( preg_replace(array("/a/", "/b/"), "X", $str, 2) ); // XXaXXbccc

配列で複数パターンを指定した場合、パターンごとに回数が適用されるようですね。

蛇足な雑談

いや、今回ハマったのが、「後読みには文字数が可変になるようなパターンを指定できない」というものでして、
具体的には、HTMLエスケープされたある文字列について、特定のタグのみエスケープを解除したいという事案でした。

例えばdivタグのみエスケープ解除するとして、

&lt;div&gt;
&lt;p&gt;
&lt;/p&gt;
&lt;/div&gt;
&lt;div class="hoge"&gt;
&lt;p&gt;
&lt;/p&gt;
&lt;/div&gt;

<div>
&lt;p&gt;
&lt;/p&gt;
</div>
<div class="hoge">
&lt;p&gt;
&lt;/p&gt;
</div>

としたい、ということですね。

これを一発

<?php
$str = " ... "; //(省略。エスケープ後文字列)

$pattern_before = array(
    "/&lt;(?=\/?div[\s\/>&])/",
    "/(?<=(<|\/)div[^&]*)&gt;/",
);
$pattern_after = array(
    "<",
    ">",
);
$str = preg_replace($pattern_before, $pattern_after, $str);

こんな感じで置換してしまいたかったのですが、後読みの方がうまくいかず。

仕方ないので、今回調べた内容を使って以下のように実装しました。

<?php
$str = " ... "; //(省略。エスケープ後文字列)

while (true)
{
    // divタグの位置を探す、なければ終了
    // 「&lt;(<)」と「div」の間はスラッシュのみ許容、「div」と「&gt;(>)」の間は&以外を許容
    if (!preg_match($"/&lt;\/?div[\s\/>&]/", $str, $matches, PREG_OFFSET_CAPTURE))
    {
        break;
    }
    
    // divタグが存在する場合:
    // 見つかった位置以降について、最初に見つかった&lt;と&gt;を置換するパターンを生成
    $pattern_before = array(
        "/(?<=.{" . $matches[0][1] . "})&lt;/",
        "/(?<=.{" . $matches[0][1] . "})&gt;/",
    );
    $pattern_after = array(
        "<",
        ">",
    );
    
    // 置換した文字列で$strを上書き
    $str = preg_replace(array(, ), array("<", ">"), $str, 1);
}

もっと美しい実装があるかもしれません。
あまり厳密ではないですしなんかダサい気もします。
(今回実装していたのはCMSなので厳密さは一応こんなもんで良いのですが)