質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
Oracle

Oracleは、米オラクルが取り扱うリレーショナルデータベース管理システムです。メインフレームからPCまで、多様なプラットフォームに対応しています。

Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

Q&A

解決済

4回答

7820閲覧

文字列を指定バイト数で分割したい。

pbdev

総合スコア21

Oracle

Oracleは、米オラクルが取り扱うリレーショナルデータベース管理システムです。メインフレームからPCまで、多様なプラットフォームに対応しています。

Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

0グッド

0クリップ

投稿2021/11/04 04:55

長くなりますが、案をいただけるとすごく助かります。

[背景]
テーブルのレコードを削除する前に、別テーブルに削除対象のレコードのバックアップ残しておくようにしたいと考えています。
ただ、バックアップを取っておく対象テーブルが複数あり、すべてのバックアップを1つのテーブルで取る必要が出ていました。
現在下記のようにイメージしています。
●バックアップ対象テーブル

hoge1hoge2hoge3
aaaあああ日時

●バックアップ用テーブル

データ作成日時バックアップデータ1バックアップデータ2
2021/11/04aaa,あああ,日時

※バックアップデータ1に収まらなかった文字列を、バックアップデータ2に格納する。

[本題]
環境としては現在Javaで作業しているのですが、バックアップ対象テーブルのデータを
登録したい状態でString型の変数に詰めるところまではできたのですが、
バックアップデータ1で収まらない文字列を、どのようにバックアップデータ2に持たせればいいかわかりません。
案としては下記を提示しましたが、文字数が多いとかなりループを回さないといけないから却下と上司から言われました。

/** * 1.文字を1文字切り出す * 2.その文字のバイト数を数える * 3.「3.」で指定バイト数(4000)より多ければ、hoge2に詰める * 4.「1.」に戻る。 * 5.「2.」のバイト数と既に検査した文字の総バイト数を足して * 指定バイト数(4000)より小さければ文字をhoge1に追加。 */ String hoge = "aaa,あああ,日時"; StringBuffer hoge1 = new StringBuffer(); StringBuffer hoge2 = new StringBuffer(); int cnt = 0; for (int i = 0; i < hoge.length(); i++) { String tmpStr = hoge.substring(i, i + 1); byte[] b = tmpStr.getBytes("UTF-8"); if (cnt + b.length > 4000) { hoge2.append(tmpStr); } else { hoge1.append(tmpStr); cnt += b.length; } }

自分ではこれくらいしか思いつかないため、ほかにも案をいただきたいと思っています。
もちろんJavaでの実装でなくても、バックアップテーブルへのINSERT時に切り分けができるのであれば
DMLでの案でも大丈夫です。
環境はOracleを使用しています。

お手数ですが、どなたか力を貸してください。。。
説明に不足等あれば、別途説明いたします。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

KOZ6.0

2021/11/04 05:49

上司はあなたに悩んでほしいんだと思いますのでヒントだけ。 hoge1 に入る文字数が分かったら、それ以外は hoge2 に入りますから、1文字ずつ分解する必要はありません。
Luice

2021/11/04 06:01

通常あまり見ないバックアップ手法ですね… 正直あまり良いバックアップ方法ではないと思います。 提示された要件とは別の方向性の回答なので、追記欄に記載します。 - 物理削除ではなく論理削除にする - ログに出力する - バックアップデータ1のサイズを十分に確保する - lob型(clob)のカラムにする - table毎にバックアップテーブルを用意してトリガーで操作する あたりが今まで見てきた実装です。
pbdev

2021/11/04 06:04

おっしゃる通り、指定文字数で分割するのであれば1文字ずつ分解する必要はなく、substring実現可能です。 今回は分割した文字列をさらにテーブルへINSERTするため、バイト単位で考えています。 文字数で分割してしまうとINSERT時に1カラムに収まりきらないバイト数である場合、エラーが発生してしまうためです。
Luice

2021/11/04 06:04

ちなみにbyte単位で分割とありますが、文字の途中でぶった切る事もありえます。 そうするとカラム単位で参照した時に文字化けする事になりますが、 それについては問題無いのでしょうか?
KOZ6.0

2021/11/04 06:12

hoge1 に入る文字数を求めるために、1文字ずつ分解していくところはいいんですよ。 hoge2 は必要ないですよね?
pbdev

2021/11/04 06:13

Luiceさん アドバイスありがとうございます。 提示いただいた内容でしか私も実装したことがなく、今の案件について少し戸惑っています。 >ちなみにbyte単位で分割とありますが、文字の途中でぶった切る事もありえます。 おっしゃる通り途中でぶった切る可能性があるため、私の出した案では1バイトでも指定したバイト数を超えるようであれば文字単位でバックアップデータ2に入れるようにしています。 今考えているのは1カラムの情報量はそれほど多くないので、カラム単位でループしてやれば繰り返す回数も少なくて済むのかなと考えています。。。
Luice

2021/11/04 06:18

バックアップデータ2が溢れた場合はどうするんでしょうか?
pbdev

2021/11/04 06:19

KOZ6.0さん 申し訳ありません。 勘違いしていました。 おっしゃる通りで、hoge1に入れる文字数が決まればそこからhoge - hoge1で残りのhoge2に入れる文字列が決まりますね。
pbdev

2021/11/04 06:21

Luiceさん 現状だとバックアップデータ3のカラムを用意して。。。 とかなり不細工な状態になると思います。
pbdev

2021/11/04 07:01

jimbeさん 拝借して一部変更しただけですねw
guest

回答4

0

質問のコードは間違っています。
20バイトの "aaa,あああ,日時" を 12バイトまでで分割すると
10バイトの "aaa,ああ" と 10バイトの "あ,日時" になるべきなのに、
11バイトの "aaa,ああ," と 9バイトの "あ日時" になってしまいます。

さて、次のコードはいかがでしょうか?

java

1class Main { 2 public static void main(String[] args) throws Exception { 3 String hoge = "aaa,あああ,日時"; 4 for (int i = 0; i < 22; i++) { 5 String[] ss = split(hoge, i); 6 System.out.println(i + " [" + ss[0] + "] [" + ss[1] + "]"); 7 } 8 } 9 10 // 文字列 s を kバイトまでの文字列と、それ以降の文字列に分割する 11 static String[] split(String s, int k) { 12 byte[] b = s.getBytes(); 13 String[] s2 = { s, "" }; 14 if (b.length > k) { 15 while ((b[k] & 0xc0) == 0x80) k--; 16 s2[0] = new String(b, 0, k); 17 s2[1] = s.substring(s2[0].length()); 18 } 19 return s2; 20 } 21}

実行結果

text

10 [] [aaa,あああ,日時] 21 [a] [aa,あああ,日時] 32 [aa] [a,あああ,日時] 43 [aaa] [,あああ,日時] 54 [aaa,] [あああ,日時] 65 [aaa,] [あああ,日時] 76 [aaa,] [あああ,日時] 87 [aaa,あ] [ああ,日時] 98 [aaa,あ] [ああ,日時] 109 [aaa,あ] [ああ,日時] 1110 [aaa,ああ] [あ,日時] 1211 [aaa,ああ] [あ,日時] 1312 [aaa,ああ] [あ,日時] 1413 [aaa,あああ] [,日時] 1514 [aaa,あああ,] [日時] 1615 [aaa,あああ,] [日時] 1716 [aaa,あああ,] [日時] 1817 [aaa,あああ,日] [時] 1918 [aaa,あああ,日] [時] 2019 [aaa,あああ,日] [時] 2120 [aaa,あああ,日時] [] 2221 [aaa,あああ,日時] []

split は指定のバイトで分割した時、後半の文字列が先頭バイトから
始まっていない場合、1バイトずつ戻って先頭バイトを探していきます。
ループは数回で済みます。

投稿2021/11/04 11:04

kazuma-s

総合スコア8224

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

ベストアンサー

つらつら考えまして、java でやるとすればどこかでループは避けられないと思いますが、上司はその点はどう考えているのでしょう。

例えば UTF8 では

java

1int getUTF8Length(byte b) throws UnsupportedEncodingException { 2 if((b & 0x80) == 0) return 1; 3 if((b & 0xe0) == 0xc0) return 2; 4 if((b & 0xf0) == 0xe0) return 3; 5 if((b & 0xf8) == 0xf0) return 4; 6 throw new UnsupportedEncodingException(); 7}

といった感じで先頭のバイト値でその文字の必要バイト数が出せます。
これを用いて文字数とバイト数を加算していけば、限界バイト数を超えた時点での文字数を得たりできると思いますが、ループなんですよね ^^;


ループはしますが内部で文字列編集(や getBytes() 等)はしないという方向で、 substring に使える値を返すメソッドにしてみました。

テスト文字列は shiketa さんの回答から頂きました。

java

1package teratail_java.q367700; 2 3import java.io.UnsupportedEncodingException; 4 5public class Q367700 { 6 public static void main(String[] args) throws UnsupportedEncodingException { 7 final String data = "あいaうg\uD867\uDE3Dえhおjかkきlくlけこhdさjしuすuせそ"; 8 System.out.println("'"+data+"'"); 9 10 for(int i=0; i<20; i++) { 11 int v = countChar(data, i, 0); 12 System.out.print("i="+i+", v="+v); 13 String s = data.substring(0, v); 14 System.out.println(": '"+s+"'="+s.getBytes("UTF8").length); 15 } 16 17 System.out.println("------------"); 18 19 for(int start=0, v=0; (v=countChar(data,13,start)) > 0; start+=v) { 20 System.out.print("v="+v); 21 String s = data.substring(start, start+v); 22 System.out.println(": '"+s+"'="+s.getBytes("UTF8").length); 23 } 24 } 25 26 //UTF8 にした時に nbyte に収まる文字数を返す. 27 private static int countChar(String src, int nbyte, int start) throws UnsupportedEncodingException { 28 int last = start; 29 for(int i=0; last<src.length(); ) { 30 char c = src.charAt(last); 31 i += getUTF8Length(c); 32 if(i > nbyte) break; 33 last += getUTF16Length(c); 34 } 35 return last - start; 36 } 37 38 private static int getUTF16Length(char c) throws UnsupportedEncodingException { 39 int v = c & 0xffff; 40 if(isHighSurrogate(v)) return 2; 41 if(isLowSurrogate(v)) throw new UnsupportedEncodingException(); 42 return 1; 43 } 44 45 private static int getUTF8Length(char c) throws UnsupportedEncodingException { 46 int v = c & 0xffff; 47 if(isHighSurrogate(v)) return 4; 48 if(isLowSurrogate(v)) throw new UnsupportedEncodingException(); 49 if(v < 128) return 1; 50 if(v < 2048) return 2; 51 return 3; 52 } 53 54 private static boolean isHighSurrogate(int c) { 55 return 0xd800 <= c && c < 0xdc00; 56 } 57 58 private static boolean isLowSurrogate(int c) { 59 return 0xdc00 <= c && c < 0xe000; 60 } 61}

plain

1'あいaうg????えhおjかkきlくlけこhdさjしuすuせそ' 2i=0, v=0: ''=0 3i=1, v=0: ''=0 4i=2, v=0: ''=0 5i=3, v=1: 'あ'=3 6i=4, v=1: 'あ'=3 7i=5, v=1: 'あ'=3 8i=6, v=2: 'あい'=6 9i=7, v=3: 'あいa'=7 10i=8, v=3: 'あいa'=7 11i=9, v=3: 'あいa'=7 12i=10, v=4: 'あいaう'=10 13i=11, v=5: 'あいaうg'=11 14i=12, v=5: 'あいaうg'=11 15i=13, v=5: 'あいaうg'=11 16i=14, v=5: 'あいaうg'=11 17i=15, v=7: 'あいaうg????'=15 18i=16, v=7: 'あいaうg????'=15 19i=17, v=7: 'あいaうg????'=15 20i=18, v=8: 'あいaうg????え'=18 21i=19, v=9: 'あいaうg????えh'=19 22------------ 23v=5: 'あいaうg'=11 24v=6: '????えhおj'=12 25v=6: 'かkきlくl'=12 26v=6: 'けこhdさj'=12 27v=5: 'しuすuせ'=11 28v=1: 'そ'=3

投稿2021/11/04 07:41

編集2021/11/04 14:44
jimbe

総合スコア13209

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

pbdev

2021/11/04 07:52

おそらく上司も特に案はなく、ただループの処理が気に食わなかっただけだと思います。。。 (世知辛い世の中ですね) 貴重な案をいただきありがとうございます! 含めて再度検討してみようと思います。
kazuma-s

2021/11/04 14:24

(0xd800 <= v || v < 0xdc00) は (0xd800 <= v && v < 0xdc00) の間違いではありませんか? 2か所あります。
jimbe

2021/11/04 14:33

・・・その通りです、ありがとうございます orz
guest

0

サロゲート文字とか考えるとうんざりなので、codePoints()をつかいますかねぇ。

java

1import java.nio.ByteBuffer; 2import java.nio.charset.StandardCharsets; 3 4public class Hokke{ 5 public static void main(final String[] args) throws Exception { 6 final String data = "あいaうg\uD867\uDE3Dえhおjかkきlくlけこhdさjしuすuせそ"; 7 8 final ByteBuffer buf = ByteBuffer.allocate(13); 9 10 data.codePoints().forEach(cp -> { 11 final String ch = new String(new int[] { cp }, 0, 1); 12 final byte[] bytes = ch.getBytes(StandardCharsets.UTF_8); 13 14 if (buf.remaining() < bytes.length) { 15 final String backup = new String(buf.array(), 0, buf.position()); 16 System.out.println(String.format("backup:<%s> pos:%d", backup, buf.position())); 17 buf.clear(); 18 } 19 20 buf.put(bytes); 21 // System.out.println(String.format("pos:%d, rem:%d", buf.position(), buf.remaining())); 22 }); 23 24 final String backup = new String(buf.array(), 0, buf.position()); 25 System.out.println(String.format("backup:<%s> pos:%d", backup, buf.position())); 26 } 27} 28 29//backup:<あいaうg> pos:11 30//backup:<????えhおj> pos:12 31//backup:<かkきlくl> pos:12 32//backup:<けこhdさj> pos:12 33//backup:<しuすuせ> pos:11 34//backup:<そ> pos:3

投稿2021/11/04 06:48

shiketa

総合スコア4061

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

pbdev

2021/11/04 07:53

貴重な案をいただきありがとうございます! いただいた案含めて検討したいと思います。
guest

0

byte配列を区切るなら、ArrayUtilsを使うのがベターでしょう。

see : https://commons.apache.org/proper/commons-lang/javadocs/api-release/org/apache/commons/lang3/ArrayUtils.html#subarray-byte:A-int-int-

バックアップ用テーブルの定義はわかりませんが、
先に指摘した通り、全カラムが溢れた場合のガード処理は入れておく必要があると思います。

投稿2021/11/04 06:41

Luice

総合スコア771

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

jimbe

2021/11/04 06:57

このメソッドは文字の境目は意識しないのではないでしょうか。
Luice

2021/11/04 07:02

文字列考慮ならこれ駄目ですね
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問