<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hyunyong Park</title>
    <description>The latest articles on DEV Community by Hyunyong Park (@oneeyeautomation).</description>
    <link>https://dev.to/oneeyeautomation</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4054217%2F6d7105aa-2c2e-458b-9ed1-cae96428ea19.png</url>
      <title>DEV Community: Hyunyong Park</title>
      <link>https://dev.to/oneeyeautomation</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oneeyeautomation"/>
    <language>en</language>
    <item>
      <title>I Built a CSV Cleaner That Explains Every Deleted Row</title>
      <dc:creator>Hyunyong Park</dc:creator>
      <pubDate>Thu, 30 Jul 2026 21:41:18 +0000</pubDate>
      <link>https://dev.to/oneeyeautomation/i-built-a-csv-cleaner-that-explains-every-deleted-row-3oge</link>
      <guid>https://dev.to/oneeyeautomation/i-built-a-csv-cleaner-that-explains-every-deleted-row-3oge</guid>
      <description>&lt;p&gt;Deleting a duplicate row is easy. Explaining why the output has fewer rows is&lt;br&gt;
the harder product problem.&lt;/p&gt;

&lt;p&gt;I ran into that distinction while packaging a small Windows CSV cleaner. A&lt;br&gt;
typical cleanup script can trim whitespace, drop empty rows, and remove&lt;br&gt;
duplicates in a few lines. But when the input has four rows and the output has&lt;br&gt;
two, the person using it still needs answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Was the original file changed?&lt;/li&gt;
&lt;li&gt;Which rule removed each row?&lt;/li&gt;
&lt;li&gt;Did every file finish?&lt;/li&gt;
&lt;li&gt;Can I inspect the result without rerunning the job?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That changed the design. The audit report became a first-class output rather&lt;br&gt;
than an optional log written after the "real" work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9cwi1hq4m2nz0fjx2hbu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9cwi1hq4m2nz0fjx2hbu.png" alt="CSV Audit Cleaner actual product screen" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Start with a reconciliation rule
&lt;/h2&gt;

&lt;p&gt;The most useful invariant is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input rows = output rows + blank rows removed + duplicate rows removed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The header is tracked separately. A duplicate can mean either an exact repeated&lt;br&gt;
row or a repeated value in one or more key columns chosen by the operator.&lt;/p&gt;

&lt;p&gt;If that equation does not balance, the run should not quietly display&lt;br&gt;
"success." It has lost the ability to explain the output.&lt;/p&gt;

&lt;p&gt;For each CSV, the cleaner records the input count, output count, blank-row&lt;br&gt;
count, duplicate count, failure state, detected encoding, and original file&lt;br&gt;
hash. A run summary then aggregates success and failure across the batch.&lt;/p&gt;

&lt;p&gt;This is intentionally narrower than a general data-cleaning platform. It does&lt;br&gt;
not guess what a broken address should be, rewrite phone numbers, edit Excel&lt;br&gt;
workbooks, or use AI to infer missing values. The rules are deterministic so&lt;br&gt;
the evidence can be checked.&lt;/p&gt;
&lt;h2&gt;
  
  
  Never make the original the rollback plan
&lt;/h2&gt;

&lt;p&gt;An undo button is not a substitute for leaving the source file alone.&lt;/p&gt;

&lt;p&gt;The program creates a new run directory and writes cleaned files there. The&lt;br&gt;
input CSV remains untouched. Before processing, it calculates the original&lt;br&gt;
SHA-256 and includes that value in the report.&lt;/p&gt;

&lt;p&gt;That hash is not proof that the content is correct. It answers a narrower,&lt;br&gt;
useful question: "Is this the same input file I intended to clean?"&lt;/p&gt;

&lt;p&gt;The output is written as UTF-8 with a BOM because it opens predictably in common&lt;br&gt;
Windows spreadsheet tools. The input reader accepts UTF-8, UTF-8 with BOM, and&lt;br&gt;
CP949. Unsupported formats fail explicitly instead of being guessed into&lt;br&gt;
plausible-looking text.&lt;/p&gt;
&lt;h2&gt;
  
  
  Treat the report as a product output
&lt;/h2&gt;

&lt;p&gt;The run folder contains three different kinds of evidence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;report.html&lt;/code&gt; for a person reviewing the run;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;summary.json&lt;/code&gt; for another program reading aggregate results;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;run_log.jsonl&lt;/code&gt; for chronological success and failure events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The HTML report is the first thing I open after a run. It shows which file was&lt;br&gt;
processed, the input and output counts, the removal reasons, the encoding, and&lt;br&gt;
whether the file failed. The JSON files exist for automation, but the human&lt;br&gt;
report keeps verification from requiring Python or a log viewer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://oneeyeview-automation.vercel.app/csv-cleaner-sample-report.html" rel="noopener noreferrer"&gt;Open the actual fixed-sample HTML report&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That public report uses bundled test data. It is safe to inspect, but it is not&lt;br&gt;
a claim that unrelated CSV files will produce the same numbers.&lt;/p&gt;
&lt;h2&gt;
  
  
  Isolate failures in a batch
&lt;/h2&gt;

&lt;p&gt;Batch processing creates another easy failure mode: one bad file can hide the&lt;br&gt;
results of all the good files.&lt;/p&gt;

&lt;p&gt;The cleaner processes each file independently. If one file cannot be decoded,&lt;br&gt;
has an invalid duplicate-key column, or exceeds a product limit, that failure is&lt;br&gt;
recorded for the file while eligible files continue. The final summary reports&lt;br&gt;
both counts.&lt;/p&gt;

&lt;p&gt;This matters because "the window stayed open" and "every file succeeded" are&lt;br&gt;
different facts. A useful batch result has to make partial success visible.&lt;/p&gt;

&lt;p&gt;I also avoided writing a cleaned file before the input contract is accepted.&lt;br&gt;
For example, the free edition checks its row limit before writing an output.&lt;br&gt;
Rejecting an oversized file after creating a partial result would make the&lt;br&gt;
folder harder to interpret.&lt;/p&gt;
&lt;h2&gt;
  
  
  Test the packaged program, not only the source tree
&lt;/h2&gt;

&lt;p&gt;A passing development folder does not prove that the downloadable ZIP contains&lt;br&gt;
the right executable, samples, documentation, and runtime files.&lt;/p&gt;

&lt;p&gt;For the public sales build, I extracted the ZIP into a fresh directory and ran&lt;br&gt;
the packaged Windows executable. The release contract passed &lt;strong&gt;32/32 automated&lt;br&gt;
tests&lt;/strong&gt;, including UTF-8, UTF-8 BOM, CP949, exact duplicates, key-column&lt;br&gt;
duplicates, empty rows, original preservation, failure isolation, and the free&lt;br&gt;
edition's 101-row rejection path.&lt;/p&gt;

&lt;p&gt;I then ran one fixed local sample through the packaged product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 CSV succeeded and 0 failed;&lt;/li&gt;
&lt;li&gt;4 input rows became 2 output rows;&lt;/li&gt;
&lt;li&gt;1 blank row was removed;&lt;/li&gt;
&lt;li&gt;1 duplicate was removed using &lt;code&gt;order_id&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the original stayed untouched;&lt;/li&gt;
&lt;li&gt;the HTML report reconciled the counts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/h8MerbgmRRY"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The 51-second clip starts from a fresh ready state, selects the bundled sample,&lt;br&gt;
runs the cleaner, and opens the generated report. It demonstrates that one&lt;br&gt;
reproducible path; it is not a benchmark or a promise about every CSV.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the same workflow for free
&lt;/h2&gt;

&lt;p&gt;I published a free Lite edition so the first test does not require a purchase:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/loved0543-dotcom/csv-audit-cleaner-lite/releases/tag/v1.0.0" rel="noopener noreferrer"&gt;Download CSV Audit Cleaner Lite from the public GitHub release&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It processes up to 100 data rows per CSV and uses the same cleanup and evidence&lt;br&gt;
workflow. The ZIP includes the Windows executable, a sample CSV, quick-start&lt;br&gt;
instructions, license, and checksum. It does not require installation, Python,&lt;br&gt;
a cloud account, a subscription, or a paid API.&lt;/p&gt;

&lt;p&gt;The README publishes the release size and SHA-256 so the download can be checked&lt;br&gt;
before use. As with any data tool, keep backups and review the report before&lt;br&gt;
putting cleaned files into a production workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the paid edition changes
&lt;/h2&gt;

&lt;p&gt;The full edition removes the per-CSV row limit and includes the complete source&lt;br&gt;
code and a commercial-use license. It is a finished download, not custom&lt;br&gt;
development or a remote setup service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lovelife717.gumroad.com/l/csv-audit-cleaner" rel="noopener noreferrer"&gt;The full product page&lt;/a&gt;&lt;br&gt;
offers Personal for &lt;strong&gt;$15 one time&lt;/strong&gt; and Team for &lt;strong&gt;$49 one time&lt;/strong&gt;. It does not&lt;br&gt;
include custom cleaning rules, Excel editing, ongoing support, or a future-update&lt;br&gt;
commitment.&lt;/p&gt;

&lt;p&gt;There are no customer reviews or market-validation claims yet. The evidence I&lt;br&gt;
can publish today is narrower: the packaged program, the fixed sample, the&lt;br&gt;
report, the downloadable Lite edition, and the test contract.&lt;/p&gt;

&lt;p&gt;That is also the main lesson from building it: a cleanup tool becomes easier to&lt;br&gt;
trust when deletion is not the final output. The result needs an explanation&lt;br&gt;
that survives after the window closes.&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Retry Is Not Resume: Building a Web-to-CSV Pipeline That Survives Interruptions</title>
      <dc:creator>Hyunyong Park</dc:creator>
      <pubDate>Thu, 30 Jul 2026 04:45:48 +0000</pubDate>
      <link>https://dev.to/oneeyeautomation/retry-is-not-resume-building-a-web-to-csv-pipeline-that-survives-interruptions-3fko</link>
      <guid>https://dev.to/oneeyeautomation/retry-is-not-resume-building-a-web-to-csv-pipeline-that-survives-interruptions-3fko</guid>
      <description>&lt;p&gt;The first run is the easy part.&lt;/p&gt;

&lt;p&gt;The real test starts after record 70 of 100 succeeds, the process stops, and you&lt;br&gt;
run it again. If the pipeline fetches the first 70 pages again, appends duplicate&lt;br&gt;
rows, or trusts a half-written state file, it is not resumable. It is only&lt;br&gt;
restartable.&lt;/p&gt;

&lt;p&gt;While building a small Python + Playwright source kit, I separated retry from&lt;br&gt;
resume and gave the output data, progress state, and run evidence different&lt;br&gt;
responsibilities. This post walks through the decisions that made the biggest&lt;br&gt;
difference.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8aqsbwzkwoizqzcx3eby.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8aqsbwzkwoizqzcx3eby.png" alt="Resumable Web-to-CSV Pipeline Kit overview" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Retry and resume solve different failures
&lt;/h2&gt;

&lt;p&gt;A retry handles a temporary failure inside one run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a request times out;&lt;/li&gt;
&lt;li&gt;a page returns incomplete markup;&lt;/li&gt;
&lt;li&gt;a required field is temporarily missing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Resume handles a different event: the run itself ends and a later process must&lt;br&gt;
continue from durable evidence.&lt;/p&gt;

&lt;p&gt;Putting both concerns inside one retry loop hides the distinction. Increasing&lt;br&gt;
the retry count does nothing after a terminal closes or a machine restarts.&lt;br&gt;
Instead, I used an item-level retry loop and wrote a completed-record map only&lt;br&gt;
after parsing succeeded.&lt;/p&gt;

&lt;p&gt;The essential flow is small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config_fingerprint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;input_rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;record_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;log_skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;write_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hash_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;write_state_atomically&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config_fingerprint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retry decides whether the current item gets another attempt. Resume decides&lt;br&gt;
whether the item should be fetched at all in a future run.&lt;/p&gt;
&lt;h2&gt;
  
  
  The CSV is not the progress database
&lt;/h2&gt;

&lt;p&gt;It is tempting to inspect the last CSV row and treat it as a cursor. That works&lt;br&gt;
until input order changes, a blank line appears, or the output schema changes.&lt;br&gt;
The data file then has two jobs: representing results and controlling execution.&lt;/p&gt;

&lt;p&gt;I kept them separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;normalized.csv&lt;/code&gt; contains the current result rows;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;state.json&lt;/code&gt; stores completed record IDs and output hashes;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;run_log.jsonl&lt;/code&gt; records succeeded, failed, retrying, and skipped events;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;report.html&lt;/code&gt; gives a human-readable result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The input contract requires &lt;code&gt;record_id,url&lt;/code&gt;. A &lt;code&gt;seen&lt;/code&gt; set catches duplicate IDs&lt;br&gt;
inside the current input. The durable completed map catches IDs that succeeded&lt;br&gt;
in an earlier run.&lt;/p&gt;

&lt;p&gt;This also makes a repeated run understandable. A row is not silently absent; the&lt;br&gt;
log says whether it was skipped because it was a duplicate or because it had&lt;br&gt;
already completed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Old state must not silently match a new configuration
&lt;/h2&gt;

&lt;p&gt;Resume state becomes dangerous when extraction rules change.&lt;/p&gt;

&lt;p&gt;Suppose the first run extracted &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;price&lt;/code&gt;. Later, the configuration&lt;br&gt;
adds &lt;code&gt;availability&lt;/code&gt;. If the old completed IDs are accepted without question,&lt;br&gt;
the pipeline skips pages that have never been processed with the new rule.&lt;/p&gt;

&lt;p&gt;The kit hashes a canonical representation of the field configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;canonical_fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fingerprint is written beside the completed map. When a later run points&lt;br&gt;
to the same output directory with a different configuration, the pipeline&lt;br&gt;
raises an explicit error instead of combining incompatible results.&lt;/p&gt;

&lt;p&gt;The fix is intentionally boring: use a new output directory for a new&lt;br&gt;
extraction contract. Quietly guessing would be more convenient and less safe.&lt;/p&gt;
&lt;h2&gt;
  
  
  Atomic writes protect the mechanism that enables recovery
&lt;/h2&gt;

&lt;p&gt;A state file is useful only if it survives the interruption it is supposed to&lt;br&gt;
help recover from.&lt;/p&gt;

&lt;p&gt;Writing directly to &lt;code&gt;state.json&lt;/code&gt; can leave truncated JSON if the process stops&lt;br&gt;
at the wrong moment. The next run then fails before it can resume. The same risk&lt;br&gt;
applies to the normalized CSV.&lt;/p&gt;

&lt;p&gt;The pipeline writes a complete temporary file and replaces the destination:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;temporary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_suffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.tmp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;temporary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;temporary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation uses the same pattern for JSON, CSV, and the append-style&lt;br&gt;
run log. It does not make every filesystem failure impossible, but it removes&lt;br&gt;
the common partial-overwrite window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence should be useful without leaking page content
&lt;/h2&gt;

&lt;p&gt;Debug logs often become accidental data stores. Dumping raw HTML, full query&lt;br&gt;
strings, or local paths would make troubleshooting easier and distribution less&lt;br&gt;
safe.&lt;/p&gt;

&lt;p&gt;The public log records the record ID, status, attempt number, and exception type.&lt;br&gt;
It does not record page HTML or local filesystem paths. The HTML report escapes&lt;br&gt;
output values before rendering them.&lt;/p&gt;

&lt;p&gt;That boundary matters for a reusable source kit: the person running it controls&lt;br&gt;
the permitted target pages, while the library keeps operational evidence&lt;br&gt;
focused on the run.&lt;/p&gt;

&lt;h2&gt;
  
  
  I tested the extracted delivery archive, not only the development folder
&lt;/h2&gt;

&lt;p&gt;A passing development environment is not proof that a buyer's ZIP contains the&lt;br&gt;
same working pieces.&lt;/p&gt;

&lt;p&gt;I extracted the delivery archive into a clean directory and ran its included&lt;br&gt;
test suite. The result was &lt;strong&gt;10/10 tests passed&lt;/strong&gt;. One regression test forces&lt;br&gt;
&lt;code&gt;KeyboardInterrupt&lt;/code&gt; immediately after the first record is durably checkpointed. A&lt;br&gt;
second process then resumes and proves that the completed record is not fetched&lt;br&gt;
again.&lt;/p&gt;

&lt;p&gt;I also ran the offline fixture demo twice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2 records succeeded;&lt;/li&gt;
&lt;li&gt;0 records failed;&lt;/li&gt;
&lt;li&gt;1 duplicate ID was skipped safely;&lt;/li&gt;
&lt;li&gt;the next run read the completed state instead of fetching finished records
again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fixture demo uses local HTML and requires no browser, account, proxy, cloud&lt;br&gt;
service, or paid API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5j0hsyd6g2c7hlqm0a0m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5j0hsyd6g2c7hlqm0a0m.png" alt="Actual test and demo report" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Playwright fits
&lt;/h2&gt;

&lt;p&gt;The state, retry, parsing, reporting, and fixture paths do not import Playwright.&lt;br&gt;
The browser dependency is loaded only for permitted &lt;code&gt;http://&lt;/code&gt; or &lt;code&gt;https://&lt;/code&gt;&lt;br&gt;
pages.&lt;/p&gt;

&lt;p&gt;That keeps the offline demo deterministic and lets the same pipeline accept a&lt;br&gt;
real browser fetcher when JavaScript-rendered pages are in scope.&lt;/p&gt;

&lt;p&gt;This is not a universal scraper. Selectors are deliberately simple, and the&lt;br&gt;
operator must define them for pages they own or are allowed to automate. The kit&lt;br&gt;
does not bypass login, CAPTCHA, access controls, rate limits, robots rules, or&lt;br&gt;
website terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finished source kit
&lt;/h2&gt;

&lt;p&gt;I packaged the implementation as the&lt;br&gt;
&lt;a href="https://lovelife717.gumroad.com/l/resumable-web-to-csv-pipeline/REDDIT25" rel="noopener noreferrer"&gt;Resumable Web-to-CSV Pipeline Kit&lt;/a&gt;.&lt;br&gt;
It includes the full Python source, tests, configuration examples, offline&lt;br&gt;
fixtures, and run documentation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Personal: $18 launch price (normally $24) for one user&lt;/li&gt;
&lt;li&gt;Team: $44.25 launch price (normally $59) for up to five users in one legal entity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 25% launch discount is automatically applied through August 1, 20:13 KST for the first 20 uses.&lt;/p&gt;

&lt;p&gt;The public&lt;br&gt;
&lt;a href="https://github.com/loved0543-dotcom/resumable-web-pipeline-delivery" rel="noopener noreferrer"&gt;delivery repository&lt;/a&gt;&lt;br&gt;
shows the actual product screens and verification evidence before purchase.&lt;br&gt;
Custom selectors, custom development, remote setup, ongoing support, and future&lt;br&gt;
updates are not included.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch the crash window in 38 seconds
&lt;/h2&gt;

&lt;p&gt;The short proof reel shows the exact failure path: interrupt after useful work, rerun with retry-only logic, then resume from durable state without refetching completed records.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/jRI8GmuvE2M" rel="noopener noreferrer"&gt;Watch the 38-second crash-proof Web-to-CSV demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The reel uses the same fixed offline fixture documented above. It is evidence for this included example, not a promise about every website.&lt;/p&gt;

&lt;p&gt;You can also&lt;br&gt;
&lt;a href="https://oneeyeview-automation.vercel.app/interactive-resume-demo.html" rel="noopener noreferrer"&gt;run the interruption walkthrough in your browser&lt;/a&gt;&lt;br&gt;
without installing Python. Run 1 saves the first record and stops; after a real&lt;br&gt;
page reload, Resume skips that completed record before reading its fixture and&lt;br&gt;
finishes the second. This is a fixed offline walkthrough, not an arbitrary-site&lt;br&gt;
benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer the guided lab instead?
&lt;/h2&gt;

&lt;p&gt;If you want the failure map and the interruption exercise without buying source&lt;br&gt;
code, I also published the 20-page&lt;br&gt;
&lt;a href="https://oneeyeview-automation.vercel.app/#manual" rel="noopener noreferrer"&gt;Retry Is Not Resume field manual&lt;/a&gt;.&lt;br&gt;
It includes a 30-minute offline run → stop → resume lab, checkpoint crash windows,&lt;br&gt;
a four-file responsibility map, and a production checklist.&lt;/p&gt;

&lt;p&gt;The launch price is $9, and the product page includes a free 6-page preview. The&lt;br&gt;
v1.1 purchase ZIP contains the PDF and a self-contained lab that runs with&lt;br&gt;
&lt;code&gt;python lab/run_demo.py&lt;/code&gt;. It is a self-serve download: no custom setup, remote&lt;br&gt;
installation, ongoing support, or future updates are included.&lt;/p&gt;

&lt;p&gt;The larger lesson is independent of this kit: retries recover an attempt;&lt;br&gt;
durable state recovers a workflow. Treating those as separate responsibilities&lt;br&gt;
turns "run it again" from a gamble into a defined operation.&lt;/p&gt;

</description>
      <category>python</category>
      <category>playwright</category>
      <category>automation</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
