はじめに
バイナリデータから日本語メッセージだけを抽出したいと思いました。
バイナリエディタを使えば簡単に出来そうですが、しっくりくるものを持っていなかったのでデータを見るための機能だけを作ってみました。
環境
Windows 11 Pro
Python 3.12.0 (tags/v3.12.0:0fb18b0, Oct 2 2023, 13:03:39) [MSC v.1935 64 bit (AMD64)]
コード
import argparse
import codecs
import math
# import pdb
import re
from pathlib import Path
import Decorator as D
import Utility as U
NoBreakSpace = "\u00a0"
ReplacementCharacter = "\ufffd" # �
def printHexList(l):
print(len(l))
print([f"{x:02X}" for x in l])
class Binary:
Separator = " | "
SeparatorNoBreakSpace = " |" + NoBreakSpace # 結合させない為にNoBreakSpaceにする
ReplacementCode = " "
Replacement = "__"
def __init__(self, inputPath, outputPath, encoding="utf-8", outputLength=16, replChar=" ", *, outputVertical=False):
self.inputPath = Path(inputPath)
self.outputPath = Path(outputPath)
self.encoding = encoding
self.outputLength = outputLength # 表示幅 Byte
self.errorPosition = set()
self.rawData = b"" # バイナリデータ
self.lineNumber = -1 # バイナリデータを表示するのに必要な行数
self.size = -1
self.offsetTemplate = ""
self.string = [] # 文字データ
self.codePoint = [] # Unicode code point
self.replChar = replChar # デコード出来ない場合に置き換える文字
self.outputVertical = outputVertical # 垂直方向に並べて出力するかどうか
codecs.register_error("customReplace", self.customErrorHandler)
self.checkEncoding()
# encode/decode時にエラーが発生した場合、その場所を記録する
def customErrorHandler(self, e):
self.errorPosition.update(range(e.start, e.end))
return ((e.end - e.start) * self.replChar, e.end)
# Bomが付くと動作がおかしくなるので変更する
def checkEncoding(self):
utf16 = re.compile("utf[ _-]?16$", re.IGNORECASE)
if utf16.match(self.encoding) is not None:
self.encoding = "utf_16le"
utf32 = re.compile("utf[ _-]?32$", re.IGNORECASE)
if utf32.match(self.encoding) is not None:
self.encoding = "utf_32le"
print(f"Encoding = {self.encoding}")
# データをメモリに読み込むが、処理に必要なだけ読み込むようにした方が良いかも
def read(self):
with self.inputPath.open("rb") as file:
self.rawData = file.read()
self.size = len(self.rawData)
self.lineNumber = math.ceil(self.size / self.outputLength)
offsetLength = len(f"{self.size:X}")
self.offsetTemplate = f"{{:{offsetLength}X}}{{:2}}{Binary.Separator}"
def getRawData(self, line):
start = self.outputLength * line
end = start + self.outputLength
return self.rawData[start:end]
# Bom付きのencodingの場合、バイト数が正しく取得できない
def getEncodeLength(self, data):
l = len(data.encode(self.encoding, errors="ignore")) # decodeが成功したものをencodeするのでignoreで良い
return l if l > 0 else 1
# encodeした結果、3batesならば"あ____"のような文字列を作成する
def getCharacter(self, c, length):
string = [U.replaceControl(c, self.Replacement)]
string += [self.Replacement for _ in range(length - 1)]
return string
def getCodePoint(self, c, length):
code = [f"{ord(c):X}"]
code += [self.ReplacementCode for _ in range(length - 1)]
return code
def getData(self, s):
string = []
code = []
i = 0
length = 0
for c in s:
if i in self.errorPosition: # decode出来なかった文字の場合、そこを別の文字で置き換える
string += [self.replChar]
code += [self.ReplacementCode]
i += 1
else:
length = self.getEncodeLength(c)
string += self.getCharacter(c, length)
code += self.getCodePoint(c, length)
i += length
return string, code
# 1列のデータを行毎に折り返す。
def wrapData(self, data):
ret = []
for i in range(self.lineNumber):
start = i * self.outputLength
end = start + self.outputLength
ret.append(data[start:end])
return ret
def getString(self):
string = self.rawData.decode(self.encoding, errors="customReplace")
s, c = self.getData(string)
self.string = self.wrapData(s)
self.codePoint = self.wrapData(c)
def getTitleHorizontal(self, *, outputString=True, outputCodePoint=True):
offset = len(self.getOffset(0)) * " "
addList = [f"{i:02X}" for i in range(self.outputLength)]
address = " ".join(addList)
ret = offset + address
if outputCodePoint:
ret += Binary.Separator + " ".join(addList) + " "
if outputString:
ret += Binary.Separator + address
ret += "\n\n"
return ret
def getTitleVertical(self, *, outputCodePoint=True):
offset = len(self.getOffset(0)) * " "
addList = [f"{i:02X}" for i in range(self.outputLength)]
address = " ".join(addList)
ret = offset + address
if outputCodePoint:
ret = offset + " ".join(addList) + " "
ret += "\n\n"
return ret
def getOffset(self, line, t=""):
return self.offsetTemplate.format(line, t)
def getRawString(self, line, *, fill=True):
string = self.getRawData(line).hex().upper()
if fill and len(string) < self.outputLength * 2:
string += " " * (self.outputLength * 2 - len(string))
return string
def outputStringLine(self, line, *, vertical=False):
string = ""
sep = NoBreakSpace
if vertical:
sep = NoBreakSpace * 5
for s in self.string[line]:
string += s + sep
# East Asian Widthは、結局はFontが対応しているかに係っているので信用できない
if s != self.Replacement and U.getEaWidth(s) == 1:
string += NoBreakSpace
return string
def getCodePointString(self, line):
return " ".join([f"{x:6}" for x in self.codePoint[line]])
def outputCodePointLine(self, line, *, fill=True):
if not fill or line != self.lineNumber - 1:
return self.getCodePointString(line)
diff = self.outputLength - len(self.codePoint[line])
tail = " " * 7 * diff
return self.getCodePointString(line) + tail
def outputLineHorizontal(self, line, *, outputString=True, outputCodePoint=True):
offset = self.getOffset(line * self.outputLength)
rawString = self.getRawString(line)
output = offset + U.insertSeparator(rawString, 2, " ")
if outputCodePoint:
output += Binary.Separator
output += self.outputCodePointLine(line)
if outputString:
output += Binary.SeparatorNoBreakSpace
output += self.outputStringLine(line)
output += "\n"
return output
def outputLineVertical(self, line, *, outputString=True, outputCodePoint=True):
offsetR = self.getOffset(line * self.outputLength, " B")
rawString = self.getRawString(line, fill=False)
output = offsetR + U.insertSeparator(rawString, 2, " " * 5) + "\n"
if outputCodePoint:
offsetC = self.getOffset(line * self.outputLength, " C")
output += offsetC + self.outputCodePointLine(line, fill=False) + "\n"
if outputString:
offsetD = self.getOffset(line * self.outputLength, " S")
output += offsetD + self.outputStringLine(line, vertical=True) + "\n"
output += "\n"
return output
def writeHorizontal(self, file):
file.write(self.getTitleHorizontal())
for i in range(self.lineNumber):
file.write(self.outputLineHorizontal(i))
def writeVertical(self, file):
file.write(self.getTitleVertical())
for i in range(self.lineNumber):
file.write(self.outputLineVertical(i))
def write(self):
with self.outputPath.open("w", encoding="utf-8") as file:
if self.outputVertical:
self.writeVertical(file)
else:
self.writeHorizontal(file)
def perform(self):
self.read()
self.getString()
self.write()
def argumentParser():
parser = argparse.ArgumentParser()
parser.add_argument("inputPath", help="input path")
parser.add_argument("-o", "--outputPath", default="output.txt", help="output path")
parser.add_argument("-e", "--encoding", default="utf-8", help="encodeing")
parser.add_argument("-l", "--length", type=int, default=16, help="length")
parser.add_argument("-v", "--outputVertical", action="store_true", help="output vertical")
parser.add_argument("-a", "--showArgument", action="store_true", help="show arguments.")
return parser.parse_args()
if __name__ == "__main__":
args = argumentParser()
if args.showArgument:
print(args)
binary = Binary(
inputPath=args.inputPath,
outputPath=args.outputPath,
encoding=args.encoding,
outputLength=args.length,
outputVertical=args.outputVertical,
)
binary.perform()
ControlCharacters = {
"\u0000": "\u2400", # NUL
"\u0001": "\u2401", # SOH
"\u0002": "\u2402", # STX
"\u0003": "\u2403", # ETX
"\u0004": "\u2404", # EOT
"\u0005": "\u2405", # ENQ
"\u0006": "\u2406", # ACK
"\u0007": "\u2407", # BEL
"\u0008": "\u2408", # BS
"\u0009": "\u2409", # HT
"\u000a": "\u240a", # LF
"\u000b": "\u240b", # VT
"\u000c": "\u240c", # FF
"\u000d": "\u240d", # CR
"\u000e": "\u240e", # SO
"\u000f": "\u240f", # SI
"\u0010": "\u2410", # DLE
"\u0011": "\u2411", # DC1
"\u0012": "\u2412", # DC2
"\u0013": "\u2413", # DC3
"\u0014": "\u2414", # DC4
"\u0015": "\u2415", # NAK
"\u0016": "\u2416", # SYN
"\u0017": "\u2417", # ETB
"\u0018": "\u2418", # CAN
"\u0019": "\u2419", # EOM
"\u001a": "\u241a", # SUB
"\u001b": "\u241b", # ESC
"\u001c": "\u241c", # FS
"\u001d": "\u241d", # GS
"\u001e": "\u241e", # RS
"\u001f": "\u241f", # US
# "\u0020": "\u2420", # SP
"\u007f": "\u2421", # DEL
}
def replaceControl(c, repl="_"):
if unicodedata.category(c) == "Cc":
return ControlCharacters.get(c, repl)
return c
def replaceControls(string, repl="_"):
replace = functools.partial(replaceControl, repl=repl)
return "".join(list(map(replace, string)))
# east_asian_width フォントが対応しているとは限らない
def getEaWidth(c):
if unicodedata.east_asian_width(c) in {"W", "F", "A"}:
return 2
# {"N", "Na", "H"}:
return 1
def reversedStr(string):
return string[::-1]
def insertSeparator(data, digit=3, sep=","):
pat = f"(.{{{digit}}}(?=.))"
repl = f"\\1{sep}"
return reversedStr(re.sub(pat, repl, reversedStr(data)))
実行結果
解説を後回しにして、どのような実行結果になるか示してみます。
そのために、以下で、簡単なテストデータを作成します。
import pickle
from pathlib import Path
test = {
"func1": ord,
"1": 1,
"2": 2,
"cat": "私は、猫が大好きです。",
"dog": "I love dog.",
"func2": chr,
}
path = Path("test.pkl")
with path.open("bw") as file:
pickle.dump(test, file)
出来上がったデータを読み込ませて実行すると、
py binary.py test.pkl -o out1h.txt
以下のように、バイナリデータ・コードポイント・デコードデータと横に並んでテキストファイルへ出力されます。
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
0 | 80 04 95 7C 00 00 00 00 00 00 00 7D 94 28 8C 05 | 4 7C 0 0 0 0 0 0 0 7D 28 5 | ␄ | ␀ ␀ ␀ ␀ ␀ ␀ ␀ } ( ␅
10 | 66 75 6E 63 31 94 8C 08 62 75 69 6C 74 69 6E 73 | 66 75 6E 63 31 8 62 75 69 6C 74 69 6E 73 | f u n c 1 ␈ b u i l t i n s
20 | 94 8C 03 6F 72 64 94 93 94 8C 01 31 94 4B 01 8C | 3 6F 72 64 1 31 4B 1 | ␃ o r d ␁ 1 K ␁
30 | 01 32 94 4B 02 8C 03 63 61 74 94 8C 21 E7 A7 81 | 1 32 4B 2 3 63 61 74 21 79C1 | ␁ 2 K ␂ ␃ c a t ! 私 __ __
40 | E3 81 AF E3 80 81 E7 8C AB E3 81 8C E5 A4 A7 E5 | 306F 3001 732B 304C 5927 597D | は __ __ 、 __ __ 猫 __ __ が __ __ 大 __ __ 好
50 | A5 BD E3 81 8D E3 81 A7 E3 81 99 E3 80 82 94 8C | 304D 3067 3059 3002 | __ __ き __ __ で __ __ す __ __ 。 __ __
60 | 03 64 6F 67 94 8C 0B 49 20 6C 6F 76 65 20 64 6F | 3 64 6F 67 B 49 20 6C 6F 76 65 20 64 6F | ␃ d o g ␋ I l o v e d o
70 | 67 2E 94 8C 05 66 75 6E 63 32 94 68 02 8C 03 63 | 67 2E 5 66 75 6E 63 32 68 2 3 63 | g . ␅ f u n c 2 h ␂ ␃ c
80 | 68 72 94 93 94 75 2E | 68 72 75 2E | h r u .
実行時オプションを以下のようにして実行すると、
py binary.py test.pkl -v -o out1v.txt
バイナリデータ・コードポイント・デコードデータが縦に並んでテキストファイルへ出力されます。
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
0 B | 80 04 95 7C 00 00 00 00 00 00 00 7D 94 28 8C 05
0 C | 4 7C 0 0 0 0 0 0 0 7D 28 5
0 S | ␄ | ␀ ␀ ␀ ␀ ␀ ␀ ␀ } ( ␅
10 B | 66 75 6E 63 31 94 8C 08 62 75 69 6C 74 69 6E 73
10 C | 66 75 6E 63 31 8 62 75 69 6C 74 69 6E 73
10 S | f u n c 1 ␈ b u i l t i n s
20 B | 94 8C 03 6F 72 64 94 93 94 8C 01 31 94 4B 01 8C
20 C | 3 6F 72 64 1 31 4B 1
20 S | ␃ o r d ␁ 1 K ␁
30 B | 01 32 94 4B 02 8C 03 63 61 74 94 8C 21 E7 A7 81
30 C | 1 32 4B 2 3 63 61 74 21 79C1
30 S | ␁ 2 K ␂ ␃ c a t ! 私 __ __
40 B | E3 81 AF E3 80 81 E7 8C AB E3 81 8C E5 A4 A7 E5
40 C | 306F 3001 732B 304C 5927 597D
40 S | は __ __ 、 __ __ 猫 __ __ が __ __ 大 __ __ 好
50 B | A5 BD E3 81 8D E3 81 A7 E3 81 99 E3 80 82 94 8C
50 C | 304D 3067 3059 3002
50 S | __ __ き __ __ で __ __ す __ __ 。 __ __
60 B | 03 64 6F 67 94 8C 0B 49 20 6C 6F 76 65 20 64 6F
60 C | 3 64 6F 67 B 49 20 6C 6F 76 65 20 64 6F
60 S | ␃ d o g ␋ I l o v e d o
70 B | 67 2E 94 8C 05 66 75 6E 63 32 94 68 02 8C 03 63
70 C | 67 2E 5 66 75 6E 63 32 68 2 3 63
70 S | g . ␅ f u n c 2 h ␂ ␃ c
80 B | 68 72 94 93 94 75 2E
80 C | 68 72 75 2E
80 S | h r u .
解説
データの読み込み
self.read()
でファイルからバイナリデータを読み込みます。
データをprint()
で表示すると以下になります。
b'\x80\x04\x95|\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x05func1\x94\x8c\x08builtins\x94\x8c\x03ord\x94\x93\x94\x8c\x011\x94K\x01\x8c\x012\x94K\x02\x8c\x03cat\x94\x8c!\xe7\xa7\x81\xe3\x81\xaf\xe3\x80\x81\xe7\x8c\xab\xe3\x81\x8c\xe5\xa4\xa7\xe5\xa5\xbd\xe3\x81\x8d\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\x94\x8c\x03dog\x94\x8c\x0bI love dog.\x94\x8c\x05func2\x94h\x02\x8c\x03chr\x94\x93\x94u.'
16進数で表示すると以下になります。
80 04 95 7C 00 00 00 00 00 00 00 7D 94 28 8C 05 66 75 6E 63 31 94 8C 08 62 75 69 6C 74 69 6E 73 94 8C 03 6F 72 64 94 93 94 8C 01 31 94 4B 01 8C 01 32 94 4B 02 8C 03 63 61 74 94 8C 21 E7 A7 81 E3 81 AF E3 80 81 E7 8C AB E3 81 8C E5 A4 A7 E5 A5 BD E3 81 8D E3 81 A7 E3 81 99 E3 80 82 94 8C 03 64 6F 67 94 8C 0B 49 20 6C 6F 76 65 20 64 6F 67 2E 94 8C 05 66 75 6E 63 32 94 68 02 8C 03 63 68 72 94 93 94 75 2E
バイナリデータは、容量が大きいものも多くメモリに全て読み込むのは避けたいところです。
必要な範囲だけ読み込むことも検討しましたが、デコードするときにデータが区切られた場所によっては、正しくデコードされない事があるので先頭から全て読み込んでいます。
例えば、b'\xe7\x8c\xab'
を読み込むと"猫"にデコードされます。
これがb'\x8c\xab'
のように区切られてしまうとデコードエラーになってしまいます。
データを全て保持する事に加えて、処理の過程でファイルの容量の数倍のデータを保持するので余りに大きなファイルを扱うのには向いていません。
デコード
次に、self.getSrgint()
でバイナリデータからコードポイントとデコードデータを作成します。
ここでは、まずはバイナリデータをデコードします。
デコードする時に、以下のようなカスタムエラーハンドラーを使用しています。
これは、
- 変換できなかった文字の位置を記録する
- 変換できなかった文字を指定した文字(空白)で置き換える
ために使用します。
codecs.register_error("customReplace", self.customErrorHandler)
def customErrorHandler(self, e):
self.errorPosition.update(range(e.start, e.end))
return ((e.end - e.start) * self.replChar, e.end)
string = self.rawData.decode(self.encoding, errors="customReplace")
デコードした文字列は、以下のようなものになります。
実際には、制御文字が含まれているので表示が崩れてしまいます。
|} ( func1 builtins ord 1 K 2 K cat !私は、猫が大好きです。 dog
I love dog. func2 h chr u.
デコード出来なかった文字は、空白に置き換わっています。
デコード文字列だけを見た場合、
- デコード結果で空白が得られた
- エラーで空白に置き換わった
が区別出来ません。
これを避けるためにカスタムエラーハンドラーでエラーが起こった場所(インデックス)を記録しています。
29
['00', '02', '82', '83', '84', '0C', '0E', '15', '16', '20', '21', '26', '27', '28', '29', '2C', '2F', '32', '35', '3A', '3B', '5E', '5F', '64', '65', '72', '73', '7A', '7D']
位置合わせ
self.getData(string)
でバイナリデータ・デコードデータ・コードポイントの位置合わせを行います。
これは、デコードデータを1文字ずつ読み込んで処理を行います。
説明のためにデコードデータをa 猫 b 犬
とします。
始めに、a
を取り出します。
a
は、エンコードデータが\x61
で1バイト、コードポイントがU+0061
となります。
エンコード時のバイト数は、非効率ですが、デコードデータをエンコードし直して求めています。
1バイトでかつデコードエラーも出ない文字なので、["a"]
と["62"]
を対応するリストにそのまま加算します。
そして、バイナリデータの位置を表すインデックスを1バイト分だけ進めます。
次の
が、デコードエラーによって置き換わった空白とすると、エラーの位置が記録されているので置き換え文字[" "]
と[""]
をそれぞれリストに保存します。
バイナリデータのインデックスを1バイト分だけ進めます。
猫
は、エンコードデータが\xE7\x8C\xAB
で3バイト、コードポイントがU+732B
となります。
2バイト以上の場合、["猫", "__", "__"]
、["732B", "", ""]
のような文字列をリストに保存します。
3バイトのデータなので、バイナリデータのインデックスを3バイト分だけ進めます。
上記のテストデータで対応付けを行った場合は、以下のようなデータが得られます。
9
0 : 16 : [' ', '␄', ' ', '|', '␀', '␀', '␀', '␀', '␀', '␀', '␀', '}', ' ', '(', ' ', '␅']
1 : 16 : ['f', 'u', 'n', 'c', '1', ' ', ' ', '␈', 'b', 'u', 'i', 'l', 't', 'i', 'n', 's']
2 : 16 : [' ', ' ', '␃', 'o', 'r', 'd', ' ', ' ', ' ', ' ', '␁', '1', ' ', 'K', '␁', ' ']
3 : 16 : ['␁', '2', ' ', 'K', '␂', ' ', '␃', 'c', 'a', 't', ' ', ' ', '!', '私', '__', '__']
4 : 16 : ['は', '__', '__', '、', '__', '__', '猫', '__', '__', 'が', '__', '__', '大', '__', '__', '好']
5 : 16 : ['__', '__', 'き', '__', '__', 'で', '__', '__', 'す', '__', '__', '。', '__', '__', ' ', ' ']
6 : 16 : ['␃', 'd', 'o', 'g', ' ', ' ', '␋', 'I', ' ', 'l', 'o', 'v', 'e', ' ', 'd', 'o']
7 : 16 : ['g', '.', ' ', ' ', '␅', 'f', 'u', 'n', 'c', '2', ' ', 'h', '␂', ' ', '␃', 'c']
8 : 7 : ['h', 'r', ' ', ' ', ' ', 'u', '.']
9
0 : 16 : ['', '4', '', '7C', '0', '0', '0', '0', '0', '0', '0', '7D', '', '28', '', '5']
1 : 16 : ['66', '75', '6E', '63', '31', '', '', '8', '62', '75', '69', '6C', '74', '69', '6E', '73']
2 : 16 : ['', '', '3', '6F', '72', '64', '', '', '', '', '1', '31', '', '4B', '1', '']
3 : 16 : ['1', '32', '', '4B', '2', '', '3', '63', '61', '74', '', '', '21', '79C1', '', '']
4 : 16 : ['306F', '', '', '3001', '', '', '732B', '', '', '304C', '', '', '5927', '', '', '597D']
5 : 16 : ['', '', '304D', '', '', '3067', '', '', '3059', '', '', '3002', '', '', '', '']
6 : 16 : ['3', '64', '6F', '67', '', '', 'B', '49', '20', '6C', '6F', '76', '65', '20', '64', '6F']
7 : 16 : ['67', '2E', '', '', '5', '66', '75', '6E', '63', '32', '', '68', '2', '', '3', '63']
8 : 7 : ['68', '72', '', '', '', '75', '2E']
ファイルへの書き込み
self.write()
でファイルへデータを書き込んでいます。
大部分は位置を合わせて書き込んでいるだけなので、解説の必要も無いかと思います。
ただ、位置合わせのために考慮すべき事が2点あるのでそれだけは解説します。
East Asian Width
表示位置を合わせるために、全角か半角かを判定する必要があります。
これの判定には、UnicodeのEast_Asian_Width 参考特性
を使用しています。
仕様上では、これを見れば全角か半角かが分かります。
しかし、実際の所、フォントがこの情報を守って作成されているかに係っていますのであまり参考になる情報ではありません。
def getEaWidth(c):
if unicodedata.east_asian_width(c) in {"W", "F", "A"}:
return 2
return 1
結合文字を結合させない
Unicodeには、先行する文字と組み合わせて字形を変化させる結合文字が存在します。
このような文字を単独で表示させたい場合は、先行する文字を"No-break space"(U+00A0
)や、"Zero width non-joiner"(U+200C
)とする事で結合させないように出来ます。
(もっとも、ちょっとうろ覚えですが、それでも結合してしまう文字があった気もしますが。)
デコードデータに関しては、文字の間を"No-break space"としています。
def outputStringLine(self, line, *, vertical=False):
string = ""
sep = NoBreakSpace
if vertical:
sep = NoBreakSpace * 5
for s in self.string[line]:
string += s + sep
if s != self.Replacement and U.getEaWidth(s) == 1:
string += NoBreakSpace
return string
エンコード指定の制限
デコードデータをエンコードし直してバイト数を求めている関係上、エンコード時にBom(Byte Order Mark)を付けるエンコーディングを指定すると想定通りに動きません。
そのため、簡単ですがエンコーディングを変更する処理を入れています。
def checkEncoding(self):
utf8 = re.compile("utf[ _-]?8[ _-]sig$", re.IGNORECASE)
if utf8.match(self.encoding) is not None:
self.encoding = "utf_8"
return
utf16 = re.compile("utf[ _-]?16$", re.IGNORECASE)
if utf16.match(self.encoding) is not None:
self.encoding = "utf_16le"
return
utf32 = re.compile("utf[ _-]?32$", re.IGNORECASE)
if utf32.match(self.encoding) is not None:
self.encoding = "utf_32le"
return
別解(と言うか元々の目的を達成したもの)
バイナリデータから日本語データだけを抽出したい場合は、以下のコードで十分です。
アルファベット等も抽出したい場合は、正規表現を工夫すれば対応できます。
本当に必要なデータだけを取り出すのは大変だとは思いますが。
import argparse
from pathlib import Path
import regex
def argumentParser():
parser = argparse.ArgumentParser()
parser.add_argument("inputPath", type=Path, help="input path")
parser.add_argument("-e", "--encoding", default="utf_8", help="encoding")
parser.add_argument("-o", "--outputPath", default="", help="output path")
parser.add_argument("-a", "--showArgument", action="store_true", help="show arguments.")
return parser.parse_args()
if __name__ == "__main__":
args = argumentParser()
if args.showArgument:
print(args)
output = args.outputPath
if args.outputPath is None:
output = Path(f"extract_{args.inputPath.stem}.txt")
data = b""
with args.inputPath.open("rb") as file:
data = file.read()
string = data.decode(args.encoding, "ignore")
string = regex.sub(
r"[^\p{scx=Han}\p{scx=Hiragana}\p{scx=Katakana}]",
"",
string,
)
with output.open("w", encoding="utf_8") as file:
file.write(string)
上記で示したテストデータに対して実行した結果は、以下になります。
私は、猫が大好きです。
フォントについて
余談ですが、使用しているフォントによっては、表示がずれてしまいます。
こればっかりはどうしようもありません。
なるべく対応する文字が多いフォントを見つけるか、表示幅が同じフォントを複数使用するなどするしかありません。
あまり多くのフォントを試していませんが、筆者は、使い勝手が良いのでMigu 1M
を使用しています。
自分の記事ですが、フォントの情報を得る方法も示しておきます。
横幅が1の文字があれば無理やり位置を合わせる事も出来そうです。
もっとも、そんなことをしても面倒なだけで実用的でないと思います。