<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[CommentTok Engineering]]></title><description><![CDATA[CommentTok Engineering]]></description><link>https://commenttok.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>CommentTok Engineering</title><link>https://commenttok.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 21 Sep 2026 08:02:53 GMT</lastBuildDate><atom:link href="https://commenttok.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[CSV vs Excel vs PDF: Choosing a Safer Format for Comment Review]]></title><description><![CDATA[By tokviewer.app editorial team · September 18, 2026
Disclosure: Prepared with AI assistance for tokviewer.app. This note describes our current export implementation, not a live TikTok capture or a co]]></description><link>https://commenttok.hashnode.dev/csv-vs-excel-vs-pdf-choosing-a-safer-format-for-comment-review</link><guid isPermaLink="true">https://commenttok.hashnode.dev/csv-vs-excel-vs-pdf-choosing-a-safer-format-for-comment-review</guid><dc:creator><![CDATA[carl kevin]]></dc:creator><pubDate>Fri, 18 Sep 2026 07:52:15 GMT</pubDate><content:encoded><![CDATA[<p>By tokviewer.app editorial team · September 18, 2026</p>
<p>Disclosure: Prepared with AI assistance for tokviewer.app. This note describes our current export implementation, not a live TikTok capture or a complete dataset.</p>
<h2>Choose the format for the next action</h2>
<p>CSV fits scripts, databases and spreadsheet imports. Excel fits filtering and review in a workbook. Word fits editing and annotations. PDF fits a fixed visual review copy. TXT fits plain-text search and simple processing.</p>
<h2>Preserve identifiers and text</h2>
<p>CSV does not declare spreadsheet column types. Import long comment and parent IDs as text before analysis. Our CSV exporter includes a UTF-8 BOM and prefixes formula-risk values with an apostrophe. Use a BOM-aware reader and account for these transformations; do not blindly remove leading apostrophes. XLSX stores identifiers and comment text as literal strings, while counts remain numeric and reply flags remain Boolean. Neither format repairs identifiers already damaged upstream.</p>
<h2>Keep a structured source alongside a document</h2>
<p>Word provides editable comment blocks and available metadata. Our current PDF exporter renders page images without a selectable text layer. Do not rely on those PDFs for copying, text search or screen-reader access. Use Word or a structured export when those tasks matter. Any OCR provided by another application is separate from the export. TXT is readable and searchable, but lacks the column structure useful for joins or aggregation.</p>
<h2>Record the source boundary</h2>
<p>An export packages the rows currently available. Changing formats does not retrieve more comments or reconstruct missing parent relationships. Preserve the video URL, capture time and result status; do not label a partial result a complete inventory. Keep CSV or Excel alongside a PDF when later filtering or auditing may be needed.</p>
<p>For the full comparison and download requirements, see <a href="https://tokviewer.app/blog/tiktok-comment-export-formats/">Which TikTok Comment Export Format Should You Use?</a>. Downloads require signing in to tokviewer.app, not providing a TikTok password.</p>
]]></content:encoded></item><item><title><![CDATA[Why an Empty Result Is Not a Zero: Track Availability in CSV Reports]]></title><description><![CDATA[By tokviewer.app editorial team · September 17, 2026
An empty cell tells you that a number is absent. It does not tell you whether the number was zero, whether the source omitted it, or whether the re]]></description><link>https://commenttok.hashnode.dev/why-an-empty-result-is-not-a-zero</link><guid isPermaLink="true">https://commenttok.hashnode.dev/why-an-empty-result-is-not-a-zero</guid><category><![CDATA[Python]]></category><category><![CDATA[csv]]></category><dc:creator><![CDATA[carl kevin]]></dc:creator><pubDate>Thu, 17 Sep 2026 03:49:58 GMT</pubDate><content:encoded><![CDATA[<p>By tokviewer.app editorial team · September 17, 2026</p>
<p>An empty cell tells you that a number is absent. It does not tell you whether the number was zero, whether the source omitted it, or whether the request failed. Give availability its own column before calculating a total or an average. That small schema choice keeps a temporary failure from silently changing your report.</p>
<p>Disclosure: This tutorial was prepared with AI assistance for TokViewer, a product associated with the author. Every record below is fictional; the script reads a local CSV and makes no network requests.</p>
<blockquote>
<p><strong>Key takeaways</strong></p>
<ul>
<li>Store an observed zero as <code>available,0</code>, not as a blank cell.</li>
<li>Keep missing data and failed attempts distinct, with a reason for each.</li>
<li>Validate the whole input before printing a summary.</li>
<li>Report the observed denominator alongside the result.</li>
</ul>
</blockquote>
<p><img src="https://tokviewer.app/blog/view-tiktok-profiles-and-available-videos/hero.png" alt="TokViewer public account page before any account has been collected." /></p>
<p><em>Product-owned interface screenshot captured September 17, 2026. No account has been collected. This illustrates the separate viewing workflow; it is not output from the Python example.</em></p>
<h2>Define what the number actually measures</h2>
<p>Start with the measurement boundary. In this example, <code>visible_count</code> is a count supplied by one observation, not a lifetime total or proof of complete coverage. If a system only supplies a partial list, its length describes that list. A successfully returned empty list is a zero for that returned list only; it does not prove the underlying account or collection is empty.</p>
<p>This distinction matters when reviewing public profile information. The accompanying <a href="https://tokviewer.app/blog/view-tiktok-profiles-and-available-videos/">guide to viewing TikTok profiles and available videos</a> describes a user-facing viewing workflow. The reporting pattern here is separate: it is a suggested schema for your own analysis, not a claim about fields exported by that product.</p>
<p>Write the interpretation next to the schema so another developer cannot accidentally rename this field to <code>total_videos</code>. A production dataset may also need observation time, source, and a scope identifier. This tutorial intentionally handles one observation per fictional record, so duplicate IDs are rejected rather than merged.</p>
<h2>Give each state one unambiguous meaning</h2>
<p>Use the following three-state contract. These are design choices for this tutorial, not statuses defined by Python or a social platform.</p>
<table>
<thead>
<tr>
<th>Status</th>
<th>Count</th>
<th>Reason</th>
<th>Meaning</th>
</tr>
</thead>
<tbody><tr>
<td><code>available</code></td>
<td>Nonnegative integer, including 0</td>
<td>Empty</td>
<td>The observation supplied a usable count</td>
</tr>
<tr>
<td><code>missing</code></td>
<td>Empty</td>
<td>Required</td>
<td>The observation completed without supplying this field</td>
</tr>
<tr>
<td><code>failed</code></td>
<td>Empty</td>
<td>Required</td>
<td>The attempt did not complete successfully</td>
</tr>
</tbody></table>
<p>A timeout is <code>failed</code>; an omitted field in an otherwise completed observation is <code>missing</code>. Neither is a numeric zero. If the upstream system cannot distinguish the two, do not invent a cause. Extend the contract with an explicit unknown state and define how it will be counted.</p>
<p>There is also a fourth condition: invalid input. A row such as <code>failed,12,timeout</code> contradicts the contract. The script rejects it; it does not reinterpret it as available or convert it into a legitimate failed attempt. Keep data validation errors separate from operational failures.</p>
<h2>Create a small, fictional fixture</h2>
<p>Save the following as <code>fixture.csv</code>. The three available values are 0, 12, and 6. The remaining rows deliberately have no numeric value.</p>
<pre><code class="language-csv">record_id,status,visible_count,reason
sample-a,available,0,
sample-b,available,12,
sample-c,missing,,not_provided
sample-d,failed,,timeout
sample-e,available,6,
</code></pre>
<p>Python's default CSV reader returns text fields without automatic numeric conversion. Its writer also serializes <code>None</code> as an empty string, losing that distinction on a normal round trip. An explicit status column therefore carries information the count cell alone cannot preserve. Open CSV files with <code>newline=""</code> as recommended by the <a href="https://docs.python.org/3/library/csv.html">Python CSV documentation</a>.</p>
<p>Avoid compact expressions such as <code>int(value or 0)</code>. They make a blank field indistinguishable from an observed zero. Similarly, filtering numeric values by truthiness would discard the valid zero. Selection should depend on the validated status.</p>
<h2>Validate first, then calculate</h2>
<p>Save this complete program as <code>report.py</code>. It uses only Python's standard library. Run it with Python 3.9 or later; this example was tested with Python 3.14.6. Other versions were not executed.</p>
<pre><code class="language-python">"""Validate a fictional availability CSV and print an observed-only summary."""
import csv
import re
import sys
from collections import Counter
from decimal import Decimal

FIELDS = ["record_id", "status", "visible_count", "reason"]


def summarize(path):
    counts = Counter()
    observed = []
    seen = set()
    with open(path, encoding="utf-8", newline="") as source:
        reader = csv.DictReader(source, strict=True)
        if reader.fieldnames != FIELDS:
            raise ValueError("header must be: " + ",".join(FIELDS))
        for row in reader:
            line = reader.line_num
            if None in row or any(value is None for value in row.values()):
                raise ValueError(f"line {line}: wrong number of fields")
            record_id, status, value, reason = (row[key] for key in FIELDS)
            if not record_id or record_id in seen:
                raise ValueError(f"line {line}: empty or duplicate record_id")
            seen.add(record_id)
            if status not in {"available", "missing", "failed"}:
                raise ValueError(f"line {line}: unknown status")
            if status == "available":
                if not re.fullmatch(r"0|[1-9][0-9]*", value) or reason:
                    raise ValueError(f"line {line}: available needs a nonnegative integer and empty reason")
                observed.append(int(value))
            elif value != "" or not reason.strip():
                raise ValueError(f"line {line}: {status} needs an empty count and a reason")
            counts[status] += 1
    total = sum(counts.values())
    print(f"rows={total}")
    for status in ("available", "missing", "failed"):
        print(f"{status}={counts[status]}")
    print(f"observed_sum={sum(observed) if observed else 'NA'}")
    mean = f"{Decimal(sum(observed)) / Decimal(len(observed)):.2f}" if observed else "NA"
    print(f"observed_mean={mean}")
    print(f"observation_fraction={len(observed)}/{total}" if total else "observation_fraction=NA")


if __name__ == "__main__":
    if len(sys.argv) != 2:
        sys.exit("usage: python3 report.py fixture.csv")
    try:
        summarize(sys.argv[1])
    except (ValueError, csv.Error, OSError) as error:
        sys.exit(f"invalid input: {error}")
</code></pre>
<p>The schema is deliberately strict. Headers must match in name and order; extra or missing cells fail validation. Counts accept ASCII decimal digits in canonical nonnegative form: <code>0</code> is valid, but <code>-1</code>, <code>1.5</code>, <code>+2</code>, <code>01</code>, and surrounding whitespace are rejected. That policy is useful when you control the exporter. If you accept other formats, normalize them explicitly before validation and record the transformation.</p>
<p>The script retains valid zeros by selecting rows with <code>status == "available"</code>. It prints nothing until all rows have been checked, so a contradiction near the end cannot leave an apparently complete summary on standard output. Invalid input produces a message on standard error and a nonzero exit status.</p>
<p>For compact output, the mean uses <code>Decimal</code> arithmetic and is formatted to two decimal places. It is a display value, not an assertion of additional measurement precision. Python documents the decimal arithmetic and rounding behavior in its <a href="https://docs.python.org/3/library/decimal.html">decimal module reference</a>.</p>
<h2>Run the report and inspect the denominator</h2>
<p>Run both files from the same directory:</p>
<pre><code class="language-sh">python3 report.py fixture.csv
</code></pre>
<p>Expected output:</p>
<pre><code class="language-text">rows=5
available=3
missing=1
failed=1
observed_sum=18
observed_mean=6.00
observation_fraction=3/5
</code></pre>
<p>The observed mean is 18 divided by 3, or 6.00. Filling the two absent values with zeros would instead divide 18 by 5 and report 3.60. Neither missing row supplies evidence for that substitution. The valid zero remains part of the observed denominator, which is why the result is not 9.00 either.</p>
<p><code>observation_fraction=3/5</code> describes this input fixture. It does not estimate platform-wide coverage, success rates for future requests, or how representative the observed records are. Likewise, <code>observed_sum=18</code> is the sum of the observed rows, not a complete total across all five records.</p>
<p>The mean among available records can still be a biased description of the intended population. For example, if larger collections fail more often, excluding failed attempts will not fix that sampling problem. Keep the descriptive label and investigate why data is absent before drawing broader conclusions.</p>
<h2>Check the cases that usually get hidden</h2>
<p>Try a contradiction by changing the failed row to contain <code>0</code>. The program must stop with a nonzero status rather than produce a new mean. Also try an unknown status, a duplicate ID, a negative number, and a missing cell. These checks exercise the contract, not just the happy path.</p>
<p>When every record is missing or failed, the script prints <code>observed_sum=NA</code> and <code>observed_mean=NA</code>. Although an empty mathematical sum is conventionally zero, a report displaying zero here could be read as an observation. The explicit <code>NA</code> is a reporting decision. When the file contains only its header, the observation fraction is also <code>NA</code> because there are no records to use as a denominator.</p>
<table>
<thead>
<tr>
<th>Symptom</th>
<th>Interpretation</th>
<th>Action</th>
</tr>
</thead>
<tbody><tr>
<td><code>available</code> with a blank count</td>
<td>Required observation is absent</td>
<td>Correct the upstream row; do not fill with zero</td>
</tr>
<tr>
<td><code>failed</code> with a numeric count</td>
<td>Two incompatible claims in one row</td>
<td>Resolve the source state before reporting</td>
</tr>
<tr>
<td>All counts absent</td>
<td>No numeric observations exist</td>
<td>Display <code>NA</code> and retain the state counts</td>
</tr>
<tr>
<td>Duplicate record ID</td>
<td>More than one observation for this fixture key</td>
<td>Choose a documented snapshot rule or add a proper observation key</td>
</tr>
<tr>
<td>Extra or missing CSV cells</td>
<td>Input shape differs from the schema</td>
<td>Fix the export before aggregation</td>
</tr>
</tbody></table>
<p>For a recurring job, keep the raw input and its validation result together. Retry failed observations according to your own collection policy, then create a new snapshot rather than overwriting history invisibly. If a retry succeeds, its value belongs to that later observation. A change from unknown to available does not by itself show that the underlying count increased.</p>
<p>The practical next step is to add availability to one existing report and make its denominator visible. Keep zero as data, keep unknown as unknown, and require every derived number to say which observations it includes.</p>
]]></content:encoded></item><item><title><![CDATA[Validate Identifier Columns Before Trusting a CSV Export]]></title><description><![CDATA[Disclosure: Prepared with AI assistance for CommentTok, whose editorial team is affiliated with TokViewer. All sample records and identifiers are fictional.

Before using an identifier column for join]]></description><link>https://commenttok.hashnode.dev/validate-identifier-columns-before-trusting-a-csv-export</link><guid isPermaLink="true">https://commenttok.hashnode.dev/validate-identifier-columns-before-trusting-a-csv-export</guid><category><![CDATA[Python]]></category><category><![CDATA[csv]]></category><dc:creator><![CDATA[carl kevin]]></dc:creator><pubDate>Wed, 16 Sep 2026 03:35:51 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p><strong>Disclosure:</strong> Prepared with AI assistance for CommentTok, whose editorial team is affiliated with TokViewer. All sample records and identifiers are fictional.</p>
</blockquote>
<p>Before using an identifier column for joins or deduplication, check the values actually stored in the CSV. A column can contain missing IDs, exponent notation, unexpected characters, or repeated strings while still looking tidy in a spreadsheet.</p>
<p>This tutorial builds a read-only Python validator for those observable problems. It cannot establish that an ID is authentic, detect every rounded value, or reconstruct lost digits. A passing result means only that this file passed the checks we defined.</p>
<h2>Define the contract first</h2>
<p>Use Python 3.9 or later, a terminal, and a comma-delimited UTF-8 CSV with a header. No packages, accounts, or network access are needed. Pick one required identifier column whose values should be unique within this file. Additional columns are allowed.</p>
<p>Our contract accepts only nonempty strings of ASCII digits. Leading zeros stay significant: <code>00042</code> and <code>42</code> are different identifiers. Whitespace-only values are blank; spaces around digits are invalid. We do not convert values to integers or floats, remove punctuation, or silently trim IDs into matches.</p>
<p>Uniqueness is a separate business rule. A repeated video ID is normal when many comments belong to one video. An optional parent ID may legitimately be blank. Do not apply this required, unique-column contract to either without changing the policy. For IDs unique only within a source, validate a properly scoped file or implement a composite key.</p>
<h2>Save the validator</h2>
<p>Save the complete program as <code>validate_ids.py</code>. Python's official <a href="https://docs.python.org/3/library/csv.html#csv.reader">CSV reader documentation</a> specifies string fields by default and recommends opening files with <code>newline=""</code>. Those choices keep numeric-looking identifiers as text and support quoted commas and embedded newlines.</p>
<pre><code class="language-python">import argparse
import csv
import re
from pathlib import Path

SCIENTIFIC = re.compile(r"[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)[eE][+-]?[0-9]+")


def validate(path, column):
    issues = []
    seen = {}
    count = 0
    with Path(path).open(encoding="utf-8-sig", newline="") as source:
        reader = csv.reader(source, strict=True)
        header = next(reader, None)
        if not header or any(not name.strip() for name in header):
            raise ValueError("Expected nonempty column names")
        if len(set(header)) != len(header) or column not in header:
            raise ValueError("Expected unique headers and the requested column")
        position = header.index(column)
        for count, row in enumerate(reader, 1):
            if len(row) != len(header):
                raise ValueError(f"Record {count}: wrong field count")
            value = row[position]
            if not value.strip():
                reason = "blank"
            elif SCIENTIFIC.fullmatch(value.strip()):
                reason = "scientific_notation"
            elif not re.fullmatch(r"[0-9]+", value):
                reason = "non_digit"
            else:
                reason = None
            if reason:
                issues.append(f"record {count}: {reason}")
            if value.strip():
                if value in seen:
                    issues.append(f"record {count}: duplicate of record {seen[value]}")
                else:
                    seen[value] = count
    if count == 0:
        raise ValueError("Expected at least one data record")
    return count, issues


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("csv_file", type=Path)
    parser.add_argument("column")
    args = parser.parse_args()
    try:
        count, issues = validate(args.csv_file, args.column)
    except (OSError, UnicodeError, ValueError, csv.Error) as error:
        parser.exit(2, f"Input error: {error}\n")
    for issue in issues:
        print(issue)
    print(f"{count} records; {len(issues)} issues")
    return 1 if issues else 0


if __name__ == "__main__":
    raise SystemExit(main())
</code></pre>
<p>The reader uses the default comma-separated dialect. Duplicate headers, missing columns, empty datasets, and incorrect record widths are input errors. Python documents <a href="https://docs.python.org/3/library/csv.html#csv.Dialect.strict">strict parsing</a> as raising an error on bad CSV input; our explicit header and width checks add the tutorial's structural rules. This is not a universal validator for every CSV dialect.</p>
<p>Record numbers count parsed data records, starting after the header. They are not physical line numbers: one quoted field can span several lines. A completely empty line fails the width check rather than disappearing from the audit.</p>
<p>The exponent pattern recognizes conventional scientific notation, including signed exponents. Other invalid spellings still fail the digit check. Exact nonblank duplicates are reported even when the repeated value is malformed; a row can therefore generate two issues. Blank values receive only the blank finding.</p>
<h2>Run an explicitly fictional fixture</h2>
<p>Create <code>fictional-ids.csv</code> exactly as shown. These invented numbers have no asserted relationship to real platform comments.</p>
<pre><code class="language-csv">comment_id,note
9000000000000000001,fictional baseline
,fictional missing value
abc123,fictional letters
9.000000000000001e18,fictional exponent
9000000000000000001,fictional repeated value
9000000000000000000,fictional rounded-looking value
00042,fictional leading zeros
42,fictional distinct string
</code></pre>
<p>Run the validator directly, without redirecting output into the input file:</p>
<pre><code class="language-sh">python3 validate_ids.py fictional-ids.csv comment_id
</code></pre>
<p>Expected terminal output:</p>
<pre><code class="language-text">record 2: blank
record 3: non_digit
record 4: scientific_notation
record 5: duplicate of record 1
8 records; 4 issues
</code></pre>
<p>The exit status is 1 because findings exist. Status 0 means no findings under this contract; status 2 means an input or parsing error prevented a complete report. The program finishes reading before printing findings, so malformed input does not produce a misleading partial success summary.</p>
<p>Record 6 deliberately passes. Its trailing zeros could be genuine, or its digits could have changed earlier. The validator has no evidence to choose between those explanations. Records 7 and 8 also pass because exact string comparison preserves their different spellings.</p>
<h2>Test the boundary, not just the happy path</h2>
<p>Replace one ID in a disposable fixture with whitespace, <code>12x</code>, or <code>1E+18</code> and expect blank, non-digit, and scientific-notation findings respectively. Repeat an existing ID and expect a duplicate referencing its first record. Repeating malformed text should report both its format problem and its duplication.</p>
<p>Also test quoted multiline notes, an initial UTF-8 byte-order mark, missing headers, short rows, and an unterminated quoted field. Compare the input bytes before and after execution: this program opens the CSV only for reading. It prints diagnostics without writing a cleaned file or deleting questionable records.</p>
<p>Local editorial validation exercised these cases, subprocess exit statuses, and the intentional inability to distinguish two different, unique digit-only IDs. The script retains a dictionary of seen values and a list of findings, so memory grows with the file. Keep this minimal version for manageable exports.</p>
<h2>Decide what a passing check permits</h2>
<p>A clean report is a useful gate before attempting a join, not proof that the join keys survived every previous application. To establish preservation, compare exact ID strings with an original immutable source retained before conversion, matching records through an independently reliable association. Matching on the suspect ID alone is circular; row position is unsuitable if ordering changed.</p>
<p>For the spreadsheet handoff, the related guide to <a href="https://tokviewer.app/blog/keep-tiktok-comment-ids-intact-in-excel/">keeping TikTok comment IDs intact in Excel</a> addresses preserving identifiers during that workflow. Run this check on the actual exported bytes, not merely the displayed cells.</p>
<p>If the original is unavailable, record that identity preservation is unverified. Do not expand exponent notation and label the result recovered, or infer missing digits from neighboring rows. Preserve the suspect input, investigate findings against the source, and write any justified corrections to a separate derivative.</p>
]]></content:encoded></item><item><title><![CDATA[Keep Manual Labels Stable When a CSV Changes]]></title><description><![CDATA[Disclosure: Prepared with AI assistance for CommentTok, whose editorial team is affiliated with TokViewer. All sample records are fictional.

You have labeled a comment as “pricing,” downloaded anothe]]></description><link>https://commenttok.hashnode.dev/keep-manual-labels-stable-when-a-csv-changes</link><guid isPermaLink="true">https://commenttok.hashnode.dev/keep-manual-labels-stable-when-a-csv-changes</guid><dc:creator><![CDATA[carl kevin]]></dc:creator><pubDate>Tue, 15 Sep 2026 04:02:58 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p><strong>Disclosure:</strong> Prepared with AI assistance for CommentTok, whose editorial team is affiliated with TokViewer. All sample records are fictional.</p>
</blockquote>
<p>You have labeled a comment as “pricing,” downloaded another CSV, and discovered that its row number changed. Copying yesterday’s label column into today’s file can silently attach decisions to different comments. Instead, compare the content and transfer a label only when its match occurs exactly once in each file.</p>
<p>This tutorial targets legacy or reduced CSVs without stable IDs. CommentTok’s current main exporter has 12 columns, including IDs when available. When genuine IDs exist in both files, match <code>video_id</code> plus <code>comment_id</code>, checking uniqueness and reviewing edited content. Prefer that to this less reliable text fallback; an extension’s sequential row number is not a stable comment ID.</p>
<p>Use Python 3.9 or later and comma-delimited UTF-8 files. The fresh file needs exactly <code>author,handle,comment,likes</code>; the old copy adds <code>label</code>. No packages are required. Do not discard available stable IDs just to run this example.</p>
<blockquote>
<p><strong>Key takeaways:</strong> Match exact text, check uniqueness on both sides, and leave uncertain rows blank with an explicit review status.</p>
</blockquote>
<h2>Choose what counts as a match</h2>
<p>Our key is the tuple <code>(author, handle, comment)</code>. We deliberately exclude <code>likes</code>: a changed count should not detach a content-topic label. The output keeps the fresh count. If your labels describe popularity, rankings, or engagement thresholds, this policy is inappropriate; those labels need recalculation or review when likes change.</p>
<p>In this reduced input, a unique text match establishes uniqueness within these two files, not identity across the platform. Use exports from the same known source and scope. An identical comment disappearing and being replaced by another identical comment can remain undetectable.</p>
<p>Matching is case-sensitive and preserves whitespace. A changed author name, handle, punctuation mark, or comment body produces an unmatched row. We do not lowercase, trim, or fuzzy-match text into equivalence. Whitespace-only key components are treated as missing. This conservative choice sacrifices some transfers rather than guessing whether an edit preserved meaning.</p>
<h2>Save the complete program</h2>
<p>Save this as <code>transfer_labels.py</code>. Headers must appear in the stated order. The loader rejects incorrect headers and record widths before creating output. Python’s <a href="https://docs.python.org/3/library/csv.html">CSV documentation</a> explains quoted fields and opening files with <code>newline=""</code>; these matter when comments contain commas or embedded line breaks.</p>
<pre><code class="language-python">import argparse
import csv
from collections import Counter
from pathlib import Path

FIELDS = ["author", "handle", "comment", "likes"]


def read_rows(path, fields):
    with path.open(encoding="utf-8-sig", newline="") as source:
        reader = csv.reader(source, strict=True)
        if next(reader, None) != fields:
            raise ValueError(f"{path}: expected {','.join(fields)}")
        rows = []
        for values in reader:
            if len(values) != len(fields):
                raise ValueError(f"{path}: wrong field count")
            rows.append(dict(zip(fields, values)))
        return rows


def key(row):
    return tuple(row[field] for field in FIELDS[:3])


def transfer(old, fresh):
    old_counts = Counter(key(row) for row in old)
    new_counts = Counter(key(row) for row in fresh)
    unique = {key(row): row for row in old if old_counts[key(row)] == 1}
    for row in fresh:
        match = key(row)
        label = ""
        if not all(value.strip() for value in match):
            status = "review_missing_key"
        elif old_counts[match] &gt; 1 or new_counts[match] &gt; 1:
            status = "review_duplicate"
        elif old_counts[match] == 0:
            status = "review_unmatched"
        elif not unique[match]["label"].strip():
            status = "review_unlabeled"
        else:
            label = unique[match]["label"]
            status = "transferred"
        yield {**row, "label": label, "status": status}


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("old", type=Path)
    parser.add_argument("fresh", type=Path)
    parser.add_argument("output", type=Path)
    args = parser.parse_args()
    try:
        old = read_rows(args.old, FIELDS + ["label"])
        fresh = read_rows(args.fresh, FIELDS)
        with args.output.open("x", encoding="utf-8", newline="") as target:
            writer = csv.DictWriter(target, fieldnames=FIELDS + ["label", "status"])
            writer.writeheader()
            writer.writerows(transfer(old, fresh))
    except (OSError, ValueError, csv.Error) as error:
        parser.exit(1, f"{error}\n")


if __name__ == "__main__":
    main()
</code></pre>
<p>The two counts prevent a one-to-many transfer as well as a many-to-one transfer. Python’s <a href="https://docs.python.org/3/library/collections.html#collections.Counter">Counter documentation</a> describes the counting container used here. Even duplicate old rows carrying the same label go to review: agreement between labels does not establish which record matched.</p>
<h2>Run a tiny example</h2>
<p>Create <code>old.csv</code>:</p>
<pre><code class="language-csv">author,handle,comment,likes,label
Ada,@ada,Price please,2,pricing
Bo,@bo,More colors,1,request
</code></pre>
<p>Create <code>fresh.csv</code>, with reordered rows, an edited comment, and a changed count:</p>
<pre><code class="language-csv">author,handle,comment,likes
Bo,@bo,More colors please,1
Ada,@ada,Price please,9
</code></pre>
<p>Run:</p>
<pre><code class="language-sh">python3 transfer_labels.py old.csv fresh.csv labeled.csv
</code></pre>
<p>The expected output is:</p>
<pre><code class="language-csv">author,handle,comment,likes,label,status
Bo,@bo,More colors please,1,,review_unmatched
Ada,@ada,Price please,9,pricing,transferred
</code></pre>
<p>The output follows the fresh file’s order. Neither input is modified. An existing output path is refused, so choose a new filename for another run. The program loads both inputs into memory; it is intended for small working exports, not an unbounded stream.</p>
<h2>Validate uncertainty, then review it</h2>
<p>Try copying Ada’s row twice into either input. Every fresh Ada match should now have an empty label and <code>review_duplicate</code>, even if its likes differ. A unique old row with an empty label produces <code>review_unlabeled</code>; an empty key component produces <code>review_missing_key</code>. A new comment or any unmatched edit produces <code>review_unmatched</code>.</p>
<p>Before publication, local fixture checks covered those cases, row reordering, changed likes, quoted multiline Unicode text, malformed input, and refusal to overwrite an existing file. These are synthetic checks of this policy, not evidence about real export completeness. The small example and duplicate-row exercise above can be reproduced using only the code in this article.</p>
<p>Filter all <code>review_</code> statuses and make fresh decisions. Keep the old labeled CSV: rows absent from the fresh export do not appear in this output, so it is not an audit of deletions. Before using a reviewed output as the next old file, remove its <code>status</code> column and retain the five required columns.</p>
<p>For the next analysis step, the <a href="https://tokviewer.app/blog/analyze-tiktok-comments-in-google-sheets/">Google Sheets comment-analysis workflow</a> explains the spreadsheet side. Keep the transfer decision separate from your grouping and charts: a blank label with a review reason is useful information.</p>
]]></content:encoded></item><item><title><![CDATA[Keep Capture Metadata Beside Your CSV]]></title><description><![CDATA[Disclosure: Prepared with AI assistance by the CommentTok editorial team, which is affiliated with the exporter described here. All teaching examples are fictional.

A filename is a weak memory of how]]></description><link>https://commenttok.hashnode.dev/keep-capture-metadata-beside-your-csv</link><guid isPermaLink="true">https://commenttok.hashnode.dev/keep-capture-metadata-beside-your-csv</guid><category><![CDATA[Python]]></category><dc:creator><![CDATA[carl kevin]]></dc:creator><pubDate>Mon, 14 Sep 2026 08:17:41 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p><strong>Disclosure:</strong> Prepared with AI assistance by the CommentTok editorial team, which is affiliated with the exporter described here. All teaching examples are fictional.</p>
</blockquote>
<p>A filename is a weak memory of how a dataset was collected. After a few downloads, you can have several plausible versions of the same CSV and no reliable record of which video, collection time, or review note belongs to each one.</p>
<p>A sidecar manifest helps by keeping that context in a separate JSON file. The CSV stays unchanged, while a fingerprint connects the manifest to the exact bytes you analyzed. This tutorial uses Python's standard library and a deliberately small four-column comment export.</p>
<blockquote>
<p><strong>What the manifest establishes:</strong> which bytes the notes refer to. It does not establish that those bytes are authentic, complete, or collected at the time someone entered.</p>
</blockquote>
<h2>Separate observed facts from supplied context</h2>
<p>The export has <code>author</code>, <code>handle</code>, <code>comment</code>, and <code>likes</code>. It lacks source-video URLs, capture times, comment posting times, comment IDs, and parent IDs. That makes provenance useful, but a manifest cannot recover information that was never recorded.</p>
<p>Some manifest fields are computed: filename, byte length, column names, record count, and SHA-256 fingerprint. Others are supplied: the source URL and known capture time. A third time, <code>manifest_created_at</code>, describes when the script ran. Keeping those categories distinct prevents a recent processing date from masquerading as a collection date.</p>
<h2>Generate a manifest from the bytes you parse</h2>
<p>Save the code below as <code>capture_manifest.py</code>. It reads a small file into memory, checks the expected header and record widths, and prints JSON. No package installation, account, or network connection is required.</p>
<pre><code class="language-python">"""Print a sidecar manifest; does not modify the input CSV."""
import argparse
import csv
import hashlib
import io
import json
from datetime import datetime, timezone
from pathlib import Path

parser = argparse.ArgumentParser()
parser.add_argument("csv_file", type=Path)
parser.add_argument("--source-url", required=True)
parser.add_argument("--captured-at", help="Actual ISO capture time with timezone, if known")
args = parser.parse_args()

if args.captured_at:
    captured = datetime.fromisoformat(args.captured_at.replace("Z", "+00:00"))
    if captured.utcoffset() is None:
        parser.error("--captured-at needs a timezone offset")

raw = args.csv_file.read_bytes()
reader = csv.reader(io.StringIO(raw.decode("utf-8-sig"), newline=""), strict=True)
header = next(reader, None)
if header != ["author", "handle", "comment", "likes"]:
    parser.error("Expected author,handle,comment,likes header")
count = 0
for row in reader:
    if len(row) != len(header):
        parser.error("A record has the wrong number of fields")
    count += 1

manifest = {
    "manifest_version": 1,
    "file_name": args.csv_file.name,
    "sha256": hashlib.sha256(raw).hexdigest(),
    "byte_length": len(raw),
    "columns": header,
    "records": count,
    "source_video_url": args.source_url,
    "captured_at": args.captured_at,
    "manifest_created_at": datetime.now(timezone.utc).isoformat(),
    "coverage": "unknown",
}
print(json.dumps(manifest, indent=2))
</code></pre>
<p>The same <code>raw</code> value feeds the parser and fingerprint, so the count and hash describe the same in-memory snapshot. The script does not validate every field's meaning; for example, it does not check whether <code>likes</code> is numeric or whether the supplied URL exists.</p>
<p>Python's <a href="https://docs.python.org/3/library/csv.html"><code>csv</code> documentation</a> explains why parsing should handle quoted delimiters and newlines. Counting physical lines would overcount a comment that contains a line break. The <code>utf-8-sig</code> decoder also accommodates an initial UTF-8 byte-order mark.</p>
<h2>Try it with explicitly fictional data</h2>
<p>Create <code>sample.csv</code> with the following invented record. Neither the author label nor the comment represents a real person or captured discussion.</p>
<pre><code class="language-csv">author,handle,comment,likes
"Example A","example_a","How should I store it?","2"
</code></pre>
<p>Then run:</p>
<pre><code class="language-bash">python3 capture_manifest.py sample.csv \
  --source-url 'https://example.com/fictional-video' \
  &gt; sample.manifest.json
</code></pre>
<p>The example URL is deliberately fictional. The manifest will show one record, the four expected columns, <code>coverage</code> set to <code>unknown</code>, and <code>captured_at</code> set to <code>null</code>. Its creation time reflects your run. The fingerprint depends on the exact bytes you saved, including line endings.</p>
<p>For a real capture, supply the real URL. Add <code>--captured-at</code> only if you know the original collection time, using an ISO value with a timezone offset. Omitting an unknown capture time is more honest than deriving it from the file modification time.</p>
<h2>Verify before attaching downstream results</h2>
<p>Before using the manifest with another copy, recompute the SHA-256 digest and compare it with the saved value. Python provides SHA-256 in its standard <a href="https://docs.python.org/3/library/hashlib.html"><code>hashlib</code> module</a>.</p>
<p>Different digests mean the byte sequences differ. An unchanged digest is a useful integrity check, but does not authenticate the original collector or prove the truth of manually entered metadata. Anyone who can replace both files can recompute a matching manifest.</p>
<p>Renaming a file leaves its byte fingerprint unchanged, while changing line endings can change the fingerprint without changing the apparent table. Decide whether your workflow tracks exact files or normalized records before treating hashes as dataset identities.</p>
<h2>Carry limitations into the next artifact</h2>
<p>If you create a cleaned CSV, keep it separate and generate a new manifest. Record the original fingerprint as a parent reference in your processing notes, alongside the transformation performed. Do not overwrite the original manifest with the cleaned file's fingerprint and lose that relationship.</p>
<p>A local reference combining fingerprint and record number can locate a row in a saved file. It cannot identify the same platform comment across captures. Missing comment IDs still make deduplication uncertain, and missing parent IDs still prevent reliable thread reconstruction.</p>
<p>For large files, replace the in-memory approach with a carefully designed streaming workflow. For small exports, the more immediate improvement is disciplined metadata: preserve the input, record what you know, mark what you do not, and keep the limits attached when you share the analysis.</p>
<hr />
<p>By the <a href="https://tokviewer.app/blog/export-tiktok-comments-to-csv/">CommentTok editorial team</a>, affiliated with the exporter described here. Prepared with AI assistance. The sample record and URL are fictional. The included script was exercised locally; no complete-data guarantee, verified collection identity, or human editorial review is claimed.</p>
]]></content:encoded></item></channel></rss>