DB Clob data 문자열 치환

이슈

DB에 저장되어있는 게시판 내용 중 img 태그 안에 있는 width, height 속성을 삭제하거나 변경해야 할 일이생겼다.

목표

  • inline width, height 속성은 삭제
  • style 안의 width, height 값은 100% 로 변경
1
2
3
4
5
6
7
//변경 전
<img src="..." width="200" height="300">
<img alt="..." style="width:200px; height:500px;">

//변경 후
<img src="...">
<img alt="..." style="width:100%; height:100%;">

분석

게시글이 천개가 넘게 등록되어있어서 내용을 하나하나 고치는 것은 매우 비효율적이다.
그래서 DB에서 데이터를 불러와 자바 정규식을 이용해 치환하여 UPDATE 하는 방법을 선택했다.
우선 변경할 테이블을 백업해준다.

1
create table board_bak as select * from board

업데이트 소스는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
public class ImgWidthHeightReplacer {
private Connection connection = null;
private PreparedStatement selectPs = null;
private PreparedStatement updatePs = null;
private static final String UPDATE_EMPTY = "UPDATE board SET contents = empty_clob() WHERE board_seq = ?";
private static final String SELECT_LOCK = "SELECT contents FROM board WHERE board_seq = ? for update";

public void replaceContents() throws SQLException, IOException {
String sql = "select board_seq, contents from board where use_html = 'Y'";

getConnection();
selectPs = connection.prepareStatement(sql);
ResultSet rs = selectPs.executeQuery();

while(rs.next()) {
String contents = rs.getString("contents");

if(contents != null) {
String reContents = contents.replaceAll("(<img[^>]*)(width=[\"]?[^>\"]+[\"]?)([^>]*>)", "$1$3")
.replaceAll("(<img[^>]*)(height=[\"]?[^>\"]+[\"]?)([^>]*>)", "$1$3")
.replaceAll("(<img[^>]*style=\"[^>]*width:)([a-zA-Z0-9]*;?)([^>]*\")", "$1100%;$3")
.replaceAll("(<img[^>]*style=\"[^>]*height:)([a-zA-Z0-9]*;?)([^>]*\")", "$1100%;$3");

//System.out.println(reContents);
updateContents(reContents, rs.getInt("board_seq"));
}
}

closeResultSet(rs);
closeSelectPs();
closeConneciton();
}

private void updateContents(String contents, int boardSeq) throws IOException {
ResultSet rs = null;

try {
connection.setAutoCommit(false);

updatePs = connection.prepareStatement(UPDATE_EMPTY);
updatePs.setInt(1, boardSeq);
if(updatePs.executeUpdate() > 0) {
closeUpdatePs();
updatePs = connection.prepareStatement(SELECT_LOCK);
updatePs.setInt(1, boardSeq);

rs = updatePs.executeQuery();
while(rs.next()) {
CLOB cl = ((OracleResultSet)rs).getCLOB("contents");
BufferedWriter writer = new BufferedWriter(cl.getCharacterOutputStream());
writer.write(contents);
writer.close();
}
connection.commit();
}
} catch (SQLException e) {
e.printStackTrace();
try { connection.rollback(); } catch (SQLException e1) { System.out.println("rollback fail"); }
} finally {
try { connection.setAutoCommit(true); } catch (SQLException e) {}
closeResultSet(rs);
closeUpdatePs();
}

}

private void getConnection() {
try {
if(connection == null || connection.isClosed()) {
Class.forName("oracle.jdbc.OracleDriver");
connection = DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:ORA9", "user", "password");
}
} catch (Exception e) {
System.out.println("connection fail");
}
}

private void closeConneciton() {
if(connection != null) {
try { connection.close(); } catch (Exception e) { e.printStackTrace(); }
}
}

private void closeUpdatePs() {
if(updatePs != null) {
try { updatePs.close(); } catch (Exception e) { e.printStackTrace(); }
}
}

private void closeSelectPs() {
if(selectPs != null) {
try { selectPs.close(); } catch (Exception e) { e.printStackTrace(); }
}
}

private void closeResultSet(ResultSet rs) {
if(rs != null) {
try { rs.close(); } catch (Exception e) { e.printStackTrace(); }
}
}

public static void main(String[] args) throws Exception {
ImgWidthHeightReplacer instance = new ImgWidthHeightReplacer();
instance.replaceContents();
}
}

중요한 부분은 replaceContents() 메서드의 정규식 부분이다.

치환 정규식

  • (<img[^>])(width=["]?[^>"]+["]?)([^>]>)
    • 1번 그룹 (<img[^>]*)
      • <img 문자열 뒤에 > 가 아닌 문자가 0개 이상
    • 2번 그룹 (width=["]?[^>"]+["]?)
      • width= 문자열 뒤에 “ 문자가 0 이나 1개
      • 문자 > 와 “ 가 아닌 문자가 하나 이상
      • “ 가 있거나 없음
    • 3번 그룹 ([^>]*>)
      • 문자 > 가 아닌 문자가 0개 이상

그룹캡쳐를 사용해 치환자를 $1$3으로하여 2번 그룹을 삭제한다. 결국 img 태그의 width=”…” 부분이 삭제된다.

  • (<img[^>]*style="[^>]width:)([a-zA-Z0-9];?)([^>]*")
    • 1번 그룹 (<img[^>]*style="[^>]*width:)
      • img 태그 안에 style=”… width: 까지 캡쳐
    • 2번 그룹 ([a-zA-Z0-9]*;?)
      • width: 후에 value 값
    • 3번 그룹 ([^>]*")
      • style 속성의 마지막 quote(“)

실행결과 제대로 변경된 것을 확인할 수 있었다.
실무에서 이런 류의 작업을 해야하는 경우가 종종 있는데 정규식을 적절하게 사용하면 쉽게 수정이 가능하다.
끝으로 리눅스에서 파일의 특정 문자를 치환하는 방법을 소개하면서 마친다.

리눅스 파일 문자열 치환

1
find ./ -name "*.jsp" |xargs sed -i "s/jquery-3.2.1.min.js/jquery-3.3.1.min.js/g"

현재폴더 아래의 jsp 파일 모두를 검색하여 “jquery-3.2.1.min.js” 문자열을 “jquery-3.3.1.min.js” 로 치환