<?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: ebhow</title>
    <description>The latest articles on DEV Community by ebhow (@ebhow).</description>
    <link>https://dev.to/ebhow</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%2F4240510%2F12c298ad-a775-4dde-8960-4cdfe23bd3c3.png</url>
      <title>DEV Community: ebhow</title>
      <link>https://dev.to/ebhow</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ebhow"/>
    <language>en</language>
    <item>
      <title>How to merge Shift_JIS, UTF-8 BOM, and Windows-1252 CSVs without guessing the encoding</title>
      <dc:creator>ebhow</dc:creator>
      <pubDate>Thu, 12 Nov 2026 07:41:17 +0000</pubDate>
      <link>https://dev.to/ebhow/how-to-merge-shiftjis-utf-8-bom-and-windows-1252-csvs-without-guessing-the-encoding-128i</link>
      <guid>https://dev.to/ebhow/how-to-merge-shiftjis-utf-8-bom-and-windows-1252-csvs-without-guessing-the-encoding-128i</guid>
      <description>&lt;h1&gt;
  
  
  How to merge Shift_JIS, UTF-8 BOM, and Windows-1252 CSVs without guessing the encoding
&lt;/h1&gt;

&lt;p&gt;A shop export from Japan, another from the UK, a third from a US system, and a Chinese marketplace file. You want one table. &lt;code&gt;pandas.read_csv()&lt;/code&gt; with the default UTF-8 encoding fails on three of the four files. Dates arrive as &lt;code&gt;2026/3/2&lt;/code&gt;, &lt;code&gt;20260310&lt;/code&gt;, &lt;code&gt;2026年3月14日&lt;/code&gt;, and &lt;code&gt;3/15/2026&lt;/code&gt;. Two rows are exact duplicates, one row is empty, and one amount is written in full-width digits (&lt;code&gt;２４９&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;This article walks through that exact problem using &lt;a href="https://github.com/ebhow9986-ctrl/sheetstack-lite" rel="noopener noreferrer"&gt;sheetstack-lite&lt;/a&gt;, a small MIT-licensed Python tool. Every command below was run against the repository's fictional sample files. The output is copied from those runs, not reconstructed from memory.&lt;/p&gt;

&lt;p&gt;sheetstack-lite is a free, in-memory merge-and-clean utility. It is not a tax tool, and this article does not claim compatibility with Google Sheets or WPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the sample files actually contain
&lt;/h2&gt;

&lt;p&gt;Clone the repo and install the two dependencies (&lt;code&gt;pandas&lt;/code&gt; and &lt;code&gt;openpyxl&lt;/code&gt;). Python 3.10 or later is required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ebhow9986-ctrl/sheetstack-lite.git
&lt;span class="nb"&gt;cd&lt;/span&gt; sheetstack-lite
python3 -m venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate   &lt;span class="c"&gt;# Windows: .venv\Scripts\activate&lt;/span&gt;
pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;samples/&lt;/code&gt; folder ships with five fictional CSVs (none of the rows are real customer records):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;What the tool reported&lt;/th&gt;
&lt;th&gt;What is messy about it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;store_us.csv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;utf-8-sig&lt;/code&gt; (UTF-8 with a BOM)&lt;/td&gt;
&lt;td&gt;Mixed date formats, a blank row, a duplicate &lt;code&gt;US-1002&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;store_uk.csv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cp1252&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The word “Café” is a single &lt;code&gt;0xE9&lt;/code&gt; byte; one date is &lt;code&gt;3/6/2026&lt;/code&gt;; &lt;code&gt;UK-2003&lt;/code&gt; is duplicated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;store_jp.csv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;shift_jis&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Japanese headers (&lt;code&gt;伝票番号&lt;/code&gt;, &lt;code&gt;日付&lt;/code&gt;, &lt;code&gt;商品名&lt;/code&gt;…); dates such as &lt;code&gt;2026年3月14日&lt;/code&gt; and &lt;code&gt;20260310&lt;/code&gt;; &lt;code&gt;JP-3005&lt;/code&gt; is duplicated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;store_cn.csv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gb18030&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Chinese headers; a full-width amount &lt;code&gt;２４９&lt;/code&gt;; a date written as &lt;code&gt;2026年3月13日&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;customers_messy.csv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;utf-8-sig&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Duplicate ids, an empty row, mixed dates, a full-width &lt;code&gt;０８&lt;/code&gt; in the points column&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The README describes the four store files as “GBK” for the Chinese export. When the CLI sniffed the same file, it printed &lt;code&gt;gb18030&lt;/code&gt; (a superset of GBK). The rest of this article uses the string the tool actually printed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a default pandas read fails
&lt;/h2&gt;

&lt;p&gt;On the same machine, &lt;code&gt;pd.read_csv(path)&lt;/code&gt; with no &lt;code&gt;encoding=&lt;/code&gt; argument succeeded only for the UTF-8 BOM file. The other three store exports raised &lt;code&gt;UnicodeDecodeError&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;store_cn.csv: 'utf-8' codec can't decode byte 0xb6 in position 0: invalid start byte
store_jp.csv: 'utf-8' codec can't decode byte 0x93 in position 0: invalid start byte
store_uk.csv: 'utf-8' codec can't decode byte 0xe9 in position 67: invalid continuation byte
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last one is the é in “Café”. If you force &lt;code&gt;latin-1&lt;/code&gt; or &lt;code&gt;cp1252&lt;/code&gt; onto the Japanese file, the read may “succeed” and give you mojibake instead of an error. Encoding has to be chosen per file, then the headers have to be lined up, then the dates and duplicates have to be cleaned. Doing that by hand for every export is the tedious part.&lt;/p&gt;

&lt;p&gt;This is a well-worn problem — the Stack Overflow thread &lt;a href="https://stackoverflow.com/questions/18171739/unicodedecodeerror-when-reading-csv-file-in-pandas" rel="noopener noreferrer"&gt;“UnicodeDecodeError when reading CSV file in Pandas”&lt;/a&gt; has been viewed more than a million times. The usual advice is “pass &lt;code&gt;encoding=...&lt;/code&gt;”. That works when you already know the encoding and you only have one file. It does not merge four different code pages, or rename &lt;code&gt;伝票番号&lt;/code&gt; and &lt;code&gt;Order ID&lt;/code&gt; into the same column.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merge the four store exports
&lt;/h2&gt;

&lt;p&gt;The tool is invoked as &lt;code&gt;python3 sheetstack_lite.py&lt;/code&gt;. Two subcommands exist: &lt;code&gt;merge&lt;/code&gt; and &lt;code&gt;clean&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Point &lt;code&gt;merge&lt;/code&gt; at the four store files, ask it to normalize header aliases, and write a single CSV:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 sheetstack_lite.py merge \
  samples/store_us.csv \
  samples/store_uk.csv \
  samples/store_jp.csv \
  samples/store_cn.csv \
  -o stores.csv \
  --normalize-headers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exact output from the run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Merged 4 file(s), 21 rows, 7 columns
  - store_us.csv: 7 rows / encoding utf-8-sig
  - store_uk.csv: 5 rows / encoding cp1252
  - store_jp.csv: 6 rows / encoding shift_jis
  - store_cn.csv: 3 rows / encoding gb18030
Wrote stores.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--normalize-headers&lt;/code&gt; maps common aliases onto one English name. In these samples that means, among others:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Order ID&lt;/code&gt; / &lt;code&gt;伝票番号&lt;/code&gt; / &lt;code&gt;订单编号&lt;/code&gt; → &lt;code&gt;order_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Date&lt;/code&gt; / &lt;code&gt;日付&lt;/code&gt; / &lt;code&gt;成交时间&lt;/code&gt; → &lt;code&gt;order_date&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Item&lt;/code&gt; / &lt;code&gt;商品名&lt;/code&gt; / &lt;code&gt;商品名称&lt;/code&gt; → &lt;code&gt;product&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Quantity&lt;/code&gt; / &lt;code&gt;数量&lt;/code&gt; / &lt;code&gt;件数&lt;/code&gt; → &lt;code&gt;qty&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Sales&lt;/code&gt; / &lt;code&gt;売上&lt;/code&gt; / &lt;code&gt;销售额&lt;/code&gt; → &lt;code&gt;amount&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Remaining&lt;/code&gt; / &lt;code&gt;在庫&lt;/code&gt; / &lt;code&gt;库存数&lt;/code&gt; → &lt;code&gt;stock&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;code&gt;source_file&lt;/code&gt; column is added unless you pass &lt;code&gt;--no-source&lt;/code&gt;. The merged file still has 21 rows, including the blank US row and the three duplicate orders. Dates are not rewritten during merge. “Café mug”, &lt;code&gt;スマホスタンド&lt;/code&gt;, and &lt;code&gt;键盘&lt;/code&gt; all sit in the same &lt;code&gt;product&lt;/code&gt; column, and the file is written as UTF-8 with a BOM (&lt;code&gt;utf-8-sig&lt;/code&gt;) so Excel on Windows will not treat the CJK text as mojibake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you follow the README literally&lt;/strong&gt; and run &lt;code&gt;python3 sheetstack_lite.py merge samples -o merged.csv --normalize-headers&lt;/code&gt;, the tool walks the whole folder. That includes &lt;code&gt;customers_messy.csv&lt;/code&gt;, which has a different schema. In that run the CLI reported &lt;code&gt;Merged 5 file(s), 29 rows, 12 columns&lt;/code&gt;. For a store-export merge, name the four files (or keep customers in a separate folder).&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean dates, drop the empty row, remove duplicates
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 sheetstack_lite.py clean stores.csv \
  -o stores_clean.csv \
  --date-cols order_date \
  --dedupe-cols order_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exact output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cleaned 21 rows → 17 rows (empty 1, duplicates 3)
Detected encoding: utf-8-sig
Wrote stores_clean.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 17 remaining &lt;code&gt;order_id&lt;/code&gt; values are unique: &lt;code&gt;US-1001&lt;/code&gt; through &lt;code&gt;US-1005&lt;/code&gt;, &lt;code&gt;UK-2001&lt;/code&gt; through &lt;code&gt;UK-2004&lt;/code&gt;, &lt;code&gt;JP-3001&lt;/code&gt; through &lt;code&gt;JP-3005&lt;/code&gt;, and &lt;code&gt;CN-4001&lt;/code&gt; through &lt;code&gt;CN-4003&lt;/code&gt;. Every &lt;code&gt;order_date&lt;/code&gt; is &lt;code&gt;YYYY-MM-DD&lt;/code&gt;. A few examples of what changed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input (&lt;code&gt;order_date&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2026/3/2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-03-02&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;20260305&lt;/code&gt; / &lt;code&gt;20260310&lt;/code&gt; / &lt;code&gt;20260318&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;2026-03-05&lt;/code&gt; / &lt;code&gt;2026-03-10&lt;/code&gt; / &lt;code&gt;2026-03-18&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2026.3.9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-03-09&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;3/15/2026&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-03-15&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;2026年3月14日&lt;/code&gt; / &lt;code&gt;2026年3月13日&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;2026-03-14&lt;/code&gt; / &lt;code&gt;2026-03-13&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cells are trimmed, and full-width digits are folded to ASCII, so the Chinese amount &lt;code&gt;２４９&lt;/code&gt; becomes &lt;code&gt;249&lt;/code&gt;. Duplicate keys keep the first row (&lt;code&gt;US-1002&lt;/code&gt;, &lt;code&gt;UK-2003&lt;/code&gt;, &lt;code&gt;JP-3005&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Ambiguous dates: &lt;code&gt;3/6/2026&lt;/code&gt; is not obvious
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;UK-2002&lt;/code&gt; is dated &lt;code&gt;3/6/2026&lt;/code&gt;. Without &lt;code&gt;--dayfirst&lt;/code&gt;, the cleaner treated that as 6 March (&lt;code&gt;2026-03-06&lt;/code&gt;). With &lt;code&gt;--dayfirst&lt;/code&gt;, the same value became 3 June (&lt;code&gt;2026-06-03&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 sheetstack_lite.py clean stores.csv \
  -o stores_clean_dayfirst.csv \
  --date-cols order_date \
  --dedupe-cols order_id \
  --dayfirst
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unambiguous values did not flip: &lt;code&gt;2026.3.9&lt;/code&gt; stayed &lt;code&gt;2026-03-09&lt;/code&gt;, and &lt;code&gt;3/15/2026&lt;/code&gt; stayed &lt;code&gt;2026-03-15&lt;/code&gt; (there is no 15th month). If a file is known to be day-first, pass the flag. If a folder mixes US and UK exports, inspect the ambiguous rows before you trust a single convention. sheetstack-lite does not invent a locale per source file during &lt;code&gt;clean&lt;/code&gt;; it applies one rule to the column you name.&lt;/p&gt;

&lt;h2&gt;
  
  
  A second, smaller clean: messy customer dates and duplicates
&lt;/h2&gt;

&lt;p&gt;The customer list is a better illustration of empty rows and full-width numbers mixed with several date styles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 sheetstack_lite.py clean samples/customers_messy.csv \
  -o customers.csv \
  --dedupe-cols customer_id \
  --date-cols signup_date
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exact output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cleaned 8 rows → 5 rows (empty 1, duplicates 2)
Detected encoding: utf-8-sig
Wrote customers.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cleaned table is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer_id  signup_date  city      notes            points
U001         2026-02-01   Tokyo     paid             120
U002         2026-02-03   Osaka     pending          08
U003         2026-02-08   Nagoya    follow-up        56
U004         2026-02-11   Fukuoka   shipped          230
U005         2026-02-15   Sapporo   address needed   15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;2026/2/1&lt;/code&gt;, &lt;code&gt;20260203&lt;/code&gt;, &lt;code&gt;2026年2月8日&lt;/code&gt;, &lt;code&gt;2026-02-11&lt;/code&gt;, and &lt;code&gt;2/15/2026&lt;/code&gt; all became &lt;code&gt;YYYY-MM-DD&lt;/code&gt;. The full-width points value &lt;code&gt;０８&lt;/code&gt; became &lt;code&gt;08&lt;/code&gt;. Leading and trailing spaces (including a full-width space after &lt;code&gt;Nagoya&lt;/code&gt;) were stripped. The second &lt;code&gt;U001&lt;/code&gt; and the second &lt;code&gt;U003&lt;/code&gt; were dropped; the blank row was dropped.&lt;/p&gt;

&lt;p&gt;One thing not to do: after merging unrelated schemas, do not dedupe on a column that is empty for some of the rows. In a trial clean of the five-file merge, &lt;code&gt;--dedupe-cols order_id&lt;/code&gt; treated every customer row (blank &lt;code&gt;order_id&lt;/code&gt;) as a duplicate of the first blank key and deleted most of the customer data. Dedupe on a key that actually identifies the row, or clean each kind of file on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the free tool does not do
&lt;/h2&gt;

&lt;p&gt;sheetstack-lite version &lt;code&gt;1.0.0-lite&lt;/code&gt; loads each file into memory. It will not help if Excel already refuses to open the file, and it has no chunked reads, no split-into-pieces command, and no Excel summary workbook. The tests in the repo (&lt;code&gt;python3 tests/test_lite.py&lt;/code&gt;) cover the README examples; they passed on the same machine used for this article.&lt;/p&gt;

&lt;p&gt;If you only need to repair a file's encoding and would rather not use the command line, there is a free &lt;a href="https://ebhow9986-ctrl.github.io/csv-fixer/?ref=devto" rel="noopener noreferrer"&gt;CSV encoding fixer that runs entirely in the browser&lt;/a&gt;. The file never leaves your computer.&lt;/p&gt;

&lt;p&gt;If you later need chunked reads for large CSVs, a split into Excel-friendly pieces, an Excel summary with conditional formatting, or a one-step &lt;code&gt;run&lt;/code&gt; pipeline, those are in the paid toolkit (&lt;a href="https://ebhow.gumroad.com/l/sheetstack?ref=devto" rel="noopener noreferrer"&gt;Sheetstack on Gumroad&lt;/a&gt;). The free repository does not include that source, and the two editions have different licenses.&lt;/p&gt;

&lt;p&gt;For the everyday case — a handful of exports, mixed encodings, messy dates, a few duplicates — the commands above are enough, and they stay on your machine.&lt;/p&gt;

</description>
      <category>python</category>
      <category>csv</category>
      <category>excel</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
