Skip to content

Instantly share code, notes, and snippets.

@tos-kamiya
Last active September 18, 2023 15:46
Show Gist options
  • Save tos-kamiya/ab08986fbc2539dc55b495a7f092c1b9 to your computer and use it in GitHub Desktop.
Save tos-kamiya/ab08986fbc2539dc55b495a7f092c1b9 to your computer and use it in GitHub Desktop.
ChatGPTさんとやり取りしながら日常使いのスクリプトを制作する例

Python nkf ドロップインツール

You

Pyhtonでファイルの文字コードを変換するユーティリティであるnkfのドロップインリプレースメントとなるツールを作りたいと思います。

  • システムのデフォルトの文字コードを取得する
  • システムのデフォルトの改行文字を取得する
  • chardetにより文字コードを判定する
  • python-iconvにより文字コードを変換する
  • このとき、改行文字の変換も行う

ChatGPT

Pythonを使ってnkf(Network Kanji Filter)のような機能を実装することは可能です。以下はその簡単な例です。

必要なライブラリのインストール

chardetpython-iconvを使うので、これらのライブラリをインストールしてください。

pip install chardet python-iconv

コード例

import os
import sys
import chardet
import iconv

def get_system_encoding():
    return sys.getfilesystemencoding()

def get_system_newline():
    return os.linesep

def detect_encoding(file_path):
    with open(file_path, 'rb') as f:
        result = chardet.detect(f.read())
    return result['encoding']

def convert_file(input_path, output_path, from_encoding, to_encoding, newline):
    with open(input_path, 'r', encoding=from_encoding) as f:
        content = f.read()

    # 改行文字の変換
    content = content.replace('\r\n', '\n').replace('\r', '\n').replace('\n', newline)

    with open(output_path, 'w', encoding=to_encoding) as f:
        f.write(content)

def main():
    input_path = 'input.txt'  # 入力ファイル
    output_path = 'output.txt'  # 出力ファイル
    to_encoding = get_system_encoding()  # システムのデフォルトの文字コード
    newline = get_system_newline()  # システムのデフォルトの改行文字

    from_encoding = detect_encoding(input_path)  # 入力ファイルの文字コードを判定

    print(f'Converting from {from_encoding} to {to_encoding}...')
    convert_file(input_path, output_path, from_encoding, to_encoding, newline)
    print('Conversion completed.')

if __name__ == '__main__':
    main()

処理の流れ

  1. システムのデフォルトの文字コードを取得します。
  2. システムのデフォルトの改行文字を取得します。
  3. chardetを使用して入力ファイルの文字コードを判定します。
  4. python-iconvを使用して文字コードを変換します(このコード例ではPythonの標準ライブラリを使用していますが、python-iconvでも同様の処理ができます)。
  5. この際、改行文字も適切に変換します。

この例は基本的なものですので、オプションやエラーハンドリングなど、nkfの多くの機能は省略しています。必要に応じて機能を追加してください。

You

python-iconvではなく、標準ライブラリのiconvでできるのですね。標準ライブラリを利用します。ありがとうございます。

nkfのオプションを調べてみたのですが、

❯ nkf --help
Usage:  nkf -[flags] [--] [in file] .. [out file for -O flag]
 j/s/e/w  Specify output encoding ISO-2022-JP, Shift_JIS, EUC-JP
          UTF options is -w[8[0],{16,32}[{B,L}[0]]]
 J/S/E/W  Specify input encoding ISO-2022-JP, Shift_JIS, EUC-JP
          UTF option is -W[8,[16,32][B,L]]
 m[BQSN0] MIME decode [B:base64,Q:quoted,S:strict,N:nonstrict,0:no decode]
 M[BQ]    MIME encode [B:base64 Q:quoted]
 f/F      Folding: -f60 or -f or -f60-10 (fold margin 10) F preserve nl
 Z[0-4]   Default/0: Convert JISX0208 Alphabet to ASCII
          1: Kankaku to one space  2: to two spaces  3: HTML Entity
          4: JISX0208 Katakana to JISX0201 Katakana
 X,x      Convert Halfwidth Katakana to Fullwidth or preserve it
 O        Output to File (DEFAULT 'nkf.out')
 L[uwm]   Line mode u:LF w:CRLF m:CR (DEFAULT noconversion)
 --ic=<encoding>        Specify the input encoding
 --oc=<encoding>        Specify the output encoding
 --hiragana --katakana  Hiragana/Katakana Conversion
 --katakana-hiragana    Converts each other
 --{cap, url}-input     Convert hex after ':' or '%'
 --numchar-input        Convert Unicode Character Reference
 --fb-{skip, html, xml, perl, java, subchar}
                        Specify unassigned character's replacement
 --in-place[=SUF]       Overwrite original files
 --overwrite[=SUF]      Preserve timestamp of original files
 -g --guess             Guess the input code
 -v --version           Print the version
 --help/-V              Print this help / configuration
Network Kanji Filter Version 2.1.5 (2018-12-15) 
Copyright (C) 1987, FUJITSU LTD. (I.Ichikawa).
Copyright (C) 1996-2018, The nkf Project.

これらのオプションのうち、次に対応したいと思います。

Usage:  nkf -[flags] [--] [in file] .. [out file for -O flag]
 j/s/e/w  Specify output encoding ISO-2022-JP, Shift_JIS, EUC-JP
          UTF options is -w[8[0],{16,32}[{B,L}[0]]]
 J/S/E/W  Specify input encoding ISO-2022-JP, Shift_JIS, EUC-JP
          UTF option is -W[8,[16,32][B,L]]
 O        Output to File (DEFAULT 'nkf.out')
 L[uwm]   Line mode u:LF w:CRLF m:CR (DEFAULT noconversion)
 --ic=<encoding>        Specify the input encoding
 --oc=<encoding>        Specify the output encoding
 --in-place[=SUF]       Overwrite original files
 --overwrite[=SUF]      Preserve timestamp of original files
 -g --guess             Guess the input code

ChatGPT

Pythonを使ってnkf(Network Kanji Filter)の一部のオプションに対応したツールを作ることは十分可能です。以下のサンプルコードは、指定したオプションに基づいて動作する簡単な例を示しています。

import argparse
import os
import sys
import chardet

def detect_encoding(file_path):
    with open(file_path, 'rb') as f:
        result = chardet.detect(f.read())
    return result['encoding']

def convert_file(input_path, output_path, from_encoding, to_encoding, newline):
    with open(input_path, 'r', encoding=from_encoding) as f:
        content = f.read()

    content = content.replace('\r\n', '\n').replace('\r', '\n').replace('\n', newline)

    with open(output_path, 'w', encoding=to_encoding) as f:
        f.write(content)

def main():
    parser = argparse.ArgumentParser(description='Python nkf drop-in replacement')
    parser.add_argument('input_file', help='Input file path')
    parser.add_argument('-o', '--output_file', help='Output file path')
    parser.add_argument('-j', '--iso_2022_jp', action='store_true', help='Output encoding as ISO-2022-JP')
    parser.add_argument('-s', '--shift_jis', action='store_true', help='Output encoding as Shift_JIS')
    parser.add_argument('-e', '--euc_jp', action='store_true', help='Output encoding as EUC-JP')
    parser.add_argument('-w', '--utf_8', action='store_true', help='Output encoding as UTF-8')
    parser.add_argument('-J', '--input_iso_2022_jp', action='store_true', help='Input encoding as ISO-2022-JP')
    parser.add_argument('-S', '--input_shift_jis', action='store_true', help='Input encoding as Shift_JIS')
    parser.add_argument('-E', '--input_euc_jp', action='store_true', help='Input encoding as EUC-JP')
    parser.add_argument('-W', '--input_utf_8', action='store_true', help='Input encoding as UTF-8')
    parser.add_argument('-L', '--line_mode', choices=['u', 'w', 'm'], help='Line mode u:LF w:CRLF m:CR')
    parser.add_argument('-g', '--guess', action='store_true', help='Guess the input encoding')

    args = parser.parse_args()

    input_path = args.input_file
    output_path = args.output_file if args.output_file else 'nkf.out'

    if args.guess:
        from_encoding = detect_encoding(input_path)
    else:
        from_encoding = 'iso-2022-jp' if args.input_iso_2022_jp else 'shift_jis' if args.input_shift_jis else 'euc-jp' if args.input_euc_jp else 'utf-8'

    to_encoding = 'iso-2022-jp' if args.iso_2022_jp else 'shift_jis' if args.shift_jis else 'euc-jp' if args.euc_jp else 'utf-8'

    line_mode = args.line_mode if args.line_mode else 'u'
    newline = '\n' if line_mode == 'u' else '\r\n' if line_mode == 'w' else '\r'

    convert_file(input_path, output_path, from_encoding, to_encoding, newline)

if __name__ == '__main__':
    main()

使用方法

コマンドラインからこのスクリプトを呼び出す際に、上記で列挙したオプションを適用することができます。

例えば、次のように使います。

python script_name.py input.txt -o output.txt -j -W -Lw

このコマンドは、input.txt ファイルを読み取り、その内容をISO-2022-JP(-j オプション)に変換して output.txt に保存します。入力エンコーディングはUTF-8(-W オプション)とし、改行コードはCRLF(-Lw オプション)とします。

このサンプルコードは非常に簡単なものであり、エラーハンドリングや多くのnkfの機能は省略されています。必要に応じて機能を追加してください。

You

main関数の次の部分ですが、args.guess、args.input_XXXXのいずれも指定しない場合はargs.guessが指定されたものと解釈するようにしてください。

if args.guess:
    from_encoding = detect_encoding(input_path)
else:
    from_encoding = 'iso-2022-jp' if args.input_iso_2022_jp else 'shift_jis' if args.input_shift_jis else 'euc-jp' if args.input_euc_jp else 'utf-8'

ChatGPT

指定されていない場合にargs.guessが指定されたものと解釈するように、if-elseブロックを少し調整できます。

以下のようにコードを修正することで、args.guessまたはargs.input_XXXXが指定されていない場合に自動的に文字エンコーディングを推測するようになります。

    if args.guess or not (args.input_iso_2022_jp or args.input_shift_jis or args.input_euc_jp or args.input_utf_8):
        from_encoding = detect_encoding(input_path)
    else:
        from_encoding = 'iso-2022-jp' if args.input_iso_2022_jp else 'shift_jis' if args.input_shift_jis else 'euc-jp' if args.input_euc_jp else 'utf-8'

この変更により、-gフラグが指定されているか、どのargs.input_XXXXフラグも指定されていない場合に、detect_encoding()関数が呼び出されます。それ以外の場合は、指定された入力エンコーディングが使用されます。

You

main関数の次の部分ですが、args.iso_2022_jpやargs.shift_jisなどが指定されていない場合には、system.get_default_ecoding()で取得するようにしてください

ChatGPT

Pythonのlocaleモジュールを使用してシステムのデフォルトエンコーディングを取得できます。指定されていない場合にシステムのデフォルトエンコーディングを使用するようにコードを修正することができます。

以下に、そのように修正されたmain関数の一部を示します。

import locale

# ...(他の部分はそのまま)

def main():
    # ...(他の部分はそのまま)

    if args.guess or not (args.input_iso_2022_jp or args.input_shift_jis or args.input_euc_jp or args.input_utf_8):
        from_encoding = detect_encoding(input_path)
    else:
        from_encoding = 'iso-2022-jp' if args.input_iso_2022_jp else 'shift_jis' if args.input_shift_jis else 'euc-jp' if args.input_euc_jp else 'utf-8'

    if args.iso_2022_jp or args.shift_jis or args.euc_jp or args.utf_8:
        to_encoding = 'iso-2022-jp' if args.iso_2022_jp else 'shift_jis' if args.shift_jis else 'euc-jp' if args.euc_jp else 'utf-8'
    else:
        to_encoding = locale.getpreferredencoding()

    # ...(他の部分はそのまま)

if __name__ == '__main__':
    main()

この修正により、args.iso_2022_jpargs.shift_jisargs.euc_jpargs.utf_8のいずれも指定されていない場合に、locale.getpreferredencoding()が呼び出されてシステムのデフォルトエンコーディングが使用されます。それ以外の場合は、指定された出力エンコーディングが使用されます。

You

次の2つのオプションについて

    parser.add_argument('-w', '--utf_8', action='store_true', help='Output encoding as UTF-8')
    parser.add_argument('-W', '--input_utf_8', action='store_true', help='Input encoding as UTF-8')

-w8でutf-8、-w16でutf-16、-w32でutf-32、 さらに-w16と-w32では、後ろにLをつけるとリトリエンディアン、Bをつけるとビッグエンディアン(デフォルトはリトルエンディアン) さらに-w80や-w160や-w320や-16L0などと、0をつけるとBOMなしを指定できるようにしたいのですが、可能でしょうか。

ChatGPT

指定されたような複雑なオプションをargparseで実装することは可能ですが、そのためには少し工夫が必要です。

argparsechoicestypeオプションを使用して、ユーザーが指定する引数に対するカスタムの検証を行えます。以下は、このような複雑なオプションを実装する一例です。

import argparse

def validate_utf_option(value):
    base = value[:-1] if value[-1] in 'LB0' else value
    endian = value[-1] if value[-1] in 'LB' else 'L'
    bom = True if value[-1] != '0' else False

    if base not in ('8', '16', '32'):
        raise argparse.ArgumentTypeError(f"Invalid UTF option: {value}")

    return base, endian, bom

def main():
    parser = argparse.ArgumentParser(description="Your NKF replacement tool.")
    parser.add_argument('-w', '--utf_8', type=validate_utf_option, help='Output encoding as UTF (e.g., -w8, -w16L, -w32B0)')
    parser.add_argument('-W', '--input_utf_8', type=validate_utf_option, help='Input encoding as UTF (e.g., -W8, -W16L, -W32B0)')
    
    args = parser.parse_args()

    if args.utf_8:
        utf_base, utf_endian, utf_bom = args.utf_8
        print(f"Output encoding: UTF-{utf_base} {utf_endian}-endian, BOM: {utf_bom}")

    if args.input_utf_8:
        utf_base, utf_endian, utf_bom = args.input_utf_8
        print(f"Input encoding: UTF-{utf_base} {utf_endian}-endian, BOM: {utf_bom}")

if __name__ == '__main__':
    main()

この例では、validate_utf_option関数がUTFに関するオプションを検証します。この関数は、指定されたオプションが有効かどうかをチェックし、その結果をタプルとして返します。タプルの要素は、基本となるUTF(8、16、32)、エンディアン(LまたはB)、BOM(TrueまたはFalse)です。

このようにして、ユーザーが複雑なエンコーディングオプションを指定できるようになります。後は、この情報を使用して実際のエンコーディングやデコーディングを行えば良いわけです。

You

-w16B0を渡すと、validate_utf_optionでエラーが出るように思います。 ロジックを変更して、次のようにしてください 最後の文字が"0"かどうか判定して、もし"0"ならbomの値を1に設定して最後の文字を除去する 最後の文字が"B"か"L"かを判定して、・・・

ChatGPT

確かに、-w16B0のようなオプションでエラーが発生する可能性があります。ご指摘のとおり、ロジックを変更することで解消できます。

以下に修正後のvalidate_utf_option関数を示します。

import argparse

def validate_utf_option(value):
    bom = 0  # BOMなしをデフォルトとする
    endian = 'L'  # リトルエンディアンをデフォルトとする

    # 最後の文字が0ならBOMなし
    if value[-1] == '0':
        bom = 1
        value = value[:-1]

    # 最後の文字が'B'または'L'ならエンディアンを設定
    if value[-1] in 'BL':
        endian = value[-1]
        value = value[:-1]

    if value not in ('8', '16', '32'):
        raise argparse.ArgumentTypeError(f"Invalid UTF option: {value}")

    return value, endian, bom

# 以下はその他のコード(main関数など)

この修正により、-w16B0のようなオプションが正しく処理されるようになります。最初にBOM(Byte Order Mark)の有無を確認し、次にエンディアン(ビッグエンディアンまたはリトルエンディアン)を確認します。そして最後に、残った値(8, 16, 32)が有効なUTF形式であるかを確認します。

You

open関数のencoding=の引数に、utf_base, utf_endian, utf_bomを渡せるようにできますか。 BOMについてはencoding=の引数ではなく、中身を見て削除することが必要でしょうか。

ChatGPT

Pythonのopen関数にencoding引数を渡す際、utf-8, utf-16-le, utf-16-be, utf-32-le, utf-32-beなどを指定できますが、BOM(Byte Order Mark)に関してはこの引数で制御することはできません。したがって、以下のようなアプローチが考えられます:

  1. エンディアンに応じてutf-16-leやutf-16-beをencoding引数に渡す。
  2. BOMが必要ない場合、ファイルを読み込んだ後で最初の数バイトを削除するか、書き込む前にBOMを追加する。

例として、UTF-16LE(リトルエンディアン)でBOMを含まないファイルを書き込むコードを以下に示します。

# BOMなしでUTF-16LE(リトルエンディアン)ファイルを書き込む
with open("file_without_bom.txt", "w", encoding="utf-16-le") as f:
    f.write("Hello, world!")

同様に、BOMを含まないファイルを読み込む際には、最初の2バイト(UTF-16の場合)をスキップします。

# BOMなしでUTF-16LE(リトルエンディアン)ファイルを読み込む
with open("file_without_bom.txt", "r", encoding="utf-16-le") as f:
    f.read(1)  # 最初の2バイトをスキップ
    content = f.read()
    print(content)

ただし、BOMを手動で削除または追加する場合、バイナリモードでファイルを開いて操作する必要があります。それ以外の場面では、encoding引数にエンコーディング名とエンディアンを渡すことで、簡単にエンコーディングを制御できます。

You

上で作成していただいた、validate_utf_optionを修正して、 encoding=に渡すための、"utf-32-le"といったビット幅とエンディアンを示す識別子の文字列と、bomの有無を示す値を返すようにしてください。

ChatGPT

validate_utf_optionを修正して、encoding引数に渡せるような文字列と、BOM(Byte Order Mark)の有無を示す値を返すようにすることは可能です。

以下に修正後のコードを示します。

import argparse

def validate_utf_option(value):
    bom = False  # BOMなしをデフォルトとする
    endian = 'le'  # リトルエンディアンをデフォルトとする

    # 最後の文字が0ならBOMなし
    if value[-1] == '0':
        bom = False
        value = value[:-1]
    else:
        bom = True  # BOMあり

    # 最後の文字が'B'または'L'ならエンディアンを設定
    if value[-1] == 'B':
        endian = 'be'  # ビッグエンディアン
        value = value[:-1]
    elif value[-1] == 'L':
        endian = 'le'  # リトルエンディアン(デフォルトなので本当は不要)
        value = value[:-1]

    if value not in ('8', '16', '32'):
        raise argparse.ArgumentTypeError(f"Invalid UTF option: {value}")

    encoding_str = f"utf-{value}-{endian}"
    return encoding_str, bom

# 使用例
try:
    encoding_str, bom = validate_utf_option('16B0')
    print(f"Encoding: {encoding_str}, BOM: {bom}")
except argparse.ArgumentTypeError as e:
    print(e)

このコードにより、たとえばvalidate_utf_option('16B0')を呼び出すと、('utf-16-be', False)というタプルが返されます。このタプルの第一要素はencoding引数に渡せる文字列(この場合はutf-16-be)、第二要素はBOMの有無を示すブール値(この場合はFalse)です。

You

入力ファイルに対してBOMが指定されたときは無視したいのですが、次のようなメッセージで良いでしょうか print("Warning: BOM specified for input file. Neglects it.", file=sys.stderr)

ChatGPT

はい、そのメッセージは警告の意図を明確に伝えていて良いと思います。sys.stderrに出力することで、エラーメッセージとして適切に扱われます。ただし、「Neglects it」は文法的に少しだけ不自然ですので、より正確な表現に変えることをお勧めします。

例えば、「Ignoring it」とした場合、警告メッセージは以下のようになります。

print("Warning: BOM specified for input file. Ignoring it.", file=sys.stderr)

このメッセージであれば、「入力ファイルでBOMが指定されましたが、それは無視されます」という意味がより明確に伝わります。

You

次のコードで読み出されたファイルの中身に対して、

    with open(input_path, 'r', encoding=from_encoding) as f:
        content = f.read()

BOMが含まれていたときに除去するというコードを書いてください。

ChatGPT

Pythonの標準ライブラリでは、BOMがある場合にそれを自動的に取り除いてくれるcodecsモジュールが存在します。ただし、BOMが含まれていない場合はそのままファイルが読み込まれるので、BOMがない場合との互換性もあります。

以下はBOMの取り除き方の一例です:

import codecs

with open(input_path, 'rb') as f:  # バイナリモードでファイルを開く
    bom_len = len(codecs.BOM_UTF8)
    sample = f.read(bom_len)  # ファイルの先頭を読む
    if sample.startswith(codecs.BOM_UTF8):  # UTF-8のBOMがあるか確認
        bom_len = len(codecs.BOM_UTF8)
    elif sample.startswith(codecs.BOM_UTF16_LE) or sample.startswith(codecs.BOM_UTF16_BE):  # UTF-16のBOMがあるか確認
        bom_len = len(codecs.BOM_UTF16_LE)
    elif sample.startswith(codecs.BOM_UTF32_LE) or sample.startswith(codecs.BOM_UTF32_BE):  # UTF-32のBOMがあるか確認
        bom_len = len(codecs.BOM_UTF32_LE)
    else:
        bom_len = 0  # BOMがない
    f.seek(bom_len)  # ファイルポインタをBOMの後ろに移動
    content = f.read().decode(from_encoding)  # BOMを除いてから残りを読む

上記のコードでは、ファイルをバイナリモードで開いて先頭の数バイトを読み、それがBOMであるかどうかを確認しています。BOMがあれば、その長さ分だけファイルポインタを進めてから内容を読み込みます。これによって、ファイルの内容にBOMが含まれていた場合、それが除去されます。

You

その方法だと、読み込む前にファイル内容の最初の数バイトを調べるという風になっていますが、読み込んだあとにBOMを除去する方法は取れますか。あるいは、content = f.read()で読み込まれた段階ですでにBOMは除去されているということでしょうか

ChatGPT

open関数でテキストモード('r')とともにエンコーディングを指定した場合、Pythonは自動的にUTFエンコードされたテキストの先頭にあるBOM(Byte Order Mark)を取り除いてくれます。これはUTF-8、UTF-16、UTF-32に共通の動作です。

したがって、以下のようなコードでは:

with open(input_path, 'r', encoding='utf-8') as f:
    content = f.read()

content変数にはBOMが除去されたテキストが格納されます。

ただし、異なるエンコーディング(例えば、ISO-2022-JP、Shift_JISなど)を使っている場合や、バイナリモードでファイルを読んだ場合にはこの動作は保証されません。

すでにf.read()で文字列として読み込んだ後でBOMを取り除きたい場合、Pythonの文字列メソッドを使って取り除くことができます。例えばUTF-8のBOMはUnicodeで'\ufeff'と表現されるので、以下のようにできます:

with open(input_path, 'r', encoding=from_encoding) as f:
    content = f.read()

if content.startswith('\ufeff'):
    content = content[1:]

この方法で、すでに読み込まれたテキストデータからBOMを取り除くことができます。ただし、この方法はUTF-8に特有のものですので、他のエンコーディングに対応する場合は適宜調整が必要です。

You

ありがとうございます。それでは、UTF-8, UTF-16, UTF-32のファイルをバイナリとして読み込んだあと、明示的にBOMを検査して除去するようなルーチンを作成してください。

# ファイルをバイナリとして読み込む
with open(input_path, 'rb') as f:
    bin_content = f.read()

# オプションに応じてBOMを削除する
if option_bom_removal:
    ....

# デコードする
....

ChatGPT

以下のようにBOMを検査して削除するコードを書くことができます。この例では、UTF-8、UTF-16(LEおよびBE)、UTF-32(LEおよびBE)の各エンコーディングに対応しています。

# ファイルをバイナリとして読み込む
with open(input_path, 'rb') as f:
    bin_content = f.read()

# オプションに応じてBOMを削除する
if option_bom_removal:
    # BOMのバイト列
    BOMS = {
        'utf-8': b'\xef\xbb\xbf',
        'utf-16-le': b'\xff\xfe',
        'utf-16-be': b'\xfe\xff',
        'utf-32-le': b'\xff\xfe\x00\x00',
        'utf-32-be': b'\x00\x00\xfe\xff',
    }
    
    # BOMを検査して削除
    for encoding, bom in BOMS.items():
        if bin_content.startswith(bom):
            bin_content = bin_content[len(bom):]
            break  # BOMが見つかったらループを抜ける

# デコードする
# ここではfrom_encodingが既に設定されていると仮定しています。
content = bin_content.decode(from_encoding)

このコードは、オプションに応じてBOMを削除してから、指定されたエンコーディング(from_encoding)を使用してバイナリデータをデコードします。option_bom_removalがTrueの場合にBOMを削除し、Falseの場合にはそのままにします。

注意:この方法では、ファイルを一度に全て読み込むため、非常に大きなファイルではメモリに負荷がかかります。また、from_encodingが設定されている必要があります。それはchardetやその他の方法で予め設定しておいてください。

You

bom入のバイナリの文字列(bytes)に対して、デコードしてstrにしたあと、最初の文字がBOMかどうかを確認するという方法は取れますか

ChatGPT

はい、Pythonの文字列にデコードした後で、最初の文字がUnicodeのBOM文字かどうかを確認する方法もあります。PythonではBOMは特定のUnicodeコードポイントにマッピングされています。

  • UTF-8のBOMは通常は見えないが、実際はU+FEFFというUnicodeコードポイントです。
  • UTF-16およびUTF-32のBOMも同じくU+FEFFです。

以下はその例です。

# ファイルをバイナリとして読み込む
with open(input_path, 'rb') as f:
    bin_content = f.read()

# デコードする
# ここではfrom_encodingが既に設定されていると仮定しています。
content = bin_content.decode(from_encoding)

# オプションに応じてBOMを削除する
if option_bom_removal:
    if content and content[0] == '\ufeff':
        content = content[1:]

# 以降の処理

この方法では、最初の文字がUnicodeのBOM(U+FEFF)であるかを確認して、該当する場合はそれを削除します。こちらの方法でも、option_bom_removalがTrueの場合にのみBOMを削除し、Falseの場合にはそのままにします。

You

ありがとうございます。スッキリしました。

chardet.detectのテストをしたいのですが、iso-2022-jp, shift_jis, euc-jp, utf-8, utf-16-le, utf-16-be, utf-32-le, utf-32-beが戻り値となるようなサンプルデータを含むテストを作成してください。

ChatGPT

chardet.detectをテストするためのサンプルデータとテストコードを以下に示します。各エンコーディングに対して適当な日本語のテキストをエンコードし、chardet.detectで正しくエンコーディングが判定されるかテストしています。

import chardet
import unittest

class TestChardetDetect(unittest.TestCase):

    def test_iso_2022_jp(self):
        text = "こんにちは"
        encoded = text.encode('iso-2022-jp')
        result = chardet.detect(encoded)
        self.assertEqual(result['encoding'], 'ISO-2022-JP')

    def test_shift_jis(self):
        text = "こんにちは"
        encoded = text.encode('shift_jis')
        result = chardet.detect(encoded)
        self.assertEqual(result['encoding'], 'SHIFT_JIS')

    def test_euc_jp(self):
        text = "こんにちは"
        encoded = text.encode('euc-jp')
        result = chardet.detect(encoded)
        self.assertEqual(result['encoding'], 'EUC-JP')

    def test_utf_8(self):
        text = "こんにちは"
        encoded = text.encode('utf-8')
        result = chardet.detect(encoded)
        self.assertEqual(result['encoding'], 'utf-8')

    def test_utf_16_le(self):
        text = "こんにちは"
        encoded = text.encode('utf-16-le')
        result = chardet.detect(encoded)
        self.assertEqual(result['encoding'], 'UTF-16LE')

    def test_utf_16_be(self):
        text = "こんにちは"
        encoded = text.encode('utf-16-be')
        result = chardet.detect(encoded)
        self.assertEqual(result['encoding'], 'UTF-16BE')

    def test_utf_32_le(self):
        text = "こんにちは"
        encoded = text.encode('utf-32-le')
        result = chardet.detect(encoded)
        self.assertEqual(result['encoding'], 'UTF-32LE')

    def test_utf_32_be(self):
        text = "こんにちは"
        encoded = text.encode('utf-32-be')
        result = chardet.detect(encoded)
        self.assertEqual(result['encoding'], 'UTF-32BE')

if __name__ == '__main__':
    unittest.main()

このテストコードを実行すると、各エンコーディングに対してchardet.detectが正しく動作するかどうかが確認できます。ただし、chardetはヒューリスティックに基づいてエンコーディングを判定するので、短いテキストでは正確な判定ができない可能性があります。この点には注意が必要です。

You

実行してみると全てのテストでWindows-1252が戻り値となるようです。 例えば、次のように出力されます

======================================================================
FAIL: test_utf_16_be (__main__.TestChardetDetect)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/toshihiro/playground/nkf-minus/./test2.py", line 40, in test_utf_16_be
    self.assertEqual(result['encoding'], 'UTF-16BE')
AssertionError: 'Windows-1252' != 'UTF-16BE'
- Windows-1252
+ UTF-16BE

ChatGPT

申し訳ありません、chardetの挙動が期待通りでないことがあります。特に、短いテキストデータではchardetが正確にエンコーディングを推定できない場合があります。こういった場合は、あらかじめエンコーディングがわかっている状況でない限り、実用的なテストは難しいと言えるでしょう。

ただし、文字列が長くなれば、より正確にエンコーディングを推測できる可能性が高くなります。ですので、テストデータをもう少し長い文字列にして試すことが一つの方法です。

例えば:

text = "こんにちは、これはテスト用の長い文章です。" * 10

このような形でテストデータを作成してみてください。それでも結果が改善しない場合は、chardetの限界とも言えるかもしれません。その場合は、エンコーディングの情報が他の方法で得られるような状況を想定する必要があるかもしれません。

You

頂いたソースファイルのconvert_fileを次の2つに分割し、

def read_with_conversion(input_path: str, from_encoding: str, to_newline: Option[str], to_bom_removal: bool) -> str:
    ....
def write_with_conversion(output_path: str, content: str, to_encoding: str) -> None:
    ....

main関数の関係する部分も次のように書き換えました。

    content = read_with_conversion(input_path, output_path, from_encoding, to_newline, to_bom_removal)
    write_with_conversion(output_path, content, to_encoding)

この状態から、次の処理になるようにmain関数の該当部分を修正してください

  • output_pathが'-'の場合は標準出力に出力する
  • args.in_placeが指定されていたら、input_pathと同じ名前のファイルに上書きする
  • args.overwriteが指定されていたら、input_pathと同じ名前のファイルに上書きし、かつ、タイムスタンプも変更しない

ChatGPT

Pythonのargparseを使っていると仮定して、main関数の一部を以下のように修正することができます。

import os
import sys
import shutil

def main():
    parser = argparse.ArgumentParser(description='Convert file encodings.')
    # ... (ここで他のコマンドライン引数を追加する)
    parser.add_argument('--in-place', action='store_true', help='Overwrite the input file.')
    parser.add_argument('--overwrite', action='store_true', help='Overwrite the input file and preserve its timestamp.')
    args = parser.parse_args()

    # ...(エンコーディングや改行コードなどの引数を解析するコード)

    # 読み込みと変換
    content = read_with_conversion(input_path, from_encoding, to_newline, to_bom_removal)

    # 出力先を決定
    final_output_path = output_path
    if args.in_place or args.overwrite:
        final_output_path = input_path

    # 書き込み
    if final_output_path == '-':
        sys.stdout.write(content)
    else:
        # タイムスタンプを保存しておく
        if args.overwrite:
            original_timestamp = os.path.getmtime(input_path)

        write_with_conversion(final_output_path, content, to_encoding)

        # タイムスタンプを復元
        if args.overwrite:
            os.utime(input_path, (os.path.getatime(input_path), original_timestamp))

if __name__ == '__main__':
    main()

このコードでは次のことを行っています:

  • --in-place オプションが指定された場合、または --overwrite オプションが指定された場合は、input_pathをそのまま出力先として使います。
  • --overwriteが指定されていた場合、ファイルの元のタイムスタンプを保存し、書き込み後にそのタイムスタンプに戻します。
  • 出力先が '-'(ハイフン)であれば、標準出力に出力します。

これで、args.in_placeargs.overwriteの指定に対応したコードが完成するはずです。注意点としては、このコード例はread_with_conversionwrite_with_conversionがすでに定義されていることを前提としています。それらの関数は適切に動作するようにしてください。

You

--in-placeオプションと--overwriteオプションを、 --in-placeのみでも、--in-place=SUFFIXとしても指定できるようにして、 かつSUFFIXが指定されたときは出力ファイルに.SUFFIXという拡張子をつけるようにできますか

ChatGPT

もちろんです、argparseではオプション引数を持つフラグを作ることができます。以下にそれを考慮したmain関数の一部を示します。

import os
import sys
import argparse
import shutil

def main():
    parser = argparse.ArgumentParser(description='Convert file encodings.')
    # ... (ここで他のコマンドライン引数を追加する)
    parser.add_argument('--in-place', nargs='?', const=True, default=False, help='Overwrite the input file. Optionally add a SUFFIX.')
    parser.add_argument('--overwrite', action='store_true', help='Overwrite the input file and preserve its timestamp.')
    args = parser.parse_args()

    # ...(エンコーディングや改行コードなどの引数を解析するコード)

    # 読み込みと変換
    content = read_with_conversion(input_path, from_encoding, to_newline, to_bom_removal)

    # 出力先を決定
    final_output_path = output_path
    if args.in_place is not False or args.overwrite:
        final_output_path = input_path
        if args.in_place is not True:
            final_output_path += '.' + args.in_place

    # 書き込み
    if final_output_path == '-':
        sys.stdout.write(content)
    else:
        # タイムスタンプを保存しておく
        if args.overwrite:
            original_timestamp = os.path.getmtime(input_path)

        write_with_conversion(final_output_path, content, to_encoding)

        # タイムスタンプを復元
        if args.overwrite:
            os.utime(input_path, (os.path.getatime(input_path), original_timestamp))

if __name__ == '__main__':
    main()

このコードは次のように動きます:

  1. --in-placeが指定されていても値(SUFFIX)が与えられていない場合、args.in_placeはTrueになります。
  2. --in-place=SUFFIXが指定された場合、args.in_placeはそのSUFFIX(文字列)になります。
  3. --in-placeが指定されていない場合、args.in_placeはFalseになります。

final_output_pathがどのように設定されるかに注意してください。これにより、--in-placeオプションによって適切な出力先が選択されます。

このコードは元のファイルを上書きするか、元のファイル名に指定されたSUFFIXを追加した新しいファイルを作成します。そして、--overwriteが指定された場合にはタイムスタンプもそのまま保持されます。

You

--overwriteにも同様に、=SUFFIXをつけることも(つけないことも)可能としてください

ChatGPT

もちろん、--overwriteについても同様にnargs='?'const=Trueを設定してSUFFIXのオプションをサポートすることができます。以下はその変更を行ったmain関数の一部です。

import os
import sys
import argparse
import shutil

def main():
    parser = argparse.ArgumentParser(description='Convert file encodings.')
    # ... (ここで他のコマンドライン引数を追加する)
    parser.add_argument('--in-place', nargs='?', const=True, default=False, help='Overwrite the input file. Optionally add a SUFFIX.')
    parser.add_argument('--overwrite', nargs='?', const=True, default=False, help='Overwrite the input file and preserve its timestamp. Optionally add a SUFFIX.')
    args = parser.parse_args()

    # ...(エンコーディングや改行コードなどの引数を解析するコード)

    # 読み込みと変換
    content = read_with_conversion(input_path, from_encoding, to_newline, to_bom_removal)

    # 出力先を決定
    final_output_path = output_path
    suffix = ''
    if args.in_place is not False or args.overwrite is not False:
        final_output_path = input_path
        suffix = args.in_place if args.in_place is not True else args.overwrite if args.overwrite is not True else ''
    if suffix:
        final_output_path += '.' + suffix

    # 書き込み
    if final_output_path == '-':
        sys.stdout.write(content)
    else:
        # タイムスタンプを保存しておく
        if args.overwrite:
            original_timestamp = os.path.getmtime(input_path)

        write_with_conversion(final_output_path, content, to_encoding)

        # タイムスタンプを復元
        if args.overwrite:
            os.utime(input_path, (os.path.getatime(input_path), original_timestamp))

if __name__ == '__main__':
    main()

このようにして、--in-place--overwriteのどちらにもオプションでSUFFIXを追加できるようになりました。それぞれのフラグが指定されていると、args.in_placeまたはargs.overwriteTrueに設定されます。SUFFIXが指定されている場合はその値(文字列)になります。どちらも指定されていない場合、それらはFalseに設定されます。それに基づいて適切な出力ファイル名が決定されます。

You

ありがとうございます。

標準出力へ出力する次の部分について、

    sys.stdout.write(content)

標準出力に対してもバイナリで書き込むことは可能ですか。

参考までに、関数wirte_with_conversionは次のように定義されています。

def write_with_conversion(output_path: str, content: str, to_encoding: str) -> None:
    with open(output_path, 'w', encoding=to_encoding) as f:
        f.write(content)

ChatGPT

Pythonで標準出力に対してバイナリデータを書き込むことは可能ですが、そのためには通常sys.stdout.buffer.write()を使用します。しかし、この方法はプラットフォームと環境に依存する場合があります。特に、Windowsではコマンドプロンプトが通常テキストモードで動作するため、改行文字が自動的に変更される可能性があります。

以下のようにwrite_with_conversion関数を修正できます。

def write_with_conversion(output_path: str, content: str, to_encoding: str) -> None:
    if output_path == '-':
        # 標準出力にバイナリとして書き込む場合
        binary_content = content.encode(to_encoding)
        sys.stdout.buffer.write(binary_content)
    else:
        with open(output_path, 'w', encoding=to_encoding) as f:
            f.write(content)

この修正によって、output_path'-'(標準出力を意味する)である場合には、sys.stdout.buffer.write()を使ってバイナリデータを直接書き込むようになります。

注意: この方法はUnix系OSでよく機能しますが、Windowsで問題が生じる可能性があります。Windowsでの動作が重要であれば、追加のテストと調整が必要かもしれません。

You

ありがとうございます。標準出漁ではない通常のファイルの場合もcontent.encodeで変換するようにしてください。

ChatGPT

もちろん、その場合はファイルをバイナリモード('wb')で開く必要があります。以下が修正版です。

def write_with_conversion(output_path: str, content: str, to_encoding: str) -> None:
    binary_content = content.encode(to_encoding)

    if output_path == '-':
        # 標準出力にバイナリとして書き込む場合
        sys.stdout.buffer.write(binary_content)
    else:
        # ファイルにバイナリとして書き込む場合
        with open(output_path, 'wb') as f:
            f.write(binary_content)

この修正により、出力先が標準出力であるか通常のファイルであるかに関わらず、contentは指定されたエンコーディングでバイナリにエンコードされます。その後、対応する出力先にバイナリデータとして書き込まれます。

You

次のargparseへの指定の部分について、

    parser = argparse.ArgumentParser(description='Python nkf drop-in replacement')
    parser.add_argument('input_file', help='Input file path')
    parser.add_argument('-o', '--output-file', help='Output file path')
    parser.add_argument('-g', '--guess', action='store_true', help='Guess the input encoding (default)')
    parser.add_argument('-j', '--iso-2022-jp', action='store_true', help='Output encoding as ISO-2022-JP')
    parser.add_argument('-s', '--shift_jis', action='store_true', help='Output encoding as Shift_JIS')
    parser.add_argument('-e', '--euc-jp', action='store_true', help='Output encoding as EUC-JP')
    parser.add_argument('-w', '--utf', type=validate_utf_option, help='Output encoding as UTF (e.g., -w8, -w16L, -w32B0)')
    parser.add_argument('-J', '--input-iso-2022-jp', action='store_true', help='Input encoding as ISO-2022-JP')
    parser.add_argument('-S', '--input-shift_jis', action='store_true', help='Input encoding as Shift_JIS')
    parser.add_argument('-E', '--input-euc-jp', action='store_true', help='Input encoding as EUC-JP')
    parser.add_argument('-W', '--input-utf', type=validate_utf_option, help='Input encoding as UTF (e.g., -W8, -W16L, -W32B0)')
    parser.add_argument('-L', '--line-mode', choices=['u', 'w', 'm'], help='Line mode u:LF w:CRLF m:CR')
    parser.add_argument('--in-place', nargs='?', const=True, default=False, help='Overwrite the input file. Optionally add a SUFFIX.')
    parser.add_argument('--overwrite', nargs='?', const=True, default=False, help='Overwrite the input file and preserve its timestamp. Optionally add a SUFFIX.')

次を明示するようにできますか。 互いに排他なオプション -j, -s, -e, -w 互いに排他なオプション -g, -J, -S, -E, -W 互いに排他なオプション --in-place, --overwrite input_fileが指定されないときあるいは'-'のときは、--in-placeや--overwriteは指定できない

ChatGPT

argparse には排他的なオプショングループを作成するための機能があります。この機能を使って、排他的なオプションを設定できます。また、指定されたオプションによる矛盾をチェックするカスタムロジックも追加できます。

以下に、この要件に対応したコードの例を示します。

import argparse

def validate_arguments(args):
    # input_file が指定されていない、または '-' の場合、--in-place や --overwrite は指定できない
    if args.input_file in [None, '-']:
        if args.in_place or args.overwrite:
            raise argparse.ArgumentError(None, "--in-place and --overwrite can't be used when input_file is not specified or is '-'.")

# argparse の設定
parser = argparse.ArgumentParser(description='Python nkf drop-in replacement')

# 入力・出力ファイル
parser.add_argument('input_file', nargs='?', help='Input file path')
parser.add_argument('-o', '--output-file', help='Output file path')

# エンコーディングオプション(排他的)
group_output = parser.add_mutually_exclusive_group()
group_output.add_argument('-j', '--iso-2022-jp', action='store_true', help='Output encoding as ISO-2022-JP')
group_output.add_argument('-s', '--shift_jis', action='store_true', help='Output encoding as Shift_JIS')
group_output.add_argument('-e', '--euc-jp', action='store_true', help='Output encoding as EUC-JP')
group_output.add_argument('-w', '--utf', type=str, help='Output encoding as UTF (e.g., -w8, -w16L, -w32B0)')

# 入力エンコーディングオプション(排他的)
group_input = parser.add_mutually_exclusive_group()
group_input.add_argument('-g', '--guess', action='store_true', help='Guess the input encoding (default)')
group_input.add_argument('-J', '--input-iso-2022-jp', action='store_true', help='Input encoding as ISO-2022-JP')
group_input.add_argument('-S', '--input-shift_jis', action='store_true', help='Input encoding as Shift_JIS')
group_input.add_argument('-E', '--input-euc-jp', action='store_true', help='Input encoding as EUC-JP')
group_input.add_argument('-W', '--input-utf', type=str, help='Input encoding as UTF (e.g., -W8, -W16L, -W32B0)')

# その他のオプション
parser.add_argument('-L', '--line-mode', choices=['u', 'w', 'm'], help='Line mode u:LF w:CRLF m:CR')
group_overwrite = parser.add_mutually_exclusive_group()
group_overwrite.add_argument('--in-place', nargs='?', const=True, default=False, help='Overwrite the input file. Optionally add a SUFFIX.')
group_overwrite.add_argument('--overwrite', nargs='?', const=True, default=False, help='Overwrite the input file and preserve its timestamp. Optionally add a SUFFIX.')

# オプションを解析
args = parser.parse_args()

# オプションの矛盾をチェック
validate_arguments(args)

このようにして、指定されたオプションが矛盾しないようにチェックを入れることができます。

You

すいません。「input_fileが指定されないときあるいは'-'のときは、--in-placeや--overwriteは指定できない」の部分は、「output_pathが'-'のときは、--in-placeや--overwriteは指定できない」の間違いでした。

ChatGPT

ご指摘ありがとうございます。この要件に対応するように引数のバリデーション関数を修正できます。

以下は修正後の関数です。

def validate_arguments(args):
    # output_file が '-' の場合、--in-place や --overwrite は指定できない
    if args.output_file == '-':
        if args.in_place or args.overwrite:
            raise argparse.ArgumentError(None, "--in-place and --overwrite can't be used when output_file is '-'.")

# 以下はその他の argparse の設定、同様にしてください。
# ...

args.output_file'-' である場合、--in-place または --overwrite オプションが指定されているかどうかをチェックします。指定されていた場合、argparse.ArgumentError をスローしてエラーを通知します。このバリデーション関数は、parser.parse_args() の後に呼び出してください。

You

少し修正して次のようにしました。レビューをお願いします。

from typing import Option, Tuple

import argparse
import os
import sys
import chardet
import locale

def detect_encoding(bin_content: bytes) -> str:
    result = chardet.detect(bin_content)
    return result['encoding']

def decode(bin_content: bytes, from_encoding: str, to_newline: Option[str], to_bom_removal: bool) -> str:
    content = bin_content.decode(from_encoding)

    if to_bom_removal:
        if content and content[0] == '\ufeff':
            content = content[1:]

    if to_newline is not None:
        content = content.replace('\r\n', '\n').replace('\r', '\n').replace('\n', to_newline)

    return content

def validate_utf_option(value: str) -> Tuple[str, bool]:
    ....省略....

def main():
    # argparse の設定
    parser = argparse.ArgumentParser(description='Python nkf drop-in replacement')

    ....省略....

    # ファイルの読み込み
    if input_path == '-':
        bin_content = sys.stdin.buffer.read()
    else:
        with open(input_path, 'rb') as f:
            bin_content = f.read()

    # 入力ファイルのエンコーディングを特定、あるいは、指定
    if args.guess or not (args.input_iso_2022_jp or args.input_shift_jis or args.input_euc_jp or args.input_utf):
        from_encoding = detect_encoding(bin_content)
        if not (from_encoding in ['ISO-2022-JP', 'Shift_JIS', 'EUC-JP'] or from_encoding.startswith('utf-')):
            exit("Error: fail to detect input encoding")
    elif args.input_utf:
        from_encoding, from_bom_removal = args.input_utf
        if from_bom_removal:
            print("Warning: BOM specified for input file. Ignoring it.", file=sys.stderr)
    else:
        from_encoding = 'iso-2022-jp' if args.input_iso_2022_jp else 'shift_jis' if args.input_shift_jis else 'euc-jp' if args.input_euc_jp else 'utf-8'

    # 出力ファイルのエンコーディング、BOM、改行の指定
    to_bom_removal = False
    if args.utf:
        to_encoding, to_bom_removal = args.utf
    elif args.iso_2022_jp or args.shift_jis or args.euc_jp:
        to_encoding = 'iso-2022-jp' if args.iso_2022_jp else 'shift_jis' if args.shift_jis else 'euc-jp' if args.euc_jp else 'utf-8'
    else:
        to_encoding = locale.getpreferredencoding()

    to_newline = '\n' if args.line_mode == 'u' else '\r\n' if args.line_mode == 'w' else '\r' if args.line_mode == 'm' else None

    # フィルの読み込みとエンコーディングの変換
    content = decode(bin_content, from_encoding, to_newline, to_bom_removal)
    encoded_content = content.encode(to_encoding)

    # 書き出し
    final_output_path = output_path
    suffix = ''
    if args.in_place is not False or args.overwrite is not False:
        final_output_path = input_path
        suffix = args.in_place if args.in_place is not True else args.overwrite if args.overwrite is not True else ''
    if suffix:
        final_output_path += '.' + suffix

    if args.overwrite:
        original_timestamp = os.path.getmtime(input_path)

    if output_path == '-':
        sys.stdout.buffer.write(encoded_content)
    else:
        with open(output_path, 'wb') as f:
            f.write(encoded_content)

    if args.overwrite:
        os.utime(input_path, (os.path.getatime(input_path), original_timestamp))

if __name__ == '__main__':
    main()

ChatGPT

コード全体について数点コメントをさせていただきます。

  1. Typing: Python 標準ライブラリの typing モジュールから Option がインポートされていますが、Python 標準の typing には Option は存在しないはずです。おそらく Optional のことかと思います。
  2. Argument Validation: --in-place、--overwrite、output_file の関係については、コードにバリデーションが見当たらないようです。これらのオプションが特定の条件でのみ許可されるという要件に対応していないように見えます。
  3. Variable Name: output_path がどこから定義されたのかが不明確です。args.output_file から取得されるのかと思いますが、その辺りの変数の取扱いがコード上で明示されていると良いでしょう。
  4. Output File Name: final_output_path の設定について、args.in_place と args.overwrite の両方が指定された場合の挙動が明示されていないようです。この点も考慮するとよいでしょう。

以下にその辺りを修正した例を示します。

from typing import Optional, Tuple  # Option -> Optional

# ... (略)

def main():
    # argparse の設定
    parser = argparse.ArgumentParser(description='Python nkf drop-in replacement')

    # ... (略)

    args = parser.parse_args()

    # Validate arguments here
    if args.output_file == '-' and (args.in_place or args.overwrite):
        exit("Error: --in-place and --overwrite can't be used when output_file is '-'.")

    # ... (略)

    output_path = args.output_file  # ここで output_path を明示

    # ... (略)

    final_output_path = output_path if output_path != '-' else input_path
    suffix = ''
    if args.in_place or args.overwrite:
        final_output_path = input_path
        suffix = args.in_place if args.in_place not in [True, False] else args.overwrite if args.overwrite not in [True, False] else ''

    # ... (略)

    if output_path == '-':
        sys.stdout.buffer.write(encoded_content)
    else:
        with open(final_output_path, 'wb') as f:  # final_output_path を使用
            f.write(encoded_content)

    # ... (略)

if __name__ == '__main__':
    main()

この修正でご指摘の点に対応できたと思います。何か他に気になる点があれば、どうぞご指摘ください。

You

ありがとうございます。

関数decodeについて、unittestモジュールを利用して単体テストを作成してください。

引数のfrom_encodingについては、ISO-2022-JP, SHIFT_JIS, EUC-JP, UTF-8, UTF-16-LE, UTF-16-BE, UTF-32-LE, UTF-32-BEについてテストケースを作成してください。 これらのうちのUTFについては引数to_bom_removalがTrueとFalseの場合のテストも作成してください。

変換する文字列は、グローバル変数として、中身が「Hello, world. 日本語と英語が混じったサンプルテキストです。\n2行目です。」というものを用意してください。

ChatGPT

以下にunittestを使った単体テストの例を示します。このテストは、decode関数に対していくつかのエンコーディングと条件で動作するか確認します。

import unittest

# テスト対象の関数
def decode(bin_content: bytes, from_encoding: str, to_newline: Optional[str], to_bom_removal: bool) -> str:
    # (実装内容)
    pass

# グローバル変数
SAMPLE_TEXT = "Hello, world. 日本語と英語が混じったサンプルテキストです。\n2行目です。"

class TestDecode(unittest.TestCase):
    def test_iso_2022_jp(self):
        encoded = SAMPLE_TEXT.encode('iso-2022-jp')
        self.assertEqual(decode(encoded, 'iso-2022-jp', None, False), SAMPLE_TEXT)

    def test_shift_jis(self):
        encoded = SAMPLE_TEXT.encode('shift_jis')
        self.assertEqual(decode(encoded, 'shift_jis', None, False), SAMPLE_TEXT)

    def test_euc_jp(self):
        encoded = SAMPLE_TEXT.encode('euc_jp')
        self.assertEqual(decode(encoded, 'euc_jp', None, False), SAMPLE_TEXT)

    def test_utf_8(self):
        encoded = SAMPLE_TEXT.encode('utf-8')
        self.assertEqual(decode(encoded, 'utf-8', None, False), SAMPLE_TEXT)

    def test_utf_8_bom_removal(self):
        encoded = b'\xef\xbb\xbf' + SAMPLE_TEXT.encode('utf-8')  # UTF-8 with BOM
        self.assertEqual(decode(encoded, 'utf-8', None, True), SAMPLE_TEXT)

    def test_utf_16_le(self):
        encoded = SAMPLE_TEXT.encode('utf-16-le')
        self.assertEqual(decode(encoded, 'utf-16-le', None, False), SAMPLE_TEXT)

    def test_utf_16_be(self):
        encoded = SAMPLE_TEXT.encode('utf-16-be')
        self.assertEqual(decode(encoded, 'utf-16-be', None, False), SAMPLE_TEXT)

    def test_utf_32_le(self):
        encoded = SAMPLE_TEXT.encode('utf-32-le')
        self.assertEqual(decode(encoded, 'utf-32-le', None, False), SAMPLE_TEXT)

    def test_utf_32_be(self):
        encoded = SAMPLE_TEXT.encode('utf-32-be')
        self.assertEqual(decode(encoded, 'utf-32-be', None, False), SAMPLE_TEXT)

if __name__ == '__main__':
    unittest.main()

このテストケースでは、サンプルテキストを指定されたエンコーディングでエンコードした後、decode関数を使ってデコードし、元のテキストと一致するか確認しています。

また、UTF-8についてはBOM(Byte Order Mark)を付けたバージョンに対してもテストを行っています。

各エンコーディングや条件で適切にデコードできるかどうかを確認するためには、このようなテストが有用です。上記のテストコードは例なので、実際のdecode関数の実装に合わせて調整が必要かもしれません。

You

utf-16-le, utf-16-be, utf-32-le, utf-32-beについて、BOMのある場合のテストを作成してください。

ChatGPT

もちろん、UTF-16およびUTF-32でBOM(Byte Order Mark)がある場合のテストを追加することができます。以下はそのテストケースを含めたunittestです。

import unittest
from typing import Optional

# テスト対象の関数
def decode(bin_content: bytes, from_encoding: str, to_newline: Optional[str], to_bom_removal: bool) -> str:
    # (実装内容)
    pass

# グローバル変数
SAMPLE_TEXT = "Hello, world. 日本語と英語が混じったサンプルテキストです。\n2行目です。"

class TestDecode(unittest.TestCase):
    # ...(他のテストケースはそのまま)...

    def test_utf_16_le_with_bom(self):
        encoded = b'\xff\xfe' + SAMPLE_TEXT.encode('utf-16-le')  # UTF-16 LE with BOM
        self.assertEqual(decode(encoded, 'utf-16-le', None, True), SAMPLE_TEXT)

    def test_utf_16_be_with_bom(self):
        encoded = b'\xfe\xff' + SAMPLE_TEXT.encode('utf-16-be')  # UTF-16 BE with BOM
        self.assertEqual(decode(encoded, 'utf-16-be', None, True), SAMPLE_TEXT)

    def test_utf_32_le_with_bom(self):
        encoded = b'\xff\xfe\x00\x00' + SAMPLE_TEXT.encode('utf-32-le')  # UTF-32 LE with BOM
        self.assertEqual(decode(encoded, 'utf-32-le', None, True), SAMPLE_TEXT)

    def test_utf_32_be_with_bom(self):
        encoded = b'\x00\x00\xfe\xff' + SAMPLE_TEXT.encode('utf-32-be')  # UTF-32 BE with BOM
        self.assertEqual(decode(encoded, 'utf-32-be', None, True), SAMPLE_TEXT)

if __name__ == '__main__':
    unittest.main()

これで、BOMが付いている場合にも正しくデコードできるかどうかを確認するテストが追加されました。to_bom_removalオプションがTrueである場合、BOMが正しく取り除かれることを確認しています。このオプションがFalseであれば、BOMは残るべきですが、このテストではそのケースは考慮していません。必要であれば、それも追加できます。

You

ありがとうございます。コマンドラインからスクリプトを実行するテストをお願いします。

  • 一時ディレクトリを作成する
  • 一時ディレクトリ内に入力ファイルと、期待される出力ファイルをフィクスチャとして生成する
  • 実際に変換を行った出力ファイルが期待される出力と同じかをdiffなどのツールで比較する

ChatGPT

Pythonのunittestフレームワークとtempfileモジュール、そしてsubprocessモジュールを使用して、コマンドラインからスクリプトを実行するテストを書くことができます。以下に一例を示します。

まず、テスト対象のスクリプトがmy_script.pyという名前で保存されていると仮定します。

# test_my_script.py
import unittest
import tempfile
import os
import subprocess
import shutil
import filecmp

class TestCommandLine(unittest.TestCase):

    def setUp(self):
        # 一時ディレクトリを作成
        self.test_dir = tempfile.mkdtemp()

        # 入力ファイルを作成
        self.input_path = os.path.join(self.test_dir, "input.txt")
        with open(self.input_path, "w", encoding="utf-8") as f:
            f.write("Hello, world. 日本語と英語が混じったサンプルテキストです。\n2行目です。")

        # 期待される出力ファイルを作成
        self.expected_output_path = os.path.join(self.test_dir, "expected_output.txt")
        with open(self.expected_output_path, "w", encoding="shift_jis") as f:
            f.write("Hello, world. 日本語と英語が混じったサンプルテキストです。\n2行目です。")

    def tearDown(self):
        # 一時ディレクトリを削除
        shutil.rmtree(self.test_dir)

    def test_command_line_execution(self):
        # 出力ファイルのパス
        output_path = os.path.join(self.test_dir, "output.txt")

        # コマンドラインからスクリプトを実行
        subprocess.run(["python", "my_script.py", self.input_path, "-o", output_path, "--shift_jis"])

        # 生成された出力ファイルと期待される出力ファイルが同じかどうかを確認
        self.assertTrue(filecmp.cmp(output_path, self.expected_output_path, shallow=False))

if __name__ == "__main__":
    unittest.main()

このテストスクリプトでは以下のことを行っています:

  1. setUp メソッドで一時ディレクトリを作成し、その中に入力ファイルと期待される出力ファイルを作成します。
  2. test_command_line_execution メソッドでmy_script.pyスクリプトをコマンドラインから実行します。
  3. 生成された出力ファイルが期待される出力ファイルと同じかどうかをfilecmp.cmpで確認します。
  4. 最後に、tearDown メソッドで一時ディレクトリを削除します。

このスクリプトを実行すると、my_script.pyのコマンドライン実行が正しく動作するかどうかがテストされます。注意点としては、このテストが外部プロセスを起動するため、環境によっては実行時間が少し長くなる可能性があります。

You

  • pythonのインタプリタをsysモジュールに問い合わせるようにしてください
  • setUpでは入力ファイルを作成せずに、一時ディレクトリを作成するだけにしてください。入力ファイルの生成はテストケースのメソッドで行ってください

ChatGPT

ご指摘に基づき、コードを修正しました。

import unittest
import tempfile
import os
import subprocess
import shutil
import filecmp
import sys

class TestCommandLine(unittest.TestCase):

    def setUp(self):
        # 一時ディレクトリを作成
        self.test_dir = tempfile.mkdtemp()

    def tearDown(self):
        # 一時ディレクトリを削除
        shutil.rmtree(self.test_dir)

    def create_input_file(self, content, encoding):
        # 入力ファイルを作成
        input_path = os.path.join(self.test_dir, "input.txt")
        with open(input_path, "w", encoding=encoding) as f:
            f.write(content)
        return input_path

    def create_expected_output(self, content, encoding):
        # 期待される出力ファイルを作成
        expected_output_path = os.path.join(self.test_dir, "expected_output.txt")
        with open(expected_output_path, "w", encoding=encoding) as f:
            f.write(content)
        return expected_output_path

    def test_command_line_execution(self):
        # テストケース
        content = "Hello, world. 日本語と英語が混じったサンプルテキストです。\n2行目です。"
        
        # 入力と期待される出力ファイルを作成
        input_path = self.create_input_file(content, "utf-8")
        expected_output_path = self.create_expected_output(content, "shift_jis")
        
        # 出力ファイルのパス
        output_path = os.path.join(self.test_dir, "output.txt")

        # コマンドラインからスクリプトを実行
        subprocess.run([sys.executable, "my_script.py", input_path, "-o", output_path, "--shift_jis"])

        # 生成された出力ファイルと期待される出力ファイルが同じかどうかを確認
        self.assertTrue(filecmp.cmp(output_path, expected_output_path, shallow=False))

if __name__ == "__main__":
    unittest.main()
  1. Pythonインタプリタのパスはsys.executableを使用して取得しています。
  2. setUpでは一時ディレクトリのみを作成し、入力ファイルと期待される出力ファイルの作成は各テストケースで行います。

このようにすることで、テストの設定がより柔軟になり、さまざまなケースで入力ファイルと期待される出力ファイルを簡単に生成できます。

You

テスト結果は失敗でした。 parser.add_argument("-o", "--output-file", help="Output file path") というコードで、-oがフラグになっていて、ファイル名を引数として取らないことが原因でした

ChatGPT

申し訳ありません、その部分を見落としていました。

argparseで出力ファイル名を引数として取る場面では、通常このように設定します。

parser.add_argument("-o", "--output-file", type=str, help="Output file path")

こちらを修正した後、再度テストを実行してみてください。

そして、テストコードのsubprocess.run()の部分も確認してください。もしコマンドラインオプションが誤っていれば、修正が必要かもしれません。

テストコードにあるこの行:

subprocess.run([sys.executable, "my_script.py", input_path, "-o", output_path, "--shift_jis"])

この行があなたのスクリプトと互換性があるかどうか確認してください。もしそうでなければ、テストが失敗する可能性が高いです。

You

ありがとうございます。成功しました。

  • テストコードの2つの関数create_input_fileとcreate_expected_outputは中身が同じなので1つの関数にしてください。ただし、ファイル名を引数で指定するようにしてください。
  • テストケースとして、--in-placeを指定するものを作成してください。

ChatGPT

素晴らしい、テストが成功したようで何よりです。

関数create_input_filecreate_expected_outputを1つの関数にまとめ、さらに--in-placeオプションのテストケースを追加するという理解でよろしいでしょうか。以下にそのように修正したテストコードのスケルトンを示します。

import unittest
import os
import tempfile
import subprocess
import sys

SAMPLE_TEXT = 'Hello, world. 日本語と英語が混じったサンプルテキストです。\n2行目です。'

def create_file(file_path: str, content: str, encoding='utf-8'):
    with open(file_path, 'w', encoding=encoding) as f:
        f.write(content)

class TestEncodingConversion(unittest.TestCase):

    def setUp(self):
        self.temp_dir = tempfile.mkdtemp()

    def test_shift_jis_conversion(self):
        input_path = os.path.join(self.temp_dir, 'input.txt')
        expected_output_path = os.path.join(self.temp_dir, 'expected_output.txt')
        actual_output_path = os.path.join(self.temp_dir, 'actual_output.txt')

        create_file(input_path, SAMPLE_TEXT, encoding='utf-8')
        create_file(expected_output_path, SAMPLE_TEXT, encoding='shift_jis')

        subprocess.run([sys.executable, "my_script.py", input_path, "-o", actual_output_path, "--shift_jis"])
        self.assertTrue(filecmp.cmp(expected_output_path, actual_output_path))

    def test_in_place_conversion(self):
        input_path = os.path.join(self.temp_dir, 'input.txt')
        expected_output_path = os.path.join(self.temp_dir, 'expected_output.txt')

        create_file(input_path, SAMPLE_TEXT, encoding='utf-8')
        create_file(expected_output_path, SAMPLE_TEXT, encoding='shift_jis')

        subprocess.run([sys.executable, "my_script.py", input_path, "--in-place", "--shift_jis"])
        self.assertTrue(filecmp.cmp(input_path, expected_output_path))

if __name__ == '__main__':
    unittest.main()

上記のスケルトンでは、create_fileという名前で共通のファイル作成関数を作成しています。また、--in-placeオプションを使用するテストケースtest_in_place_conversionを追加しています。

必要に応じてテストケースをさらに追加或いは調整してください。そして、これを実行してすべてのテストが通るかどうか確認してみてください。

You

ありがとうございます。テストを成功するようにできました。

  • --overwriteオプションのテストを追加してください。タイムスタンプも確認するようにしてください。
  • 出力ファイルと--in-placeや--overwriteを指定してエラーを出るようなテストも追加してください。

ChatGPT

素晴らしいです、テストが成功したようで何よりです。

指示に基づいて--overwriteオプションと出力エラーのテストを追加します。以下がそのコードスケルトンです。

import unittest
import os
import tempfile
import subprocess
import sys
import filecmp
import time

SAMPLE_TEXT = 'Hello, world. 日本語と英語が混じったサンプルテキストです。\n2行目です。'

def create_file(file_path: str, content: str, encoding='utf-8'):
    with open(file_path, 'w', encoding=encoding) as f:
        f.write(content)

class TestEncodingConversion(unittest.TestCase):

    def setUp(self):
        self.temp_dir = tempfile.mkdtemp()

    def test_overwrite_conversion(self):
        input_path = os.path.join(self.temp_dir, 'input.txt')
        expected_output_path = os.path.join(self.temp_dir, 'expected_output.txt')

        create_file(input_path, SAMPLE_TEXT, encoding='utf-8')
        create_file(expected_output_path, SAMPLE_TEXT, encoding='shift_jis')

        original_timestamp = os.path.getmtime(input_path)

        subprocess.run([sys.executable, "my_script.py", input_path, "--overwrite", "--shift_jis"])
        self.assertTrue(filecmp.cmp(input_path, expected_output_path))

        # Confirm the timestamp is preserved
        self.assertEqual(original_timestamp, os.path.getmtime(input_path))

    def test_error_on_output_with_in_place(self):
        input_path = os.path.join(self.temp_dir, 'input.txt')
        output_path = os.path.join(self.temp_dir, 'output.txt')

        create_file(input_path, SAMPLE_TEXT, encoding='utf-8')

        with self.assertRaises(subprocess.CalledProcessError):
            subprocess.run([sys.executable, "my_script.py", input_path, "-o", output_path, "--in-place", "--shift_jis"], check=True)

    def test_error_on_output_with_overwrite(self):
        input_path = os.path.join(self.temp_dir, 'input.txt')
        output_path = os.path.join(self.temp_dir, 'output.txt')

        create_file(input_path, SAMPLE_TEXT, encoding='utf-8')

        with self.assertRaises(subprocess.CalledProcessError):
            subprocess.run([sys.executable, "my_script.py", input_path, "-o", output_path, "--overwrite", "--shift_jis"], check=True)

if __name__ == '__main__':
    unittest.main()

ここでは、--overwriteをテストするtest_overwrite_conversionと、--in-placeおよび--overwriteと共に出力ファイルを指定するとエラーになるテストを追加しています。タイムスタンプの保存も確認しています。

このテストスケルトンを参考に、必要な調整をして実行してみてください。そして、すべてのテストが通るかどうか確認してみてください。

You

このコードの次の部分ですが、 with self.assertRaises(subprocess.CalledProcessError): スクリプトを実行すると終了コードが0ではないというのをチェ区するようになっていますか

ChatGPT

はい、その通りです。subprocess.CalledProcessErrorは、サブプロセス(この場合はmy_script.py)が0以外の終了コードで終了したときに発生します。check=Trueが指定された場合、サブプロセスが成功(終了コード0)で終了しなかった場合にこのエラーが発生します。

したがって、このテストケースは、スクリプトが指定されたオプションで成功しない(終了コードが0でない)場合にテストが成功するようになっています。逆に言えば、スクリプトが問題なく実行され(終了コードが0)、subprocess.CalledProcessErrorが発生しなかった場合、このテストは失敗します。

You

原因が特定できた(コマンドライン引数やオプションのチェックが甘かった)ので修正いたしました。

次のコードのレビューをお願いします。

    if args.in_place or args.overwrite:
        if args.input_file == "-":
            raise argparse.ArgumentError(None, "--in-place and --overwrite can't work with the standard input.")
        if args.output_file is not None:
            raise argparse.ArgumentError(None, "--in-place and --overwrite are mutually exclusive to --output-file")

ChatGPT

修正されたコードは、概ね良いと思いますが、いくつかの点に気をつけてください。

  1. args.in_place or args.overwrite という条件が True の場合にのみ、次のチェックが行われます。これは期待通りの動作です。
  2. args.input_file == "-" の場合、argparse.ArgumentError が投げられます。このエラーメッセージは明確であり、何が問題かユーザーに伝わりやすいです。
  3. args.output_file is not None の場合、--in-place または --overwrite が --output-file と排他的であるということを示すエラーが投げられます。

ただし、argparse.ArgumentError は一般的には argparse ライブラリ内で使用されるものであり、直接使われることは少ないです。代わりに、raise ValueErrorsys.exit() を用いて、エラーメッセージを表示する方が一般的です。

例:

if args.in_place or args.overwrite:
    if args.input_file == "-":
        sys.exit("--in-place and --overwrite can't work with the standard input.")
    if args.output_file is not None:
        sys.exit("--in-place and --overwrite are mutually exclusive to --output-file")

または、

if args.in_place or args.overwrite:
    if args.input_file == "-":
        raise ValueError("--in-place and --overwrite can't work with the standard input.")
    if args.output_file is not None:
        raise ValueError("--in-place and --overwrite are mutually exclusive to --output-file")

このような手法が、エラー発生時により明確なメッセージをユーザーに提供します。

You

ありがとうございます。 追加のテストケースとして、次をお願いします。

  • iso-2022-jpやeuc-jpからutf-8に変換するもの
  • utf-16からutf-8に変換するもの

ChatGPT

テストケースを追加するのは素晴らしい考えです。特に異なるエンコーディング間の変換を扱う場合、このようなテストは非常に有用です。以下にテストケースの例を示します:

  1. iso-2022-jp から utf-8 への変換のテスト:
def test_iso2022jp_to_utf8(self):
    input_file_path = os.path.join(self.temp_dir, "input_iso2022jp.txt")
    output_file_path = os.path.join(self.temp_dir, "output_utf8.txt")
    expected_output_path = os.path.join(self.temp_dir, "expected_utf8.txt")

    self.create_file(input_file_path, SAMPLE_TEXT, encoding="iso-2022-jp")
    self.create_file(expected_output_path, SAMPLE_TEXT, encoding="utf-8")

    subprocess.check_call([sys.executable, "your_script.py", "-i", "iso-2022-jp", "--utf", "utf-8", input_file_path, "-o", output_file_path])

    self.assert_file_content_equals(output_file_path, expected_output_path)
  1. euc-jp から utf-8 への変換のテスト:
def test_eucjp_to_utf8(self):
    input_file_path = os.path.join(self.temp_dir, "input_eucjp.txt")
    output_file_path = os.path.join(self.temp_dir, "output_utf8.txt")
    expected_output_path = os.path.join(self.temp_dir, "expected_utf8.txt")

    self.create_file(input_file_path, SAMPLE_TEXT, encoding="euc-jp")
    self.create_file(expected_output_path, SAMPLE_TEXT, encoding="utf-8")

    subprocess.check_call([sys.executable, "your_script.py", "-i", "euc-jp", "--utf", "utf-8", input_file_path, "-o", output_file_path])

    self.assert_file_content_equals(output_file_path, expected_output_path)
  1. utf-16 から utf-8 への変換のテスト:
def test_utf16_to_utf8(self):
    input_file_path = os.path.join(self.temp_dir, "input_utf16.txt")
    output_file_path = os.path.join(self.temp_dir, "output_utf8.txt")
    expected_output_path = os.path.join(self.temp_dir, "expected_utf8.txt")

    self.create_file(input_file_path, SAMPLE_TEXT, encoding="utf-16")
    self.create_file(expected_output_path, SAMPLE_TEXT, encoding="utf-8")

    subprocess.check_call([sys.executable, "your_script.py", "-i", "utf-16", "--utf", "utf-8", input_file_path, "-o", output_file_path])

    self.assert_file_content_equals(output_file_path, expected_output_path)

上記のテストケースでは、your_script.py はテスト対象のスクリプトの名前、SAMPLE_TEXT は「Hello, world. 日本語と英語が混じったサンプルテキストです。\n2行目です。」といった具体的なサンプルテキストです。

これらのテストケースをテストスイートに追加すれば、変換が正確に行われているかを確認できます。また、assert_file_content_equals は、2つのファイルの内容が等しいことを確認するためのカスタムメソッドです。このメソッドの具体的な実装は、テストフレームワークやプロジェクトの要件に応じて異なる可能性があります。

You

次のオプションの意味はわかりますか

    parser.add_argument("--errors", type=str, help="Replacement on conversion error. 'strict', 'ignore', or any text to be placed in (default: 'strict').")

ChatGPT

はい、この--errorsオプションは文字列変換エラーが発生したときの動作を指定するためのものです。Pythonのstr.encode()bytes.decode()メソッドでは、このようなエラーハンドリングのオプションを提供しています。以下の3つの主なオプションが一般的です:

  1. 'strict': 変換できない文字が存在する場合にエラーを投げます(デフォルト)。
  2. 'ignore': エラーを無視して変換できない文字をスキップします。
  3. 'replace': 変換できない文字を特定の文字(通常は '?')で置き換えます。

このオプションが提供されている場合、ユーザーは上記のいずれかを選択して、特定のエンコーディングエラーにどのように対処するかを指定できます。たとえば、次のようなコマンドが考えられます。

python your_script.py --errors=ignore

このコマンドを実行すると、エンコード/デコード中に不正な文字が見つかった場合、その文字は単に無視されます。このオプションは、エラー処理の柔軟性を高めるために有用です。

You

コマンドラインオプションとしては、strictignoreはencode(), decode()と同じにして、それ以外の文字列があった場合にはそれに置き換えるようにしたいと考えています。ヘルプメッセージの推敲をお願いします。

ChatGPT

コマンドラインオプションのヘルプメッセージを推敲するのは良いアイデアです。あなたの要件に基づいて、次のように考えられます:

"Specifies how to handle encoding and decoding errors. Options are 'strict' to raise an error, 'ignore' to skip invalid characters, or any other string to replace invalid characters with that string. Default is 'strict'."

このメッセージは次の点を明確にしています:

  1. strict と ignore のオプションが何をするのか。
  2. それ以外の文字列が指定された場合の挙動。
  3. デフォルトの動作は何か。

これでユーザーはこのオプションがどのように動作するのかをより明確に理解できるでしょう。

You

pythonのcodecsのreplace_errorsの使い方を教えてください。

ChatGPT

Python の codecs モジュールは、エンコードおよびデコードの際にエラーが発生した場合の処理をカスタマイズするためのいくつかのオプションを提供しています。主に encodedecode メソッドの errors 引数でこれらのオプションを指定することができます。

replace という値を errors 引数に指定すると、無効な文字は特定の「置換文字」に置き換えられます。たとえば、UTF-8 でエンコードできない文字がある場合、その文字は U+FFFD (REPLACEMENT CHARACTER)に置き換えられます。

以下に簡単な例を示します:

import codecs

# デコードの例
broken_text = b"Hello, world!\x80\x81"
decoded_text = codecs.decode(broken_text, "utf-8", errors="replace")
print(decoded_text)  # 出力: "Hello, world!��"

# エンコードの例
broken_text = "Hello, world!\u0080\u0081"
encoded_text = codecs.encode(broken_text, "ascii", errors="replace")
print(encoded_text)  # 出力: b"Hello, world!??"

この例では、無効なバイト \x80\x81 が UTF-8 でデコードできないため、replace オプションによってそれぞれ (U+FFFD)に置き換えられています。同様に、ASCII でエンコードできない文字 \u0080\u0081? に置き換えられています。

このように replace オプションは、エンコード/デコードエラーが発生した場合に有用な方法で処理を続行するために使われます。

You

勘違いしていました。codecsモジュールは変換エラーが起きたときに、指定した文字列に置き換える方法を提供していないのですね。

ChatGPT

はい、codecs モジュールは基本的には指定した文字列で置き換えるという機能は提供していません。一般的なエラーハンドリングの方法としては以下があります。

  • strict: エラーが発生したら例外を発生させる(デフォルト)。
  • ignore: エラーが発生した場合、その部分を無視する。
  • replace: エラーが発生した場合、代替文字(通常は U+FFFD)で置き換える。

もし特定の文字列に置き換えたい場面がある場合は、自前でエラーハンドリングを行う必要があります。例えば、文字列を一文字ずつエンコード(もしくはデコード)し、エラーが出たらその位置に特定の文字列を挿入するといった処理が考えられます。

このようなカスタムエラーハンドリングは少し手間がかかりますが、それが最も確実な方法です。

You

chardetのエンコーディング判定で、windows-1252にならないように細工をしている部分があるのですが、この部分のコメントのレビューをお願いします。

def detect_encoding(bin_content: bytes) -> str:
    # chardet's algorithm fall back to the ASCII encoding ("windows-1252", exactly) when text can be regarded ASII or multi-byte encodings such as Shift_JIS, so we need to redirect to to the fallback to Shift_JIS.
    result = chardet.detect(bin_content)
    if result is None or result["encoding"].lower() == "windows-1252":  # can't identify encoding
        try:
            bin_content.decode("shift_jis")
        except:
            return None
        return "shift_jis"  # fall back to shift_jis!!

    return result["encoding"].lower()

ChatGPT

コメントにはいくつかの誤字や不明瞭な表現があるので、それを修正するとより理解しやすくなるでしょう。

以下は改良されたコメントの一例です。

def detect_encoding(bin_content: bytes) -> str:
    # chardet's algorithm falls back to the "windows-1252" encoding when the text could be considered ASCII or a multi-byte encoding like Shift_JIS.
    # To address this issue, we manually fall back to Shift_JIS instead.
    result = chardet.detect(bin_content)
    if result is None or result["encoding"].lower() == "windows-1252":  # Couldn't identify encoding
        try:
            bin_content.decode("shift_jis")
        except:
            return None
        return "shift_jis"  # Manually fall back to Shift_JIS

    return result["encoding"].lower()

このコメントでは以下の点を明確にしています:

  • chardetがASCIIテキストやShift_JISのような多バイトエンコーディングに対してwindows-1252にフォールバックするという問題点。
  • この問題を解決するために、手動でShift_JISにフォールバックしている理由。

このようにすることで、このコードの目的と動作がより明確になります。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment