0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

一括置換で直してよい行の見分け方——251記事で数えたら半分は対象外だった

0
Posted at

売っている技術書の値段を上げた。販売ページは同じ日に直した。
それで終わったつもりでいた。

11日後、自分が書いた記事が古い値段を案内し続けていることに気づいた。
数え直したら106本あった。読者が押した先で、値段が1.9倍になる。

自分は非エンジニアで、Claude Code——AIがコマンドを実行しながら作業を代行するツール——に
記事の管理や公開の作業を任せている。コードは書けないので、実装はAIがやる。
今回の話は、その「一括で直しておいて」を投げる前に、1回だけ確かめたことについて。

確かめてよかった。素朴にやっていたら、直してはいけない121行まで書き換えていた。

この記事は2026年8月時点の実測に基づく。


「¥800 を ¥1,500 に置換して」で壊れるもの

思いつく解はこれだ。全記事を取ってきて、文字列を置換して、書き戻す。
5分で終わる。AIに頼めば3分で終わる。

壊れる。

自分は同じ ¥800 という値段の本を、その時点で4冊売っていた。
値上げしたのは、そのうち1冊だけ。

残り3冊はいまも ¥800 で正しい。加えて Kindle 版が1つあって、
これは「買った時点の版で、毎月の追補は付かない」という違いがあるので、
意図して ¥800 のまま残してある。

つまり文字列 ¥800 は、直すべきものと直すと嘘になるものが混ざっている。
grepは両者を区別しない。


実測:244行のうち、直してよかったのは113行だった

推測で書きたくなかったので、記事251本の本文をAPIで全部取ってきて数えた。

数えたもの 実測
走査した記事 251本
¥800 / 800円 を含む行 244行(165本)
そのうち、値上げした本のリンクと同じ行にあるもの 113行(106本)
残り 131行=この時点では対象から外した(うち10行は取りこぼしだった。後述)

半分より少ない。文字列で拾った作業表を鵜呑みにしていたら、
正しい記述を121箇所こわしていた。

混ざっていたものの内訳はこうだった。

  1. いまも ¥800 で正しい別の本(記事に実際に出てきたのは2冊・76行)
  2. Kindle 版の ¥800(40行)。同じ本のKindle版は「買った時点の版で追補は付かない」ので、
    値段も据え置くと決めてある。直すと、その判断ごと消える
  3. リンクが同じ行に無い行(15行)。当時の値段の引用が4行、別の本の古い値段が1行、
    そして、あとで述べる取りこぼしが10行

3番目の中の「当時の値段の引用」が特に危ない。
「| 4 | ○○さん | 1冊 | ¥800 |」や「3月の初回の公開。¥800で14章の構成で公開した」。
直しても文法は壊れないし、見た目にも違和感がない。
気づけるのは、その行が何を指しているか読んだ人だけだ。

補足(技術)
一括置換の事故がやっかいなのは、失敗が静かなことにある。
構文エラーなら実行時に落ちる。だが文章の置換は必ず「成功」する。
誤って置換した121行も、置換処理としては正常終了する。
だから対象の選び方そのものに検査を入れるしかない。


対象は文字列で決めない。行き先で決める

やり方を変えた。「¥800 を含む行」ではなく、
「値上げした本へのリンクと、¥800 が同じ行にあるもの」を対象にした。

リンクのURLには本の識別子が入っている。これは一意で、他の本と衝突しない。
値段の数字は衝突するが、識別子は衝突しない。

import re

#「1800円」「¥8000」に当てないよう、前後の数字を除く
PRICE = re.compile(r'(?<![0-9,,])(?:[¥¥]\s?800|800\s?円)(?![0-9])')
TARGET = '6076c23b1cb18b'   # 値上げした本の識別子

def is_target(line):
    """その行が「直すべき古い値段」かどうか。
    値段の文字列ではなく、同じ行のリンクの行き先で決める。"""
    return TARGET in line and PRICE.search(line)

これで 244行 → 113行に絞れた。

数字で対象を決めると、同じ数字を持つ全員が引っかかる。
行き先で決めると、その商品の話をしている行だけが残る。


同じ行に2つの商品があるとき

絞り込んでも、まだ罠があった。1行の中に2冊が並んでいる記事があった。

…をまとめた本(https://zenn.dev/.../6076c23b1cb18b)(¥800・第3章まで無料)もあります。
800時間走らせた全記録は(https://zenn.dev/.../3c3c3baee85f0a19)(¥800・第2章まで無料)。

表示の都合で折り返しているが、これは1行だ。
前半は直す。後半は直さない。同じ行にある。同じ文字列だ。

この行を対象に入れると、正しいほうの ¥800 まで消える。
対象から外すと、間違っているほうが残る。どちらも不正解。

やったのは、先に「正しいほう」を退避してから置換し、最後に戻すという手順。

KEEP = '@@KEEP@@'
# 直さない本のリンクの直後にある値段を、いったん別の文字列へ逃がす
PROTECT = re.compile(r'(3c3c3baee85f0a19[^)]*\)[^¥¥0-9]{0,8})([¥¥]\s?800)')

def strip_price(line):
    out = PROTECT.sub(lambda m: m.group(1) + KEEP, line)   # 逃がす
    out = re.sub(r'([¥¥]\s?800[・、]\s*', '', out)       # 置換する
    # …他の書き方のぶんも同様に…
    return out.replace(KEEP, '¥800')                        # 戻す

やっていることは単純だが、この手順を入れるかどうかで結果が変わる行が3本あった。
251本のうち3本。見落としても気づかない量だ。

補足(技術)
「保護してから置換して戻す」は、テンプレート処理やコードの整形でよく使う型。
置換の規則を複雑にして例外を書き分けるより、
対象から先に抜いて、後で戻すほうが規則が単純に保たれる。
正規表現に否定先読みを積み上げると、次に読む人(数日後の自分を含む)が読めなくなる。


行き先で決めても、まだ取りこぼす

絞り込みが効いたので満足しかけたが、対象から外れた131行を1行ずつ読んでみた。
そこに、リンクが同じ行に無いだけで、値上げした本を指している記述が10行あった。

### 事故の全体像を体系で押さえたい方へ(¥800)
…事故の型ごとに止め方をまとめた800円の事故防止本(無料章あり)にまとめてある。
…実際の事故を集めて対策を整理した本(¥800)も用意しています。記事の最後に置いておきます。

見出しに値段を書いた行。本文で「800円の◯◯本」と呼んだ行。
リンクは2行下や、記事の末尾にある。

行き先で決める方法は、リンクと値段が同じ行に居ることを前提にしている。
その前提は、自分で書いた文章のうち9割では成り立っていて、1割で成り立っていなかった。

ここは自動判定を諦めて、外れた15行を目視で分類した(うち10行が取りこぼし、
4行が当時の値段の引用、1行は別の本の古い値段)。
機械で絞ってから人が読む範囲を小さくする、というのが結局いちばん速い。


一度直したはずのものが、逆向きに壊れていた

ここまでで気が済んだので、念のため売っている12冊すべての現在の値段を取ってきて、
記事に書かれた金額と突き合わせた。値上げした1冊だけでなく、全部。

8本出た。

実物が ¥800 の別の本を、記事が「¥1,500」と案内していた。

[AIに仕事を任せてみた——800時間の全記録](https://…/3c3c3baee85f0a19)(¥1,500・第2章まで無料)

押した先で、思っていたより安い。損はしないが、書いてあることは間違っている。

閲覧数を足すと 11,807。8本のうち5本は、直そうとしていた106本のどれより読まれていた。

そこで、この本にリンクしている記事を全部(74本)並べて、更新日時で割ってみた。

記事 「¥1,500」と書いていた
前日の14時から16時のあいだに更新された7本 7本
それ以外の67本 1本

前日のその時間にやっていたのが、値上げした本の価格の書き換えだ。
触った7本は、7本とも間違っていた。触っていない67本は、66本まで正しかった。

Qiitaに版の履歴は無いので、何がその値を書いたかは確定できない。
残る1本は25日前の更新で、こちらは是正では説明がつかない。
値段の写しは、ひとつの経路だけで腐るわけではない。

是正が、別の場所に同じ種類の誤りを作る。
これが一番怖いところだと思う。直した側は「直した」と記録して終わる。
その記録には、直していない側を測った形跡がない。

今回は、7本が翌日に見つかって、1本は26日ぶん残っていた。
翌日に見つかったのは、たまたま次の日に全部を数え直したからだ。
数え直していなければ、次に誰かが同じ棚卸しをするまで残っていた。

そのまま8本とも直した。ここでも ¥1,500¥800 に書き換えるのではなく、
数字ごと落とした。


検算は「APIが成功を返したか」ではやらない

置換を書き戻したあと、APIは全件 200 を返した。

それを信じない、というのは今回意図的に決めたことだった。
以前、公開ページの更新でボタンを3回押したのに画面が変わらず「失敗した」と判断した件がある。
実際には最初から成功していて、非同期で処理されていただけだった。
逆もある。応答は成功なのに、書き戻された中身が期待と違うことがある。

なので、1件ごとに書き戻した直後にもう一度取得して、残っている件数を数えるようにした。

api('PATCH', f'/items/{aid}', payload)
again = api('GET', f'/items/{aid}')          # 応答ではなく、取り直した本文で見る
left = [l for l in again['body'].split('\n') if is_target(l)]
ok = (len(left) == 0) and (again['body'] == new)

30本流して、1本だけ left = 1 が出た。
調べたら、それが上の「1行に2冊」の行だった。残っていたのは正しいほうの値段で、
つまり検算の条件のほうが雑だった。壊れていたわけではない。

このときも、条件が雑だと分かったのは中身を見たからで、
件数だけ見ていたら「1件失敗した」で終わっていた。

なお、この記事を書いている時点で、残りはまだ76本ある。
1日15〜20本ずつ直している。一晩で全部叩かないのは、
この面が1日にどれだけの更新を許すのかを知らないからだ。


恒久策:値段を書かない

直し方をもう一つ選べた。¥800¥1,500 に書き換える、という選択だ。
選ばなかった。

次に値段を動かしたとき、また106本が古くなるからだ。
今回12日放置したのは、値上げの手順に「その値段を書いた場所を数える」という段が
無かったからだ。だが、その段を足しても、人間もAIもまた忘れる。

なので、値段の数字ごと落とした。

(¥800・第3章まで無料)  →  (第3章まで無料)

「第3章まで無料」は値段が変わっても嘘にならない。
商品ページの値段は常に正しいのだから、記事の側に写しを持つ必要がない。

計算できる値、どこかに正解がある値を、別の場所へ手で書き写した瞬間、
更新されない複製がひとつ増える。

今回の106本は、その複製が12日ぶん腐ったものだった。


まとめ

  • 一括置換の対象を文字列で決めると、同じ文字列を持つ無関係なものが混ざる。
    今回は 244行のうち121行(50%)が「直してはいけない」側で、
    同時に10行を取りこぼしていた。多すぎるのと足りないのは同時に起きる
  • 対象は行き先(URL・識別子)で決める。数字は衝突するが識別子は衝突しない
  • ただし行き先で絞っても、リンクと値が別の行にある記述は取りこぼす。
    絞ったあと、外れたほうも一度は読む
  • 1行に対象と非対象が同居することがある。先に逃がして、後で戻す
  • 検算は応答の成功ではなく、取り直した中身で数える
  • 是正のあとに、直していない側も測る。今回いちばん読まれている誤りは、
    古い値を探す片方向の検索では出てこない向きに入っていた
  • そもそも、他所に正解がある値を自分の文章へ書き写さない

AIに「全部直しておいて」と投げる前に、その作業表が
一致した件数なのか直すべき件数なのかを一度だけ見る。
今回はそこが2倍ずれていた。

そして直したあと、直したものだけを数えない。
自分の場合、直したつもりの作業が、隣で新しい誤りを作っていた。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?