크롤링기초/ 1:N Model 연결/ 여러 app 연결/ 코드의 재사용
- 0 크롤링, 파싱의 개념
- 1 여러개의 app의 url 관리하기 위해 include
- 2 Model의 1:N 관계(foreing key) PK(Primory key)기본 의 이해
- 3 form으로 사용자 입력받고 DB에 저장하기 (시간 남으면)
- 4 코드의 재사용
크롤링(crawling) : 웹 상의 페이지를 수집하는 작업
파싱(Parsing) : 어떠한 웹 페이지에서 내가 원하는 데이터를 특정 패턴이나 순서로 추출하여 정보로 가공하는 것
파서(Parser) : 파싱을 수행하는 프로그램 / 원시 프로그램의 명령문, HTML 문서 등에서 구문을 해석할 수 있는 단위로 분할 해 주는 역할을 함.
크롤링을 할 때 수집은 총 2가지로 나뉩니다. 바로 정적 수집과 동적 수집이죠.
정적 수집 특징은 다음과 같습니다.
- 주소를 통해 단발적으로 접근한다.
- 속도가 빠르다.
- 수집 대상에 한계가 크다 (ex, 연속적 대상, 동적 대상 불가)
- 사용 라이브러리 : requests
이에 반해서 동적 수집 특징은 다음과 같아요.
- 브라우저를 사용해서 연속적으로 접근한다.
- 속도가 느리다.
- 수집 대상에 한계가 거의 없다.
- selenium
저희는 보다 더 간단한 정적 수집에 대해서 다뤄볼거에요.
pandas는 python에서 사용하는 데이터 분석 라이브러리입니다. 데이터 구조를 조작하기 아주 편리한 언어죠.
pandas모듈에서는 html에 있는 표를 간단하게 가져올 수 있게 하는 명령어가 있습니다. 바로 pandas.read_html() 입니다. 이 함수를 사용하면
tag로 구성되어 있는 부분을 가져와서 list를 자동으로 만들어주게 됩니다.$ pip install pandas
$ pip install lxml
from django.shortcuts import render
import pandas as pd
url = 'https://search.naver.com/search.naver?sm=top_hty&fbm=1&ie=utf8&query=%EB%84%A4%EC%9D%B4%EB%B2%84+%ED%99%98%EC%9C%A8%EC%A1%B0%ED%9A%8C'
tables = pd.read_html(url)
df = tables[1]
df.columns
df=df[['매매기준율']]
exchange_list = df.values.tolist()
exchange_list = sum(exchange_list,[])
print(exchange_list)
# Create your views here.
def home(request):
return render(request,'exchangeapp/home.html')
def result(request):
nation = list(['USD','JPY','EUR','CNY','GBP','AUD','CAD','NZD'])
for i in range(8):
if nation[i] in request.POST:
name = nation[i]
korea = request.POST['korea']
tocountry = int(korea)
tocountry = tocountry / exchange_list[i]
return render(request,'exchangeapp/result.html',{'country':tocountry,'name':name,'korea':korea})
def about(request):
return render(request,'exchangeapp/about.html')
HTML 및 XML 문서를 구문 분석하기 위한 Python 패키지입니다.
웹문서의 구조를 찾아내는 파서를 이용하여서 찾고자 하는 데이터의 위치를 찾아 내어 값을 추출하게 됩니다.
$ pip install beautifulsoup4
$ pip install html5lib
from django.shortcuts import render
from urllib.request import urlopen
from bs4 import BeautifulSoup
# Create your views here.
url = "https://search.naver.com/search.naver?sm=top_hty&fbm=1&ie=utf8&query=%ED%99%98%EC%9C%A8"
html = urlopen(url)
source = html.read()
#해당 URL에 있는 html 데이터를 바이트 문자열로 반환한다.
html.close()
soup = BeautifulSoup(source, "html5lib")
#html5lib = html문서를 트리구조로 분석해주는 라이브러리 - 원하는 내용 추출 위함 (html dom 참조)
country = soup.select('table > tbody > tr > th > a > span > em')
tables = soup.find("table",class_="rate_table_info")
rate = tables.find_all("td",{'class': ''})
list = []
for item in zip(country,rate):
list.append(
{
'country' : item[0].text,
'rate' : item[1].text
}
)
def home(request):
return render(request,'exchangeapp/home.html')
def result(request):
nation = [0 for i in range(8)]
exchange_list = [0 for i in range(8)]
for i in range(8):
t = listup[i]
nation[i] = t['country']
te = t['rate']
te = te.replace(',', '')
exchange_list[i] = float(te)
for i in range(8):
if nation[i] in request.POST:
name = nation[i]
korea = request.POST['korea']
tocountry = int(korea)
tocountry = tocountry / exchange_list[i]
return render(request,'exchangeapp/result.html',{'country':tocountry,'name':name,'korea':korea})
urllib.request = URL을 가져오기 위한 파이썬 모듈입니다.
오늘 다룬 네이버 페이지는 로그인을 하지 않아도 정보를 가져올 수 있으며, 새로고침을 하기전까지는 데이터가 변하지 않는 정적인 페이지 입니다.
다음에 함께 크롤링이 필요할 때가 오면 그땐 동적페이지, 사진을 불러오고 해당 내용을 model에 까지 저장하는 것을 해봅시다~!
2주차에 model을 migrate해서 DB를 만들어주고 Data를 생성한 후 view로 처리해준 뒤 html로 보내봤죠?
app을 하나 더 만들어주고 model을 만들어 준 뒤 기존의 모델과 연결시키고, 앱의 url을 프로젝트에 연결해볼게요
/project 위치
python manage.py startapp post
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'myapp',
'post',
]
from django.urls import path
import post.views
urlpatterns = [
path('', post.views.post_list, name = 'post_list'),
#''(공백) url이 들어오면 post_list 함수를 호출한다
]
from django.contrib import admin
from django.urls import path, include # include import 해주기
import myapp.views
urlpatterns = [
path('admin/', admin.site.urls),
path('', myapp.views.index, name = 'home'),
path('lions/', myapp.views.lion_list, name = 'lions'),
path('post/', include('post.urls'))
# post.urls를 include 해주는데 처음 url이 `post/` 가 된다
]
나중엔 기능별로 앱이 생길텐데 project/urls.py에 모든 path를 관리하면 보기 안좋겠죠?
그래서 보통 app에다가 urls.py 를 만들어 주고 project/urls.py와 연결해줍니다
# post/views.py
from django.shortcuts import render
def post_list(request):
return render(request, 'post.html')
<!-- templates/post.html --!>
<h2>하위</h2>
그리고 runserver를 돌려서 url 뒤에 /post를 쳐주면하위가 나오죠?
post/urls.py와 myproject/urls.py에서 처리해준것 같이 post.html을 요청해줍니다
이제 post 앱에도 model을 만들고 makemigrations -> migrate 해서 DB를 만들어줄게요
# post/models.py
from django.db import models
from django.utils import timezone
# Create your models here.
class Post(models.Model):
title = models.CharField(max_length = 100) # 제목
content = models.TextField(max_length = 200) # 내용
create_data = models.DateTimeField(default = timezone.now)
objects = models.Manager() # 저번시간에 말해준 Manager인데 다시한번 집고 넘어가
def __str__(self): # 얘는 데이터 생성 시 생성값을 title 칼럼으로 해준다는 것
return str(self.title)
python manage.py makemigrations
랑python manage.py migrate
ㄱㄱ
아 근데 해주고 싶은게 있어요
저번에 Lion 모델에서 생성한 객체들이 이 Post 모델을 이용해서 글을 작성하게 하고 싶어요
보통 한 사람이 여러개의 글을 작성하죠 ?
그러려면 Lion 모델과 Post 모델을 1 : N으로 묶어주면 됩니다
뭔소리야?
저번에 이렇게 Lion 모델을 만들어 줬죠? Post 모델도 똑같아요
Post 모델도 이렇게 만들어지는데 아까 뭐라그랬죠?
한 사람이 여러개의 글을 쓰도록 만들게 하자고 했죠 ?
이렇게 Post모델에 최광일 소유를 부여해서 여러개의 글을 작성하도록 할 수 있어요
Post 모델에 뭐가 추가됬죠 ? 최광일이가 추가됬죠? 그럼 Post 모델에 name 칼럼을 추가하면 되겠네요?
# post/models.py
from django.db import models
from django.utils import timezone
class Post(models.Model):
name = models.ForeignKey (Lion, on_delete=models.CASCADE, blank=True, null = True) # 추가
title = models.CharField(max_length = 100)
content = models.TextField(max_length = 200)
create_data = models.DateTimeField(default = timezone.now)
objects = models.Manager()
def __str__(self):
return str(self.title)
1 : N 관계로 모델을 묶을때는 이렇게 models.ForeignKey 를 사용해줍니다
왜냐구요 ? django한테 물어보세요 걔가 Class로 만들어논거에요...
근데 뒤에 뭐가 붙었죠 ? ForeignKey field 값엔 여러가지가 있는데 4가지만 알아볼게여
먼저 ForeignKey 에는 Post모델과 연결해줄 모델(Lion)을 연결해주라는 거입니다
on_delete 라는 걸 해주는데 lion 한명이 여러 post를 작성할 수 있다고 했죠 ?
근데 lion 한명의 데이터를 지우면 걔가 작성한 post 모델의 데이터들은 어떻게 될까요 ? 상식적으로 없어져야겟죠 ?
그런데 장고는 인간이 아니라서 그걸 알려줘야 하는데 그게 on_delete=models.CASCADE
입니다.
아마 이미 Lion 모델로 DB를 만들어서 admin page에서 lion 객체를 생성해줬을거에요
그런데 이미 생성된 것들은 Post 모델에서 ForeignKey로 연결되지 않았을때 만들어 진 것이죠?
이미 생성된 Lion 객체들은 name 이라는게 없어서 장고는 이미 생성된 애들은 어떻게 해?
라는 식의 오류를 보낼거에요
그래서 post 객체를 만들어주려고 할때
오류가 생기거나 migrate 할 때
오류가 뜰꺼에요
다음에 모델을 수정하거나 추가할 때에도 문제가 발생하지 않습니다
그러면 이제 admin에서 post data를 만들고 view를 통해 templates으로 넘겨줘볼게요
우선 모든 post를 가져와서 render 시켜볼게요
# post/views.py
from django.shortcuts import render
from post.models import Post # post모델 임포트
# Create your views here.
def post_list(request):
post_list = Post.objects.all() # Post 모델에있는 객체 다 가져와
return render(request, 'post_list.html', {'post_list' : post_list}) # 변수 넘겨줘
<!-- post/templates/post_list.html --!>
{% if post_list %} <!-- 작성한 글이 있다면 --!>
{% for post in post_list %} <!-- for 문 돌려 --!>
<h2>{{post.pk}}번째 포스트 입니다</h2>
<p>작성자 : {{post.name}}<p>
<p>제목 : {{post.title}}</p>
<p>내용 : {{post.content}}</p>
<p>작성시각 : {{post.create_data}}</p>
{% endfor %} <!-- for 문 끝 --!>
{% else %} <!-- 작성한 글이 없다면 --!>
<h2>작성된 Post가 없습니다</h2>
{% endif %} <!-- if 끝 --!>
좋아요 이러면 모든 lion 객체가 작성한 글들이 불러와질꺼에요
근데 저는 한 사람이 작성한 글만 보고싶어요 그러면 어떡해야할까요?
그러면 사람별로 페이지를 렌더해주기 위해서는 우선 url을 수정해야겠죠?
# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
import myapp.views
urlpatterns = [
path('admin/', admin.site.urls),
path('', myapp.views.index, name = 'home'),
path('lions/', myapp.views.lion_list, name = 'lions'),
path('lions/<int:pk>', myapp.views.lion_posts, name = 'lion_posts'), # 추가
path('post/', include('post.urls'))
]
보시면 기존에 lion 객체들을 render해주는 url 뒤에 <int:pk>
라는 것이 붙었죠?
path('lions/<int:pk>', myapp.views.lion_posts, name = 'lion_posts')
이렇게 해주고 http://127.0.0.1:8000/lions/1
내가 이런식의 url을 입력하면
1이라는 값이 integer(정수) pk 값에 저장되고 views.py 에서 사용할 수 있어요
최광일이 작성한 글만 보고싶어요, 그러면 views에서 데이터(post_list)만 처리해 주면 될까요? 해보죠
# myapp/views.py
def lion_posts(request):
post_list = Post.objects.all().filter(name = '최광일')
return render(request, 'lion_posts.html', {
'post_list' : post_list,
})
이렇게 데이터에서 name이 최광일인 것을 filter 해주면 될까요? 맞습니다
근데 filter하고 싶은 name이 최광일 이라는걸 django가 어떻게 알죠?
즉 name이 최광일, 박규리 등등..이렇게 바뀔텐데 말이죠!
그러면 사람별로 html과 url, views.py 에서 함수 처리해줘야 할까요? No
각 사람이 가진 Lion 모델의 값을 활용하면 되겠죠?
Lion 모델에 어떤 칼럼이 있었죠? 이름(name)과 PK(Primary Key)가 있었죠
PK는 위에서 말한것처럼 그냥 객체 별 순번을 지정해준 것인데 주요키라고 불리는 이유는
이름은 동명이인이 있거나 철자가 틀려서 잘못 될 수 있는데
PK는 유일성, 무결성등을 만족하기 때문에 PK를 활용해주는 것이 좋아요(순서도 order 되어잇구요)
#myapp(처음 만든 앱)/views.py
def lion_posts(request, pk): # pk 를 받아서 처리해주니 인자에 pk를 추가해주고
post_pk = pk # post_pk 에 넘겨받은 pk를 저장
author = Lion.objects.get(pk = post_pk) # 작성자를 Lion model의 해당 pk를 가진 사람 가져와
post_list = Post.objects.all().filter(name = author)
# 게시글을 Post model의 해당 작성자를 가진 게시물들 가져와
return render(request, 'lion_posts.html', { # 만든 변수들 넘겨주기
'post_list' : post_list,
'author' : author,
'post_pk' : pk,
})
이제 html만 만들어주면 되겠죠 ??
views.py 에서 넘겨준 author, post_pk, post_list를 사용해주시면 됩니다
<!-- lion_posts.html -->
<body>
<h1>{{author}}님이 작성한 Post들 입니다</h1>
<a href = "{% url 'lions' %}">뒤로가기</a>
{% if post_list %}
{% for post in post_list %}
<h2>{{post.pk}}번째 포스트 입니다</h2>
<p>작성자 : {{author}}<p>
<p>제목 : {{post.title}}</p>
<p>내용 : {{post.content}}</p>
<p>작성시각 : {{post.create_data}}</p>
{% endfor %}
{% else %}
<h2>작성된 Post가 없습니다</h2>
{% endif %}
</div>
</body>
저번시간에 for 문으로 Lion 모델에 있는 객체들을 전부 html 에 나타내봤죠?
그러면 이제 각 사람의 이름은 눌르면 해당 페이지에 가도록 하여 개인의 게시물이 나타나도록 해볼게요
<!-- lion_list.html --!>
{% for lion in lion_list %}
<h1><a href="{% url 'lion_posts' pk=lion.pk %}">{{ lion.name }}</a></h1>
<p>가입 날짜 : {{ lion.create_data }}</p>
{% endfor %}
<a href="{% url 'lion_posts' pk=lion.pk %}"> </a>
원래 우리가 기존에 해주던 <a href = "{% url <url_name> %}">
이거에 pk만 추가해주면 됩니다
<a href = "{% url <url_name> pk=lion.pk %}">
이렇게여
왜 pk = lion.pk 인지는 본인이 생각해보시길 바랄게용
form으로 입력받은 값을 model에 저장하기 위해서는 ModelForm을 알아야 해요
ModelForm이란 form 과 그 폼을 이용하여 내가 저장할 Model을 연결시켜주는 애인데요
역시 궁금하면 django 공식문서나 구글링을 추천
일단 해볼게요
startapp으로 app을 만들면 forms.py가 만들어지지 않을꺼에요, app내부에 forms.py 를 만들고
modelform을 작성해 볼게요
# myapp/forms.py
from django import forms # django에서 forms import 해
from post.models import Post # 저장할 모델 가져와
class PostForm(forms.ModelForm): # django에서 forms.ModelForm를 상속받는다
class Meta:
model = Post # 저장하기를 원하는 데이터 모델 지정
fields = ['title','content']
ModelForm이 궁금하니 한번 Ctrl + 좌클릭으로 들어가볼게요
보시면 model과 fields 변수가 있죠?
우선은 model엔 내가 form 과 연결할 모델을 넣어주는 곳이다~ 라고 생각하시고
fields 값은 해당 모델에서 내가 form 을 통해 값을 넣어줄 칼럼이다~ 정도만 알고 넘어갈게요
그러면 이제 새 post를 작성할 html 공간이 필요하겠죠 ? 먼저 urls.py 부터 작성해볼게요
# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
import myapp.views
urlpatterns = [
path('admin/', admin.site.urls),
path('', myapp.views.index, name = 'home'),
path('lions/', myapp.views.lion_list, name = 'lions'),
path('lions/<int:pk>', myapp.views.post_list, name = 'lion_posts'),
path('new_post/<int:pk>', myapp.views.new_post, name = "new_post"), # 추가
path('post/', include('post.urls')),
]
지금 하고싶은건 어떤 사람이 post를 작성할 html을 만들어 주는거죠?
그래서 어떤 사람을 확인해 줄 용도로 url에서 pk를 받아줄거에요
이제 new_post.html
을 만들고 form 을 입력해줄게요
<div class = "header">
<h1>{{author}}님 새로운 POST를 작성하세요</h1>
</div>
<a href = "{% url 'lion_posts' lion_pk %}">뒤로가기</a>
<form method='POST'>
{% csrf_token %}
<div>
{{form}}
</div>
<div>
{{form.title}}
</div>
<div>
{{form.content}}
</div>
<div>
{{form.as_p}}
</div>
<br>
<input type="submit" value="제출하기">
</form>
기존에 form과 다른점은 input tag가 없다는 점인데요 그건 view에서 처리해준 뒤 설명할게요
# myapp(firstapp)/views.py
from django.shortcuts import render, redirect # redirect import 추가
from myapp.models import Lion
from post.models import Post
from .forms import PostForm # 사용할 form 가져와
def new_post(request, pk):
lion_pk = pk # 어떤 사람이 post를 작성하는 거니까 pk는 어떤 사람의 pk
author = Lion.objects.get(pk = lion_pk) # 해당 pk로 author 가져온다.
if request.method == 'POST': # POST 요청이면
# Post 모델에서 이름이 author인 새로운 Row를 만듭니다
post = Post.objects.create(name = author)
# 그리고 우리가 만든 PostForm을 사용해서 요청받은 request.POST 값을
# 위에서 만든 Post 모델에 새로운 Row(post)에 넣어주기 위해 이렇게 해요
# 클래스의 인스턴스가 객체죠 ?
form = PostForm(request.POST, instance = post)
# 이제 form 이 유효하다면 post 인스턴스를 저장해줍니다
if form.is_valid():
post.save()
# 게시글이 저장되면 다시 `lion_posts/pk` url로 redirect 해줄게요
return redirect('lion_posts', pk = lion_pk)
else: # GET 요청이면 그냥 form 보여줘
form = PostForm()
return render(request, 'new_post.html', { # templates에서 사용할 변수들을 넘겨줍니다
'form' : form,
'author' : author,
'lion_pk' :lion_pk,
})
이제 새 글을 작성하고 admin 페이지와 render된 페이지를 확인해보죠
장고의 또 다른 멋진 기능중에 템플릿 확장(template extending) 이 있어요
바로 여러분의 웹사이트 안의 서로 다른 페이지에서 HTML의 일부를 동일하게 재사용 할 수 있다는 뜻인데요
백문이 불여일타
그냥 바로 해볼게요
기존에 lion 모델의 객체들을 가져오는 html 을 바꿔볼게요
<!-- myapp/templte/base.html --!>
<head>
{% load static%}
<link rel="stylesheet" href="{% static 'css/index.css' %}">
<!-- <link> 태그의 rel 속성은 현재 문서와 외부 리소스 사이의 연관 관계를 명시합니다. -->
<!-- 스타일 시트(stylesheet)로 사용할 외부 리소스를 불러옴. -->
</head>
<body>
<div class = "header">
<h1>여기에 아기사자들 가져와</h1>
</div>
<div>
<a href = "{% url 'home' %}">뒤로가기</a>
</div>
{% block content %}
{% endblock %}
<div class = "list">
</div>
</body>
이렇게 내가 넣고싶은 것들은 block 처리해준 뒤 나머지 템플릿(header, body 등등)은 그대로 두고
lions.html에 만들어준 block을 가져다가 써볼게요
{% extends 'block.html' %}
{% block content %}
{% for lion in lion_list %}
<div class = "lion">
<h1><a href="{% url 'lion_posts' pk=lion.pk %}">{{ lion.name }}</a></h1>
<p>가입 날짜 : {{ lion.create_data }}</p>
</div>
{% endfor %}
{% endblock %}
{% extends 'block.html' %}
로 block.html을 가져와서
block 처리해준 곳을 어떻게 쓸지 적어주면 lions.html은 원래와 동일하게 작동합니다