Posts 크롤링(Crawling) 기초(2)
Post
Cancel

크롤링(Crawling) 기초(2)

1. 네이버 금융


1.1 네이버 금융의 환율 Crawling


1.2 파싱(parsing)

1
2
3
4
5
6
7
8
9
from bs4 import BeautifulSoup
from urllib.request import urlopen

url = 'https://finance.naver.com/marketindex/'
page = urlopen(url)

soup = BeautifulSoup(page, 'html.parser')

print(soup.prettify())
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
<script language="javascript" src="/template/head_js.nhn?referer=info.finance.naver.com&amp;menu=marketindex&amp;submenu=market">
</script>
<script src="/js/info/jindo.min.ns.1.5.3.euckr.js" type="text/javascript">
</script>
<script src="/js/jindo.1.5.3.element-text-patch.js" type="text/javascript">
</script>
<div id="container" style="padding-bottom:0px;">
 <script language="JavaScript" src="/js/flashObject.js?20201007184346">
 </script>
 <div class="market_include">
  <div class="market_data">
   <div class="market1">
    <div class="title">
     <h2 class="h_market1">
      <span>
       환전 고시 환율
      </span>
     </h2>
    </div>
    <!-- data -->
    <div class="data">
     <ul class="data_lst" id="exchangeList">
      <li class="on">
       <a class="head usd" href="/marketindex/exchangeDetail.nhn?marketindexCd=FX_USDKRW" onclick="clickcr(this, 'fr1.usdt', '', '', event);">
        <h3 class="h_lst">
         <span class="blind">
          미국 USD
         </span>
        </h3>
        <div class="head_info point_dn">
         <span class="value">
          1,145.80
         </span>
         <span class="txt_krw">
          <span class="blind">
           원
          </span>
         </span>
         <span class="change">
          0.20
         </span>
         <span class="blind">
          하락
         </span>
        </div>
       </a>
       <a class="graph_img" href="/marketindex/exchangeDetail.nhn?marketindexCd=FX_USDKRW" onclick="clickcr(this, 'fr1.usdc', '', '', event);">
        <img alt="" height="153" src="https://ssl.pstatic.net/imgfinance/chart/marketindex/FX_USDKRW.png" width="295"/>
       </a>
       <div class="graph_info">
        <span class="time">
         2020.10.14 14:37
        </span>
        <span class="source">
         하나은행 기준
        </span>
        <span class="count">
         고시회차
         <span class="num">
          293
         </span>
         회
        </span>
       </div>
      </li>
...

var isIE = (navigator.userAgent.toLowerCase().indexOf("msie")!=-1 && window.document.all) ? true:false;
if (isIE) {
	document.attachEvent('onmousedown', gnbLayerClose);
} else {
	window.addEventListener('mousedown', gnbLayerClose);
}
</script>
  • 시장지표 페이지 html을 파싱 해옴


1.3 Span의 Value 클래스 찾기

1
soup.find_all('span', 'value')
1
2
3
4
5
6
7
8
9
10
11
12
[<span class="value">1,145.80</span>,
 <span class="value">1,086.68</span>,
 <span class="value">1,345.51</span>,
 <span class="value">170.15</span>,
 <span class="value">105.5100</span>,
 <span class="value">1.1744</span>,
 <span class="value">1.2985</span>,
 <span class="value">93.5300</span>,
 <span class="value">40.2</span>,
 <span class="value">1334.11</span>,
 <span class="value">1888.5</span>,
 <span class="value">69759.23</span>]


1
soup.find_all('span', 'value')[0].string
1
'1,145.80'
  • 해당 위치의 순서를 보니 첫번째가 미국 환율인것같다
  • 2020년 10월 14일 기준 1,144.40이 미국 환율임


2. 네이버 책


2.1 네이버 책의 IT 전문서적 Top 100 Crawling


2.2 파싱(Parsing)

1
2
3
4
5
6
url = 'https://book.naver.com/category/index.nhn?cate_code=280020&list_type=list&tab=top100'
page = urlopen(url)

soup = BeautifulSoup(page, 'html.parser')

print(soup.prettify())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE HTML>
<html lang="ko">
 <head>
  <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
  <meta content="IE=edge" http-equiv="X-UA-Compatible"/>
  <title>
   TOP100, IT 전문서, 컴퓨터/IT : 네이버 책
  </title>
  <meta content="article" property="og:type"/>
  <meta content="네이버 책" property="og:title"/>
  <meta content="http://book.naver.com/category/index.nhn?cate_code=280020&amp;list_type=list&amp;tab=top100" property="og:url"/>
  <meta content="https://ssl.pstatic.net/static/m/book/icons/book_og_270x270.png" property="og:image"/>
  <meta content="책으로 만나는 새로운 세상" property="og:description"/>
  <meta content="" property="og:article:thumbnailUrl"/>
  <meta content="네이버 책" property="og:article:author"/>
  <meta content="https://book.naver.com" property="og:article:author:url"/>
  <link href="https://www.naver.com/favicon.ico?book" rel="shortcut icon" type="image/x-icon"/>
 ...

	}).attach(window, "load");
  </script>
 </body>
</html>


2.4 책 제목

  • 책 재목은 a 태그의 class = ‘N=a:bta.title’임


2.5 Find_all

1
soup.find_all(class_ = 'N=a:bta.title')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[<a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=15431390">한국어 임베딩</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=15478327">Do it! 정직하게 코딩하며 배우는 딥러닝 입문</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=10835404">데이터 분석 전문가 가이드</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=6379151">SQL 전문가 가이드</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=15372757">리액트를 다루는 기술</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=15372183">나는 LINE 개발자입니다</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=15303798">클린 아키텍처</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=14519321">한 권으로 끝내는 아두이노 입문 + 실전(종합편)</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=15260256">다시 미분 적분</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=15085920">파이썬 날코딩으로 알고 짜는 딥러닝</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=15058325">소문난 명강의 김기현의 자연어 처리 딥러닝 캠프</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=15052904">Do it! 점프 투 파이썬</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=15007773">오브젝트</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=15028688">혼자 공부하는 파이썬</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=15028694">혼자 공부하는 C 언어</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=15028693">혼자 공부하는 자바</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=14922892">머신 러닝 교과서 with 파이썬, 사이킷런, 텐서플로</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=14922211">파이썬 라이브러리를 활용한 데이터 분석</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=14829160">알고리즘 트레이닝</a>,
 <a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=14797086">밑바닥부터 시작하는 딥러닝 2</a>]
  • Find_all 명령어로 class 속성을 검색하니 책 제목과 detail url이 나옴


2.6 Detail url과 제목

1
soup.find_all(class_ = 'N=a:bta.title')[0]
1
<a class="N=a:bta.title" href="http://book.naver.com/bookdb/book_detail.nhn?bid=15431390">한국어 임베딩</a>
  • detail url과 책 제목


1
soup.find_all(class_ = 'N=a:bta.title')[0]['href']
1
'http://book.naver.com/bookdb/book_detail.nhn?bid=15431390'
  • href 속성으로 detail url만 가져옴


1
soup.find_all(class_ = 'N=a:bta.title')[0].string
1
'한국어 임베딩'
  • stirng 명령어로 책 제목만 가져옴


2.7 책 제목 저장

1
2
title = [title.string for title in soup.find_all(class_ = 'N=a:bta.title')]
title
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
['한국어 임베딩',
 'Do it! 정직하게 코딩하며 배우는 딥러닝 입문',
 '데이터 분석 전문가 가이드',
 'SQL 전문가 가이드',
 '리액트를 다루는 기술',
 '나는 LINE 개발자입니다',
 '클린 아키텍처',
 '한 권으로 끝내는 아두이노 입문 + 실전(종합편)',
 '다시 미분 적분',
 '파이썬 날코딩으로 알고 짜는 딥러닝',
 '소문난 명강의 김기현의 자연어 처리 딥러닝 캠프',
 'Do it! 점프 투 파이썬',
 '오브젝트',
 '혼자 공부하는 파이썬',
 '혼자 공부하는 C 언어',
 '혼자 공부하는 자바',
 '머신 러닝 교과서 with 파이썬, 사이킷런, 텐서플로',
 '파이썬 라이브러리를 활용한 데이터 분석',
 '알고리즘 트레이닝',
 '밑바닥부터 시작하는 딥러닝 2']
  • 리스트 컴프리행션을 사용하여 책 제목만 리스트로 저장함


2.8 평점 확인

1
soup.find('dd', 'txt_desc')
1
2
3
4
5
6
7
8
9
10
11
12
13
<dd class="txt_desc">
<div class="review_point">
<span style="width:100%;"></span>
</div>
								10.0<span class="bar"> | </span>
<a class="N=a:bta.review" href="http://book.naver.com/bookdb/review.nhn?bid=15431390">네티즌리뷰 1건</a>
<span class="bar">|</span>
<a class="N=a:bta.bookbuy" href="javascript:showBuyLayerByBid('15431390')" id="buy_btn_15431390" onclick="return showAdultLayer('15431390', 'false', 'false', 'false');"><img alt="도서구매" class="btn v2" height="20" id="btn_buy_15431390" src="https://ssl.pstatic.net/static/book/image/btn_book_buy.gif" title="구매 가능한 도서입니다." width="48"/></a>
<strike>35,000원</strike> → <em class="price">31,500원(-10%)</em>
<!--  ebook 가격 정보 -->
<a class="N=a:bta.ebookbuy" href="javascript:showEbookBuyLayerByBid('15431390')" id="ebook_buy_btn_15431390" onclick="return showAdultLayer('15431390', 'false', 'false', 'false');"><img alt="ebook구매" class="btn v2" height="20" id="btn_ebook_buy_15431390" src="https://ssl.pstatic.net/static/book/image/btn_ebook_buy5.png" title="구매 또는 대여 가능한 eBook입니다." width="45"/></a>25,200원
											<!--  audio book 가격 정보 -->
</dd>
  • 위에서 했던것 처럼 엘리먼트를 사용해서 평점을 클릭해봐도 따로 tag로 되어있지 않음


2.9 평점가져오기

1
soup.find('dd', 'txt_desc').get_text()
1
'\n\n\n\r\n\t\t\t\t\t\t\t\t10.0 | \n네티즌리뷰 1건\n|\n\n35,000원 → 31,500원(-10%)\n\n25,200원\r\n\t\t\t\t\t\t\t\t\t\t\t\n'
  • get_text로 text만 따로 가져왔을때 맨 앞에 10.0 부분을 가져와야함


2.10 정규 표현식 사용

1
2
3
4
5
6
import re

tmp = soup.find('dd', 'txt_desc').get_text()

result = re.search('\d+.(\d+)?', tmp).group()
result
1
'10.0'
  • get_text를 한 내용에서 정규표현식을 사용하여 해당 평점만 따로 가져옴
  • ()?는 괄호 안에 문자가 있을수도 있고 없을수도 있다는 뜻


2.11 전체 평점 가져오기

1
2
3
4
5
6
7
8
points = []

for each in soup.find_all('dd', 'txt_desc'):
    result = re.search('\d+.(\d+)?', each.get_text()).group()
    
    points.append(result)

print(points)
1
['10.0', '10.0', '0.0', '7.5', '0.0', '9.5', '0.0', '0.0', '9.0', '10.0', '0.0', '10.0', '0.0', '9.0', '0.0', '10.0', '0.0', '0.0', '10.0', '9.0']
  • 위의 코드를 for문을 이용하여 모든 책에 적용하여 평점을 크롤링해옴
  • soup.find에서 soup.find_all로 바꾸어 모든것에 대한 내용을 가져온것에 주의


2.12 출판연도

1
soup.find('dd', 'txt_block').get_text()
1
'\n 이기창 저 |\n에이콘출판 | 2019.09.26'
  • txt_block tag에는 저자, 출판사, 출판연도가 있음
  • 여기도 정규표현식을 사용하여 출판연도만 가져와봄


1
2
3
4
tmp = soup.find('dd', 'txt_block').get_text()

result = re.search('\d+.\d+.\d+', tmp).group()
result
1
'2019.09.26'
  • \d+.\d+.\d+ 는 숫자.숫자.숫자 로 생긴 데이터란 이야기


2.13 전체 출판연도

1
2
3
4
5
6
7
8
date = []

for each in soup.find_all('dd', 'txt_block'):
    result = re.search('\d+.\d+.\d+', each.get_text()).group()
    
    date.append(result)

print(date)
1
['2019.09.26', '2019.09.20', '2019.09.06', '2019.09.06', '2019.08.31', '2019.08.23', '2019.08.20', '2019.08.15', '2019.07.31', '2019.07.15', '2019.07.01', '2019.06.20', '2019.06.17', '2019.06.10', '2019.06.10', '2019.06.10', '2019.05.24', '2019.05.20', '2019.05.09', '2019.05.01']
  • 평점을 가져온것 처럼 for문과 find_all을 사용하여 출판연도를 가져옴


2.14 데이터프레임 생성

1
2
3
4
import pandas as pd

bestseller = pd.DataFrame({'Title' : title, 'Points' : points, 'Date' : date})
bestseller
TitlePointsDate
0한국어 임베딩10.02019.09.26
1Do it! 정직하게 코딩하며 배우는 딥러닝 입문10.02019.09.20
2데이터 분석 전문가 가이드0.02019.09.06
3SQL 전문가 가이드7.52019.09.06
4리액트를 다루는 기술0.02019.08.31
5나는 LINE 개발자입니다9.52019.08.23
6클린 아키텍처0.02019.08.20
7한 권으로 끝내는 아두이노 입문 + 실전(종합편)0.02019.08.15
8다시 미분 적분9.02019.07.31
9파이썬 날코딩으로 알고 짜는 딥러닝10.02019.07.15
10소문난 명강의 김기현의 자연어 처리 딥러닝 캠프0.02019.07.01
11Do it! 점프 투 파이썬10.02019.06.20
12오브젝트0.02019.06.17
13혼자 공부하는 파이썬9.02019.06.10
14혼자 공부하는 C 언어0.02019.06.10
15혼자 공부하는 자바10.02019.06.10
16머신 러닝 교과서 with 파이썬, 사이킷런, 텐서플로0.02019.05.24
17파이썬 라이브러리를 활용한 데이터 분석0.02019.05.20
18알고리즘 트레이닝10.02019.05.09
19밑바닥부터 시작하는 딥러닝 29.02019.05.01
  • IT 전문서적 top 100의 1페이지를 크롤링하여 데이터프레임으로 정리함


2.15 데이터프레임 type 변경

1
2
3
pd.options.display.float_format = '{:.2f}'.format
bestseller['Points'] = bestseller['Points'].astype(float)
bestseller
TitlePointsDate
0한국어 임베딩10.002019.09.26
1Do it! 정직하게 코딩하며 배우는 딥러닝 입문10.002019.09.20
2데이터 분석 전문가 가이드0.002019.09.06
3SQL 전문가 가이드7.502019.09.06
4리액트를 다루는 기술0.002019.08.31
5나는 LINE 개발자입니다9.502019.08.23
6클린 아키텍처0.002019.08.20
7한 권으로 끝내는 아두이노 입문 + 실전(종합편)0.002019.08.15
8다시 미분 적분9.002019.07.31
9파이썬 날코딩으로 알고 짜는 딥러닝10.002019.07.15
10소문난 명강의 김기현의 자연어 처리 딥러닝 캠프0.002019.07.01
11Do it! 점프 투 파이썬10.002019.06.20
12오브젝트0.002019.06.17
13혼자 공부하는 파이썬9.002019.06.10
14혼자 공부하는 C 언어0.002019.06.10
15혼자 공부하는 자바10.002019.06.10
16머신 러닝 교과서 with 파이썬, 사이킷런, 텐서플로0.002019.05.24
17파이썬 라이브러리를 활용한 데이터 분석0.002019.05.20
18알고리즘 트레이닝10.002019.05.09
19밑바닥부터 시작하는 딥러닝 29.002019.05.01
  • 평점 부분을 소수점 0자리까지 출력하게 하고, float형으로 변환함


3. URL 한글 인코딩


3.1 여명의 눈동자

https://ko.wikipedia.org/wiki/여명의_눈동자


3.2 Quote & html 문서 읽기

1
2
3
4
5
6
7
8
9
10
import urllib
from urllib.request import Request

html = 'https://ko.wikipedia.org/wiki/{search_words}'
req = Request(html.format(search_words = urllib.parse.quote('여명의_눈동자')));

respones = urlopen(req)

soup = BeautifulSoup(respones, 'html.parser')
soup
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>

<html class="client-nojs" dir="ltr" lang="ko">
<head>
<meta charset="utf-8"/>
<title>여명의 눈동자 - 위키백과, 우리 모두의 백과사전</title>
<script>document.documentElement.className="client-js";RLCONF={"wgBreakFrames":!1,"wgSeparatorTransformTable":["",""],"wgDigitTransformTable":["",""],"wgDefaultDateFormat":"ko","wgMonthNames":["","1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],"wgRequestId":"ddc74774-7c08-4982-b5ce-d4538c74eb47","wgCSPNonce":!1,"wgCanonicalNamespace":"","wgCanonicalSpecialPageName":!1,"wgNamespaceNumber":0,"wgPageName":"여명의_눈동자","wgTitle":"여명의 눈동자",...

<script>(RLQ=window.RLQ||[]).push(function(){mw.config.set({"wgBackendResponseTime":187,"wgHostname":"mw2371"});});</script>
</body></html>
  • url이 이상하게 나올땐 urllib.parse.quote를 사용하여 해결함
  • 똑같이 Beautifulsoup을 사용하여 html을 읽음


3.3 등장인물 tag인 ul만 출력

1
2
3
for each_ul in soup.find_all('ul'):
    print('================================================')
    print(each_ul)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
================================================
<ul style="display: inline"><li style="display: inline">소설</li><li>《여명의 눈동자》</li></ul>
================================================
...
<ul><li><a href="/wiki/%EC%B1%84%EC%8B%9C%EB%9D%BC" title="채시라">채시라</a> : 윤여옥 역 (아역: <a href="/wiki/%EA%B9%80%EB%AF%BC%EC%A0%95_(1982%EB%85%84)" title="김민정 (1982년)">김민정</a>)</li>
<li><a href="/wiki/%EB%B0%95%EC%83%81%EC%9B%90" title="박상원">박상원</a> : 장하림(하리모토 나츠오) 역 (아역: <a href="/wiki/%EA%B9%80%ED%83%9C%EC%A7%84_(%EC%88%98%ED%95%84%EA%B0%80)" title="김태진 (수필가)">김태진</a>)</li>
<li><a href="/wiki/%EC%B5%9C%EC%9E%AC%EC%84%B1_(%EB%B0%B0%EC%9A%B0)" title="최재성 (배우)">최재성</a> : 최대치(사카이) 역 (아역: <a href="/wiki/%EC%9E%A5%EB%8D%95%EC%88%98_(%EB%B0%B0%EC%9A%B0)" title="장덕수 (배우)">장덕수</a>)</li></ul>
================================================
...
================================================
<ul class="noprint" id="footer-icons">
<li id="footer-copyrightico"><a href="https://wikimediafoundation.org/"><img alt="Wikimedia Foundation" height="31" loading="lazy" src="/static/images/footer/wikimedia-button.png" srcset="/static/images/footer/wikimedia-button-1.5x.png 1.5x, /static/images/footer/wikimedia-button-2x.png 2x" width="88"/></a></li>
<li id="footer-poweredbyico"><a href="https://www.mediawiki.org/"><img alt="Powered by MediaWiki" height="31" loading="lazy" src="/static/images/footer/poweredby_mediawiki_88x31.png" srcset="/static/images/footer/poweredby_mediawiki_132x47.png 1.5x, /static/images/footer/poweredby_mediawiki_176x62.png 2x" width="88"/></a></li>
</ul>
  • 여명의 눈동자에 출연한 등장인물 이름을 가져와보자
  • 등장인물의 tag는 ul인데, 해당 tag를 찾으면 엄청 많음


3.4 그 중 내가 원하는 위치

1
soup.find_all('ul')[3]
1
2
3
<ul><li><a href="/wiki/%EC%B1%84%EC%8B%9C%EB%9D%BC" title="채시라">채시라</a> : 윤여옥 역 (아역: <a href="/wiki/%EA%B9%80%EB%AF%BC%EC%A0%95_(1982%EB%85%84)" title="김민정 (1982년)">김민정</a>)</li>
<li><a href="/wiki/%EB%B0%95%EC%83%81%EC%9B%90" title="박상원">박상원</a> : 장하림(하리모토 나츠오) 역 (아역: <a href="/wiki/%EA%B9%80%ED%83%9C%EC%A7%84_(%EC%88%98%ED%95%84%EA%B0%80)" title="김태진 (수필가)">김태진</a>)</li>
<li><a href="/wiki/%EC%B5%9C%EC%9E%AC%EC%84%B1_(%EB%B0%B0%EC%9A%B0)" title="최재성 (배우)">최재성</a> : 최대치(사카이) 역 (아역: <a href="/wiki/%EC%9E%A5%EB%8D%95%EC%88%98_(%EB%B0%B0%EC%9A%B0)" title="장덕수 (배우)">장덕수</a>)</li></ul>
  • 주요 등장인물이 나오는 위치는 4번째(파이썬은 0부터 시작하니 3)
  • 페이지가 바뀔떄마다 해당 위치는 변경되니 주의


3.5 등장인물 이름

1
type(soup.find_all('ul')[3])
1
bs4.element.Tag
  • 해당 데이터의 타입은 tag임


1
2
tmp = soup.find_all('ul')[3]
[each.string for each in tmp.find_all('a')][::2]
1
['채시라', '박상원', '최재성']
  • a 태그의 string이 등장인물이름이라는 것을 알수 있었음
  • 리스트 컴프리행션으로 등장인물이름만 리스트로 저장함
  • 주요 인물의 아역 배우가 뒤에 붙기에 홀수만 출력 하게 [::2]를 붙임
This post is licensed under CC BY 4.0 by the author.