-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtool-regex.qmd
949 lines (698 loc) · 34.1 KB
/
tool-regex.qmd
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
```{r}
#| include: false
source("_common.R")
```
# 정규 표현식 {#regex}
\index{정규 표현식}
\index{regex}
\index{패키지!stringr}
\index{stringr}
지금까지 파일을 훑어서 패턴을 찾고, 관심 있는 라인에서 다양한 비트(bits)를 뽑아냈다.
`stringr` 패키지 `str_split`, `str_detect` 같은 문자열 함수를 사용하였고,
라인에서 일정 부분을 뽑아내기 위해서 리스트와 문자열 슬라이싱(slicing)을 사용했다.
검색하고 추출하는 작업은 너무 자주 있는 일이어서
R과 파이썬 모두 상기와 같은 작업을 매우 우아하게 처리하는 **정규 표현식(regular expressions)**으로 불리는 매우 강력한 라이브러리를 제공한다.
정규 표현식을 책의 앞부분에 소개하지 않은 이유는 정규 표현식 라이브러리가 매우 강력하지만, 약간 복잡하고, 구문에 익숙해지는 데 시간이 필요하기 때문이다.
정규표현식은 문자열을 검색하고 파싱하는 데 그 자체가 작은 프로그래밍 언어다.
사실, 책 전체가 정규 표현식을 주제로 쓰여진 책이 몇 권 있다.
이번 장에서는 정규 표현식의 기초만을 다룰 것이다. 정규 표현식의 좀 더 자세한 사항은 다음을 참조한다.
- <http://en.wikipedia.org/wiki/Regular_expression>
- <https://cran.r-project.org/web/packages/stringr/vignettes/regular-expressions.html>
R에서 정규표현식을 지원하는 패키지는 많지만, 대표적으로 [`stringr`](https://stringr.tidyverse.org/) 패키지가 활용 사례도 많고 문서화도 충실하다.
정규 표현식 패키지를 사용하기 전에 패키지를 가져와야 한다.
정규 표현식 패키지의 가장 간단한 쓰임은 `str_detect()` 검색 함수다. 다음 프로그램은 검색 함수의 사소한 사용 예를 보여준다.
\index{정규 표현식!str\_detect}
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-regex-stringr
#|
download.file("https://www.dr-chuck.com/py4inf/code/mbox-short.txt",
destfile = "mbox-short.txt")
library(stringr)
lines <- readLines("mbox-short.txt")
for (line in lines) {
if (str_detect(line, "From:")) {
print(line)
}
}
#> [1] "From: stephen.marquard@uct.ac.za"
#> [1] "From: louis@media.berkeley.edu"
#> [1] "From: zqian@umich.edu"
#> ...
```
### 파이썬
```{pyodide-python}
#| label: py-regex-stringr
# 실습파일 다운로드: mbox-short.txt
import pyodide_js
await pyodide_js.loadPackage('requests')
import requests
url = "https://www.dr-chuck.com/py4inf/code/mbox-short.txt"
destination_file = "mbox-short.txt"
response = requests.get(url)
response.raise_for_status()
with open(destination_file, 'wb') as file:
file.write(response.content)
# 정규표현식 실습
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
if re.search('From:', line) :
print(line)
```
:::
파일을 열고, 각 라인을 루프로 반복해서 정규 표현식 `stringr::str_detect()` 함수를 호출하여 문자열 "From"이 포함된 라인만 출력한다.
상기 프로그램에는 진정으로 강력한 정규 표현식 기능이 사용되지 않았다.
왜냐하면, 다른 함수를 가지고도 동일한 결과를 쉽게 구현할 수 있기 때문이다.
\index{문자열!str\_detect}
정규 표현식의 강력한 기능은 문자열에 해당하는 라인을 좀 더 정확하게 제어하기 위해서 검색 문자열에 특수문자를 추가할 때 확인될 수 있다.
매우 적은 코드를 작성할지라도, 정규 표현식에 특수 문자를 추가하는 것만으로도 정교한 일치(matching)와 추출이 가능하게 한다.
예를 들어, 탈자 기호(caret)는 라인의 "시작"과 일치하는 정규 표현식에 사용된다.
다음과 같이 "From:"으로 시작하는 라인만 일치하도록 응용프로그램을 변경할 수 있다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-regex-caret
library(stringr)
lines <- readLines("mbox-short.txt")
for (line in lines) {
if (str_detect(line, "From:")) {
print(line)
}
}
```
### 파이썬
```{pyodide-python}
#| label: py-regex-caret
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
if re.search('^From:', line) :
print(line)
```
:::
"From:" 문자열로 *시작*하는 라인만 일치할 수 있다.
여전히 매우 간단한 프로그램으로 다른 패키지에서도 다양한 함수로 동일하게 수행할 수 있다.
하지만, 무엇을 정규 표현식과 매칭하는가에 대해서 특수 액션 문자(`^`)를 담아 강력한 제어를 수행하는 정규 표현식 개념을 소개하기에는 충분하다.
\index{와일드 카드}
\index{정규 표현식!화일드 카드}
## 문자 매칭 {#regex-char-matching}
좀 더 강력한 정규 표현식을 작성할 수 있는 다른 특수문자는 많이 있다.
가장 자주 사용되는 특수 문자는 임의 문자를 매칭하는 마침표다.
다음 예제에서 정규 표현식 "F..m:"은 "From:", "Fxxm:", "F12m:", "F!\@m:' 같은 임의 문자열을 매칭한다.
왜냐하면 정규 표현식 마침표 문자가 임의의 문자와 매칭되기 때문이다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-regex-char-matching
library(stringr)
lines <- readLines("mbox-short.txt")
for (line in lines) {
if (str_detect(line, "^F..m:")) {
print(line)
}
}
```
### 파이썬
```{pyodide-python}
#| label: py-regex-char-matching
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
if re.search('^F..m:', line) :
print(line)
```
:::
정규 표현식에 "\*", "+' ' 문자를 사용하여 문자가 원하는 만큼 반복을 나타내는 기능과 결합되었을 때는 더욱 강력해진다.
"\*", "+' ' 특수 문자가 검색 문자열에 문자 하나만을 매칭하는 대신에 별표 기호인 경우 0회 혹은 그 이상의 매칭, 더하기 기호인 경우 1회 혹은 그 이상의 문자의 매칭을 의미한다.
다음 예제에서 반복 **와일드 카드(wild card)** 문자를 사용하여 매칭하는 라인을 좀 더 좁힐 수 있다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-regex-char-matching-wild
library(stringr)
lines <- readLines("mbox-short.txt")
for (line in lines) {
if (str_detect(line, "^From:.+@")) {
print(line)
}
}
```
### 파이썬
```{pyodide-python}
#| label: py-regex-char-matching-wild
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
if re.search('^From:.+@', line) :
print(line)
```
:::
검색 문자열 "`^`From:.+@" 은 "From:" 으로 시작하고, ".+" 하나 혹은 그 이상의 문자들, 그리고 `@` 기호와 매칭되는 라인을 성공적으로 찾아낸다.
그래서 다음 라인은 매칭이 될 것이다.
**`From:`** `stephen.marquard@uct.ac.za`
콜론(:)과 `@` 기호 사이의 모든 문자들을 매칭하도록 확장하는 것으로 ".+" 와일드 카드를 간주할 수 있다.
**`From:`**`.+ @`
더하기와 별표 기호를 "밀어내기(pushy)" 문자로 생각하는 것이 좋다.
예를 들어, 다음 문자열은 ".+" 특수문자가 다음에 보여주듯이 밖으로 밀어내는 것처럼 문자열 마지막 `@` 기호를 매칭한다.
**`From:`**`stephen.marquard@uct.ac.za, csev@umich.edu, and cwen @iupui.edu`
다른 특수문자를 추가함으로써 별표나 더하기 기호가 너무 "탐욕(greedy)"스럽지 않게 만들 수 있다.
와일드 카드 특수문자의 탐욕스러운 기능을 끄는 것에 대해서는 자세한 정보를 참조하기 바란다.
\index{탐욕}
## 데이터 추출 {#regex-extraction}
R `stringr` 패키지로 문자열에서 데이터를 추출하려면, `str_extract_all()` 함수를 사용해서 정규 표현식과 매칭되는 모든 부속 문자열을 추출할 수 있다.
형식에 관계없이 임의 라인에서 전자우편 주소 같은 문자열을 추출하는 예제를 사용해보자. 예를 들어, 다음 각 라인에서 전자우편 주소를 뽑아내고자 한다.
``` bash
From stephen.marquard@uct.ac.za Sat Jan 5 09:14:16 2008
Return-Path: <postmaster@collab.sakaiproject.org>
for <source@collab.sakaiproject.org>;
Received: (from apache@localhost)
Author: stephen.marquard@uct.ac.za
```
각각의 라인에 대해서 다르게 쪼개고, 슬라이싱하면서 라인 각각의 형식에 맞추어 코드를 작성하고는 싶지는 않다.
다음 프로그램은 `str_extract_all()` 함수를 사용하여 전자우편 주소가 있는 라인을 찾아내고 하나 혹은 그 이상의 주소를 뽑아낸다.
\index{str\_extract\_all}
\index{정규 표현식!str\_extract\_all}
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-regex-extract-email
library(stringr)
s <- 'Hello from csev@umich.edu to cwen@iupui.edu about the meeting @2PM'
lst <- str_extract_all(s, '\\S+@\\S+')
lst
#> [[1]]
#> [1] "csev@umich.edu" "cwen@iupui.edu"
```
### 파이썬
```{pyodide-python}
#| label: py-regex-extract-email
import re
s = 'Hello from csev@umich.edu to cwen@iupui.edu about the meeting @2PM'
lst = re.findall('\S+@\S+', s)
print(lst)
#> ['csev@umich.edu', 'cwen@iupui.edu']
```
:::
`str_extract_all()` 함수는 두 번째 인자 패턴을 갖는 문자열을 찾아서 전자우편 주소처럼 보이는 모든 문자열을 리스트로 반환한다.
공백이 아닌 문자(`\\`S)와 매칭되는 가운데 `@`을 갖는 두 문자열 시퀀스(sequence)를 매칭한다.
프로그램의 출력은 다음과 같다.
```{r}
#| label: regex-extract-email-output
#| eval: false
[[1]]
[1] "csev@umich.edu" "cwen@iupui.edu"
```
정규 표현식을 해석하면, 적어도 하나의 공백이 아닌 문자, `@`과 적어도 하나 이상의 공백이 아닌 문자를 가진 부속 문자열을 찾는다.
또한, "`\\`S+" 특수 문자는 가능한 많이 공백이 아닌 문자를 매칭한다. (정규 표현식에서 **"탐욕(greedy)"** 매칭이라고 부른다.)
정규 표현식은 두 번 매칭(csev\@umich.edu, cwen\@iupui.edu)하지만, 문자열 "\@2PM"은 매칭을 하지 않는다.
왜냐하면, `@` 기호 *앞*에 공백이 아닌 문자가 하나도 없기 때문이다.
프로그램의 정규 표현식을 사용해서 파일의 모든 라인을 읽고 다음과 같이 전자우편 주소처럼 보이는 모든 문자열을 출력한다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-regex-extract-emails
library(stringr)
lines <- readLines("mbox-short.txt")
for (line in lines) {
matches <- str_extract_all(line, "\\S+@\\S+")[[1]]
if (length(matches) > 0) {
print(matches)
}
}
```
### 파이썬
```{pyodide-python}
#| label: py-regex-extract-emails
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
x = re.findall('\S+@\S+', line)
if len(x) > 0 :
print(x)
```
:::
각 라인을 읽어 들이고, 정규 표현식과 매칭되는 모든 부속 문자열을 추출한다.
`str_extract()` 함수는 문자 벡터를 반환하기 때문에,
전자우편 처럼 보이는 부속 문자열을 적어도 하나 찾아서 출력하기 위해서 반환 리스트 요소 숫자가 `NA` 여부를 간단히 확인한다.
원데이터 `mbox.txt` 파일에 프로그램을 실행하면, 다음 출력을 얻는다.
``` bash
[1] "stephen.marquard@uct.ac.za"
[1] "<postmaster@collab.sakaiproject.org>"
[1] "<200801051412.m05ECIaH010327@nakamura.uits.iupui.edu>"
[1] "<source@collab.sakaiproject.org>;"
[1] "<source@collab.sakaiproject.org>;"
[1] "<source@collab.sakaiproject.org>;"
[1] "apache@localhost)"
[1] "source@collab.sakaiproject.org;"
[1] "stephen.marquard@uct.ac.za"
[1] "source@collab.sakaiproject.org"
[1] "stephen.marquard@uct.ac.za"
[1] "stephen.marquard@uct.ac.za"
[1] "louis@media.berkeley.edu"
...
```
전자우편 주소 몇몇은 "`<`", ";" 같은 잘못된 문자가 앞과 뒤에 붙어있다.
문자나 숫자로 시작하고 끝나는 문자열 부분만 관심 있다고 하자.
그러기 위해서, 정규 표현식의 또 다른 기능을 사용한다.
매칭하려는 다중 허용 문자 집합을 표기하기 위해서 꺾쇠 괄호를 사용한다.
그런 의미에서 "`\`S"은 공백이 아닌 문자 집합을 매칭하게 한다.
이제 매칭하려는 문자에 관해서 좀 더 명확해졌다.
여기 새로운 정규 표현식이 있다.
``` bash
[a-zA-Z0-9]\\S*@\\S*[a-zA-Z]
```
약간 복잡해졌다. 왜 정규 표현식이 자신만의 언어인가에 대해서 이해할 수 있다.
이 정규 표현식을 해석하면, 0회 혹은 그 이상의 공백이 아닌 문자("`\\S*`")로 하나의 소문자, 대문자 혹은 숫자("[a-zA-Z0-9]")를 가지며,
`@` 다음에 0회 혹은 그 이상의 공백이 아닌 문자("`\\S*`")로 하나의 소문자, 대문자 혹은 숫자("[a-zA-Z0-9]")로 된 부속 문자열을 찾는다.
0회 혹은 그 이상의 공백이 아닌 문자를 나타내기 위해서 "+"에서 "*"으로 바꿨다.
왜냐하면 "[a-zA-Z0-9]" 자체가 이미 하나의 공백이 아닌 문자이기 때문이다.
"*", "+"는 단일 문자에 별표, 더하기 기호 왼편에 즉시 적용됨을 기억한다.
\index{정규 표현식!문자 집합}
프로그램에 정규 표현식을 사용하면, 데이터가 훨씬 깔끔해진다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-regex-upgrade-remove
library(stringr)
lines <- readLines("mbox-short.txt")
for (line in lines) {
matches <- str_extract_all(line, "[a-zA-Z0-9]\\S*@\\S*[a-zA-Z]")[[1]]
if (length(matches) > 0) {
print(matches)
}
}
```
### 파이썬
```{pyodide-python}
#| label: py-regex-upgrade-remove
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
x = re.findall('[a-zA-Z0-9]\S*@\S*[a-zA-Z]', line)
if len(x) > 0 :
print(x)
```
:::
``` bash
[1] "stephen.marquard@uct.ac.za"
[1] "postmaster@collab.sakaiproject.org"
[1] "200801051412.m05ECIaH010327@nakamura.uits.iupui.edu"
[1] "source@collab.sakaiproject.org"
[1] "source@collab.sakaiproject.org"
[1] "source@collab.sakaiproject.org"
[1] "apache@localhost"
[1] "source@collab.sakaiproject.org"
[1] "stephen.marquard@uct.ac.za"
[1] "source@collab.sakaiproject.org"
[1] "stephen.marquard@uct.ac.za"
[1] "stephen.marquard@uct.ac.za"
[1] "louis@media.berkeley.edu"
[1] "postmaster@collab.sakaiproject.org"
[1] "200801042308.m04N8v6O008125@nakamura.uits.iupui.edu"
[1] "source@collab.sakaiproject.org"
[1] "source@collab.sakaiproject.org"
[1] "source@collab.sakaiproject.org"
[1] "apache@localhost"
...
```
"source@collab.sakaiproject.org" 라인에서 문자열 끝에 "`>`" 문자를 정규 표현식으로 제거한 것을 주목한다.
정규 표현식 끝에 "[a-zA-Z]"을 추가하여서 정규 표현식 파서가 찾는 임의 문자열은 문자로만 끝나야 되기 때문이다.
그래서, "sakaiproject.org`>`;"에서 "`>`"을 봤을 때, "g"가 마지막 맞는 매칭이 되고, 거기서 마지막 매칭을 마치고 중단한다.
`str_extract_all()` 프로그램의 출력은 리스트의 단일 요소를 가진 문자열로 R 리스트이다.
## 검색과 추출 조합 {#regex-search-extraction}
다음과 같은 "X-" 문자열로 시작하는 라인의 숫자를 찾고자 한다면,
``` bash
X-DSPAM-Confidence: 0.8475
X-DSPAM-Probability: 0.0000
```
임의의 라인에서 임의 부동 소수점 숫자가 아니라 상기 구문을 가진 라인에서만 숫자를 추출하고자 한다.
라인을 선택하기 위해서 다음과 같이 정규 표현식을 구성한다.
``` bash
^X-.*: [0-9.]+
```
정규 표현식을 해석하면, `^`에서 "X-"으로 시작하고, ".*"에서 0회 혹은 그 이상의 문자를 가지며, 콜론(":")이 나오고 나서 공백을 만족하는 라인을 찾는다.
공백 뒤에 "[0-9.]+"에서 숫자(0-9) 혹은 점을 가진 하나 혹은 그 이상의 문자가 있어야 한다.
꺾쇠 기호 사이에 마침표는 실제 마침표만 매칭함을 주목하기 바란다. (즉, 꺾쇠 기호 사이는 와일드 카드 문자가 아니다.)
관심을 가지고 있는 특정한 라인과 매우 정확하게 매칭이 되는 매우 빠듯한 정규 표현식으로 다음과 같다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-regex-search-extraction
library(stringr)
lines <- readLines("mbox-short.txt")
for (line in lines) {
if (str_detect(line, "^X\\S*: [0-9.]+")) {
print(line)
}
}
```
### 파이썬
```{pyodide-python}
#| label: py-regex-search-extraction
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
if re.search('^X\S*: [0-9.]+', line) :
print(line)
```
:::
프로그램을 실행하면, 잘 걸러져서 찾고자 하는 라인만 볼 수 있다.
``` bash
[1] "X-DSPAM-Confidence: 0.8475"
[1] "X-DSPAM-Probability: 0.0000"
[1] "X-DSPAM-Confidence: 0.6178"
[1] "X-DSPAM-Probability: 0.0000"
[1] "X-DSPAM-Confidence: 0.6961"
...
```
하지만, 이제 `str_split()` 함수를 사용해서 숫자를 뽑아내는 문제를 해결해야 한다.
`str_split()`을 사용하는 것이 간단해 보이지만, 동시에 라인을 검색하고 파싱하기 위해서 정규 표현식의 또 다른 기능을 사용할 수 있다.
\index{문자열!str\_split}
괄호는 정규 표현식의 또 다른 특수 문자다. 정규 표현식에 괄호를 추가한다면, 문자열이 매칭될 때, 무시된다.
하지만, `str_match()`를 사용할 때, 매칭할 전체 정규 표현식을 원할지라도, 정규 표현식을 매칭하는 부속 문자열의 부분만을 뽑아낸다는 것을 괄호가 표시한다.
\index{정규 표현식!괄호}
\index{괄호!정규 표현식}
그래서, 프로그램을 다음과 같이 수정한다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-regex-search-extract-prob
library(stringr)
lines <- readLines("mbox-short.txt")
for (line in lines) {
matches <- str_match(line, "^X\\S*: ([0-9.]+)")
if (!is.na(matches[1, 2])) {
print(matches[1, 2])
}
}
```
### 파이썬
```{pyodide-python}
#| label: py-regex-search-extract-prob
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
x = re.findall('^X\S*: ([0-9.]+)', line)
if len(x) > 0 :
print(x)
```
:::
`str_detect()`을 호출하는 대신에, 매칭 문자열의 부동 소수점 숫자만 뽑아내는 데 `str_match()`에 원하는 부동 소수점 숫자를 표현하는 정규 표현식 부분에 괄호를 추가한다.
프로그램의 출력은 다음과 같다.
``` bash
[1] "0.8475"
[1] "0.0000"
[1] "0.6178"
[1] "0.0000"
[1] "0.6961"
[1] "0.0000"
[1] "0.7565"
[1] "0.0000"
[1] "0.7626"
...
```
숫자가 여전히 리스트에 있어서 문자열에서 부동 소수점으로 변환할 필요가 있지만, 흥미로운 정보를 찾아 뽑아내기 위해서 정규 표현식의 강력한 힘을 사용했다.
이 기술을 활용한 또 다른 예제로, 파일을 살펴보면, 폼(form)을 가진 라인이 많다.
``` bash
Details: http://source.sakaiproject.org/viewsvn/?view=rev&rev=39772
```
상기 언급한 동일한 기법을 사용하여 모든 변경 번호(라인의 끝에 정수 숫자)를 추출하고자 한다면 다음과 같이 프로그램을 작성할 수 있다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-regex-search-extract-digits
library(stringr)
lines <- readLines("mbox-short.txt")
for (line in lines) {
matches <- str_match(line, "^Details:.*rev=([0-9.]+)")
if (!is.na(matches[1, 2])) {
print(matches[1, 2])
}
}
```
### 파이썬
```{pyodide-python}
#| label: py-regex-search-extract-digits
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
x = re.findall('^Details:.*rev=([0-9.]+)', line)
if len(x) > 0:
print(x)
```
:::
작성한 정규 표현식을 해석하면, "Details:"로 시작하는 ".*"에 임의의 문자들로, "rev="을 포함하고 나서, 하나 혹은 그 이상의 숫자를 가진 라인을 찾는다.
전체 정규 표현식을 만족하는 라인을 찾고자 하지만, 라인 끝에 정수만을 추출하기 위해서 "[0-9]+"을 괄호로 감쌌다.
프로그램을 실행하면, 다음 출력을 얻는다.
``` bash
[1] "39772"
[1] "39771"
[1] "39770"
[1] "39769"
[1] "39766"
[1] "39765"
[1] "39764"
...
```
"[0-9]+"은 "탐욕(greedy)"스러워서, 숫자를 추출하기 전에 가능한 큰 문자열 숫자를 만들려고 한다는 것을 기억하라.
이런 "탐욕(greedy)"스러운 행동으로 인해서 왜 각 숫자로 모두 5자리 숫자를 얻은 이유가 된다.
정규 표현식 라이브러리는 양방향으로 파일 처음이나 끝에 숫자가 아닌 것을 마주칠 때까지 뻗어 나간다.
이제 정규 표현식을 사용해서 각 전자우편 메시지의 요일에 관심이 있었던 책 앞의 연습 프로그램을 다시 작성한다.
다음 형식의 라인을 찾는다.
``` bash
From stephen.marquard@uct.ac.za Sat Jan 5 09:14:16 2008
```
그리고 나서, 각 라인의 요일의 시간을 추출하고자 한다. 앞에서 `str_split`를 두 번 호출하여 작업을 수행했다.
첫 번째는 라인을 단어로 쪼개고, 다섯 번째 단어를 뽑아내서, 관심 있는 두 문자를 뽑아내기 위해서 콜론 문자에서 다시 쪼갰다.
작동을 할지 모르지만, 실질적으로 정말 부서지기 쉬운 코드로 라인이 잘 짜여져 있다고 가정하에 가능하다.
잘못된 형식의 라인이 나타날 때도 결코 망가지지 않는 프로그램을 담보하기 위해서 충분한 오류 검사 기능을 추가하거나 커다란 try/except 블록을 넣으면,
참 읽기 힘든 10-15 라인 코드로 커질 것이다.
다음 정규 표현식으로 훨씬 간결하게 작성할 수 있다.
``` bash
^From .* [0-9][0-9]:
```
상기 정규 표현식을 해석하면, 공백을 포함한 "From "으로 시작해서,
".*"에 임의 개수의 문자, 그리고 공백, 두 개의 숫자 "[0-9][0-9]" 뒤에 콜론(:) 문자를 가진 라인을 찾는다.
일종의 찾고 있는 라인에 대한 정의다.
`str_extract()` 함수를 사용해서 단지 시간만 뽑아내기 위해서, 두 숫자에 괄호를 다음과 같이 추가한다.
``` bash
^From .* ([0-9][0-9]):
```
작업 결과는 다음과 같이 프로그램에 반영한다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-regex-search-extract-days
library(stringr)
lines <- readLines("mbox-short.txt")
for (line in lines) {
matches <- str_match(line, "^From .* ([0-9][0-9]):")
if (!is.na(matches[1, 2])) {
print(matches[1, 2])
}
}
```
### 파이썬
```{pyodide-python}
#| label: py-regex-search-extract-days
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
x = re.findall('^From .* ([0-9][0-9]):', line)
if len(x) > 0 : print(x)
```
:::
프로그램을 실행하면, 다음 출력 결과가 나온다.
``` bash
[1] "09"
[1] "18"
[1] "16"
[1] "15"
[1] "15"
[1] "14"
[1] "11"
[1] "11"
...
```
## 이스케이프 문자 {#regex-escape}
라인의 처음과 끝을 매칭하거나, 와일드 카드를 명세하기 위해서 정규 표현식의 특수 문자를 사용했기 때문에,
정규 표현식에 사용된 문자가 "정상(normal)"적인 문자임을 표기할 방법이 필요하고 달러 기호와 탈자 기호(`^`) 같은 실제 문자를 매칭하고자 한다.
역슬래시(``` \```)을 가진 문자를 앞에 덧붙여서 문자를 단순히 매칭하고자 한다고 나타낼 수 있다.
예를 들어, 다음 정규표현식으로 금액을 찾을 수 있다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-regex-search-extract-days-output
library(stringr)
x <- "We just received $10.00 for cookies."
y <- str_extract_all(x, "\\$[0-9.]+")[[1]]
y
#> [1] "$10.00"
```
### 파이썬
```{pyodide-python}
#| label: py-regex-search-extract-days-output
import re
x = 'We just received $10.00 for cookies.'
y = re.findall('\$[0-9.]+',x)
print(y)
#> ['$10.00']
```
:::
역슬래시 달러 기호를 앞에 덧붙여서(`\\$`), 실제로 "라인 끝(end of line)" 매칭 대신에 입력 문자열의 달러 기호와 매칭한다.
정규 표현식 나머지 부분은 하나 혹은 그 이상의 숫자 혹은 소수점 문자를 매칭한다.
*주목*: 꺾쇠 괄호 내부에 문자는 "특수 문자"가 아니다. 그래서 "[0-9.]"은 실제 숫자 혹은 점을 의미한다.
꺾쇠 괄호 외부에 점은 "와일드 카드(wild-card)" 문자이고 임의의 문자와 매칭한다.
꺾쇠 괄호 내부에서 점은 점일 뿐이다.
## 요약 {#regex-summary}
지금까지 정규 표현식의 표면을 긁은 정도지만, 정규 표현식 언어에 대해서 조금 학습했다.
정규 표현식은 특수 문자로 구성된 검색 문자열로 "매칭(matching)"을 정의하고 매칭된 문자열로부터 추출된 결과물을 정규 표현식 시스템과 프로그래머가 의도한 바를 의사소통하는 것이다. 다음에 특수 문자 및 문자 시퀀스의 일부가 있다.
- `^` 라인의 처음을 매칭.
- `$` 라인의 끝을 매칭.
- `.` 임의의 문자를 매칭(와일드 카드)
- `s` 공백 문자를 매칭.
- `S` 공백이 아닌 문자를 매칭.(`s` 의 반대).
- `*` 바로 앞선 문자에 적용되고 0회 혹은 그 이상의 앞선 문자와 매칭을 표기.
- `*?` 바로 앞선 문자에 적용되고 0회 혹은 그 이상의 앞선 문자와 매칭을 "탐욕적이지 않은(non-greedy) 방식"으로 표기.
- `+` 바로 앞선 문자에 적용되고 1회 혹은 그 이상의 앞선 문자와 매칭을 표기.
- `+?` 바로 앞선 문자에 적용되고 1회 혹은 그 이상의 앞선 문자와 매칭을 "탐욕적이지 않은(non-greedy) 방식"으로 표기.
- `[aeiou]` 명세된 집합 문자에 존재하는 단일 문자와 매칭. 다른 문자는 안 되고, "a", "e", "i", "o", "u" 문자만 매칭되는 예제.
- `[a-z0-9]` 음수 기호로 문자 범위를 명세할 수 있다. 소문자이거나 숫자인 단일 문자만 매칭되는 예제.
- `[^A-Za-z]` 집합 표기의 첫 문자가 `^`인 경우, 로직을 거꾸로 적용한다. 대문자나 혹은 소문자가 아닌 임의 단일 문자만 매칭하는 예제.
- `( )` 괄호가 정규표현식에 추가될 때, 매칭을 무시한다. 하지만 `str_extract()`을 사용할 때 전체 문자열보다 매칭된 문자열의 상세한 부속 문자열을 추출할 수 있게 한다.
- `\b` 빈 문자열을 매칭하지만, 단어의 시작과 끝에만 사용.
- `\B` 빈 문자열을 매칭하지만, 단어의 시작과 끝이 아닌 곳에 사용.
- `\d` 임의 숫자와 매칭하여 [0-9] 집합에 상응.
- `\D` 임의 숫자가 아닌 문자와 매칭하여 [^0-9] 집합에 상응.
## 유닉스 사용자 보너스 {#unix-users}
\index{grep}
정규 표현식을 사용하여 파일을 검색하는 기능은 1960년대 이래로 유닉스 운영 시스템에 내장되어 여러 가지 형태로 거의 모든 프로그래밍 언어에서 이용 가능하다.
사실, `str_detect()` 예제에서와 거의 동일한 기능을 하는 **grep** (Generalized Regular Expression Parser)으로 불리는 유닉스 내장 명령어 프로그램이 있다.
그래서, 맥킨토시나 리눅스 운영 시스템을 가지고 있다면, 명령어 창에서 다음 명령어를 시도할 수 있다.
``` bash
$ grep '^From:' mbox-short.txt
From: stephen.marquard@uct.ac.za
From: louis@media.berkeley.edu
From: zqian@umich.edu
From: rjlowe@iupui.edu
```
`grep`을 사용하여, `mbox-short.txt` 파일 내부에 "From:" 문자열로 시작하는 라인을 보여준다.
`grep` 명령어를 가지고 약간 실험을 하고 `grep`에 대한 문서를 읽는다면, 파이썬에서 지원하는 정규표현식과 `grep`에서 지원되는 정규 표현식과 차이를 발견할 것이다.
예를 들어, `grep`은 공백이 아닌 문자 "`\S`"을 지원하지 않는다.
그래서 약간 더 복잡한 집합 표기 "[^ ]"을 사용해야 한다. "[^ ]"은 간단히 정리하면, 공백을 제외한 임의의 문자와 매칭한다.
## 디버깅 {#regex-debugging}
R에 대해서 도움을 어떻게 받을 수 있을까?
만약 특정 함수의 정확한 이름을 기억해 내기 위해서 빠르게 생각나게 하는 것이 필요하다면 도움이 많이 될 수 있는 간단하고 초보적인 내장 문서가 R에 포함되어 있다.
내장 문서 도움말은 인터랙티브 모드의 R 인터프리터에서 볼 수 있다.
### 도움말 파일 읽어오기
특정한 패키지를 사용하고자 한다면, `ls(pos = "package:패키지명)` 명령어를 사용하여 다음과 같이 패키지에 포함된 함수를 찾을 수 있다. `stringr` 패키지는 파이썬 내장 `re` 패키지와 유사한 패턴 매칭 함수를 제공한다.
```{r}
ls(pos = "package:stringr")
```
R과 모든 팩키지는 함수에 대한 도움말 파일을 제공한다.
네임스페이스(인터랙티브 R 세션)에 적재된 팩키지에 있는 특정 함수에 대한 도움말은 다음과 같이 찾는다.
```{r, eval=FALSE}
?function_name
help(function_name)
```
RStudio에 도움말 페이지에 도움말이 표시된다. (혹은 R 자체로 일반 텍스트로 표시된다) 각 도움말 페이지는 절(section)로 구분된다:
- 기술(Description): 함수가 어떤 작업을 수행하는가에 대한 충분한 기술
- 사용법(Usage): 함수 인자와 기본 디폴트 설정값
- 인자(Arguments): 각 인자가 예상하는 데이터 설명
- 상세 설명(Details): 알고 있어야 되는 중요한 구체적인 설명
- 값(Value): 함수가 반환하는 데이터
- 함께 보기(See Also): 유용할 수 있는 연관된 함수.
- 예제(Examples): 함수 사용법에 대한 예제들.
함수마다 상이한 절을 갖추고 있다.
하지만, 상기 항목이 알고 있어야 하는 핵심 내용이다.
::: callout-tip
### 도움말 파일 불러 읽어오기
R에 대해 가장 기죽게 되는 한 측면이 엄청난 함수 갯수다.
모든 함수에 대한 올바른 사용법을 기억하지 못하면, 엄두가 나지 않을 것이다.
운 좋게도, 도움말 파일로 인해 기억할 필요가 없다!
:::
### 특수 연산자
특수 연산자에 대한 도움말을 찾으려면, 인용부호를 사용한다:
```{r, eval=FALSE}
?"<-"
```
### 팩키지 도움말 얻기
많은 팩키지에 "소품문(vignettes)"이 따라온다.
활용법과 풍부한 예제를 담은 문서. 어떤 인자도 없이, `vignette()` 명령어를 입력하면 설치된 모든 팩키지에 대한 모든 소품문 목록이 출력된다,
`vignette(package="package-name")` 명령어는 `package-name` 팩키지명에 대한 이용 가능한 모든 소품문 목록을 출력하고,
`vignette("vignette-name")` 명령어는 특정된 소품문을 연다.
팩키지에 어떤 소품문도 포함되지 않는다면, 일반적으로 `help("package-name")` 명령어를 타이핑해서 도움말을 얻는다.
### 함수가 기억나지 않을 때
함수가 어느 팩키지에 있는지 확신을 못하거나, 구체적인 철자법을 모르는 경우, 퍼지 검색(fuzzy search)을 실행한다.
```{r, eval=FALSE}
??function_name
```
### 시작조차 난감할 때
어떤 함수 혹은 팩키지가 필요한지 모르는 경우, [CRAN Task Views](http://cran.at.r-project.org/web/views) 사이트가 좋은 시작점이 된다.
유지 관리되는 팩키지 목록이 필드로 묶여 잘 정리되어 있다.
### 코드가 동작하지 않을 때
동료로부터 도움을 구해 코드가 동작하지 않는 이슈를 해결한다.
함수 사용에 어려움이 있는 경우, 10 에 9 경우에 찾는 정답이 이미 [Stack Overflow](http://stackoverflow.com/)에 답글이 달려 있다.
검색할 때 `[r]` 태그를 사용한다.
원하는 답을 찾지 못한 경우, 동료에게 질문을 만드는 데 몇 가지 유용한 함수가 있다.
```{r, eval=FALSE}
?dput
```
`dput()` 함수는 작업하고 있는 데이터를 텍스트 파일 형식으로 덤프해서 저장한다.
그래서 다른 사람 R 세션으로 복사해서 붙여넣기 좋게 돕는다.
```{r}
sessionInfo()
```
`sessionInfo()`는 R 현재 버전 정보와 함께 적재된 팩키지 정보를 출력한다.
이 정보가 다른 사람이 여러분 문제를 재현하고 디버그하는 데 유용할 수 있다.
## 용어 정의 {#r-regex-terminology}
- **부서지기 쉬운 코드(brittle code)**:입력 데이터가 특정한 형식일 경우에만 작동하는 코드. 하지만 올바른 형식에서 약간이도 벗어나게 되면 깨지기 쉽다.
쉽게 부서지기 때문에 "부서지기 쉬운 코드(brittle code)"라고 부른다.
- **욕심쟁이 매칭(greedy matching)**:정규 표현식의 "+", "\*" 문자는 가능한 큰 문자열을 매칭하기 위해서 밖으로 확장하는 개념.
\index{욕심쟁이}
\index{욕심쟁이 매칭}
\index{greedy}
- **grep**: 정규 표현식에 매칭되는 파일을 탐색하여 라인을 찾는데 대부분의 유닉스 시스템에서 사용 가능한 명령어. "Generalized Regular Expression Parser"의 약자.
\index{grep}
- **정규 표현식(regular expression)**: 좀 더 복잡한 검색 문자열을 표현하는 언어. 정규 표현식은 특수 문자를 포함해서 검색 라인의 처음 혹은 끝만 매칭하거나 많은 비슷한 것을 매칭한다.
\index{정규 표현식}
- **와일드 카드(wild card)**: 임의 문자를 매칭하는 특수 문자. 정규 표현식에서 와일드 카드 문자는 마침표 문자다.
\index{와일드 카드}
## 연습문제 {.unnumbered #r-regex-ex}
1. 유닉스의 `grep` 명령어를 모사하는 간단한 프로그램을 작성한다.
사용자가 정규 표현식을 입력하고 정규 표현식에 매칭되는 라인 수를 세는 프로그램이다.
``` bash
$ Rscript grep.R
Enter a regular expression: ^Author
mbox.txt had 1798 lines that matched ^Author
$ Rscript grep.R
Enter a regular expression: ^X-
mbox.txt had 14368 lines that matched ^X-
$ Rscript grep.R
Enter a regular expression: java$
mbox.txt had 4218 lines that matched java$
```
2. 다음 형식의 라인만을 찾는 프로그램을 작성하세요.
``` bash
New Revision: 39772
```
그리고, 정규 표현식과 `str_extract()` 함수를 사용하여 각 라인으로부터 숫자를 추출한다.
숫자들의 평균을 구하고 출력한다.
``` bash
Enter file:mbox.txt
38549.7949721
Enter file:mbox-short.txt
39756.9259259
```