2022년 11월 15일, 전날에 스파르타 코딩 클럽의 왕초보 SQL 1주차 과정을 끝내고, 과제를 제출한 뒤 Python 강의를 시작했다.
사실 대학교 1학년 강의 중 '프로그래밍 및 실습'이라는 강의가 있었다. 해당 강의에선 Python의 기초를 충분히 다진 상태였다. 그래서 이것도 왕초보로 들어야 할까? 했지만, 그 때는 프로그래밍에 대해 Python으로 배운 것일 뿐이였고 지금은 데이터 분석을 Python으로 배우는 것이기에 다르다 생각하여 그냥 왕초보로 들었다.
결론부터 말하자면 내 머리는 하얀 백지에 10pt로 한 줄 쓰여진 상태였다. 나는 정말 극히 일부만 배웠구나 싶었다.
이 글엔 스파르타 코딩 클럽에서 시작한 Python, 1주차에 배운 것에 대한 개발일지를 작성하였다.
강의의 1주차 수업 목표는 이러했다.
[수업 목표] 1. 구글 Colab 환경에 익숙해진다. 2. 파이썬 기초 문법을 익힌다. 3. 간단한 크롤링을 실습해본다. 4. 엑셀 다루기, 파일 이름바꾸기, 이미지 다운받기를 해본다. |
아래부터는 수업을 요약한 내용이다.
1. 서론
- 구글 Colab 환경 이해하기
구글 Colab : 온라인에서 파이썬 데이터분석을 학습할 수 있는 환경
* 브라우저 상에서 파이썬 코딩을 할 수 있게 해준다.
* 내 컴퓨터에 파이썬을 설치 할 필요가 없고 인터넷만 되면 어디서든 접근 가능하며 내 컴퓨터보다 빠르다는 이점이 있다.
* Google Colab은 무료로 제공되는 만큼 '연속 연결 시간 최대 90분, 하루 이용 제한 12시간'이라는 사용제한이 있다.
- 왜 파이썬인가?
문법이 직관적인 편이다
ex) print('hello world') -> hello world를 보여줘라!
데이터분석, 업무자동화, 서버 프로그래밍까지 아주 광범위하게 쓰이고 있다.
* 5주 수업을 마칠 때 쯤이면, "익숙해졌다"라고 자신있게 이야기할 수 있을 것이라 했다.
2. 본론
1) 파이썬 기초
- 변수
기본적으로 변수란 값을 담아두는 박스이다.
따라서, 아래처럼 사용 가능하다.
a = 2 # 변수 a에 숫자 2 대입
b = 3 # 변수 b에 숫자 3 대입
print(a+b) # a+b 출력 -> 결과 5
그렇다면 왜 변수를 쓰는가?
변수의 값만 바꿔서 관련된 모든 명령을 일일이 고칠 필요를 없애기 위해서이다.
a = 2
b = 5
print(2+5) # 결과 7
print(2+5+5) # 결과 12
print(2+5+5+5) # 결과 17
print(a+b) # 결과 7
print(a+b+b) # 결과 12
print(a+b+b+b) # 결과 17
위의 출력물들의 결과는 같다.
하지만 5를 다른 값으로 변경할 경우, 위의 명령어들은 5들을 하나하나 바꿔줘야 하지만, 아래의 명령어들은 b의 변수를 바꿔주기만 해도 값이 변한다.
한마디로, 편의성을 위해서이다.
* 변수의 이름은 마음대로 지을 수 있음
* 하지만 '편의성'을 위한 만큼, 변수의 용도에 맞춰 이름을 짓는 것이 좋을 것임
- 자료형
자료형은 대표적으로 4가지(문자, 숫자, 리스트, 딕셔너리)가 있다.
본 문단에선 기본적인 문자,숫자를 제외하고 리스트, 딕셔너리에 대해 다루겠다.
* List 형
a_list = ['사과','배','감','수박'] # [~,~,~,~~~~]
print(a_list) # 결과 ['사과','배','감','수박']
print(a_list[1]) # 코딩을 할 땐 0부터 센다. -> a_list[1] : a_list의 2번째
# a_list 의 2번째 원소 추출 -> 결과 '배'
a_list.append('딸기') # a_list 에 '딸기'원소 추가
print(a_list) # 결과 ['사과','배,'감','수박','딸기']
print(a_list[4]) # a_list 의 5번째 원소 추출 -> 결과 '딸기'
위와 같이, 리스트 형에선 순서가 중요하다.
※주의사항※ 1.대괄호, 소괄호 등에 대해 표준화 하지 말 것 ex) 리스트는 대괄호를 써서 쓰는가 보구나(o) 대괄호는 언제쓰고 소괄호는 언제 쓰는거지?(x) 2.'append'등 명령어 외울 필요 없음 물론 어느 정도는 외워야겠지만 구글에 검색할 수 있음 |
* Dictionary 형
a_dict = {'name' : '철수', 'age' : 15} #{key1 : value1, key2 : value2 ~~}
print(a_dict) # 결과 {'name' : '철수', 'age' : 15}
print(a_dict['name']) # 'name'에 대한 value 추출 -> 결과 '철수'
print(a_dict['age']) # 'age'에 대한 value 추출 -> 결과 15
a_dict['height'] = 180 # 'height' = 180 추가
print(a_dict) # 결과 {'name' : '철수', 'age' : 15, 'height' : 180}
print(a_dict['height']) # 'height'에 대한 value 추출 -> 결과 180
위와 같이, 딕셔너리 형은 { key : value } 형태가 중요하다.
* 조금 더 나아가기 : Dictionary 형과 List 형의 조합
a_list = [{'name' : '철수', 'age' : 15},{'name' : '영희', 'age' : 25}]
print(a_list) # 결과 [{'name' : '철수', 'age' : 15},{'name' : '영희', 'age' : 25}]
print(a_list[0]) # a_list 의 1번째 원소 추출 -> 결과 {'name': '철수', 'age': 15}
print(a_list[0]['age']) # a_list 의 1번째 원소의 'age'의 value 추출 -> 결과 15
print(a_list[1]) # a_list 의 2번째 원소 추출 -> 결과 {'name': '영희', 'age': 25}
print(a_list[1]['name']) # a_list 의 2번째 원소의 'name'의 value 추출 -> 결과 25
보통 리스트와 딕셔너리는 리스트 안에 딕셔너리가 들어가 있는 형태로 많이 나옴
- 함수
수학에서의 함수는 보통 f(x) =2x+3 같이 x에 값을 넣으면 그에 맞는 값이 나오는 것이다.
하지만 프로그래밍을 하는 이유가 컴퓨터에게 주로 반복적인 일을 시키는 것인 만큼,
프로그래밍에서의 함수는 정해진 동작을 하게 하는 것이다.
def sum(a,b) : # 함수 정의
print('hello world') # 'hello world'를 출력
return a+b # a와 b를 a+b로 변환
result = sum(2,3) # a에 2를 넣고 b에 3을 넣어 함수 사용
print(result) # 결과 5
파이썬에서, 들여쓰기는 위의 명령에 포함되는 것을 뜻한다.
따라서 함수를 정의하기 위한 def, 이후에 나올 for, if, else 등과 같은 것들에 이어 쓸 때에는 들여쓰기를 신경써야 한다.
- 조건문
age = 15
if age>20 :
print('성인') # 만약 age가 20보다 높다면 '성인' 출력
else :
print('청소년') # 만약 그렇지 않다면 (위에 조건에 맞지 않는다면) '청소년' 출력
# 결과 청소년 (age = 15가 20보다 크지 않기 때문)
위의 조건문은 함수를 활용하여 아래와 같이 응용할 수 있다.
def is_adult(age): # is_adult라는 함수 설정, 변수는 age
if age>20:
print('성인')
else:
print('청소년')
is_adult(30) # age를 30으로 대입 -> 결과 성인
is_adult(25) # age를 25으로 대입 -> 결과 성인
is_adult(15) # age를 15으로 대입 -> 결과 청소년
- 반복문
파이썬에서 반복문에는 무언가를 꺼낼 꾸러미(= 묶음 = 리스트)가 필요하다.
따라서 반복문을 쓸 때는 늘 리스트와 함께 쓴다.
a_list = ['사과','배','감','귤']
for a in a_list: # 리스트 a_list에서 각 원소를 변수 a에 대입하여 반복
print(a) # 결과 사과 배 감 귤
반복문 또한, 응용하여 조건문, 함수와 함께 사용 가능하다.
def is_adult(age):
if age>20:
print('성인')
else:
print('청소년')
ages = [15,25,30,8,13]
for age in ages: # 리스트 ages의 원소들을 변수 age에 대입하여 반복
is_adult(age) # 함수 is_adult에 변수 age의 값을 적용
# 결과 청소년 성인 성인 청소년 청소년
* 복습
함수 : 같은 일을 시키기 위해 정의하는 것
조건 : ~라면 ~해라
반복 : 꺼내서 쓰는 것
2) 업무 자동화
우선, 파이썬에는 다른 사람들이 만들어둔 '라이브러리'가 무척 많다.
이것들만 잘 조합하여 이용할 줄만 알아도, 어려운 작업들을 쉽게 해낼 수 있다.
- 스크래핑 실습
웹 스크래핑을 하기 위해선 웹 서비스(페이지)의 동작 원리에 대해서 알아야 한다.
웹 페이지는, URL을 넣고 엔터를 치면 서버에서 정보를 가져와 보여주는 것이 역할이다.
더불어 웹 페이지는 기본적으로 HTML이라고 하는 '뼈대'로 구성되어 있다.
* 여러가지 HTML의 태그가 있지만 외울 필요는 없다.
네이버에서 '삼성전자'를 검색한 후 기사 하나의 제목에 우클릭하여 '검사'를 들어가서 조금 둘러보면,
'li'에 해당하는 박스 안에 기사가 담겨 있는 것을 확인 할 수 있다.
여기서 우리는
'서버에서 정보를 가져온 후 'li'에 담긴 것들을 솎아내면 되겠다' 라고 생각 할 수 있다.
웹 스크래핑을 하기 위해선 우선 라이브러리를 설치해야 한다.
!pip install bs4 requests # !pip : 뭔가를 설치할 때 쓰는 명령어
# requsts 라이브러리 : enter 치는 라이브러리
# bs4 : beautiful soup 버전 4, 잘 솎아내는 라이브러리
라이브러리를 설치했다면, 웹스크롤링 기본 코드를 활용하여 웹 스크랩핑을 시작할 수 있다.
# 웹스크롤링 기본 코드(크롤링 기본 코드)
import requests # requests 라이브러리 불러오기
from bs4 import BeautifulSoup # bs4 라이브러리에서 BeautifylSoup 프로그램(?) 불러오기
headers = {'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
data = requests.get('https://search.naver.com/search.naver?where=news&ie=utf8&sm=nws_hty&query=삼성전자',headers=headers)
soup = BeautifulSoup(data.text, 'html.parser')
# 변수 soup에 홈페이지에 나온 것들이 html에 담겨있음
# --- 기본코드 끝
a = soup.select_one('#sp_nws1 > div.news_wrap.api_ani_send > div > a')
# "~~" : 기사 우클릭 > 검사 > 표시 부분 우클릭 > Copy > Copy selector
# select_one : 기사 하나 선택
# BeautifulSoup 프로그램으로 기사 하나 솎아내서 변수 a에 넣기
print(a) # 선택한 기사 제목에 관한 정보
print(a.text) # 선택한 기사 제목
print(a['href']) # 선택한 기사 링크
하지만, 한 개의 기사만으로는 의미가 없고 반복문을 활용하여 여러 개를 솎아낸 후 정리하는 작업도 할 수 있다.
import requests
from bs4 import BeautifulSoup
headers = {'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
data = requests.get('https://search.naver.com/search.naver?where=news&ie=utf8&sm=nws_hty&query=삼성전자',headers=headers)
soup = BeautifulSoup(data.text, 'html.parser')
lis = soup.select('#main_pack > section > div > div.group_news > ul > li')
# 기사들을 li를 기준으로 리스트 lis에 대입
# #main_pack > section > div > div.group_news > ul에서 >li로 들어감
for li in lis: # 리스트 lis 에서 li 별로 반복문 돌리기
a = li.select_one('a.news_tit') # 그 안에 있는 기사 갖고 와서 변수 a에 대입
# 어떤 것으로 기사를 특정 짓는가? : 보통 class로..
print(a.text,a['href']) # 변수 a에서 텍스트와 링크 출력
더 나아가서 원하는 종목을 넣으면 관련된 기사들을 위와같이 정리해주는 함수또한 작성할 수 있다.
import requests
from bs4 import BeautifulSoup
def get_news(keyword): # 함수 정의 ; keyword를 넣으면 그 keyword에 관련된 기사를 정리한다.
headers = {'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
data = requests.get(f'https://search.naver.com/search.naver?where=news&ie=utf8&sm=nws_hty&query={keyword}',headers=headers)
# keyword에 따라 request하는게 달라져야 하기 때문에 {변수}를 검색어 자리에 넣고 앞에 f를 붙인다.
soup = BeautifulSoup(data.text, 'html.parser')
lis = soup.select('#main_pack > section > div > div.group_news > ul > li')
print(lis[0])
for li in lis:
a = li.select_one('a.news_tit')
print(a.text,a['href'])
get_news('현대자동차') # '현대자동차'에 관련된 기사 정리
get_news('LG전자') # 'LG전자'에 관련된 기사 정리
- 엑셀 다루기
기사를 정리했다면, 그것을 단순히 데이터로 가지고 있지 않고 엑셀로 정리하면 조금 더 편할 것이다.
그렇기 때문에 또다른 라이브러리인 'openpyxl'라이브러리를 가지고 엑셀로 정리할 것이다.
크롤링과 같이 엑셀로 정리하는 라이브러리를 쓰기 위해선 당연하게도 설치를 먼저 해야 한다.
!pip install openpyxl # openpyxl 라이브러리 : python에서 excel을 다루게 해주는 라이브러리
또 크롤링과 같이, 'openpyxl'라이브러리 또한 기본 코드를 사용하여 다룰 수 있다.
이 기본 코드를 사용하면, 엑셀 파일을 생성할 수 있다.
# openpyxl 기본코드
from openpyxl import Workbook # openpyxl 라이브러리에서 Workbook 프로그램 불러오기
wb= Workbook() # Workbook()을 변수 wb에 대입
sheet = wb.active # 워크북을 활성화해서 sheet 생성
sheet['A1'] = '안녕하세요!' # 시트 A1 셀에 '안녕하세요!' 입력
wb.save("샘플파일.xlsx") # "샘플파일.xlsx"로 저장
wb.close() # 프로그램 종료
엑셀을 생성했다면 생성한 엑셀파일 혹은 기존의 엑셀파일을 읽을 수도 있다.
# 엑셀 읽기
import openpyxl
wb = openpyxl.load_workbook('샘플파일.xlsx')
sheet = wb['Sheet']
print(sheet['A1'].value) # sheet의 A1의 값 출력
print(sheet['B1'].value) # sheet의 B1의 값 출력
rows = sheet.rows # sheet에 있는 모든 row를 다 갖고 와서 리스트 rows로 저장
for row in rows: # 리스트 rows의 각 원소들을 변수 row로 반복문 실행행
#print(row[0].value) # 첫번째 row 부터 마지막 row까지 1번째 값 가져오기
print(row[0].value,row[1].value,row[2].value) # 첫번째 row 부터 마지막 row까지 1,2,3번째 값 가져오기
하지만, 대부분의 자료들은 첫번째 행은 중요하지 않기 때문에 첫 행을 제외하고도 읽을 수 있다.
더불어 원하는 조건의 값들만 출력하고 싶다면, 반복문에 조건문을 활용하여 작성할 수 있다.
import openpyxl
wb = openpyxl.load_workbook('샘플파일.xlsx')
sheet = wb['Sheet']
rows = list(sheet.rows)[1:] # sheet에 있는 row를 리스트로 만들어서 2번째부터만 가져온다다
for row in rows:
if row[2].value < 300 : # 각 row의 3번째 값이 300보다 작다면 출력
print(row[0].value,row[1].value,row[2].value,)
※주의사항※
Colab은 기본적으로 파일을 저장해두는 공간이 아님.
파일을 저장한다 생각하면 안되고
올렸다가 작업하고 내려 받는 형식으로 생각해야 함
작업 용도로만 사용해야 함!
|
여기서 그치는 것이 아니라 크롤링을 할 수 있고 엑셀파일을 생성 및 불러올 수 있다면 그 두가지를 함께 활용할 수 있다.
# 필요한 라이브러리 불러오기
import requests
from bs4 import BeautifulSoup
from openpyxl import Workbook
# 함수 정의
def get_news(keyword):
wb= Workbook() # 워크북 생성
sheet = wb.active # 시트 생성성
# 크롤링 기본 코드
headers = {'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
data = requests.get(f'https://search.naver.com/search.naver?where=news&ie=utf8&sm=nws_hty&query={keyword}',headers=headers)
soup = BeautifulSoup(data.text, 'html.parser')
lis = soup.select('#main_pack > section > div > div.group_news > ul > li')
for li in lis: # 리스트 lis에 대하여 변수 li로 반복
a = li.select_one('a.news_tit') # 기사 하나를 변수 a에 대입
row = [a.text,a['href']] # 기사의 제목과 링크를 리스트 row로 저장
sheet.append(row) # 시트에 리스트 row 추가
wb.save(f"{keyword}.xlsx") # keyword를 제목으로 저장
wb.close() # 프로그램 종료
get_news("삼성전자") # 삼성전자에 관련된 기사 정리 파일 생성
get_news("현대자동차") # 현대자동차에 관련된 기사 정리 파일 생성
추가로 파이썬의 기존 라이브러리를 사용하 파일에 날짜를 달아서 저장할 수도 있다.
우선적으로 날짜 라이브러리의 사용법을 알아본다.
from datetime import datetime # datetime 라이브러리에서 datetime 프로그램 불러오기
print(datetime.today().strftime("%Y-%m-%d")) # 오늘 날짜를 yyyy-mm-dd로 출력
print(datetime.today().strftime("%Y/%m/%d")) # 오늘 날짜를 yyyy/mm/dd로 출력
사용법을 알았다면, 이를 응용하여 날짜를 달아본다.
import requests
from bs4 import BeautifulSoup
from openpyxl import Workbook
from datetime import datetime # 날짜 달기 라이브러리
def get_news(keyword):
wb= Workbook()
sheet = wb.active
headers = {'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
data = requests.get(f'https://search.naver.com/search.naver?where=news&ie=utf8&sm=nws_hty&query={keyword}',headers=headers)
soup = BeautifulSoup(data.text, 'html.parser')
lis = soup.select('#main_pack > section > div > div.group_news > ul > li')
for li in lis:
a = li.select_one('a.news_tit')
row = [a.text,a['href']]
sheet.append(row)
today = datetime.today().strftime("%Y-%m-%d") # 변수 today에 yyyy-mm-dd 문자열 저장장
wb.save(f"news/{today}_{keyword}.xlsx") # news/ : news 폴더에 저장해라
# 뉴스 폴더에 yyyy-mm-dd_keyword로 파일 저장
wb.close()
- 파일 다운로드, 이름 바꾸기
뉴스 기사에 관련된 파일을 생성했다면, Colab에서 다운로드까지 받을 수 있어야 한다.
하지만 우선적으로 한 개씩 만들고 한 개씩 다운로드 받기에는 시간이 너무 많이 걸린다.
따라서 한 번에 만드는 코드부터 작성해야 한다.
keywords = ['삼성전자','LG에너지솔루션','SK하이닉스','NAVER','삼성바이오로직스','삼성전자우','카카오','삼성SDI','현대차','LG화학','기아','POSCO홀딩스','KB금융','카카오뱅크','셀트리온','신한지주','삼성물산','현대모비스','SK이노베이션','LG전자','카카오페이','SK','한국전력','크래프톤','하나금융지주','LG생활건강','HMM','삼성생명','하이브','두산중공업','SK텔레콤','삼성전기','SK바이오사이언스','LG','S-Oil','고려아연','KT&G','우리금융지주','대한항공','삼성에스디에스','현대중공업','엔씨소프트','삼성화재','아모레퍼시픽','KT','포스코케미칼','넷마블','SK아이이테크놀로지','LG이노텍','기업은행']
# 리스트 keywords에 관심 검색어들을 저장
for keyword in keywords: # 리스트 keywords의 원소들에 대해 변수 keyword로 반복
print(keyword) # 진행상황을 알도록 keyword 출력
get_news(keyword) # keyword에 대해 위의 함수 실행
한 번에 생성했다면, 일일이 다운로드 받기에도 시간이 많이 걸리기 때문에 압축하여 다운받으면 편하다.
# /content/news의 파일을 /content/에 files.zip으로 압축하여 저장
!zip -r /content/files.zip /content/news
만약 저장하기 전 파일들의 이름을 바꿔야 할 필요가 생긴다면 아래의 코드를 사용하면 된다.
import os
path = '/content/news'
# 리스트 files에 /content/news 에 있는 파일들의 이름 저장
files = os.listdir(path)
# 리스트 files의 원소에 대하여 변수 name으로 반복
for name in files:
# 변수 new_name 에 각 name을 .을 기준으로 나누어 앞부분(1번째 원소) 와 '(뉴스).xlsx'를 합하여 저장
new_name = name.split('.')[0]+'(뉴스).xlsx'
os.rename(f'/content/news/{name}',f'/content/news/{new_name}') # os.rename(원래 이름,바꿀 이름)
- 이미지 다운로드
웹 페이지에서 이미지에 대해 코드를 살펴보면 이미지마다 링크가 있는 것을 확인할 수 있다.
강의에서 예로 든 finance.naver.com 에서 종목을 검색하여 주가 등락 현황 이미지의 링크 또한 확인할 수 있다.
그 예시로 '삼성전자'의 주가 등락 현황 이미지를 다운로드 받는 코드는 이렇다.
import urllib.request # urllib.request 프로그램 불러오기
url = 'https://ssl.pstatic.net/imgfinance/chart/item/area/year3/005930.png'
# 이미지 주소를 변수 url에 저장
urllib.request.urlretrieve(url, "삼성전자.png") # url의 이미지를 "삼성전자.png"의 이름으로 저장
해당 이미지 주소를 여러 종목으로 비교하여 살펴보면, 마지막의 숫자만 바뀌는 것을 알 수 있다.
그래서 뒤의 숫자만 바꿔준다면 다른 종목의 주가 등락 현황 이미지를 확인할 수 있는 것을 알 수 있다.
이를 이용하여 숙제를 해결 할 수 있다.
3. 숙제
# 엑셀 파일 관심종목 들의 이미지를 한번에 다운로드 받기
!pip install bs4 requests
!pip install openpyxl
from openpyxl import Workbook
import urllib.request
wb = openpyxl.load_workbook('관리종목.xlsx') # openpyxl 사용, '관리종목' 파일 로드
sheet = wb['종목'] # 대괄호 안의 시트를 변수 sheet로 지정
rows = list(sheet.rows)[1:] # sheet의 row들을 2번째부터 리스트 rows로 지정
# 제목을 제외하고 값들만 리스트로 지정
for row in rows: # 리스트 rows에 대해 변수 row 반복
print(row[0].value,row[1].value) # 리스트 rows의 1번째와 2번째 값을 출력
url = f'https://ssl.pstatic.net/imgfinance/chart/item/area/year3/{row[1].value}.png'
# 행별로 2번째 값을 대입하여 url 생성
urllib.request.urlretrieve(url, f"/content/imgs/{row[0].value}.png")
# 행별로 1번째 값을 제목으로하여 /content/imgs에 이미지 다운로드
!zip -r /content/관리종목주가사진.zip /content/imgs
# 이미지들을 '관리종목주가사진'이라는 이름으로 압축 저장
'Python 개발일지' 카테고리의 다른 글
스파르타 코딩 클럽 : [왕초보] 주식 데이터를 활용한 파이썬 데이터분석 2주차 개발일지 (0) | 2022.11.29 |
---|