-
Notifications
You must be signed in to change notification settings - Fork 0
/
tool-network.qmd
784 lines (595 loc) · 30.9 KB
/
tool-network.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
```{r}
#| include: false
source("_common.R")
```
# 네트워크 프로그램 {#network}
지금까지 책의 많은 예제는 파일을 읽고 파일 속 정보를 찾는 데 집중했지만, 다양한 많은 정보의 원천이 인터넷에 있다.
이번 장에서는 웹 브라우저로 가장하고 HTTP 프로토콜(HyperText Transport Protocol, HTTP)을 사용하여 웹페이지를 검색할 것이다.
웹페이지 데이터를 읽고 파싱할 것이다.
::: callout-tip
파이썬 튜플과 마찬가지로 R에서 네트워크 프로그래밍을 위해 소켓까지 내려가서 프로그래밍을 할 경우는 많지 않다.
하지만 네트워크 프로그램에 대한 이해와 개념을 잡기 위해 파이썬 소켓 네트워크 프로그래밍을 먼저 살펴본다.
:::
## 하이퍼텍스트 전송 프로토콜 {#network-http}
웹에 동력을 공급하는 네트워크 프로토콜(HyperText Transport Protocol - HTTP)은 실제로 매우 단순하다.
파이썬에는 `소켓(sockets)`이라고 불리는 내장 지원 모듈이 있다.
파이썬 프로그램에서 소켓 모듈을 통해서 네트워크 연결을 하고, 데이터 검색을 매우 용이하게 한다.
**소켓(socket)**은 단일 소켓으로 두 프로그램 사이에 양방향 연결을 제공한다는 점을 제외하고 파일과 매우 유사하다.
동일한 소켓에 읽거나 쓸 수 있다. 소켓에 무언가를 쓰게 되면, 소켓의 다른 끝에 있는 응용프로그램에 전송된다.
소켓으로부터 읽게 되면, 다른 응용 프로그램이 전송한 데이터를 받게 된다.
하지만, 소켓의 다른쪽 끝에 프로그램이 어떠한 데이터도 전송하지 않았는데 소켓을 읽으려고 하면, 단지 앉아서 기다리기만 한다.
만약 어떠한 것도 보내지 않고 양쪽 소켓 끝의 프로그램 모두 기다리기만 한다면, 모두 매우 오랜 시간 동안 기다리게 될 것이다.
인터넷으로 통신하는 프로그램의 중요한 부분은 특정 종류의 프로토콜을 공유하는 것이다.
프로토콜(protocol)은 정교한 규칙의 집합으로 누가 메시지를 먼저 보내고, 메시지로 무엇을 하며, 메시지에 대한 응답은 무엇이고, 다음에 누가 메시지를 보내는지 등을 포함한다.
이런 관점에서 소켓 끝의 두 응용프로그램이 함께 춤을 추고 있으니, 다른 사람 발을 밟지 않도록 확인해야 한다.
네트워크 프로토콜을 기술하는 문서가 많이 있다.
하이퍼텍스트 전송 프로토콜(HyperText Transport Protocol)은 다음 문서에 기술되어 있다.
<http://www.w3.org/Protocols/rfc2616/rfc2616.txt>
매우 상세한 176페이지나 되는 장문의 복잡한 문서다.
흥미롭다면 시간을 가지고 읽어보기 바란다.
RFC2616에 36페이지를 읽어보면, GET 요청(request)에 대한 구문을 발견하게 된다.
꼼꼼히 읽게 되면, 웹서버에 문서를 요청하기 위해서, 80 포트로 `www.py4inf.com` 서버에 연결을 하고 나서 다음 양식 한 라인을 전송한다.
`GET http://www.py4inf.com/code/romeo.txt HTTP/1.0`
두 번째 매개변수는 요청하는 웹페이지가 된다.
그리고 또한 빈 라인도 전송한다.
웹서버는 문서에 대한 헤더 정보와 빈 라인 그리고 문서 본문으로 응답한다.
::: callout-warning
### 웹브라우저 실행금지 사유
`webr` `pyodide`는 웹 브라우저 샌드박스 환경에서 R, 파이썬을 실행하며, 브라우저 보안 제한으로 인해
일반 R, 파이썬 환경에서와 같은 네트워크 소켓에 대한 직접적인 접근이 불가능하다.
접근을 허용하는 것이 중대한 보안 위험을 초래할 수 있기 때문에 웹 브라우저에서 실행이 되지 않게 되어 있어 오류가 나는 것이 정상이다.
:::
## 가장 간단한 웹 브라우저 {#network-web-browser}
아마도 HTTP 프로토콜이 어떻게 작동하는지 알아보는 가장 간단한 방법은 매우 간단한 파이썬 프로그램을 작성하는 것이다.
웹서버에 접속하고 HTTP 프로토콜 규칙에 따라 문서를 요청하고 서버가 다시 보내주는 결과를 보여주는 것이다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-socket-example
con <- socketConnection(host = "data.pr4e.org", port = 80, blocking = TRUE, open = "r+b", timeout = 60)
cmd <- "GET http://data.pr4e.org/romeo.txt HTTP/1.0\r\n\r\n"
writeLines(cmd, con)
flush(con)
while(length(line <- readLines(con, warn = FALSE, n = 1)) > 0) {
cat(line, sep = "\n")
}
close(con)
```
### 파이썬
```{pyodide-python}
#| label: py-socket-example
import socket
mysock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
mysock.connect(('data.pr4e.org', 80))
cmd = 'GET http://data.pr4e.org/romeo.txt HTTP/1.0\r\n\r\n'.encode()
mysock.send(cmd)
while True:
data = mysock.recv(512)
if len(data) < 1:
break
print(data.decode(),end='')
mysock.close()
```
:::
처음에 프로그램은 [www.py4e.com](www.py4e.com) 서버에 80 포트로 연결한다.
"웹 브라우저" 역할로 작성된 프로그램이 하기 때문에 HTTP 프로토콜은 GET 명령어를 공백 라인과 함께 보낸다.
![소켓 개념도](images/socket-concept.png){#fig-network-concept}
공백 라인을 보내자마자, 512 문자 덩어리의 데이터를 소켓에서 받아 더 이상 읽을 데이터가 없을 때까지(즉, recv()이 빈 문자열을 반환한다.) 데이터를 출력하는 루프를 작성한다.
프로그램 실행 결과 다음을 얻을 수 있다.
``` bash
HTTP/1.1 200 OK
Date: Wed, 11 Apr 2018 18:52:55 GMT
Server: Apache/2.4.7 (Ubuntu)
Last-Modified: Sat, 13 May 2017 11:22:22 GMT
ETag: "a7-54f6609245537"
Accept-Ranges: bytes
Content-Length: 167
Cache-Control: max-age=0, no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Connection: close
Content-Type: text/plain
But soft what light through yonder window breaks
It is the east and Juliet is the sun
Arise fair sun and kill the envious moon
Who is already sick and pale with grief
```
출력결과는 웹서버가 문서를 기술하기 위해서 보내는 헤더(header)로 시작한다.
예를 들어, `Content-Type` 헤더는 문서가 일반 텍스트 문서(`text/plain`)임을 표기한다.
서버가 헤더를 보낸 후에, 빈 라인을 추가해서 헤더 끝임을 표기하고 나서 실제 파일`romeo.txt`을 보낸다.
이 예제를 통해서 소켓을 통해서 저수준(low-level) 네트워크 연결을 어떻게 하는지 확인할 수 있다.
소켓을 사용해서 웹서버, 메일 서버 혹은 다른 종류의 서버와 통신할 수 있다.
필요한 것은 프로토콜을 기술하는 문서를 찾고 프로토콜에 따라 데이터를 주고받는 코드를 작성하는 것이다.
하지만, 가장 흔히 사용하는 프로토콜은 HTTP (즉, 웹) 프로토콜이기 때문에, 파이썬에는 HTTP 프로토콜을 지원하기 위해 특별히 설계된 라이브러리가 있다.
이것을 통해서 웹상에서 데이터나 문서 검색을 쉽게 할 수 있다.
## HTTP 경유 이미지 가져오기 {#socket-images}
\index{download.file!이미지}
\index{이미지!jpg}
\index{jpg}
상기 예제에서는 파일에 줄바꿈(newline)이 있는 일반 텍스트 파일을 가져왔다.
그리고 나서, 프로그램을 실행해서 데이터를 단순히 화면에 복사했다.
HTTP를 사용하여 이미지를 가져오도록 비슷하게 프로그램을 작성할 수 있다.
프로그램 실행 시에 화면에 데이터를 복사하는 대신에, 데이터를 문자열로 누적하고, 다음과 같이 헤더를 잘라내고 나서 파일에 이미지 데이터를 저장한다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-socket-images
download.file("http://data.pr4e.org/cover3.jpg",
destfile = "data/cover3.jpg",
mode = "wb")
```
### 파이썬
```{pyodide-python}
#| label: py-socket-images
import socket
import time
HOST = 'data.pr4e.org'
PORT = 80
mysock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
mysock.connect((HOST, PORT))
mysock.sendall(b'GET http://data.pr4e.org/cover3.jpg HTTP/1.0\r\n\r\n')
count = 0
picture = b""
while True:
data = mysock.recv(5120)
if len(data) < 1: break
#time.sleep(0.25)
count = count + len(data)
print(len(data), count)
picture = picture + data
mysock.close()
# 헤더 종료지점 탐색 (2 CRLF)
pos = picture.find(b"\r\n\r\n")
print('Header length', pos)
print(picture[:pos].decode())
# 헤더를 건너뛰고 사진 데이터 저장
picture = picture[pos+4:]
fhand = open("images/stuff.jpg", "wb")
fhand.write(picture)
fhand.close()
```
:::
프로그램을 실행하면, 다음과 같은 출력을 생성한다.
``` bash
$ python urljpeg.py
5120 5120
5120 10240
4240 14480
5120 19600
...
5120 214000
3200 217200
5120 222320
5120 227440
3167 230607
Header length 393
HTTP/1.1 200 OK
Date: Wed, 11 Apr 2018 18:54:09 GMT
Server: Apache/2.4.7 (Ubuntu)
Last-Modified: Mon, 15 May 2017 12:27:40 GMT
ETag: "38342-54f8f2e5b6277"
Accept-Ranges: bytes
Content-Length: 230210
Vary: Accept-Encoding
Cache-Control: max-age=0, no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Connection: close
Content-Type: image/jpeg
```
상기 url에 대해서, `Content-Type` 헤더가 문서 본문이 이미지(`image/jpeg`)를 나타내는 것을 볼 수 있다.
프로그램이 완료되면, 이미지 뷰어로 `stuff.jpg` 파일을 열어서 이미지 데이터를 볼 수 있다.
프로그램을 실행하면, `recv()` 메서드를 호출할 때마다 5120 문자는 전달받지 못하는 것을 볼 수 있다.
`recv()` 호출하는 순간마다 웹서버에서 네트워크로 전송되는 가능한 많은 문자를 받을 뿐이다.
매번 5120 문자까지 요청하지만, 1460 혹은 2920 문자만 전송받는다.
결과값은 네트워크 속도에 따라 달라질 수 있다.
recv()` 메서드 마지막 호출에는 스트림 마지막인 1681 바이트만 받았고,
`recv()` 다음 호출에는 0 길이 문자열을 전송받아서, 서버가 소켓 마지막에 `close()` 메서드를 호출하고 더 이상의 데이터가 없다는 신호를 준다.
\index{시간}
\index{정지 시간}
주석 처리한 `time.sleep()`을 풀어줌으로써 `recv()` 연속 호출을 늦출 수 있다. 이런 방식으로 매번 호출 후에 0.25초 기다리게 한다.
그래서, 사용자가 `recv()` 메서드를 호출하기 전에 서버가 먼저 도착할 수 있어서 더 많은 데이터를 보낼 수가 있다.
정지 시간을 넣어서 프로그램을 다시 실행하면 다음과 같다.
``` bash
$ python urljpeg.py
5120 5120
5120 10240
5120 15360
...
5120 225280
5120 230400
207 230607
Header length 393
HTTP/1.1 200 OK
Date: Wed, 11 Apr 2018 21:42:08 GMT
Server: Apache/2.4.7 (Ubuntu)
Last-Modified: Mon, 15 May 2017 12:27:40 GMT
ETag: "38342-54f8f2e5b6277"
Accept-Ranges: bytes
Content-Length: 230210
Vary: Accept-Encoding
Cache-Control: max-age=0, no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Connection: close
Content-Type: image/jpeg
```
`recv()` 메서드 호출의 처음과 마지막을 제외하고, 매번 새로운 데이터를 요청할 때마다 이제 5120 문자가 전송된다.
서버 `send()` 요청과 응용프로그램 `recv()` 요청 사이에 버퍼가 있다.
프로그램에 지연을 넣어 실행하게 될 때, 어느 지점엔가 서버가 소켓 버퍼를 채우고 응용프로그램이 버퍼를 비울 때까지 잠시 멈춰야 된다.
송신하는 응용프로그램 혹은 수신하는 응용프로그램을 멈추게 하는 행위를 "흐름 제어(flow control)"라고 한다.
\index{흐름 제어}
## `httr` 웹페이지 가져오기 {#httr}
수작업으로 소켓 라이브러리를 사용하여 HTTP로 데이터를 주고받을 수 있지만, `httr` 패키지를 사용하여 R에서 동일한 작업을 수행하는 좀 더 간편한 방식이 있다.
`httr`을 사용하여 파일처럼 웹페이지를 다룰 수가 있다. 단순하게 어느 웹페이지를 가져올 것인지만 지정하면 `httr` 라이브러리가 모든 HTTP 프로토콜과 헤더 관련 사항을 처리해 준다.
`GET()` 함수를 사용하여 웹페이지를 열게 되면, 파일처럼 다룰 수 있고 `content()` 함수를 사용해서 데이터를 읽을 수 있다.
프로그램을 실행하면, 파일 내용 출력결과만을 볼 수 있다. 헤더 정보는 여전히 전송되었지만, `GET()` 함수가 헤더를 받아 내부적으로 처리하고, 사용자에게는 단지 데이터만 반환한다.
파이썬으로 웹에서 `romeo.txt` 파일을 읽도록 `urllib`를 사용하여 동일한 기능을 구현할 수 있다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-httr-package
library(httr)
url <- 'http://www.py4inf.com/code/romeo.txt'
response <- GET(url)
content <- content(response, "text", encoding = "UTF-8")
lines <- strsplit(content, split = "\n")[[1]]
lines
```
### 파이썬
```{pyodide-python}
#| label: py-httr-package
import urllib.request
fhand = urllib.request.urlopen('http://www.py4inf.com/code/romeo.txt')
for line in fhand:
print(line.decode().strip())
```
:::
예제로, `romeo.txt` 데이터를 가져와서 파일의 각 단어 빈도를 계산하는 프로그램을 다음과 같이 작성할 수 있다.
웹페이지를 로컬 텍스트로 변환시킨 후에 공백과 `\n`으로 텍스트를 잘게 쪼개고, 단어 빈도수를 `table()`로 계산한 후 단어 빈도수를 확인한다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-httr-package-output
library(httr)
url <- "http://www.py4inf.com/code/romeo.txt"
response <- GET(url)
content <- content(response, "text", , encoding = "UTF-8")
counts <- strsplit(content, "\\s+") |>
unlist() |>
table()
print(counts)
```
### 파이썬
```{pyodide-python}
#| label: py-httr-package-output
import urllib.request
counts = dict()
fhand = urllib.request.urlopen('http://www.py4inf.com/code/romeo.txt')
for line in fhand:
words = line.decode().strip().split()
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts)
```
:::
다시 한번, 웹페이지를 열게 되면, 로컬 파일처럼 웹페이지를 읽을 수 있다.
## HTML 파싱과 웹 스크래핑 {#html-webscraping}
\index{웹!스크래핑}
\index{HTML 피싱}
R `httr` 패키지를 활용하는 일반적인 사례는 웹 **스크래핑(scraping)**이다.
웹 스크래핑은 웹 브라우저를 가장한 프로그램을 작성하는 것이다.
웹페이지를 가져와서, 패턴을 찾아 페이지 내부의 데이터를 꼼꼼히 조사한다.
예로, 구글 같은 검색엔진은 웹 페이지의 소스를 조사해서 다른 페이지로 가는 링크를 추출하고,
그 해당 페이지를 가져와서 링크 추출하는 작업을 반복한다.
이러한 기법으로 구글은 웹상의 거의 모든 페이지를 **거미(spiders)**줄처럼 연결한다.
구글은 또한 발견한 웹페이지에서 특정 페이지로 연결되는 링크 빈도를 사용하여 얼마나 중요한 페이지인지를 측정하고 검색결과에 페이지가 얼마나 높은 순위로 노출되어야 하는지 평가한다.
## 정규 표현식 HTML 파싱 {#webscarping-regex}
HTML을 파싱하는 간단한 방식은 정규 표현식을 사용하여 특정한 패턴과 매칭되는 부속 문자열을 반복적으로 찾아 추출하는 것이다.
여기 간단한 웹페이지가 있다.
``` html
<h1>The First Page</h1>
<p>
If you like, you can switch to the
<a href="http://www.dr-chuck.com/page2.htm">
Second Page</a>.
</p>
```
모양 좋은 정규표현식을 구성해서 다음과 같이 상기 웹페이지에서 링크를 매칭하고 추출할 수 있다.
``` bash
href="http://.+?"
```
작성된 정규 표현식은 "href=http://"로 시작하고, 하나 이상의 문자를 ".+?" 가지고 큰 따옴표를 가진 문자열을 찾는다.
".+?"에 물음표가 갖는 의미는 매칭이 "욕심쟁이(greedy)" 방식보다 "비욕심쟁이(non-greedy)" 방식으로 수행됨을 나타낸다.
비욕심쟁이(non-greedy) 매칭 방식은 가능한 *가장 적게* 매칭되는 문자열을 찾는 방식이고, 욕심 방식은 가능한 *가장 크게* 매칭되는 문자열을 찾는 방식이다.
\index{욕심쟁이}
\index{비욕심쟁이}
추출하고자 하는 문자열이 매칭된 문자열의 어느 부분인지를 표기하기 위해서 정규 표현식에 괄호를 추가하여 다음과 같이 프로그램을 작성한다.
\index{정규 표현식!괄호}
\index{괄호!정규 표현식}
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-httr-extract-urls
library(stringr)
cat("웹사이트 입력 - ")
url <- readLines(file("stdin"), 1)
html <- readLines(url, warn = FALSE)
html <- paste(html, collapse = "\n")
links <- str_extract_all(html, 'href="http://[^"]*"')[[1]]
links <- str_replace_all(links, 'href="', '')
print(links)
```
### 파이썬
```{pyodide-python}
#| label: py-httr-extract-urls
import urllib.request
import re
url = input('웹사이트 입력 - ')
html = urllib.request.urlopen(url).read().decode()
links = re.findall('href="(http://.*?)"', html)
for link in links:
print(link)
```
:::
`str_extract_all` 정규 표현식 함수는 정규 표현식과 매칭되는 모든 문자열 리스트를 추출하여 큰 따옴표 사이에 링크 텍스트만을 반환한다.
프로그램을 실행하면, 다음 출력을 얻게 된다.
``` bash
$ Rscript.exe ./code/extract_link.R
웹사이트 입력 - http://www.dr-chuck.com/page1.htm
[1] "href=\"http://www.dr-chuck.com/page2.htm\""
$ Rscript.exe ./code/extract_link.R
웹사이트 입력 - http://www.py4inf.com/book.htm
[1] "href=\"http://amzn.to/1KkULF3\""
[2] "href=\"http://www.py4e.com/book\""
[3] "href=\"http://amzn.to/1KkULF3\""
[4] "href=\"http://amzn.to/1hLcoBy\""
[5] "href=\"http://amzn.to/1KkV42z\""
[6] "href=\"http://amzn.to/1fNOnbd\""
[7] "href=\"http://amzn.to/1N74xLt\""
[8] "href=\"http://do1.dr-chuck.net/py4inf/EN-us/book.pdf\""
[9] "href=\"http://do1.dr-chuck.net/py4inf/ES-es/book.pdf\""
[10] "href=\"http://do1.dr-chuck.net/py4inf/PT-br/book.pdf\""
[11] "href=\"http://www.xwmooc.net/python/\""
[12] "href=\"http://fanwscu.gitbooks.io/py4inf-zh-cn/\""
[13] "href=\"http://itunes.apple.com/us/book/python-for-informatics/id554638579?mt=13\""
[14] "href=\"http://www-personal.umich.edu/~csev/books/py4inf/ibooks//python_for_informatics.ibooks\""
[15] "href=\"http://www.py4inf.com/code\""
[16] "href=\"http://www.greenteapress.com/thinkpython/thinkCSpy/\""
[17] "href=\"http://allendowney.com/\""
```
정규 표현식은 HTML이 예측 가능하고 잘 구성된 경우에 멋지게 작동한다.
하지만, "망가진" HTML 페이지가 많아서, 정규 표현식만을 사용하는 솔루션은 유효한 링크를 놓치거나 잘못된 데이터만 찾고 끝날 수 있다.
이 문제는 강건한 HTML 파싱 라이브러리를 사용해서 해결될 수 있다.
## `rvest` HTML 파싱 {#rvest-scraping}
\index{rvest}
HTML을 파싱하여 페이지에서 데이터를 추출할 수 있는 R 패키지는 많이 있다. 패키지 각각은 강점과 약점이 있어서 사용자 필요에 따라 취사선택한다.
예로, 간단하게 HTML 입력을 파싱하여 **rvest** 라이브러리를 사용하여 링크를 추출할 것이다.
HTML이 XML처럼 보이고 몇몇 페이지는 XML로 되도록 꼼꼼하게 구축되었지만,
일반적으로 대부분의 HTML이 깨져서 XML 파서가 HTML 전체 페이지를 잘못 구성된 것으로 간주하고 받아들이지 않는다.
`rvest` 패키지는 결점 많은 HTML 페이지에 내성이 있어서 사용자가 필요로 하는 데이터를 쉽게 추출할 수 있게 한다.
`httr` 패키지를 사용하여 페이지를 읽어들이고, `rvest`를 사용해서 앵커 태그(`a`)로부터 `href` 속성을 추출한다.
\index{rvest}
\index{httr}
\index{HTML}
\index{파싱!HTML}
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-httr-rvest-combo
library(httr)
library(xml2)
library(rvest)
cat("웹사이트 입력 - ")
url <- readLines(file("stdin"), 1)
html_content <- read_html(GET(url))
hrefs <- html_nodes(html_content, "a") |>
html_attr("href")
print(hrefs)
```
### 파이썬
```{pyodide-python}
#| label: py-httr-rvest-combo
import urllib.request
from bs4 import BeautifulSoup
url = input('웹사이트 입력 - ')
html = urllib.request.urlopen(url).read()
soup = BeautifulSoup(html, 'html.parser')
tags = soup('a')
for tag in tags:
print(tag.get('href', None))
```
:::
프로그램이 웹 주소를 입력받고, 웹페이지를 열고, 데이터를 읽어서 BeautifulSoup 파서에 전달하고, 그리고 나서 모든 앵커 태그를 불러와서 각 태그별로 `href` 속성을 출력한다.
프로그램을 실행하면, 아래와 같다.
``` bash
$ Rscript.exe ./code/extract_link_rvest.R
웹사이트 입력 - http://www.dr-chuck.com/page1.htm
[1] "http://www.dr-chuck.com/page2.htm"
$ Rscript.exe ./code/extract_link_rvest.R
웹사이트 입력 - http://www.py4inf.com/book.htm
[1] "http://amzn.to/1KkULF3"
[2] "http://www.py4e.com/book"
[3] "http://amzn.to/1KkULF3"
[4] "http://amzn.to/1hLcoBy"
[5] "http://amzn.to/1KkV42z"
[6] "http://amzn.to/1fNOnbd"
[7] "http://amzn.to/1N74xLt"
[8] "http://do1.dr-chuck.net/py4inf/EN-us/book.pdf"
[9] "http://do1.dr-chuck.net/py4inf/ES-es/book.pdf"
[10] "https://twitter.com/fertardio"
[11] "http://do1.dr-chuck.net/py4inf/PT-br/book.pdf"
[12] "https://twitter.com/victorjabur"
[13] "translations/KO/book_009_ko.pdf"
[14] "http://www.xwmooc.net/python/"
[15] "http://fanwscu.gitbooks.io/py4inf-zh-cn/"
[16] "book_270.epub"
[17] "translations/ES/book_272_es4.epub"
[18] "https://www.gitbook.com/download/epub/book/fanwscu/py4inf-zh-cn"
[19] "html-270/"
[20] "html_270.zip"
[21] "http://itunes.apple.com/us/book/python-for-informatics/id554638579?mt=13"
[22] "http://www-personal.umich.edu/~csev/books/py4inf/ibooks//python_for_informatics.ibooks"
[23] "http://www.py4inf.com/code"
[24] "http://www.greenteapress.com/thinkpython/thinkCSpy/"
[25] "http://allendowney.com/"
```
`rvest`을 사용하여 다음과 같이 각 태그별로 다양한 부분을 뽑아낼 수 있다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-rvest-extract-tags
library(httr)
library(rvest)
cat("웹사이트 입력 - ")
url <- readLines(file("stdin"), 1)
response <- GET(url)
html_content <- content(response, "text", encoding = "UTF-8")
html <- read_html(html_content)
tags <- html_nodes(html, "a")
for (tag in tags) {
# Tag
cat("TAG:", toString(xml2::xml_find_first(tag, xpath = ".")), "\n")
# URL
url <- html_attr(tag, "href")
cat("URL:", url, "\n")
# Content
content <- html_text(tag)
cat("Content:", content, "\n")
# Attributes
attrs <- html_attrs(tag)
cat("Attrs:", paste(names(attrs), attrs, sep=": ", collapse=", "), "\n\n")
}
```
### 파이썬
```{pyodide-python}
#| label: py-rvest-extract-tags
import urllib.request
from bs4 import BeautifulSoup
url = input('Enter - ')
html = urllib.request.urlopen(url).read()
soup = BeautifulSoup(html, 'html.parser')
tags = soup('a')
for tag in tags:
print('TAG:', tag)
print('URL:', tag.get('href', None))
content = tag.contents[0] if tag.contents else "None"
print('Content:', content)
print('Attrs:', tag.attrs)
```
:::
상기 프로그램은 다음을 출력한다.
``` bash
$ Rscript.exe ./code/extract_link_tag.R
웹사이트 입력 - http://www.dr-chuck.com/page1.htm
경고메시지(들):
for (name in names(public_methods)) lockBinding(name, public_bind_env)에서:
사용되지 않는 커넥션 3 (stdin)를 닫습니다
TAG: <a href="http://www.dr-chuck.com/page2.htm">
Second Page</a>
URL: http://www.dr-chuck.com/page2.htm
Content:
Second Page
Attrs: href: http://www.dr-chuck.com/page2.htm
```
HTML을 파싱하는 데 `rvest`가 가진 강력한 기능을 예제로 보여줬다. 좀 더 자세한 사항은 <https://rvest.tidyverse.org/>에서 문서와 예제를 살펴보기 바란다.
## 바이너리 파일 읽기 {#GET-binary}
이미지나 비디오 같은 텍스트가 아닌 (혹은 바이너리) 파일을 가져올 때가 종종 있다.
일반적으로 이런 파일 데이터를 출력하는 것은 유용하지 않다.
하지만, `download.file()` 함수를 사용하여, 하드 디스크 로컬 파일에 URL 사본을 쉽게 만들 수 있다.
\index{바이너리 파일}
`download.file()` 함수 내부에 인터넷에 공개된 이미지 `url`을 적고, `destfile`에는 로컬 파일에 저장할 파일명을 적어준다.
중요한 것은 텍스트가 아니라 바이너리 이미지라 `mode = "wb"`를 지정한다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-download-files
url <- "https://www.dr-chuck.com/py4inf/cover.jpg"
destination_file <- "cover.jpg"
download.file(url, destfile = destination_file, mode = "wb")
```
### 파이썬
```{pyodide-python}
#| label: py-download-files
import urllib.request
url = 'http://www.py4inf.com/cover.jpg'
img = urllib.request.urlopen(url).read()
with open('cover.jpg', 'wb') as fhand:
fhand.write(img)
```
:::
작성된 프로그램은 네트워크로 모든 데이터를 한번에 읽어서 컴퓨터 `cover.jpg` 파일을 다운로드받아 로컬 디스크에 지정한 디렉터리 파일명으로 데이터를 쓴다.
이 방식은 파일 크기가 사용자 컴퓨터의 메모리 크기보다 작다면 정상적으로 작동한다.
![바이너리 파일 다운로드](images/py4inf_cover.jpeg){#fig-network-binary fig-align="center" width="213"}
하지만, 오디오 혹은 비디오 파일 대용량이면, 상기 프로그램은 멈추거나 사용자 컴퓨터 메모리가 부족할 때 극단적으로 느려질 수 있다.
메모리 부족을 회피하기 위해서, 데이터를 블록 혹은 버퍼로 가져와서, 다음 블록을 가져오기 전에 디스크에 각각의 블록을 쓴다.
이런 방식으로 사용자가 가진 모든 메모리를 사용하지 않고 어떠한 크기 파일도 읽어올 수 있다.
[Fannie Mae and Freddie Mac](https://www.fhfa.gov/)은 주택금융공사에 상응하는 기관으로
주택 구매자에게 저리의 장기 고정금리 주택담보대출을 제공하고 주택 모기지를 재융자하는 역할을 통해 주택 금융 시장의 안정을 도모하고 국민의 주거 안정에 기여한다.
[웹사이트](https://www.fhfa.gov/DataTools/Downloads)에 공공데이터로 인터넷에 공개된 주택담보대출 나름 크기가 큰 데이터를 가져온다.
:::{.panel-tabset}
### R
```{webr-r}
#| label: r-download-big-file
library(httr)
url <- 'https://www.fhfa.gov/DataTools/Downloads/Documents/Enterprise-PUDB/National-File-A/2022_SFNationalFileA2022.zip'
resp <- GET(url, write_disk("2022_SFNationalFileA2022.zip", overwrite = TRUE))
# Check if the request was successful and report the size
if (status_code(resp) == 200) {
file_info <- file.info("2022_SFNationalFileA2022.zip")
size <- file_info$size
message(size, "문자 복사완료!")
} else {
message("파일 다운로드 실패!")
}
#> 20232194문자 복사완료!
```
### 파이썬
```{pyodide-python}
#| label: py-download-big-file
import urllib.request
url = 'https://www.fhfa.gov/DataTools/Downloads/Documents/Enterprise-PUDB/National-File-A/2022_SFNationalFileA2022.zip'
zip_file = urllib.request.urlopen(url)
fhand = open('2022_SFNationalFileA2022.zip', 'wb') # Open in binary write mode
size = 0
while True:
info = zip_file.read(100000)
if len(info) < 1:
break
size += len(info)
fhand.write(info)
fhand.close()
print(size, '문자 복사완료!')
#> 20232194문자 복사완료!
```
:::
UNIX 혹은 매킨토시 컴퓨터를 가지고 있다면, 다음과 같이 상기 동작을 수행하는 명령어가 운영체제 자체에 내장되어 있다.
\index{curl}
``` bash
curl -O https://www.dr-chuck.com/py4inf/cover.jpg
```
## 용어 정의 {#network-terminology}
- **BeautifulSoup**: 파이썬 라이브러리로 HTML 문서를 파싱하고 브라우저가 일반적으로 생략하는 HTML의 불완전한 부분을 보정하여 HTML 문서에서 데이터를 추출한다. [www.crummy.com](www.crummy.com) 사이트에서 BeautifulSoup 코드를 다운로드 받을 수 있다.
\index{뷰티풀수프}
\index{BeautifulSoup}
- **`rvest`**: 파이썬 BeautifulSoup에 대응되는 R 크롤링 패키징
\index{rvest}
- **포트(port)**: 서버에 소켓 연결을 만들 때, 사용자가 무슨 응용프로그램을 연결하는지 나타내는숫자. 예로, 웹 트래픽은 통상 80 포트, 전자우편은 25 포트를 사용한다.
\index{포트}
- **스크래핑(scraping)**: 프로그램이 웹 브라우저를 가장하여 웹페이지를 가져와서 웹 페이지의 내용을 검색한다. 종종 프로그램이 한 페이지의 링크를 따라 다른 페이지를 찾게 된다. 그래서, 웹페이지 네트워크 혹은 소셜 네트워크 전체를 훑을 수 있다.
\index{스크래핑}
- **소켓(socket)**: 두 응용프로그램 사이 네트워크 연결. 두 응용프로그램은 양방향으로 데이터를 주고받는다.
\index{소켓}
- **스파이더(spider)**:검색 색인을 구축하기 위해서 한 웹페이지를 검색하고, 그 웹페이지에 링크된 모든 페이지 검색을 반복하여 인터넷에 있는 거의 모든 웹페이지를 가져오기 위해서 사용되는 검색엔진 행동.
\index{스파이더}
## 연습문제 {#network-ex .unnumbered}
1. 소켓 프로그램 `socket1.py`을 변경하여 임의 웹페이지를 읽을 수 있도록 URL을 사용자가 입력하도록 수정한다.
`split('/')`을 사용하여 URL을 컴포넌트로 쪼개서 소켓 `connect` 호출에 대해 호스트 명을 추출할 수 있다.
사용자가 적절하지 못한 형식 혹은 존재하지 않는 URL을 입력하는 경우를 처리할 수 있도록 `try`, `except`를 사용하여 오류 검사기능을 추가한다.
2. 소켓 프로그램을 변경하여 전송받은 문자를 계수(count)하고 3000 문자를 출력한 후에 그 이상 텍스트 출력을 멈추게 한다.
프로그램은 전체 문서를 가져와야 하고, 전체 문자를 계수(count)하고, 문서 마지막에 문자 계수(count)결과를 출력해야 한다.
3. `httr` 패키지를 사용하여 이전 예제를 반복한다. (1) 사용자가 입력한 URL에서 문서 가져오기 (2) 3000 문자까지 화면에 보여주기 (3) 문서의 전체 문자 계수(count)하기.
이 연습문제에서 헤더에 대해서는 걱정하지 말고, 단지 문서 본문에서 첫 3000 문자만 화면에 출력한다.
4. `urllinks.R` 프로그램을 변경하여 가져온 HTML 문서에서 문단(p) 태그를 추출하고 프로그램의 출력물로 문단을 계수(count)하고 화면에 출력한다.
문단 텍스트를 화면에 출력하지 말고 단지 숫자만 센다. 작성한 프로그램을 작은 웹페이지뿐만 아니라 조금 큰 웹 페이지에도 테스트한다.
5. (고급) 소켓 프로그램을 변경하여 헤더와 빈 라인 다음에 데이터만 보이도록 개발한다. `recv`는 라인이 아니라 문자(새줄(newline)과 모든 문자)를 전송받는다는 것을 기억한다.