├── chapter_2
├── books.csv
├── class.py
├── def.py
├── for_and_while.py
├── greet-with-comments.py
├── greet.py
├── if.py
├── import.py
├── python_scraper.py
├── save.py
├── save_csv.py
├── save_csv_dict.py
├── save_csv_join.py
├── save_sqlite3.py
├── scrape_re.py
├── scrape_rss.py
├── try_and_with.py
├── urlopen_encoding.py
└── urlopen_meta.py
├── chapter_3
├── python_crawler_1.py
├── python_crawler_2.py
├── python_crawler_3.py
├── python_crawler_4.py
├── python_crawler_5.py
├── python_crawler_6.py
├── python_crawler_final.py
├── save_mongo.py
├── save_mysql.py
├── scrape_by_bs4.py
├── scrape_by_feedparser.py
└── scrape_by_lxml.py
├── chapter_4
├── error_handling.py
├── error_handling_with_retrying.py
├── request_with_cache.py
├── send_email.py
├── validate_with_re.py
└── validate_with_voluptuous.py
├── chapter_5
├── get_museums.py
├── get_museums_with_location.py
├── import_from_stream_api_to_bigquery.py
├── konlpy_sample.py
├── museums.html
├── naver_order_history.py
├── plot_advanced_graph.py
├── plot_historical_data.py
├── print_pdf_textboxes.py
├── rest_api_with_requests_oauthlib.py
├── rest_api_with_tweepy.py
├── robobrowser_google.py
├── save_youtube_video_metadata.py
├── search_youtube_videos.py
├── selenium_google.py
├── shopping_rss.py
├── shopping_selenium.py
├── streaming_api_with_tweepy.py
└── word_frequency.py
├── chapter_6
├── 6-1
│ └── myspider.py
├── 6-2
│ ├── myproject
│ │ ├── __init__.py
│ │ ├── items.py
│ │ ├── pipelines.py
│ │ ├── settings.py
│ │ └── spiders
│ │ │ └── news.py
│ └── scrapy.cfg
├── 6-3
│ ├── myproject
│ │ ├── __init__.py
│ │ ├── items.py
│ │ ├── pipelines.py
│ │ ├── settings.py
│ │ └── spiders
│ │ │ ├── hanbit.py
│ │ │ └── news_crawl.py
│ └── scrapy.cfg
├── 6-4
│ └── pipelines.py
├── 6-7
│ ├── myproject
│ │ ├── __init__.py
│ │ ├── items.py
│ │ ├── pipelines.py
│ │ ├── settings.py
│ │ ├── spiders
│ │ │ ├── broad.py
│ │ │ └── visitseoul.py
│ │ └── utils.py
│ └── scrapy.cfg
└── 6-8
│ ├── extract_faces.py
│ ├── myproject
│ ├── __init__.py
│ ├── items.py
│ ├── pipelines.py
│ ├── settings.py
│ ├── spiders
│ │ └── flickr.py
│ └── utils.py
│ └── scrapy.cfg
├── chapter_7
├── crawl.py
├── crawl_images.py
├── crawl_with_aiohttp.py
├── crawl_with_multi_thread.py
├── enqueue.py
├── scraper_tasks.py
├── slow_jobs_async.py
├── slow_jobs_sync.py
└── tasks.py
└── readme.md
/chapter_2/books.csv:
--------------------------------------------------------------------------------
1 | No,도서명,가격
2 | 1,처음 시작하는 R 데이터 분석,19800원
3 | 2,데이터 과학을 위한 통계,32000원
4 | 3,오준석의 안드로이드 생존코딩(코틀린 편),32000원
5 | 4,처음 배우는 스프링 부트 2,22000원
6 | 5,회사에서 바로 통하는 실무 엑셀+파워포인트+워드&한글,22000원
7 | 6,RxJS 프로그래밍: 75가지 핵심 문법과 예제로 익히는 RxJS 기초,32000원
8 | 7,스프링 5 레시피(4판),60000원
9 | 8,자바를 활용한 딥러닝,38000원
10 | 9,회사에서 바로 통하는 엑셀 실무 강의,21000원
11 | 10,모던 스타트업,20000원
12 | 11,파이썬 웹 프로그래밍(개정판),22000원
13 | 12,피 땀 픽셀 : 트리플 A 게임은 어떻게 만들어지는가,18000원
14 | 13,엔트리 피지컬 컴퓨팅을 만나다,18000원
15 | 14,회사에서 바로 통하는 오토캐드 2019,28000원
16 | 15,맛있는 디자인 프리미어 프로&애프터 이펙트 CC 2018,24000원
17 | 16,맛있는 디자인 포토샵&인디자인 CC 2018,24000원
18 | 17,만들면서 배우는 워드프레스(개정판),26000원
19 | 18,처음 배우는 암호화,29000원
20 | 19,나의 첫 안드로이드 : 처음 시작하는 개발자를 위한,32000원
21 | 20,윤피티의 SNS 콘텐츠 만들기 with 파워포인트,18000원
22 | 21,Head First Android Development : 개념과 구조를 머릿속에 그려주는 안드로이드 개발 입문서(개정판),40000원
23 | 22,그것이 R고 싶다,32000원
24 | 23,이것이 C#이다,30000원
25 | 24,맛있는 디자인 애프터 이펙트 CC 2018,24000원
26 | 25,Hello Coding 한입에 쏙 파이썬,15000원
27 | 26,고객이 보이는 구글 애널리틱스,28000원
28 | 27,머신러닝 실무 프로젝트,18000원
29 | 28,맛있는 디자인 일러스트레이터 CC 2018,25000원
30 | 29,좋은 사진을 만드는 박승근의 드론 사진 강의,28000원
31 | 30,엔지니어를 위한 블록체인 프로그래밍,26000원
32 | 31,우아한 사이파이,28000원
33 | 32,파이썬을 활용한 금융공학 레시피,28000원
34 | 33,자바로 배우는 핵심 자료구조와 알고리즘,16000원
35 | 34,처음 배우는 블록체인,28000원
36 | 35,아무것도 모르고 시작하는 인공지능 첫걸음,22000원
37 | 36,인공지능 콘텐츠 혁명,18000원
38 | 37,맛있는 디자인 프리미어 프로 CC 2018,23000원
39 | 38,엑셀 2016 함수&수식 바이블,35000원
40 | 39,러닝 텐서플로,23000원
41 | 40,Java 9 모듈 프로그래밍,21000원
42 | 41,자바 프로젝트 필수 유틸리티,35000원
43 | 42,달인과 함께 하는 마인크래프트 세계 건축 여행 : 아시아와 아프리카,10000원
44 | 43,달인과 함께 하는 마인크래프트 세계 건축 여행: 유럽과 아메리카,10000원
45 | 44,핸즈온 머신러닝,33000원
46 | 45,NDC ART BOOK 2018,20000원
47 | 46,짧은 애니메이션 만들기 with 클립 스튜디오,25000원
48 | 47,맛있는 디자인 포토샵&일러스트레이터 CC 2018,23000원
49 | 48,파이썬 정복,22000원
50 | 49,Vue.js 첫걸음,22000원
51 | 50,데이터 분석을 위한 SQL 레시피,36000원
--------------------------------------------------------------------------------
/chapter_2/class.py:
--------------------------------------------------------------------------------
1 | # Rect라는 이름의 클래스를 지정합니다.
2 | class Rect:
3 | # 인스턴스가 생성될 때 호출되는 특수한 메서드를 정의합니다.
4 | def __init__(self, width, height):
5 | self.width = width # width 속성에 값을 할당합니다.
6 | self.height = height # height 속성에 값을 할당합니다.
7 | # 사각형의 넓이를 계산하는 메서드를 정의합니다.
8 | def area(self):
9 | return self.width * self.height
10 |
11 | r = Rect(100, 20)
12 | print(r.width, r.height, r.area()) # 100 20 2000을 출력합니다.
13 |
14 | # Rect를 상속받아 Square 클래스를 정의합니다.
15 | class Square(Rect):
16 | def __init__(self, width):
17 | # 부모 클래스의 메서드를 호출합니다.
18 | super().__init__(width, width)
--------------------------------------------------------------------------------
/chapter_2/def.py:
--------------------------------------------------------------------------------
1 | # add라는 이름의 함수를 정의합니다.
2 | # 이 함수는 매개변수로 a와 b를 받고 더한 뒤 반환합니다.
3 | def add(a, b):
4 | return a + b # return 구문으로 값을 반환합니다.
5 |
6 | # 함수를 호출할 때는 함수 이름 뒤에 괄호를 입력하고
7 | # 내부에 매개변수를 지정합니다.
8 | print(add(1, 2)) # 3라고 출력합니다.
9 |
10 | # <매개변수>=<값>이라는 형태로도 매개변수를 지정할 수 있습니다.
11 | # 이를 키워드 매개변수라고 합니다.
12 | print(add(1, b=3)) # 4라고 출력합니다.
--------------------------------------------------------------------------------
/chapter_2/for_and_while.py:
--------------------------------------------------------------------------------
1 | # 변수 x에 in의 오른쪽 리스트가 차례대로 들어갑니다.
2 | # 따라서 블록 내부의 처리가 3번 반복됩니다.
3 | for x in [1, 2, 3]:
4 | # 1, 2, 3이 차례대로 출력됩니다.
5 | print(x)
6 |
7 | # 횟수를 지정해서 반복할 때는 range()를 사용합니다
8 | for i in range(10):
9 | # 0 9가 차례대로 출력됩니다.
10 | print(i)
11 |
12 | # for 구문으로 dict를 지정하면 키를 기반으로 순회합니다.
13 | d = {'a': 1, 'b': 2}
14 | for key in d:
15 | value = d[key]
16 | print(key, value)
17 |
18 | # dict의 items() 메서드로 dict 키와 값을 순회합니다.
19 | for key, value in d.items():
20 | print(key, value)
21 |
22 | # while 구문으로 식이 참일 때 반복 처리합니다.
23 | s = 1
24 | while s < 1000:
25 | # # 1, 2, 4, 8, 16, 32, 64, 128, 256, 512가 차례대로 출력됩니다.
26 | print(s)
27 | s = s * 2
--------------------------------------------------------------------------------
/chapter_2/greet-with-comments.py:
--------------------------------------------------------------------------------
1 | # import 구문으로 sys 모듈을 읽어 들입니다.
2 | import sys
3 |
4 | # def 구문으로 greet() 함수를 정의합니다.
5 | # 들여쓰기돼 있는 줄이 함수의 내용을 나타냅니다.
6 | def greet(name):
7 | # print() 함수를 사용해 문자열을 출력합니다.
8 | print('Hello, {0}!'.format(name))
9 |
10 | # if 구문도 들여쓰기로 범위를 나타냅니다.
11 | # sys.argv는 명령줄 매개변수를 나타내는 리스트 형식의 변수입니다.
12 | if len(sys.argv) > 1:
13 | # if 구문의 조건이 참일 때
14 | # 변수는 정의하지 않고 곧바로 사용할 수 있습니다.
15 | name = sys.argv[1]
16 | # greet() 함수를 호출합니다.
17 | greet(name)
18 | else:
19 | # if 구문의 조건이 거짓일 때
20 | # greet 함수를 호출합니다.
21 | greet('world')
--------------------------------------------------------------------------------
/chapter_2/greet.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | def greet(name):
4 | print('Hello, {0}!'.format(name))
5 | if len(sys.argv) > 1:
6 | name = sys.argv[1]
7 | greet(name)
8 | else:
9 | greet('world')
--------------------------------------------------------------------------------
/chapter_2/if.py:
--------------------------------------------------------------------------------
1 | # 변수를 선언합니다.
2 | a = 1
3 |
4 | # if 구문으로 처리를 분기합니다.
5 | if a == 1:
6 | # if 구문의 식이 참일 때 실행합니다.
7 | print('a is 1')
8 | elif a == 2:
9 | # elif 절의 식이 참일 때 실행합니다.
10 | print('a is 2')
11 | else:
12 | # 어떠한 조건해도 해당하지 않을 때 실행합니다.
13 | print('a is not 1 nor 2')
14 |
15 | # 조건문을 한 줄로 적을 수 있지만 읽기 어려우므로 사용하지 않는 것이 좋습니다.
16 | print('a is 1' if a == 1 else 'a is not 1')
--------------------------------------------------------------------------------
/chapter_2/import.py:
--------------------------------------------------------------------------------
1 | # sys 모듈을 현재 이름 공간으로 읽어 들입니다.
2 | import sys
3 |
4 | # datetime 모듈에서 date 클래스를 읽어 들입니다.
5 | from datetime import date
6 |
7 | # sys 모듈의 argv라는 변수로 명령줄 매개변수 리스트를 추출하고 출력합니다.
8 | print(sys.argv)
9 | # date 클래스의 today() 메서드로 현재 날짜를 추출합니다.
10 | print(date.today())
--------------------------------------------------------------------------------
/chapter_2/python_scraper.py:
--------------------------------------------------------------------------------
1 | import re
2 | import sqlite3
3 | from urllib.request import urlopen
4 | from html import unescape
5 |
6 | def main():
7 | """
8 | 메인 처리입니다.
9 | fetch(), scrape(), save() 함수를 호출합니다.
10 | """
11 | html = fetch('http://www.hanbit.co.kr/store/books/full_book_list.html')
12 | books = scrape(html)
13 | save('books.db', books)
14 |
15 | def fetch(url):
16 | """
17 | 매개변수로 전달받을 url을 기반으로 웹 페이지를 추출합니다.
18 | 웹 페이지의 인코딩 형식은 Content-Type 헤더를 기반으로 알아냅니다.
19 | 반환값: str 자료형의 HTML
20 | """
21 | f = urlopen(url)
22 | # HTTP 헤더를 기반으로 인코딩 형식을 추출합니다.
23 | encoding = f.info().get_content_charset(failobj="utf-8")
24 | # 추출한 인코딩 형식을 기반으로 문자열을 디코딩합니다.
25 | html = f.read().decode(encoding)
26 | return html
27 |
28 | def scrape(html):
29 | """
30 | 매개변수 html로 받은 HTML을 기반으로 정규 표현식을 사용해 도서 정보를 추출합니다.
31 | 반환값: 도서(dict) 리스트
32 | """
33 | books = []
34 | # re.findall()을 사용해 도서 하나에 해당하는 HTML을 추출합니다.
35 | for partial_html in re.findall(r'
', html, re.DOTALL):
36 | # 도서의 URL을 추출합니다.
37 | url = re.search(r'', partial_html).group(1)
38 | url = 'http://www.hanbit.co.kr' + url
39 | # 태그를 제거해서 도서의 제목을 추출합니다.
40 | title = re.sub(r'<.*?>', '', partial_html)
41 | title = unescape(title)
42 | books.append({'url': url, 'title': title})
43 |
44 | return books
45 |
46 | def save(db_path, books):
47 | """
48 | 매개변수 books로 전달된 도서 목록을 SQLite 데이터베이스에 저장합니다.
49 | 데이터베이스의 경로는 매개변수 dp_path로 지정합니다.
50 | 반환값: None(없음)
51 | """
52 | # 데이터베이스를 열고 연결을 확립합니다.
53 | conn = sqlite3.connect(db_path)
54 | # 커서를 추출합니다.
55 | c = conn.cursor()
56 | # execute() 메서드로 SQL을 실행합니다.
57 | # 스크립트를 여러 번 실행할 수 있으므로 기존의 books 테이블을 제거합니다.
58 | c.execute('DROP TABLE IF EXISTS books')
59 | # books 테이블을 생성합니다.
60 | c.execute('''
61 | CREATE TABLE books (
62 | title text,
63 | url text
64 | )
65 | ''')
66 | # executemany() 메서드를 사용하면 매개변수로 리스트를 지정할 수 있습니다.
67 | c.executemany('INSERT INTO books VALUES (:title, :url)', books)
68 | # 변경사항을 커밋(저장)합니다.
69 | conn.commit()
70 | # 연결을 종료합니다.
71 | conn.close()
72 |
73 | # python 명령어로 실행한 경우 main() 함수를 호출합니다.
74 | # 이는 모듈로써 다른 파일에서 읽어 들였을 때 main() 함수가 호출되지 않게 하는 것입니다.
75 | # 파이썬 프로그램의 일반적인 작성 방식입니다.
76 | if __name__ == '__main__':
77 | main()
--------------------------------------------------------------------------------
/chapter_2/save.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | cities = [
4 | {'rank': 1, 'city': '상하이', 'population': 24150000},
5 | {'rank': 2, 'city': '카라치', 'population': 23500000},
6 | {'rank': 3, 'city': '베이징', 'population': 21516000},
7 | {'rank': 4, 'city': '텐진', 'population': 14722100},
8 | {'rank': 5, 'city': '이스탄불', 'population': 14160467},
9 | ]
10 |
11 | print(json.dumps(cities))
--------------------------------------------------------------------------------
/chapter_2/save_csv.py:
--------------------------------------------------------------------------------
1 | import csv
2 |
3 | # 파일을 엽니다. newline=''으로 줄바꿈 코드의 자동 변환을 제어합니다.
4 | with open('top_cities.csv', 'w', newline='') as f:
5 | # csv.writer는 파일 객체를 매개변수로 지정합니다.
6 | writer = csv.writer(f)
7 | # 첫 번째 줄에는 헤더를 작성합니다.
8 | writer.writerow(['rank', 'city', 'population'])
9 | # writerows()에 리스트를 전달하면 여러 개의 값을 출력합니다.
10 | writer.writerows([
11 | [1, '상하이', 24150000],
12 | [2, '카라치', 23500000],
13 | [3, '베이징', 21516000],
14 | [4, '텐진', 14722100],
15 | [5, '이스탄불', 14160467],
16 | ])
--------------------------------------------------------------------------------
/chapter_2/save_csv_dict.py:
--------------------------------------------------------------------------------
1 | import csv
2 |
3 | with open('top_cities.csv', 'w', newline='') as f:
4 | # 첫 번째 매개변수에 파일 객체
5 | # 두 번째 매개변수에 필드 이름 리스트를 지정합니다.
6 | writer = csv.DictWriter(f, ['rank', 'city', 'population'])
7 | # 첫 번째 줄에 헤더를 입력합니다.
8 | writer.writeheader()
9 | # writerows()로 여러 개의 데이터를 딕셔너리 형태로 작성합니다.
10 | writer.writerows([
11 | {'rank': 1, 'city': '상하이', 'population': 24150000},
12 | {'rank': 2, 'city': '카라치', 'population': 23500000},
13 | {'rank': 3, 'city': '베이징', 'population': 21516000},
14 | {'rank': 4, 'city': '텐진', 'population': 14722100},
15 | {'rank': 5, 'city': '이스탄불', 'population': 14160467},
16 | ])
--------------------------------------------------------------------------------
/chapter_2/save_csv_join.py:
--------------------------------------------------------------------------------
1 | # 첫 번째 줄에 헤더를 작성합니다.
2 | print('rank,city,population')
3 |
4 | # join() 메서드의 매개변수로 전달한 list는 str이어야 하므로 주의해 주세요.
5 | print(','.join(['1', '상하이', '24150000']))
6 | print(','.join(['2', '카라치', '23500000']))
7 | print(','.join(['3', '베이징', '21516000']))
8 | print(','.join(['4', '텐진', '14722100']))
9 | print(','.join(['5', '이스탄불', '14160467']))
--------------------------------------------------------------------------------
/chapter_2/save_sqlite3.py:
--------------------------------------------------------------------------------
1 | import sqlite3
2 |
3 | # top_cities.db 파일을 열고 연결을 변수에 저장합니다.
4 | conn = sqlite3.connect('top_cities.db')
5 |
6 | # 커서를 추출합니다.
7 | c = conn.cursor()
8 |
9 | # execute() 메서드로 SQL 구문을 실행합니다.
10 | # 스크립트를 여러 번 사용해도 같은 결과를 출력할 수 있게 cities 테이블이 존재하는 경우 제거합니다.
11 | c.execute('DROP TABLE IF EXISTS cities')
12 | # cities 테이블을 생성합니다.
13 | c.execute('''
14 | CREATE TABLE cities (
15 | rank integer,
16 | city text,
17 | population integer
18 | )
19 | ''')
20 |
21 | # execute() 메서드의 두 번째 매개변수에는 파라미터를 지정할 수 있습니다.
22 | # SQL 내부에서 파라미터로 변경할 부분(플레이스홀더)은 ?로 지정합니다.
23 | c.execute('INSERT INTO cities VALUES (?, ?, ?)', (1, '상하이', 24150000))
24 |
25 | # 파라미터가 딕셔너리일 때는 플레이스홀더를 :<이름> 형태로 지정합니다.
26 | c.execute('INSERT INTO cities VALUES (:rank, :city, :population)',
27 | {'rank': 2, 'city': '카라치', 'population': 23500000})
28 |
29 | # executemany() 메서드를 사용하면 여러 개의 파라미터를 리스트로 지정해서
30 | # 여러 개(현재 예제에서는 3개)의 SQL 구문을 실행할 수 있습니다.
31 | c.executemany('INSERT INTO cities VALUES (:rank, :city, :population)', [
32 | {'rank': 3, 'city': '베이징', 'population': 21516000},
33 | {'rank': 4, 'city': '텐진', 'population': 14722100},
34 | {'rank': 5, 'city': '이스탄불', 'population': 14160467},
35 | ])
36 |
37 | # 변경사항을 커밋(저장)합니다.
38 | conn.commit()
39 |
40 | # 저장한 데이터를 추출합니다.
41 | c.execute('SELECT * FROM cities')
42 | # 쿼리의 결과는 fetchall() 메서드로 추출할 수 있습니다.
43 | for row in c.fetchall():
44 | # 추출한 데이터를 출력합니다.
45 | print(row)
46 |
47 | # 연결을 닫습니다.
48 | conn.close()
--------------------------------------------------------------------------------
/chapter_2/scrape_re.py:
--------------------------------------------------------------------------------
1 | import re
2 | from html import unescape
3 |
4 | # 이전 절에서 다운로드한 파일을 열고 html이라는 변수에 저장합니다.
5 | with open('dp.html') as f:
6 | html = f.read()
7 |
8 | # re.findall()을 사용해 도서 하나에 해당하는 HTML을 추출합니다.
9 | for partial_html in re.findall(r'', html, re.DOTALL):
10 | # 도서의 URL을 추출합니다.
11 | url = re.search(r'', partial_html).group(1)
12 | url = 'http://www.hanbit.co.kr' + url
13 | # 태그를 제거해서 도서의 제목을 추출합니다.
14 | title = re.sub(r'<.*?>', '', partial_html)
15 | title = unescape(title)
16 | print('url:', url)
17 | print('title:', title)
18 | print('---')
--------------------------------------------------------------------------------
/chapter_2/scrape_rss.py:
--------------------------------------------------------------------------------
1 | # ElementTree 모듈을 읽어 들입니다.
2 | from xml.etree import ElementTree
3 |
4 | # parse() 함수로 파일을 읽어 들이고 ElementTree 객체를 만듭니다.
5 | tree = ElementTree.parse('rss.xml')
6 |
7 | # getroot() 메서드로 XML의 루트 요소를 추출합니다.
8 | root = tree.getroot()
9 |
10 | # findall() 메서드로 요소 목록을 추출합니다.
11 | # 태그를 찾습니다(자세한 내용은 RSS를 열어 참고해주세요).
12 | for item in root.findall('channel/item/description/body/location/data'):
13 | # find() 메서드로 요소를 찾고 text 속성으로 값을 추출합니다.
14 | tm_ef = item.find('tmEf').text
15 | tmn = item.find('tmn').text
16 | tmx = item.find('tmx').text
17 | wf = item.find('wf').text
18 | print(tm_ef, tmn, tmx, wf) # 출력합니다.
--------------------------------------------------------------------------------
/chapter_2/try_and_with.py:
--------------------------------------------------------------------------------
1 | d = {'a': 1, 'b': 2}
2 | try:
3 | # 예외가 발생할 가능성이 있는 처리를 넣습니다.
4 | print(d['x'])
5 | except KeyError:
6 | # try 절 내부에서 except 절에 작성된 예외(현재 예제에서는 KeyError)가 발생하면
7 | # except 절이 실행됩니다. 여기서는 키가 존재하지 않을 때의 처리 내용을 지정했습니다.
8 | print('x is not found')
9 |
10 | # open() 함수의 반환값은 변수 f에 할당되며 with 블록 내부에서 사용합니다.
11 | # 이렇게 사용하면 블록을 벗어날 때 f.close()가 자동으로 호출됩니다.
12 | with open('index.html') as f:
13 | print(f.read())
--------------------------------------------------------------------------------
/chapter_2/urlopen_encoding.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from urllib.request import urlopen
3 | f = urlopen('http://www.hanbit.co.kr/store/books/full_book_list.html')
4 |
5 | # HTTP 헤더를 기반으로 인코딩 방식을 추출합니다(명시돼 있지 않을 경우 utf-8을 사용하게 합니다).
6 | encoding = f.info().get_content_charset(failobj="utf-8")
7 | # 인코딩 방식을 표준 오류에 출력합니다.
8 | print('encoding:', encoding, file=sys.stderr)
9 |
10 | # 추출한 인코딩 방식으로 디코딩합니다.
11 | text = f.read().decode(encoding)
12 | # 웹 페이지의 내용을 표준 출력에 출력합니다.
13 | print(text)
--------------------------------------------------------------------------------
/chapter_2/urlopen_meta.py:
--------------------------------------------------------------------------------
1 | import re
2 | import sys
3 | from urllib.request import urlopen
4 |
5 | f = urlopen('http://www.hanbit.co.kr/store/books/full_book_list.html')
6 | # bytes 자료형의 응답 본문을 일단 변수에 저장합니다.
7 | bytes_content = f.read()
8 |
9 | # charset은 HTML의 앞부분에 적혀 있는 경우가 많으므로
10 | # 응답 본문의 앞부분 1024바이트를 ASCII 문자로 디코딩해 둡니다.
11 | # ASCII 범위 이위의 문자는 U+FFFD(REPLACEMENT CHARACTER)로 변환되어 예외가 발생하지 않습니다.
12 | scanned_text = bytes_content[:1024].decode('ascii', errors='replace')
13 |
14 | # 디코딩한 문자열에서 정규 표현식으로 charset 값을 추출합니다.
15 | match = re.search(r'charset=["\']?([\w-]+)', scanned_text)
16 | if match:
17 | encoding = match.group(1)
18 | else:
19 | # charset이 명시돼 있지 않으면 UTF-8을 사용합니다.
20 | encoding = 'utf-8'
21 |
22 | # 추출한 인코딩을 표준 오류에 출력합니다.
23 | print('encoding:', encoding, file=sys.stderr)
24 |
25 | # 추출한 인코딩으로 다시 디코딩합니다.
26 | text = bytes_content.decode(encoding)
27 | # 응답 본문을 표준 출력에 출력합니다.
28 | print(text)
--------------------------------------------------------------------------------
/chapter_3/python_crawler_1.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import lxml.html
3 |
4 | response = requests.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
5 | root = lxml.html.fromstring(response.content)
6 | for a in root.cssselect('.view_box a'):
7 | url = a.get('href')
8 | print(url)
--------------------------------------------------------------------------------
/chapter_3/python_crawler_2.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import lxml.html
3 |
4 | response = requests.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
5 | root = lxml.html.fromstring(response.content)
6 |
7 | # 모든 링크를 절대 URL로 변환합니다.
8 | root.make_links_absolute(response.url)
9 |
10 | # 선택자를 추가해서 명확한 선택을 할 수 있게 합니다.
11 | for a in root.cssselect('.view_box .book_tit a'):
12 | url = a.get('href')
13 | print(url)
--------------------------------------------------------------------------------
/chapter_3/python_crawler_3.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import lxml.html
3 | def main():
4 | """
5 | 크롤러의 메인 처리
6 | """
7 | # 여러 페이지에서 크롤링할 것이므로 Session을 사용합니다.
8 | session = requests.Session()
9 | # scrape_list_page() 함수를 호출해서 제너레이터를 추출합니다.
10 | response = session.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
11 | urls = scrape_list_page(response)
12 | # 제너레이터는 list처럼 사용할 수 있습니다.
13 | for url in urls:
14 | print(url)
15 |
16 | def scrape_list_page(response):
17 | root = lxml.html.fromstring(response.content)
18 | root.make_links_absolute(response.url)
19 | for a in root.cssselect('.view_box .book_tit a'):
20 | url = a.get('href')
21 | # yield 구문으로 제너레이터의 요소 반환
22 | yield url
23 |
24 | if __name__ == '__main__':
25 | main()
--------------------------------------------------------------------------------
/chapter_3/python_crawler_4.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import lxml.html
3 |
4 | def main():
5 | # 여러 페이지에서 크롤링할 것이므로 Session을 사용합니다.
6 | session = requests.Session()
7 | response = session.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
8 | urls = scrape_list_page(response)
9 | for url in urls:
10 | response = session.get(url) # Session을 사용해 상세 페이지를 추출합니다.
11 | ebook = scrape_detail_page(response) # 상세 페이지에서 상세 정보를 추출합니다.
12 | print(ebook) # 책 관련 정보를 출력합니다.
13 | break # 책 한 권이 제대로 되는지 확인하고 종료합니다.
14 |
15 | def scrape_list_page(response):
16 | root = lxml.html.fromstring(response.content)
17 | root.make_links_absolute(response.url)
18 | for a in root.cssselect('.view_box .book_tit a'):
19 | url = a.get('href')
20 | yield url
21 |
22 | def scrape_detail_page(response):
23 | """
24 | 상세 페이지의 Response에서 책 정보를 dict로 추출합니다.
25 | """
26 | root = lxml.html.fromstring(response.content)
27 | ebook = {
28 | 'url': response.url,
29 | 'title': root.cssselect('.store_product_info_box h3')[0].text_content(),
30 | 'price': root.cssselect('.pbr strong')[0].text_content(),
31 | 'content': [p.text_content()\
32 | for p in root.cssselect('#tabs_3 .hanbit_edit_view p')]
33 | }
34 | return ebook
35 |
36 | if __name__ == '__main__':
37 | main()
--------------------------------------------------------------------------------
/chapter_3/python_crawler_5.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import lxml.html
3 |
4 | def main():
5 | # 여러 페이지에서 크롤링할 것이므로 Session을 사용합니다.
6 | session = requests.Session()
7 | response = session.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
8 | urls = scrape_list_page(response)
9 | for url in urls:
10 | response = session.get(url) # Session을 사용해 상세 페이지를 추출합니다.
11 | ebook = scrape_detail_page(response) # 상세 페이지에서 상세 정보를 추출합니다.
12 | print(ebook) # 책 관련 정보를 출력합니다.
13 | break # 책 한 권이 제대로 되는지 확인하고 종료합니다.
14 |
15 | def scrape_list_page(response):
16 | root = lxml.html.fromstring(response.content)
17 | root.make_links_absolute(response.url)
18 | for a in root.cssselect('.view_box .book_tit a'):
19 | url = a.get('href')
20 | yield url
21 |
22 | def scrape_detail_page(response):
23 | """
24 | 상세 페이지의 Response에서 책 정보를 dict로 추출합니다.
25 | """
26 | root = lxml.html.fromstring(response.content)
27 | ebook = {
28 | 'url': response.url,
29 | 'title': root.cssselect('.store_product_info_box h3')[0].text_content(),
30 | 'price': root.cssselect('.pbr strong')[0].text_content(),
31 | 'content': [normalize_spaces(p.text_content())
32 | for p in root.cssselect('#tabs_3 .hanbit_edit_view p')
33 | if normalize_spaces(p.text_content()) != '']
34 | }
35 | return ebook
36 |
37 | def normalize_spaces(s):
38 | """
39 | 연결돼 있는 공백을 하나의 공백으로 변경합니다.
40 | """
41 | return re.sub(r'\s+', ' ', s).strip()
42 |
43 | if __name__ == '__main__':
44 | main()
--------------------------------------------------------------------------------
/chapter_3/python_crawler_6.py:
--------------------------------------------------------------------------------
1 | import time # time 모듈을 임포트합니다.
2 | import re
3 | import requests
4 | import lxml.html
5 |
6 | def main():
7 | session = requests.Session()
8 | response = session.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
9 | urls = scrape_list_page(response)
10 | for url in urls:
11 | time.sleep(1) # 1초 동안 휴식합니다.
12 | response = session.get(url)
13 | ebook = scrape_detail_page(response)
14 | print(ebook)
15 |
16 | def scrape_list_page(response):
17 | root = lxml.html.fromstring(response.content)
18 | root.make_links_absolute(response.url)
19 | for a in root.cssselect('.view_box .book_tit a'):
20 | url = a.get('href')
21 | yield url
22 |
23 | def scrape_detail_page(response):
24 | """
25 | 상세 페이지의 Response에서 책 정보를 dict로 추출합니다.
26 | """
27 | root = lxml.html.fromstring(response.content)
28 | ebook = {
29 | 'url': response.url,
30 | 'title': root.cssselect('.store_product_info_box h3')[0].text_content(),
31 | 'price': root.cssselect('.pbr strong')[0].text_content(),
32 | 'content': [normalize_spaces(p.text_content())
33 | for p in root.cssselect('#tabs_3 .hanbit_edit_view p')
34 | if normalize_spaces(p.text_content()) != '']
35 | }
36 | return ebook
37 |
38 | def normalize_spaces(s):
39 | """
40 | 연결돼 있는 공백을 하나의 공백으로 변경합니다.
41 | """
42 | return re.sub(r'\s+', ' ', s).strip()
43 |
44 | if __name__ == '__main__':
45 | main()
--------------------------------------------------------------------------------
/chapter_3/python_crawler_final.py:
--------------------------------------------------------------------------------
1 | import time
2 | import re
3 | import requests
4 | import lxml.html
5 | from pymongo import MongoClient
6 |
7 | def main():
8 | """
9 | 크롤러의 메인 처리
10 | """
11 | # 크롤러 호스트의 MongoDB에 접속합니다.
12 | client = MongoClient('localhost', 27017)
13 | # scraping 데이터베이스의 ebooks 콜렉션
14 | collection = client.scraping.ebooks
15 | # 데이터를 식별할 수 있는 유일키를 저장할 key 필드에 인덱스를 생성합니다.
16 | collection.create_index('key', unique=True)
17 |
18 | # 목록 페이지를 추출합니다.
19 | response = requests.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
20 | # 상세 페이지의 URL 목록을 추출합니다.
21 | urls = scrape_list_page(response)
22 | for url in urls:
23 | # URL로 키를 추출합니다.
24 | key = extract_key(url)
25 | # MongoDB에서 key에 해당하는 데이터를 검색합니다.
26 | ebook = collection.find_one({'key': key})
27 | # MongoDB에 존재하지 않는 경우만 상세 페이지를 크롤링합니다.
28 | if not ebook:
29 | time.sleep(1)
30 | response = requests.get(url)
31 | ebook = scrape_detail_page(response)
32 | # 책 정보를 MongoDB에 저장합니다.
33 | collection.insert_one(ebook)
34 | # 책 정보를 출력합니다.
35 | print(ebook)
36 |
37 | def scrape_list_page(response):
38 | """
39 | 목록 페이지의 Response에서 상세 페이지의 URL을 추출합니다.
40 | """
41 | root = lxml.html.fromstring(response.content)
42 | root.make_links_absolute(response.url)
43 | for a in root.cssselect('.view_box .book_tit a'):
44 | url = a.get('href')
45 | yield url
46 |
47 | def scrape_detail_page(response):
48 | """
49 | 상세 페이지의 Response에서 책 정보를 dict로 추출합니다.
50 | """
51 | root = lxml.html.fromstring(response.content)
52 | ebook = {
53 | 'url': response.url,
54 | 'key': extract_key(response.url),
55 | 'title': root.cssselect('.store_product_info_box h3')[0].text_content(),
56 | 'price': root.cssselect('.pbr strong')[0].text_content(),
57 | 'content': "생략"
58 | }
59 | return ebook
60 |
61 | def extract_key(url):
62 | """
63 | URL에서 키(URL 끝의 p_code)를 추출합니다.
64 | """
65 | m = re.search(r"p_code=(.+)", url)
66 | return m.group(1)
67 |
68 | def normalize_spaces(s):
69 | """
70 | 연결돼 있는 공백을 하나의 공백으로 변경합니다.
71 | """
72 | return re.sub(r'\s+', ' ', s).strip()
73 |
74 | if __name__ == '__main__':
75 | main()
--------------------------------------------------------------------------------
/chapter_3/save_mongo.py:
--------------------------------------------------------------------------------
1 | import lxml.html
2 | from pymongo import MongoClient
3 |
4 | # HTML 파일을 읽어 들이고
5 | # getroot() 메서드를 사용해 HtmlElement 객체를 추출합니다.
6 | tree = lxml.html.parse('full_book_list.html')
7 | html = tree.getroot()
8 |
9 | client = MongoClient('localhost', 27017)
10 | db = client.scraping # scraping 데이터베이스를 추출합니다.
11 | collection = db.links # links 콜렉션을 추출합니다.
12 |
13 | # 스크립트를 여러 번 사용해도 같은 결과를 출력할 수 있게 콜렉션의 문서를 제거합니다.
14 | collection.delete_many({})
15 |
16 | # cssselect() 메서드로 a 요소의 목록을 추출합니다.
17 | for a in html.cssselect('a'):
18 | # href 속성과 링크의 글자를 추출해서 저장합니다.
19 | collection.insert_one({
20 | 'url': a.get('href'),
21 | 'title': a.text.strip(),
22 | })
23 |
24 | # 콜렉션의 모든 문서를 _id 순서로 정렬해서 추출합니다.
25 | for link in collection.find().sort('_id'):
26 | print(link['_id'], link['url'], link['title'])
--------------------------------------------------------------------------------
/chapter_3/save_mysql.py:
--------------------------------------------------------------------------------
1 | import MySQLdb
2 |
3 | # MySQL 서버에 접속하고 연결을 변수에 저장합니다.
4 | # 사용자 이름과 비밀번호를 지정한 뒤 scraping 데이터베이스를 사용(USE)합니다.
5 | # 접속에 사용할 문자 코드는 utf8mb4로 지정합니다.
6 | conn = MySQLdb.connect(db='scraping', user='scraper', passwd='password', charset='utf8mb4')
7 |
8 | # 커서를 추출합니다.
9 | c = conn.cursor()
10 |
11 | # execute() 메서드로 SQL 구문을 실행합니다.
12 | # 스크립트를 여러 번 사용해도 같은 결과를 출력할 수 있게 cities 테이블이 존재하는 경우 제거합니다.
13 | c.execute('DROP TABLE IF EXISTS cities')
14 | # cities 테이블을 생성합니다.
15 | c.execute('''
16 | CREATE TABLE cities (
17 | rank integer,
18 | city text,
19 | population integer
20 | )
21 | ''')
22 |
23 | # execute() 메서드의 두 번째 매개변수에는 파라미터를 지정할 수 있습니다.
24 | # SQL 내부에서 파라미터로 변경할 부분(플레이스홀더)은 %s로 지정합니다.
25 | c.execute('INSERT INTO cities VALUES (%s, %s, %s)', (1, '상하이', 24150000))
26 |
27 | # 파라미터가 딕셔너리일 때는 플레이스홀더를 %(<이름>)s 형태로 지정합니다.
28 | c.execute('INSERT INTO cities VALUES (%(rank)s, %(city)s, %(population)s)',
29 | {'rank': 2, 'city': '카라치', 'population': 23500000})
30 |
31 | # executemany() 메서드를 사용하면 여러 개의 파라미터를 리스트로 지정해서
32 | # 여러 개(현재 예제에서는 3개)의 SQL 구문을 실행할 수 있습니다.
33 | c.executemany('INSERT INTO cities VALUES (%(rank)s, %(city)s, %(population)s)', [
34 | {'rank': 3, 'city': '베이징', 'population': 21516000},
35 | {'rank': 4, 'city': '텐진', 'population': 14722100},
36 | {'rank': 5, 'city': '이스탄불', 'population': 14160467},
37 | ])
38 |
39 | # 변경사항을 커밋(저장)합니다.
40 | conn.commit()
41 |
42 | # 저장한 데이터를 추출합니다.
43 | c.execute('SELECT * FROM cities')
44 | # 쿼리의 결과는 fetchall() 메서드로 추출할 수 있습니다.
45 | for row in c.fetchall():
46 | # 추출한 데이터를 출력합니다.
47 | print(row)
48 |
49 | # 연결을 닫습니다.
50 | conn.close()
--------------------------------------------------------------------------------
/chapter_3/scrape_by_bs4.py:
--------------------------------------------------------------------------------
1 | from bs4 import BeautifulSoup
2 |
3 | # HTML 파일을 읽어 들이고 BeautifulSoup 객체를 생성합니다.
4 | with open('full_book_list.html') as f:
5 | soup = BeautifulSoup(f, 'html.parser')
6 |
7 | # find_all() 메서드로 a 요소를 추출하고 반복을 돌립니다.
8 | for a in soup.find_all('a'):
9 | # href 속성과 글자를 추출합니다.
10 | print(a.get('href'), a.text)
--------------------------------------------------------------------------------
/chapter_3/scrape_by_feedparser.py:
--------------------------------------------------------------------------------
1 | import feedparser
2 |
3 | # 알라딘 도서 RSS를 읽어 들입니다.
4 | d = feedparser.parse('http://www.aladin.co.kr/rss/special_new/351')
5 |
6 | # 항목을 순회합니다.
7 | for entry in d.entries:
8 | print('이름:', entry.title)
9 | print('타이틀:', entry.title)
10 | print()
--------------------------------------------------------------------------------
/chapter_3/scrape_by_lxml.py:
--------------------------------------------------------------------------------
1 | import lxml.html
2 |
3 | # HTML 파일을 읽어 들이고, getroot() 메서드로 HtmlElement 객체를 생성합니다.
4 | tree = lxml.html.parse('full_book_list.html')
5 | html = tree.getroot()
6 |
7 | # cssselect() 메서드로 a 요소의 리스트를 추출하고 반복을 돌립니다.
8 | for a in html.cssselect('a'):
9 | # href 속성과 글자를 추출합니다.
10 | print(a.get('href'), a.text)
--------------------------------------------------------------------------------
/chapter_4/error_handling.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | import requests
4 | # 일시적인 오류를 나타내는 상태 코드를 지정합니다.
5 | TEMPORARY_ERROR_CODES = (408, 500, 502, 503, 504)
6 |
7 | def main():
8 | """
9 | 메인 처리입니다.
10 | """
11 | response = fetch('http://httpbin.org/status/200,404,503')
12 | if 200 <= response.status_code < 300:
13 | print('Success!')
14 | else:
15 | print('Error!')
16 |
17 | def fetch(url):
18 | """
19 | 지정한 URL에 요청한 뒤 Response 객체를 반환합니다.
20 | 일시적인 오류가 발생하면 최대 3번 재시도합니다.
21 | """
22 | max_retries = 3 # 최대 3번 재시도합니다.
23 | retries = 0 # 현재 재시도 횟수를 나타내는 변수입니다.
24 | while True:
25 | try:
26 | print('Retrieving {0}...'.format(url))
27 | response = requests.get(url)
28 | print('Status: {0}'.format(response.status_code))
29 | if response.status_code not in TEMPORARY_ERROR_CODES:
30 | return response # 일시적인 오류가 아니라면 response를 반환합니다.
31 | except requests.exceptions.RequestException as ex:
32 | # 네트워크 레벨 오류(RequestException)의 경우 재시도합니다.
33 | print('Exception occured: {0}'.format(ex))
34 | retries += 1
35 | if retries >= max_retries:
36 | # 재시도 횟수 상한을 넘으면 예외를 발생시켜버립니다.
37 | raise Exception('Too many retries.')
38 | # 지수 함수적으로 재시도 간격을 증가합니다(**는 제곱 연산자입니다).
39 | wait = 2**(retries - 1)
40 | print('Waiting {0} seconds...'.format(wait))
41 | time.sleep(wait) # 대기합니다.
42 |
43 | if __name__ == '__main__':
44 | main()
--------------------------------------------------------------------------------
/chapter_4/error_handling_with_retrying.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from retrying import retry # pip install retrying
3 | import time
4 | # 일시적인 오류를 나타내는 상태 코드를 지정합니다.
5 | TEMPORARY_ERROR_CODES = (408, 500, 502, 503, 504)
6 |
7 | def main():
8 | """
9 | 메인 처리입니다.
10 | """
11 | response = fetch('http://httpbin.org/status/200,404,503')
12 | if 200 <= response.status_code < 300:
13 | print('Success!')
14 | else:
15 | print('Error!')
16 |
17 | # stop_max_attempt_number로 최대 재시도 횟수를 지정합니다.
18 | # wait_exponential_multiplier로 특정한 시간 만큼 대기하고 재시도하게 합니다. 단위는 밀리초로 입력합니다.
19 | @retry(stop_max_attempt_number=3, wait_exponential_multiplier=1000)
20 | def fetch(url):
21 | """
22 | 지정한 URL에 접근한 뒤 Response 객체를 반환합니다.
23 | 일시적인 오류가 발생할 경우 3번까지 재시도합니다.
24 | """
25 | print('Retrieving {0}...'.format(url))
26 | response = requests.get(url)
27 | print('Status: {0}'.format(response.status_code))
28 | if response.status_code not in TEMPORARY_ERROR_CODES:
29 | # 오류가 없다면 response를 반환합니다.
30 | return response
31 | # 오류가 있다면 예외를 발생시킵니다.
32 | raise Exception('Temporary Error: {0}'.format(response.status_code))
33 |
34 | if __name__ == '__main__':
35 | main()
--------------------------------------------------------------------------------
/chapter_4/request_with_cache.py:
--------------------------------------------------------------------------------
1 | import requests
2 | # pip install CacheControl
3 | from cachecontrol import CacheControl
4 |
5 | session = requests.session()
6 | # session을 래핑한 cached_session 만들기
7 | cached_session = CacheControl(session)
8 |
9 | # 첫 번째는 캐시돼 있지 않으므로 서버에서 추출한 이후 캐시합니다.
10 | response = cached_session.get('https://docs.python.org/3/')
11 | print(response.from_cache) # False
12 |
13 | # 두 번째는 ETag와 Last-Modified 값을 사용해 업데이트됐는지 확인합니다.
14 | # 변경사항이 없는 경우에는 콘텐츠를 캐시에서 추출해서 사용하므로 빠른 처리가 가능합니다.
15 | response = cached_session.get('https://docs.python.org/3/')
16 | print(response.from_cache) # True
--------------------------------------------------------------------------------
/chapter_4/send_email.py:
--------------------------------------------------------------------------------
1 | import smtplib
2 | from email.mime.text import MIMEText
3 | from email.header import Header
4 |
5 | # MIMEText 객체로 메일을 생성합니다.
6 | msg = MIMEText('메일 본분입니다.')
7 |
8 | # 제목에 한글이 포함될 경우 Header 객체를 사용합니다.
9 | msg['Subject'] = Header('메일 제목입니다.', 'utf-8')
10 | msg['From'] = 'me@example.com'
11 | msg['To'] = 'you@example.com'
12 |
13 | # SMTP()의 첫 번째 매개변수에 SMTP 서버의 호스트 이름을 지정합니다.
14 | with smtplib.SMTP('localhost') as smtp:
15 | # 메일을 전송합니다.
16 | smtp.send_message(msg)
17 |
18 | '''
19 | with smtplib.SMTP_SSL('smtp.gmail.com') as smtp:
20 | # 구글 계정의 사용자 이름과 비밀번호를 지정해서 로그인합니다.
21 | # 2단계 인증을 설정한 경우 애플리케이션 비밀번호를 사용해 주세요.
22 | smtp.login('사용자 이름', '비밀번호')
23 | # send_message() 메서드로 메일을 전송합니다.
24 | smtp.send_message(msg)
25 | '''
26 |
--------------------------------------------------------------------------------
/chapter_4/validate_with_re.py:
--------------------------------------------------------------------------------
1 | import re
2 | value = '3,000'
3 |
4 | # 숫자와 쉼표만을 포함한 정규 표현식에 매치하는지 확인합니다.
5 | if not re.search(r'^[0-9,]+$', value):
6 | # 값이 제대로 돼 있지 않다면 예외를 발생시킵니다.
7 | raise ValueError('Invalid price')
--------------------------------------------------------------------------------
/chapter_4/validate_with_voluptuous.py:
--------------------------------------------------------------------------------
1 | # pip install voluptuous
2 | from voluptuous import Schema, Match
3 |
4 | # 다음 4개의 규칙을 가진 스키마를 정의합니다
5 | schema = Schema({ # 규칙1: 객체는 dict 자료형
6 | 'name': str, # 규칙2:name은 str(문자열) 자료형
7 | 'price': Match(r'^[0-9,]+$'), # 규칙3:price가 정규 표현식에 맞는지 확인
8 | }, required=True) # 규칙4:dict의 키는 필수
9 |
10 | # Schema 객체는 함수처럼 호출해서 사용합니다.
11 | # 매개변수에 대상을 넣으면 유효성 검사를 수행합니다.
12 | schema({
13 | 'name': '포도',
14 | 'price': '3,000',
15 | }) # 유효성 검사를 통과하므로 아무 문제 없음
16 |
17 | schema({
18 | 'name': None,
19 | 'price': '3,000',
20 | }) # 유효성 검사를 통과하지 못 하므로, MultipleInvalid 예외가 발생
--------------------------------------------------------------------------------
/chapter_5/get_museums.py:
--------------------------------------------------------------------------------
1 | # pip install SPARQLWrapper
2 | from SPARQLWrapper import SPARQLWrapper
3 |
4 | # SPARQL 엔드 포인트를 지정해서 인스턴스를 생성합니다.
5 | sparql = SPARQLWrapper('http://ko.dbpedia.org/sparql')
6 |
7 | # 한국의 박물관을 추출하는 쿼리입니다.
8 | sparql.setQuery('''
9 | SELECT * WHERE {
10 | ?s rdf:type dbpedia-owl:Museum .
11 | ?s prop-ko:소재지 ?address .
12 | } ORDER BY ?s
13 | ''')
14 |
15 | # 반환 형식을 JSON으로 지정합니다.
16 | sparql.setReturnFormat('json')
17 |
18 | # query()로 쿼리를 실행한 뒤 convert()로 파싱합니다.
19 | response = sparql.query().convert()
20 | for result in response['results']['bindings']:
21 | # 출력합니다.
22 | print(result['s']['value'], result['address']['value'])
--------------------------------------------------------------------------------
/chapter_5/get_museums_with_location.py:
--------------------------------------------------------------------------------
1 | import time
2 | import sys
3 | import os
4 | import json
5 | import dbm
6 | from urllib.request import urlopen
7 | from urllib.parse import urlencode
8 | from SPARQLWrapper import SPARQLWrapper
9 |
10 | def main():
11 | features = [] # 박물관 정보 저장을 위한 리스트
12 | for museum in get_museums():
13 | # 레이블이 있는 경우에는 레이블, 없는 경우에는 s를 추출합니다.
14 | label = museum.get('label', museum['s'])
15 | address = museum['address']
16 | lng, lat = geocode(address)
17 |
18 | # 값을 출력해 봅니다.
19 | print(label, address, lng, lat)
20 | # 위치 정보를 추출하지 못 했을 경우 리스트에 추가하지 않습니다.
21 | if lng is None:
22 | continue
23 |
24 | # features에 박물관 정보를 GeoJSON Feature 형식으로 추가합니다.
25 | features.append({
26 | 'type': 'Feature',
27 | 'geometry': {'type': 'Point', 'coordinates': [lng, lat]},
28 | 'properties': {'label': label, 'address': address},
29 | })
30 |
31 | # GeoJSON FeatureCollection 형식으로 dict를 생성합니다.
32 | feature_collection = {
33 | 'type': 'FeatureCollection',
34 | 'features': features,
35 | }
36 | # FeatureCollection을 .geojson이라는 확장자의 파일로 저장합니다.
37 | with open('museums.geojson', 'w') as f:
38 | json.dump(feature_collection, f)
39 |
40 | def get_museums():
41 | """
42 | SPARQL을 사용해 DBpedia에서 박물관 정보 추출하기
43 | """
44 | print('Executing SPARQL query...', file=sys.stderr)
45 |
46 | # SPARQL 엔드 포인트를 지정해서 인스턴스를 생성합니다.
47 | sparql = SPARQLWrapper('http://ko.dbpedia.org/sparql')
48 |
49 | # 한국의 박물관을 추출하는 쿼리입니다.
50 | sparql.setQuery('''
51 | SELECT * WHERE {
52 | ?s rdf:type dbpedia-owl:Museum .
53 | ?s prop-ko:소재지 ?address .
54 | OPTIONAL { ?s rdfs:label ?label . }
55 | } ORDER BY ?s
56 | ''')
57 |
58 | # 반환 형식을 JSON으로 지정합니다.
59 | sparql.setReturnFormat('json')
60 |
61 | # query()로 쿼리를 실행한 뒤 convert()로 파싱합니다.
62 | response = sparql.query().convert()
63 | print('Got {0} results'.format(len(response['results']['bindings']), file=sys.stderr))
64 | # 쿼리 결과를 반복 처리합니다.
65 | for result in response['results']['bindings']:
66 | # 다루기 쉽게 dict 형태로 변환해서 yield합니다.
67 | yield {name: binding['value'] for name, binding in result.items()}
68 |
69 | # Google Geolocation API
70 | GOOGLE_GEOCODER_API_URL = 'https://maps.googleapis.com/maps/api/geocode/json'
71 | # DBM(파일을 사용한 Key-Value 데이터베이스)로 지오코딩 결과를 캐시합니다.
72 | # 이 변수는 dict처럼 다룰 수 있습니다.
73 | geocoding_cache = dbm.open('geocoding.db', 'c')
74 |
75 | def geocode(address):
76 | """
77 | 매개변수로 지정한 주소를 지오코딩해서 위도와 경도를 반환합니다.
78 | """
79 | if address not in geocoding_cache:
80 | # 주소가 캐시에 존재하지 않는 경우 지오코딩합니다.
81 | print('Geocoding {0}...'.format(address), file=sys.stderr)
82 | time.sleep(1)
83 | url = GOOGLE_GEOCODER_API_URL + '?' + urlencode({
84 | 'key': os.environ['GOOGLE_API_ID'],
85 | 'language': 'ko',
86 | 'address': address,
87 | })
88 | response_text = urlopen(url).read()
89 | # API 응답을 캐시에 저장합니다.
90 | # 문자열을 키와 값에 넣으면 자동으로 bytes로 변환합니다.
91 | geocoding_cache[address] = response_text
92 |
93 | # 캐시 내의 API 응답을 dict로 변환합니다.
94 | # 값은 bytes 자료형이므로 문자열로 변환합니다.
95 | response = json.loads(geocoding_cache[address].decode('utf-8'))
96 | try:
97 | # JSON 형식에서 값을 추출합니다.
98 | lng = response['results'][0]['geometry']['location']['lng']
99 | lat = response['results'][0]['geometry']['location']['lat']
100 | # float 형태로 변환한 뒤 튜플을 반환합니다.
101 | return (float(lng), float(lat))
102 | except:
103 | return (None, None)
104 |
105 | if __name__ == '__main__':
106 | main()
--------------------------------------------------------------------------------
/chapter_5/import_from_stream_api_to_bigquery.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from datetime import timezone
4 | import tweepy
5 | import bigquery
6 |
7 | # 트위터 인증 정보를 읽어 들입니다.
8 | CONSUMER_KEY = os.environ['CONSUMER_KEY']
9 | CONSUMER_SECRET = os.environ['CONSUMER_SECRET']
10 | ACCESS_TOKEN = os.environ['ACCESS_TOKEN']
11 | ACCESS_TOKEN_SECRET = os.environ['ACCESS_TOKEN_SECRET']
12 | auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
13 | auth.set_access_token(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
14 |
15 | # BigQuery 인증 정보(credentials.json)을 지정해 BigQuery 클라이언트를 생성합니다.
16 | # 명시적으로 readonly=False를 지정하지 않으면 쓰기 작업을 할 수 없습니다.
17 | client = bigquery.get_client(json_key_file='credentials.json', readonly=False)
18 |
19 | # BigQuery 데이터 세트 이름
20 | DATASET_NAME = 'twitter'
21 |
22 | # BigQuery 테이블 이름
23 | TABLE_NAME = 'tweets'
24 |
25 | # 테이블이 존재하지 않으면 생성합니다.
26 | if not client.check_table(DATASET_NAME, TABLE_NAME):
27 | print('Creating table {0}.{1}'.format(DATASET_NAME, TABLE_NAME), file=sys.stderr)
28 | # create_table()의 3번째 매개변수로 스키마를 지정합니다.
29 | client.create_table(DATASET_NAME, TABLE_NAME, [
30 | {'name': 'id', 'type': 'string', 'description': '트윗 ID'},
31 | {'name': 'lang', 'type': 'string', 'description': '트윗 언어'},
32 | {'name': 'screen_name', 'type': 'string', 'description': '사용자 이름'},
33 | {'name': 'text', 'type': 'string', 'description': '트윗 문장'},
34 | {'name': 'created_at', 'type': 'timestamp', 'description': '트윗 날짜'},
35 | ])
36 |
37 | class MyStreamListener(tweepy.streaming.StreamListener):
38 | """
39 | Streaming API로 추출한 트윗을 처리하기 위한 클래스
40 | """
41 | status_list = []
42 | num_imported = 0
43 | def on_status(self, status):
44 | """
45 | 트윗을 추출할 때 호출되는 메서드입니다.
46 | 매개변수: 트윗을 나타내는 Status 객체
47 | """
48 | # Status 객체를 status_list에 추가합니다.
49 | self.status_list.append(status)
50 | if len(self.status_list) >= 500:
51 | # status_list에 500개의 데이터가 모이면 BigQuery에 임포트합니다.
52 | if not push_to_bigquery(self.status_list):
53 | # 임포트에 실패하면 False가 반환되므로 오류를 출력하고 종료합니다.
54 | print('Failed to send to bigquery', file=sys.stderr)
55 | return False
56 | # num_imported를 추가한 뒤 status_list를 비웁니다.
57 | self.num_imported += len(self.status_list)
58 | self.status_list = []
59 | print('Imported {0} rows'.format(self.num_imported), file=sys.stderr)
60 | # 요금이 많이 나오지 않게 5000개를 임포트했으면 종료합니다.
61 | # 계속 임포트하고 싶다면 다음 두 줄을 주석 처리해 주세요.
62 | if self.num_imported >= 5000:
63 | return False
64 |
65 | def push_to_bigquery(status_list):
66 | """
67 | 트윗 리스트를 BigQuery에 임포트하는 메서드입니다.
68 | """
69 | # Tweepy의 Status 객체 리스트를 dict 리스트로 변환합니다.
70 | rows = []
71 | for status in status_list:
72 | rows.append({
73 | 'id': status.id_str,
74 | 'lang': status.lang,
75 | 'screen_name': status.author.screen_name,
76 | 'text': status.text,
77 | # datetime 객체를 UTC POSIX 타임스탬프로 변환합니다.
78 | 'created_at': status.created_at.replace(tzinfo=timezone.utc).timestamp(),
79 | })
80 | # dict 리스트를 BigQuery에 임포트합니다.
81 | # 매개변수는 순서대로
82 | # <데이터 세트 이름>, <테이블 이름>, <데이터 리스트>, <데이터를 식별할 필드 이름>입니다.
83 | # insert_id_key는 데이터가 중복되지 않게 만들려고 사용했습니다.
84 | return client.push_rows(DATASET_NAME, TABLE_NAME, rows, insert_id_key='id')
85 |
86 | # Stream API로 읽어 들이기 시작합니다.
87 | print('Collecting tweets...', file=sys.stderr)
88 | stream = tweepy.Stream(auth, MyStreamListener())
89 |
90 | # 공개된 트윗을 샘플링한 스트림을 받습니다.
91 | # 언어를 지정하지 않았으므로 모든 언어의 트윗을 추출할 수 있습니다.
92 | stream.sample()
--------------------------------------------------------------------------------
/chapter_5/konlpy_sample.py:
--------------------------------------------------------------------------------
1 | from konlpy.tag import Kkma
2 |
3 | kkma = Kkma()
4 | malist = kkma.pos("아버지 가방에 들어가신다.")
5 | print(malist)
--------------------------------------------------------------------------------
/chapter_5/museums.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 한국의 박물관
4 |
7 |
8 |
41 |
42 |
--------------------------------------------------------------------------------
/chapter_5/naver_order_history.py:
--------------------------------------------------------------------------------
1 | import time
2 | import sys
3 | import os
4 | from robobrowser import RoboBrowser
5 |
6 | # 인증 정보를 환경변수에서 추출합니다.
7 | NAVER_ID = os.environ['NAVER_ID']
8 | NAVER_PASSWORD = os.environ['NAVER_PASSWORD']
9 |
10 | # RoboBrowser 객체를 생성합니다.
11 | browser = RoboBrowser(
12 | # Beautiful Soup에서 사용할 파서를 지정합니다.
13 | parser='html.parser',
14 | # 일반적인 웹 브라우저의 User-Agent(FireFox)를 사용합니다.
15 | user_agent='Mozilla/5.0 (Macintosh; Intel Mac macOS 10.10; rv:45.0) Gecko/20100101 Firefox/45.0')
16 |
17 | def main():
18 | # 로그인 페이지를 엽니다.
19 | print('Accessing to sign in page....', file=sys.stderr)
20 | browser.open('https://nid.naver.com/nidlogin.login')
21 |
22 | # 로그인 페이지에 들어가졌는지 확인합니다.
23 | assert '네이버 : 로그인' in browser.parsed.title.string
24 |
25 | # name='frmNIDLogin'이라는 입력 양식을 채웁니다.
26 | # 입력 양식의 name 속성은 개발자 도구로 확인할 수 있습니다.
27 | form = browser.get_form(attrs={'name': 'frmNIDLogin'})
28 |
29 | # name='id'라는 입력 양식을 채웁니다.
30 | form['id'] = NAVER_ID
31 | # name='pw'라는 입력 양식을 채웁니다.
32 | form['pw'] = NAVER_PASSWORD
33 |
34 | # 입력 양식을 전송합니다.
35 | # 로그인 때 로그인을 막는 것을 회피하고자 몇 가지 추가 정보를 전송합니다.
36 | print('Signing in...', file=sys.stderr)
37 | browser.submit_form(form, headers={
38 | 'Referer': browser.url,
39 | 'Accept-Language': 'ko,en-US;q=0.7,en;q=0.3',
40 | })
41 |
42 | # 주문 이력 페이지를 엽니다.
43 | browser.open('https://order.pay.naver.com/home?tabMenu=SHOPPING&frm=s_order')
44 |
45 | # 문제가 있을 경우 HTML 소스코드를 확인할 수 있게 출력합니다.
46 | # print(browser.parsed.prettify())
47 | # 주문 이력 페이지가 맞는지 확인합니다.
48 | assert '네이버페이' in browser.parsed.title.string
49 | # 주문 이력을 출력합니다.
50 | print_order_history()
51 |
52 | def print_order_history():
53 | """
54 | 주문 이력을 출력합니다.
55 | """
56 | # 주문 이력을 순회합니다: 클래스 이름은 개발자 도구로 확인합니다.
57 | for item in browser.select('.p_info'):
58 | # 주문 이력 저장 전용 dict입니다.
59 | order = {}
60 | # 주문 이력의 내용을 추출합니다.
61 | name_element = item.select_one('span')
62 | date_element = item.select_one('.date')
63 | price_element = item.select_one('em')
64 | # 내용이 있을 때만 저장합니다.
65 | if name_element and date_element and price_element:
66 | name = name_element.get_text().strip()
67 | date = date_element.get_text().strip()
68 | price = price_element.get_text().strip()
69 | order[name] = {
70 | 'date': date,
71 | 'price': price
72 | }
73 | print(order[name]['date'], '-', order[name]['price'] + '원')
74 |
75 | if __name__ == '__main__':
76 | main()
--------------------------------------------------------------------------------
/chapter_5/plot_advanced_graph.py:
--------------------------------------------------------------------------------
1 | import matplotlib
2 |
3 | # 렌더링 백엔드로 데스크톱 환경이 필요 없는 Agg를 사용합니다.
4 | matplotlib.use('Agg')
5 |
6 | # 한국어를 렌더링할 수 있게 폰트를 지정합니다.
7 | # macOS와 우분투 모두 정상적으로 출력하도록 2개의 폰트를 지정했습니다.
8 | # 기본 상태에서는 한국어가 □로 출력됩니다.
9 | matplotlib.rcParams['font.sans-serif'] = 'NanumGothic,AppleGothic'
10 | import matplotlib.pyplot as plt
11 |
12 | # plot()의 세 번째 매개변수로 계열 스타일을 나타내는 문자열을 지정합니다.
13 | # 'b'는 파란색, 'x'는 × 표시 마커, '-'는 마커를 실선으로 연결하라는 의미입니다.
14 | # 키워드 매개변수 label로 지정한 계열의 이름은 범례로 사용됩니다.
15 | plt.plot([1, 2, 3, 4, 5], [1, 2, 3, 4, 5], 'bx-', label='첫 번째 함수')
16 |
17 | # 'r'은 붉은색,'o'는 ○ 표시 마커, '--'는 점선을 의미합니다.
18 | plt.plot([1, 2, 3, 4, 5], [1, 4, 9, 16, 25], 'ro--', label='두 번째 함수')
19 | # xlabel() 함수로 X축의 레이블을 지정합니다.
20 | plt.xlabel('X 값')
21 | # ylabel() 함수로 Y축의 레이블을 지정합니다.
22 | plt.ylabel('Y 값')
23 | # title() 함수로 그래프의 제목을 지정합니다.
24 | plt.title('matplotlib 샘플')
25 | # legend() 함수로 범례를 출력합니다. loc='best'는 적당한 위치에 출력하라는 의미입니다.
26 | plt.legend(loc='best')
27 |
28 | # X축 범위를 0~6으로 지정합니다. ylim() 함수를 사용하면 Y축 범위를 지정할 수 있습니다.
29 | plt.xlim(0, 6)
30 |
31 | # 그래프를 그리고 파일로 저장합니다.
32 | plt.savefig('advanced_graph.png', dpi=300)
--------------------------------------------------------------------------------
/chapter_5/plot_historical_data.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | import pandas as pd
3 | import matplotlib
4 |
5 | matplotlib.use('Agg')
6 | matplotlib.rcParams['font.sans-serif'] = 'NanumGothic,AppleGothic'
7 | import matplotlib.pyplot as plt
8 |
9 | def main():
10 | # 1981년과 2014년 사이의 환율과 고용률을 출력해 봅니다.
11 | # 조금 이해하기 쉽게 Pandas 대신 기본 숫자 비교와 문자열 비교를 사용해 봤습니다.
12 | # 환율 정보 읽어 들이기
13 | df_exchange = pd.read_csv('DEXKOUS.csv', header=1,
14 | names=['DATE', 'DEXKOUS'], skipinitialspace=True, index_col=0)
15 | years = {}
16 | output = []
17 | for index in df_exchange.index:
18 | year = int(index.split('-')[0])
19 | if (year not in years) and (1981 < year < 2014):
20 | if df_exchange.DEXKOUS[index] != ".":
21 | years[year] = True
22 | output.append([year, float(df_exchange.DEXKOUS[index])])
23 | df_exchange = pd.DataFrame(output)
24 |
25 | # 고용률 통계를 구합니다.
26 | df_jobs = pd.read_excel('gugik.xlsx')
27 | output = []
28 | stacked = df_jobs.stack()[7]
29 | for index in stacked.index:
30 | try:
31 | if 1981 <= int(index) <= 2014:
32 | output.append([int(index), float(stacked[index])])
33 | except:
34 | pass
35 | s_jobs = pd.DataFrame(output)
36 |
37 | # 첫 번째 그래프 그리기
38 | plt.subplot(2, 1, 1)
39 | plt.plot(df_exchange[0], df_exchange[1], label='원/달러')
40 | plt.xlim(1981, 2014) # X축의 범위를 설정합니다.
41 | plt.ylim(500, 2500)
42 | plt.legend(loc='best')
43 |
44 | # 두 번째 그래프 그리기
45 | print(s_jobs)
46 | plt.subplot(2, 1, 2) # 3 1 の3 のサブプロットを作成。
47 | plt.plot(s_jobs[0], s_jobs[1], label='고용률(%)')
48 | plt.xlim(1981, 2014) # X축의 범위를 설정합니다.
49 | plt.ylim(0, 100) # Y축의 범위를 설정합니다.
50 | plt.legend(loc='best')
51 | plt.savefig('historical_data.png', dpi=300) # 이미지를 저장합니다.
52 |
53 | if __name__ == '__main__':
54 | main()
--------------------------------------------------------------------------------
/chapter_5/print_pdf_textboxes.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from pdfminer.converter import PDFPageAggregator
3 | from pdfminer.layout import LAParams, LTContainer, LTTextBox
4 | from pdfminer.pdfinterp import PDFPageInterpreter, PDFResourceManager
5 | from pdfminer.pdfpage import PDFPage
6 |
7 | def find_textboxes_recursively(layout_obj):
8 | """
9 | 재귀적으로 텍스트 박스(LTTextBox)를 찾고
10 | 텍스트 박스들을 리스트로 반환합니다.
11 | """
12 | # LTTextBox를 상속받은 객체의 경우 리스트에 곧바로 넣어서 반환합니다.
13 | if isinstance(layout_obj, LTTextBox):
14 | return [layout_obj]
15 | # LTContainer를 상속받은 객체의 경우 자식 요소를 포함하고 있다는 의미이므로
16 | # 재귀적으로 자식 요소를 계속 찾습니다.
17 | if isinstance(layout_obj, LTContainer):
18 | boxes = []
19 | for child in layout_obj:
20 | boxes.extend(find_textboxes_recursively(child))
21 | return boxes
22 | # 아무것도 없다면 빈 리스트를 반환합니다.
23 | return []
24 |
25 | # 공유 리소스를 관리하는 리소스 매니저를 생성합니다.
26 | laparams = LAParams()
27 | resource_manager = PDFResourceManager()
28 |
29 | # 페이지를 모으는 PageAggregator 객체를 생성합니다.
30 | device = PDFPageAggregator(resource_manager, laparams=laparams)
31 |
32 | # Interpreter 객체를 생성합니다.
33 | interpreter = PDFPageInterpreter(resource_manager, device)
34 |
35 | # 파일을 바이너리 형식으로 읽어 들입니다.
36 | with open(sys.argv[1], 'rb') as f:
37 | # PDFPage.get_pages()로 파일 객체를 지정합니다.
38 | # PDFPage 객체를 차례대로 추출합니다.
39 | # 키워드 매개변수인 pagenos로 처리할 페이지 번호(0-index)를 리스트 형식으로 지정할 수도 있습니다.
40 | for page in PDFPage.get_pages(f):
41 | # 페이지를 처리합니다.
42 | interpreter.process_page(page)
43 | # LTPage 객체를 추출합니다.
44 | layout = device.get_result()
45 | # 페이지 내부의 텍스트 박스를 리스트로 추출합니다.
46 | boxes = find_textboxes_recursively(layout)
47 | # 텍스트 박스를 왼쪽 위의 좌표부터 차례대로 정렬합니다.
48 | # y1(Y 좌표)는 위에 있을수록 크므로 음수로 변환하게 해서 비교했습니다.
49 | boxes.sort(key=lambda b: (-b.y1, b.x0))
50 | for box in boxes:
51 | # 읽기 쉽게 선을 출력합니다.
52 | print('-' * 10)
53 | # 텍스트 박스의 내용을 출력합니다.
54 | print(box.get_text().strip())
--------------------------------------------------------------------------------
/chapter_5/rest_api_with_requests_oauthlib.py:
--------------------------------------------------------------------------------
1 | import os
2 | from requests_oauthlib import OAuth1Session
3 |
4 | # 환경변수에서 인증 정보를 추출합니다.
5 | CONSUMER_KEY = os.environ['CONSUMER_KEY']
6 | CONSUMER_SECRET = os.environ['CONSUMER_SECRET']
7 | ACCESS_TOKEN = os.environ['ACCESS_TOKEN']
8 | ACCESS_TOKEN_SECRET = os.environ['ACCESS_TOKEN_SECRET']
9 |
10 | # 인증 정보를 사용해 OAuth1Session 객체를 생성합니다.
11 | twitter = OAuth1Session(CONSUMER_KEY,
12 | client_secret=CONSUMER_SECRET,
13 | resource_owner_key=ACCESS_TOKEN,
14 | resource_owner_secret=ACCESS_TOKEN_SECRET)
15 |
16 | # 사용자의 타임라인을 추출합니다.
17 | response = twitter.get('https://api.twitter.com/1.1/statuses/home_timeline.json')
18 |
19 | # API 응답이 JSON 형식의 문자열이므로 response.json()으로 파싱합니다.
20 | # status는 트윗(Twitter API에서는 Status라고 부릅니다)를 나타내는 dict입니다.
21 | for status in response.json():
22 | # 사용자 이름과 트윗을 출력합니다.
23 | print('@' + status['user']['screen_name'], status['text'])
--------------------------------------------------------------------------------
/chapter_5/rest_api_with_tweepy.py:
--------------------------------------------------------------------------------
1 | import os
2 | # pip install tweepy
3 | import tweepy
4 |
5 | # 환경변수에서 인증 정보를 추출합니다.
6 | CONSUMER_KEY = os.environ['CONSUMER_KEY']
7 | CONSUMER_SECRET = os.environ['CONSUMER_SECRET']
8 | ACCESS_TOKEN = os.environ['ACCESS_TOKEN']
9 | ACCESS_TOKEN_SECRET = os.environ['ACCESS_TOKEN_SECRET']
10 |
11 | # 인증 정보를 설정합니다.
12 | auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
13 | auth.set_access_token(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
14 |
15 | # API 클라이언트를 생성합니다.
16 | api = tweepy.API(auth)
17 |
18 | # 사용자의 타임라인을 추출합니다.
19 | public_tweets = api.home_timeline()
20 | for status in public_tweets:
21 | # 사용자 이름과 트윗을 출력합니다.
22 | print('@' + status.user.screen_name, status.text)
--------------------------------------------------------------------------------
/chapter_5/robobrowser_google.py:
--------------------------------------------------------------------------------
1 | from robobrowser import RoboBrowser
2 |
3 | # RoboBrowser 객체를 생성합니다.
4 | # 키워드 매개변수 parser는 BeautifulSoup()의 두 번째 매개변수와 같습니다.
5 | browser = RoboBrowser(parser='html.parser')
6 |
7 | # open() 메서드로 구글 메인 페이지를 엽니다.
8 | browser.open('https://www.google.co.kr/')
9 |
10 | # 키워드를 입력합니다.
11 | form = browser.get_form(action='/search')
12 | form['q'] = 'Python'
13 | browser.submit_form(form, list(form.submit_fields.values())[0])
14 |
15 | # 검색 결과 제목을 추출합니다.
16 | # select() 메서드는 BeautifulSoup의 select() 메서드와 같습니다.
17 | for a in browser.select('h3 > a'):
18 | print(a.text)
19 | print(a.get('href'))
20 | print()
--------------------------------------------------------------------------------
/chapter_5/save_youtube_video_metadata.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | from apiclient.discovery import build
5 | from pymongo import MongoClient, DESCENDING
6 |
7 | # 환경변수에서 API 키를 추출합니다.
8 | YOUTUBE_API_KEY = os.environ['YOUTUBE_API_KEY']
9 |
10 | def main():
11 | """
12 | 메인 처리
13 | """
14 | # MongoDB 클라이언트 객체를 생성합니다.
15 | mongo_client = MongoClient('localhost', 27017)
16 | # youtube 데이터베이스의 videos 콜렉션을 추출합니다.
17 | collection = mongo_client.youtube.videos
18 | # 기존의 모든 문서를 제거합니다.
19 | collection.delete_many({})
20 |
21 | # 동영상을 검색하고, 페이지 단위로 아이템 목록을 저장합니다.
22 | for items_per_page in search_videos('요리'):
23 | save_to_mongodb(collection, items_per_page)
24 |
25 | # 뷰 수가 높은 동영상을 출력합니다.
26 | show_top_videos(collection)
27 |
28 | def search_videos(query, max_pages=5):
29 | """
30 | 동영상을 검색하고, 페이지 단위로 list를 yield합니다.
31 | """
32 | # YouTube의 API 클라이언트 생성하기
33 | youtube = build('youtube', 'v3', developerKey=YOUTUBE_API_KEY)
34 | # search.list 메서드로 처음 페이지 추출을 위한 요청 전송하기
35 | search_request = youtube.search().list(
36 | part='id', # search.list에서 동영상 ID만 추출해도 괜찮음
37 | q=query,
38 | type='video',
39 | maxResults=50, # 1페이지에 최대 50개의 동영상 추출
40 | )
41 | # 요청이 성공하고 페이지 수가 max_pages보다 작을 때 반복
42 | # 페이지 수를 제한하는 것은 실행 시간이 너무 길어지는 것을 막기 위해서입니다.
43 | # 더 많은 페이지를 요청해도 상관없습니다
44 | i = 0
45 | while search_request and i < max_pages:
46 | # 요청을 전송합니다.
47 | search_response = search_request.execute()
48 | # 동영상 ID의 리스트를 추출합니다.
49 | video_ids = [item['id']['videoId'] for item in search_response['items']]
50 | # videos.list 메서드로 동영상의 상세 정보를 추출합니다.
51 | videos_response = youtube.videos().list(
52 | part='snippet,statistics',
53 | id=','.join(video_ids)
54 | ).execute()
55 | # 현재 페이지 내부의 아이템을 yield합니다.
56 | yield videos_response['items']
57 |
58 | # list_next() 메서드로 다음 페이지를 추출하기 위한 요청을 보냅니다.
59 | search_request = youtube.search().list_next(search_request, search_response)
60 | i += 1
61 |
62 | def save_to_mongodb(collection, items):
63 | """
64 | MongoDB에 아이템을 저장합니다.
65 | """
66 | # MongoDB에 저장하기 전에 이후에 사용하기 쉽게 아이템을 가공합니다.
67 | for item in items:
68 | # 각 아이템의 id 속성을 _id 속성으로 사용합니다.
69 | item['_id'] = item['id']
70 | # statistics에 포함된 viewCount 속성 등은 문자열이므로 숫자로 변환합니다.
71 | for key, value in item['statistics'].items():
72 | item['statistics'][key] = int(value)
73 |
74 | # 콜렉션에 추가합니다.
75 | result = collection.insert_many(items)
76 | print('Inserted {0} documents'.format(len(result.inserted_ids)), file=sys.stderr)
77 |
78 | def show_top_videos(collection):
79 | """
80 | MongoDB의 콜렉션 내부에서 뷰 수를 기준으로 상위 5개를 출력합니다.
81 | """
82 | for item in collection.find().sort('statistics.viewCount', DESCENDING).limit(5):
83 | print(item['statistics']['viewCount'], item['snippet']['title'])
84 |
85 | if __name__ == '__main__':
86 | main()
--------------------------------------------------------------------------------
/chapter_5/search_youtube_videos.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | # pip install google-api-python-client
4 | from apiclient.discovery import build
5 |
6 | # 환경변수에서 API 키 추출하기
7 | YOUTUBE_API_KEY = os.environ['YOUTUBE_API_KEY']
8 |
9 | # YouTube API 클라이언트를 생성합니다.
10 | # build() 함수의 첫 번째 매개변수에는 API 이름
11 | # 두 번째 매개변수에는 API 버전을 지정합니다.
12 | # 키워드 매개변수 developerKey에는 API 키를 지정합니다.
13 | # 이 함수는 내부적으로 https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest라는
14 | # URL에 접근하고 API 리소스와 메서드 정보를 추출합니다.
15 | youtube = build('youtube', 'v3', developerKey=YOUTUBE_API_KEY)
16 |
17 | # 키워드 매개변수로 매개변수를 지정하고
18 | # search.list 메서드를 호출합니다.
19 | # list() 메서드를 실행하면 googleapiclient.http.HttpRequest가 반환됩니다.
20 | # execute() 메서드를 실행하면 실제 HTTP 요청이 보내지며, API 응답이 반환됩니다.
21 | search_response = youtube.search().list(
22 | part='snippet',
23 | q='요리',
24 | type='video',
25 | ).execute()
26 |
27 | # search_response는 API 응답을 JSON으로 나타낸 dict 객체입니다.
28 | for item in search_response['items']:
29 | # 동영상 제목을 출력합니다.
30 | print(item['snippet']['title'])
--------------------------------------------------------------------------------
/chapter_5/selenium_google.py:
--------------------------------------------------------------------------------
1 | from selenium import webdriver
2 | from selenium.webdriver.common.keys import Keys
3 |
4 | # PhantomJS 모듈의 WebDriver 객체를 생성합니다.
5 | driver = webdriver.PhantomJS()
6 |
7 | # Google 메인 페이지를 엽니다.
8 | driver.get('https://www.google.co.kr/')
9 |
10 | # 타이틀에 'Google'이 포함돼 있는지 확인합니다.
11 | assert 'Google' in driver.title
12 |
13 | # 검색어를 입력하고 검색합니다.
14 | input_element = driver.find_element_by_name('q')
15 | input_element.send_keys('Python')
16 | input_element.send_keys(Keys.RETURN)
17 |
18 | # 타이틀에 'Python'이 포함돼 있는지 확인합니다.
19 | assert 'Python' in driver.title
20 |
21 | # 스크린샷을 찍습니다.
22 | driver.save_screenshot('search_results.png')
23 |
24 | # 검색 결과를 출력합니다.
25 | for a in driver.find_elements_by_css_selector('h3 > a'):
26 | print(a.text)
27 | print(a.get_attribute('href'))
28 | print()
--------------------------------------------------------------------------------
/chapter_5/shopping_rss.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import time
3 |
4 | from selenium import webdriver
5 | from selenium.webdriver.common.by import By
6 | from selenium.webdriver.support import expected_conditions as EC
7 | from selenium.webdriver.support.ui import WebDriverWait
8 | import feedgenerator
9 |
10 | # 인증 정보를 환경변수에서 추출합니다.
11 | NAVER_ID = os.environ['NAVER_ID']
12 | NAVER_PASSWORD = os.environ['NAVER_PASSWORD']
13 |
14 | def main():
15 | """
16 | 메인 처리
17 | """
18 | # PhantomJS의 WebDriver 객체를 생성합니다.
19 | driver = webdriver.PhantomJS()
20 |
21 | # 화면 크기를 설정합니다.
22 | driver.set_window_size(800, 600)
23 |
24 | # 로그인하고 이동한 뒤 주문 이력을 가져옵니다.
25 | sign_in(driver)
26 | navigate(driver)
27 | goods = scrape_history(driver)
28 |
29 | # RSS 피드로 저장합니다.
30 | with open('shopping_history.rss', 'w') as f:
31 | save_as_feed(f, goods)
32 |
33 | def sign_in(driver):
34 | """
35 | 로그인합니다
36 | """
37 | print('Navigating...', file=sys.stderr)
38 | print('Waiting for sign in page loaded...', file=sys.stderr)
39 | time.sleep(2)
40 |
41 | # 입력 양식을 입력하고 전송합니다.
42 | driver.get('https://nid.naver.com/nidlogin.login')
43 | e = driver.find_element_by_id('id')
44 | e.clear()
45 | e.send_keys(NAVER_ID)
46 | e = driver.find_element_by_id('pw')
47 | e.clear()
48 | e.send_keys(NAVER_PASSWORD)
49 | form = driver.find_element_by_css_selector("input.btn_global[type=submit]")
50 | form.submit()
51 |
52 | def navigate(driver):
53 | """
54 | 적절한 페이지로 이동한 뒤
55 | """
56 | print('Navigating...', file=sys.stderr)
57 | driver.get("https://order.pay.naver.com/home?tabMenu=SHOPPING")
58 | print('Waiting for contents to be loaded...', file=sys.stderr)
59 | time.sleep(2)
60 | # 페이지를 아래로 스크롤합니다.
61 | # 사실 현재 예제에서는 필요 없지만 활용 예를 위해 넣어봤습니다.
62 | # 스크롤을 해서 데이터를 가져오는 페이지의 경우 활용할 수 있습니다.
63 | driver.execute_script('scroll(0, document.body.scrollHeight)')
64 | wait = WebDriverWait(driver, 10)
65 |
66 | # [더보기] 버튼을 클릭할 수 있는 상태가 될 때까지 대기하고 클릭합니다.
67 | # 두 번 클릭해서 과거의 정보까지 들고옵니다.
68 | driver.save_screenshot('note-1.png')
69 | button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '#_moreButton a')))
70 | button.click()
71 | button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '#_moreButton a')))
72 | button.click()
73 | # 2초 대기합니다.
74 | print('Waiting for contents to be loaded...', file=sys.stderr)
75 | time.sleep(2)
76 |
77 | def scrape_history(driver):
78 | """
79 | 페이지에서 주문 이력을 추출합니다.
80 | """
81 | goods = []
82 | for info in driver.find_elements_by_css_selector('.p_info'):
83 | # 요소를 추출합니다.
84 | link_element = info.find_element_by_css_selector('a')
85 | title_element = info.find_element_by_css_selector('span')
86 | date_element = info.find_element_by_css_selector('.date')
87 | price_element = info.find_element_by_css_selector('em')
88 | # 텍스트를 추출합니다.
89 | goods.append({
90 | 'url': link_element.get_attribute('.a'),
91 | 'title': title_element.text,
92 | 'description': date_element.text + " - " + price_element.text + "원"
93 | })
94 | return goods
95 |
96 | def save_as_feed(f, posts):
97 | """
98 | 주문 내역을 피드로 저장합니다.
99 | """
100 | # Rss201rev2Feed 객체를 생성합니다.
101 | feed = feedgenerator.Rss201rev2Feed(
102 | title='네이버페이 주문 이력',
103 | link='https://order.pay.naver.com/',
104 | description='주문 이력')
105 |
106 | # 피드를 추가합니다.
107 | for post in posts:
108 | feed.add_item(title=post['title'],
109 | link=post['url'],
110 | description=post['description'],
111 | unique_id=post['url'])
112 |
113 | # 피드를 저장합니다.
114 | feed.write(f, 'utf-8')
115 |
116 | if __name__ == '__main__':
117 | main()
--------------------------------------------------------------------------------
/chapter_5/shopping_selenium.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import time
3 |
4 | from selenium import webdriver
5 | from selenium.webdriver.common.by import By
6 | from selenium.webdriver.support import expected_conditions as EC
7 | from selenium.webdriver.support.ui import WebDriverWait
8 |
9 | # 인증 정보를 환경변수에서 추출합니다.
10 | NAVER_ID = os.environ['NAVER_ID']
11 | NAVER_PASSWORD = os.environ['NAVER_PASSWORD']
12 |
13 | def main():
14 | """
15 | 메인 처리
16 | """
17 | # PhantomJS의 WebDriver 객체를 생성합니다.
18 | driver = webdriver.PhantomJS()
19 |
20 | # 화면 크기를 설정합니다.
21 | driver.set_window_size(800, 600)
22 |
23 | # 로그인하고 이동한 뒤 주문 이력을 가져옵니다.
24 | sign_in(driver)
25 | navigate(driver)
26 | goods = scrape_history(driver)
27 | # 출력합니다.
28 | print(goods)
29 |
30 | def sign_in(driver):
31 | """
32 | 로그인합니다
33 | """
34 | print('Navigating...', file=sys.stderr)
35 | print('Waiting for sign in page loaded...', file=sys.stderr)
36 | time.sleep(2)
37 |
38 | # 입력 양식을 입력하고 전송합니다.
39 | driver.get('https://nid.naver.com/nidlogin.login')
40 | e = driver.find_element_by_id('id')
41 | e.clear()
42 | e.send_keys(NAVER_ID)
43 | e = driver.find_element_by_id('pw')
44 | e.clear()
45 | e.send_keys(NAVER_PASSWORD)
46 | form = driver.find_element_by_css_selector("input.btn_global[type=submit]")
47 | form.submit()
48 |
49 | def navigate(driver):
50 | """
51 | 적절한 페이지로 이동한 뒤
52 | """
53 | print('Navigating...', file=sys.stderr)
54 | driver.get("https://order.pay.naver.com/home?tabMenu=SHOPPING")
55 | print('Waiting for contents to be loaded...', file=sys.stderr)
56 | time.sleep(2)
57 |
58 | # 페이지를 아래로 스크롤합니다.
59 | # 사실 현재 예제에서는 필요 없지만 활용 예를 위해 넣어봤습니다.
60 | # 스크롤을 해서 데이터를 가져오는 페이지의 경우 활용할 수 있습니다.
61 | driver.execute_script('scroll(0, document.body.scrollHeight)')
62 | wait = WebDriverWait(driver, 10)
63 |
64 | # [더보기] 버튼을 클릭할 수 있는 상태가 될 때까지 대기하고 클릭합니다.
65 | # 두 번 클릭해서 과거의 정보까지 들고옵니다.
66 | driver.save_screenshot('note-1.png')
67 | button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '#_moreButton a')))
68 | button.click()
69 | button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '#_moreButton a')))
70 | button.click()
71 | # 2초 대기합니다.
72 | print('Waiting for contents to be loaded...', file=sys.stderr)
73 | time.sleep(2)
74 |
75 | def scrape_history(driver):
76 | """
77 | 페이지에서 주문 이력을 추출합니다.
78 | """
79 | goods = []
80 | for info in driver.find_elements_by_css_selector('.p_info'):
81 | # 요소를 추출합니다.
82 | link_element = info.find_element_by_css_selector('a')
83 | title_element = info.find_element_by_css_selector('span')
84 | date_element = info.find_element_by_css_selector('.date')
85 | price_element = info.find_element_by_css_selector('em')
86 | # 텍스트를 추출합니다.
87 | goods.append({
88 | 'url': link_element.get_attribute('.a'),
89 | 'title': title_element.text,
90 | 'description': date_element.text + " - " + price_element.text + "원"
91 | })
92 | return goods
93 |
94 | if __name__ == '__main__':
95 | main()
--------------------------------------------------------------------------------
/chapter_5/streaming_api_with_tweepy.py:
--------------------------------------------------------------------------------
1 | import os
2 | import tweepy
3 |
4 | # 환경변수에서 인증 정보를 추출합니다.
5 | CONSUMER_KEY = os.environ['CONSUMER_KEY']
6 | CONSUMER_SECRET = os.environ['CONSUMER_SECRET']
7 | ACCESS_TOKEN = os.environ['ACCESS_TOKEN']
8 | ACCESS_TOKEN_SECRET = os.environ['ACCESS_TOKEN_SECRET']
9 |
10 | # 인증 정보를 설정합니다.
11 | auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
12 | auth.set_access_token(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
13 | class MyStreamListener(tweepy.StreamListener):
14 | """
15 | Streaming API로 추출한 트윗을 처리하는 클래스입니다.
16 | """
17 | def on_status(self, status):
18 | """
19 | 트윗을 받을 때 호출되는 메서드
20 | 매개변수로 트윗을 나타내는 Status 객체가 전달됩니다.
21 | """
22 | print('@' + status.author.screen_name, status.text)
23 | # 인증 정보와 StreamListener를 지정해서 Stream 객체를 추출합니다.
24 | stream = tweepy.Stream(auth, MyStreamListener())
25 |
26 | # 공개돼 있는 트윗을 샘플링한 스트림을 받습니다.
27 | # 키워드 매개변수인 languages로 한국어 트윗만 추출합니다
28 | stream.sample(languages=['ko'])
--------------------------------------------------------------------------------
/chapter_5/word_frequency.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 | from glob import glob
4 | from collections import Counter
5 | from konlpy.tag import Kkma
6 |
7 | def main():
8 | """
9 | 명령라인 매개변수로 지정한
10 | 디렉터리 내부의 파일을 읽어 들이고
11 | 빈출 단어를 출력합니다.
12 | """
13 | # 명령어의 첫 번째 매개변수로
14 | # WikiExtractor의 출력 디렉터리를 지정합니다.
15 | input_dir = sys.argv[1]
16 | kkma = Kkma()
17 | # 단어의 빈도를 저장하기 위한 Counter 객체를 생성합니다.
18 | # Counter 클래스는 dict를 상속받는 클래스입니다.
19 | frequency = Counter()
20 | count_proccessed = 0
21 | # glob()으로 와일드카드 매치 파일 목록을 추출하고
22 | # 매치한 모든 파일을 처리합니다.
23 | for path in glob(os.path.join(input_dir, '*', 'wiki_*')):
24 | print('Processing {0}...'.format(path), file=sys.stderr)
25 | # 파일을 엽니다.
26 | with open(path) as file:
27 | # 파일 내부의 모든 기사에 반복을 돌립니다.
28 | for content in iter_docs(file):
29 | # 페이지에서 명사 리스트를 추출합니다.
30 | tokens = get_tokens(kkma, content)
31 | # Counter의 update() 메서드로 리스트 등의 반복 가능 객체를 지정하면
32 | # 리스트에 포함된 값의 출현 빈도를 세어줍니다.
33 | frequency.update(tokens)
34 | # 10,000개의 글을 읽을 때마다 간단하게 출력합니다.
35 | count_proccessed += 1
36 | if count_proccessed % 10000 == 0:
37 | print('{0} documents were processed.'
38 | .format(count_proccessed),file=sys.stderr)
39 |
40 | # 모든 기사의 처리가 끝나면 상위 30개의 단어를 출력합니다
41 | for token, count in frequency.most_common(30):
42 | print(token, count)
43 |
44 | def iter_docs(file):
45 | """
46 | 파일 객체를 읽어 들이고
47 | 기사의 내용(시작 태그 와 종료 태그 사이의 텍스트)를 꺼내는
48 | 제너레이터 함수
49 | """
50 | for line in file:
51 | if line.startswith(''):
55 | # 종료 태그가 찾아지면 버퍼의 내용을 결합한 뒤 yield합니다.
56 | content = ''.join(buffer)
57 | yield content
58 | else:
59 | # 시작 태그/종료 태그 이외의 줄은 버퍼에 추가합니다.
60 | buffer.append(line)
61 |
62 | def get_tokens(kkma, content):
63 | """
64 | 문장 내부에 출현한 명사 리스트를 추출하는 함수
65 | """
66 | # 명사를 저장할 리스트입니다.
67 | tokens = []
68 | node = kkma.pos(content)
69 | for (taeso, pumsa) in node:
70 | # 고유 명사와 일반 명사만 추출합니다.
71 | if pumsa in ('NNG', 'NNP'):
72 | tokens.append(taeso)
73 | return tokens
74 |
75 | if __name__ == '__main__':
76 | main()
--------------------------------------------------------------------------------
/chapter_6/6-1/myspider.py:
--------------------------------------------------------------------------------
1 | import scrapy
2 |
3 | class BlogSpider(scrapy.Spider):
4 | # spider의 이름
5 | name = 'blogspider'
6 |
7 | # 크롤링을 시작할 URL 리스트
8 | start_urls = ['https://blog.scrapinghub.com']
9 |
10 | def parse(self, response):
11 | """
12 | 최상위 페이지에서 카테고리 페이지의 링크를 추출합니다.
13 | """
14 | for url in response.css('ul li a::attr("href")').re('.*/tag/.*'):
15 | yield scrapy.Request(response.urljoin(url), self.parse_titles)
16 |
17 | def parse_titles(self, response):
18 | """
19 | 카페고리 페이지에서 카테고리 타이틀을 모두 추출합니다.
20 | """
21 | for post_title in response.css('div.post-header > h2 > a::text').extract():
22 | yield {'title': post_title}
23 |
--------------------------------------------------------------------------------
/chapter_6/6-2/myproject/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wikibook/python-for-web-scraping/ee720e5453456650e67febc3cb7ce2bdc21b46d6/chapter_6/6-2/myproject/__init__.py
--------------------------------------------------------------------------------
/chapter_6/6-2/myproject/items.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Define here the models for your scraped items
4 | #
5 | # See documentation in:
6 | # http://doc.scrapy.org/en/latest/topics/items.html
7 |
8 | import scrapy
9 |
10 |
11 | class Headline(scrapy.Item):
12 | """
13 | 뉴스 헤드라인을 나타내는 Item 객체
14 | """
15 | title = scrapy.Field()
16 | body = scrapy.Field()
--------------------------------------------------------------------------------
/chapter_6/6-2/myproject/pipelines.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Define your item pipelines here
4 | #
5 | # Don't forget to add your pipeline to the ITEM_PIPELINES setting
6 | # See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html
7 |
8 |
9 | class MyprojectPipeline(object):
10 | def process_item(self, item, spider):
11 | return item
12 |
--------------------------------------------------------------------------------
/chapter_6/6-2/myproject/settings.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Scrapy settings for myproject project
4 | #
5 | # For simplicity, this file contains only settings considered important or
6 | # commonly used. You can find more settings consulting the documentation:
7 | #
8 | # http://doc.scrapy.org/en/latest/topics/settings.html
9 | # http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
10 | # http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
11 |
12 | BOT_NAME = 'myproject'
13 |
14 | SPIDER_MODULES = ['myproject.spiders']
15 | NEWSPIDER_MODULE = 'myproject.spiders'
16 |
17 |
18 | # Crawl responsibly by identifying yourself (and your website) on the user-agent
19 | #USER_AGENT = 'myproject (+http://www.yourdomain.com)'
20 |
21 | # Obey robots.txt rules
22 | ROBOTSTXT_OBEY = True
23 |
24 | # Configure maximum concurrent requests performed by Scrapy (default: 16)
25 | #CONCURRENT_REQUESTS = 32
26 |
27 | # Configure a delay for requests for the same website (default: 0)
28 | # See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay
29 | # See also autothrottle settings and docs
30 | DOWNLOAD_DELAY = 1
31 | # The download delay setting will honor only one of:
32 | #CONCURRENT_REQUESTS_PER_DOMAIN = 16
33 | #CONCURRENT_REQUESTS_PER_IP = 16
34 |
35 | # Disable cookies (enabled by default)
36 | #COOKIES_ENABLED = False
37 |
38 | # Disable Telnet Console (enabled by default)
39 | #TELNETCONSOLE_ENABLED = False
40 |
41 | # Override the default request headers:
42 | #DEFAULT_REQUEST_HEADERS = {
43 | # 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
44 | # 'Accept-Language': 'en',
45 | #}
46 |
47 | # Enable or disable spider middlewares
48 | # See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
49 | #SPIDER_MIDDLEWARES = {
50 | # 'myproject.middlewares.MyCustomSpiderMiddleware': 543,
51 | #}
52 |
53 | # Enable or disable downloader middlewares
54 | # See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
55 | #DOWNLOADER_MIDDLEWARES = {
56 | # 'myproject.middlewares.MyCustomDownloaderMiddleware': 543,
57 | #}
58 |
59 | # Enable or disable extensions
60 | # See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
61 | #EXTENSIONS = {
62 | # 'scrapy.extensions.telnet.TelnetConsole': None,
63 | #}
64 |
65 | # Configure item pipelines
66 | # See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
67 | #ITEM_PIPELINES = {
68 | # 'myproject.pipelines.SomePipeline': 300,
69 | #}
70 |
71 | # Enable and configure the AutoThrottle extension (disabled by default)
72 | # See http://doc.scrapy.org/en/latest/topics/autothrottle.html
73 | #AUTOTHROTTLE_ENABLED = True
74 | # The initial download delay
75 | #AUTOTHROTTLE_START_DELAY = 5
76 | # The maximum download delay to be set in case of high latencies
77 | #AUTOTHROTTLE_MAX_DELAY = 60
78 | # The average number of requests Scrapy should be sending in parallel to
79 | # each remote server
80 | #AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
81 | # Enable showing throttling stats for every response received:
82 | #AUTOTHROTTLE_DEBUG = False
83 |
84 | # Enable and configure HTTP caching (disabled by default)
85 | # See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
86 | #HTTPCACHE_ENABLED = True
87 | #HTTPCACHE_EXPIRATION_SECS = 0
88 | #HTTPCACHE_DIR = 'httpcache'
89 | #HTTPCACHE_IGNORE_HTTP_CODES = []
90 | #HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
91 |
--------------------------------------------------------------------------------
/chapter_6/6-2/myproject/spiders/news.py:
--------------------------------------------------------------------------------
1 | import scrapy
2 |
3 | # Item의 Headline 클래스를 읽어 들입니다.
4 | from myproject.items import Headline
5 |
6 | class NewsSpider(scrapy.Spider):
7 | name = 'news'
8 | # 크롤링 대상 도메인 리스트
9 | allowed_domains = ['engadget.com']
10 | # 크롤링을 시작할 URL 리스트
11 | start_urls = ['http://engadget.com/']
12 | def parse(self, response):
13 | """
14 | 메인 페이지의 토픽 목록에서 링크를 추출하고 출력합니다.
15 | """
16 | link = response.css('a.o-hit__link::attr("href")').extract()
17 | for url in link:
18 | # 광고 페이지 제외
19 | if url.find("products") == 1:
20 | continue
21 | # 의미 없는 페이지 제외
22 | if url == "#":
23 | continue
24 | # 기사 페이지
25 | yield scrapy.Request(response.urljoin(url), self.parse_topics)
26 |
27 | def parse_topics(self, response):
28 | item = Headline()
29 | item['title'] = response.css('head title::text').extract_first()
30 | item['body'] = " ".join(response.css('.o-article_block p')\
31 | .xpath('string()')\
32 | .extract())
33 | yield item
--------------------------------------------------------------------------------
/chapter_6/6-2/scrapy.cfg:
--------------------------------------------------------------------------------
1 | # Automatically created by: scrapy startproject
2 | #
3 | # For more information about the [deploy] section see:
4 | # https://scrapyd.readthedocs.org/en/latest/deploy.html
5 |
6 | [settings]
7 | default = myproject.settings
8 |
9 | [deploy]
10 | #url = http://localhost:6800/
11 | project = myproject
12 |
--------------------------------------------------------------------------------
/chapter_6/6-3/myproject/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wikibook/python-for-web-scraping/ee720e5453456650e67febc3cb7ce2bdc21b46d6/chapter_6/6-3/myproject/__init__.py
--------------------------------------------------------------------------------
/chapter_6/6-3/myproject/items.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Define here the models for your scraped items
4 | #
5 | # See documentation in:
6 | # http://doc.scrapy.org/en/latest/topics/items.html
7 |
8 | import scrapy
9 |
10 |
11 | class Headline(scrapy.Item):
12 | """
13 | 뉴스 헤드라인을 나타내는 Item 객체
14 | """
15 | title = scrapy.Field()
16 | body = scrapy.Field()
--------------------------------------------------------------------------------
/chapter_6/6-3/myproject/pipelines.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Define your item pipelines here
4 | #
5 | # Don't forget to add your pipeline to the ITEM_PIPELINES setting
6 | # See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html
7 |
8 |
9 | class MyprojectPipeline(object):
10 | def process_item(self, item, spider):
11 | return item
12 |
--------------------------------------------------------------------------------
/chapter_6/6-3/myproject/settings.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Scrapy settings for myproject project
4 | #
5 | # For simplicity, this file contains only settings considered important or
6 | # commonly used. You can find more settings consulting the documentation:
7 | #
8 | # http://doc.scrapy.org/en/latest/topics/settings.html
9 | # http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
10 | # http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
11 |
12 | BOT_NAME = 'myproject'
13 |
14 | SPIDER_MODULES = ['myproject.spiders']
15 | NEWSPIDER_MODULE = 'myproject.spiders'
16 |
17 |
18 | # Crawl responsibly by identifying yourself (and your website) on the user-agent
19 | #USER_AGENT = 'myproject (+http://www.yourdomain.com)'
20 |
21 | # Obey robots.txt rules
22 | ROBOTSTXT_OBEY = True
23 |
24 | # Configure maximum concurrent requests performed by Scrapy (default: 16)
25 | #CONCURRENT_REQUESTS = 32
26 |
27 | # Configure a delay for requests for the same website (default: 0)
28 | # See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay
29 | # See also autothrottle settings and docs
30 | DOWNLOAD_DELAY = 1
31 | # The download delay setting will honor only one of:
32 | #CONCURRENT_REQUESTS_PER_DOMAIN = 16
33 | #CONCURRENT_REQUESTS_PER_IP = 16
34 |
35 | # Disable cookies (enabled by default)
36 | #COOKIES_ENABLED = False
37 |
38 | # Disable Telnet Console (enabled by default)
39 | #TELNETCONSOLE_ENABLED = False
40 |
41 | # Override the default request headers:
42 | #DEFAULT_REQUEST_HEADERS = {
43 | # 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
44 | # 'Accept-Language': 'en',
45 | #}
46 |
47 | # Enable or disable spider middlewares
48 | # See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
49 | #SPIDER_MIDDLEWARES = {
50 | # 'myproject.middlewares.MyCustomSpiderMiddleware': 543,
51 | #}
52 |
53 | # Enable or disable downloader middlewares
54 | # See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
55 | #DOWNLOADER_MIDDLEWARES = {
56 | # 'myproject.middlewares.MyCustomDownloaderMiddleware': 543,
57 | #}
58 |
59 | # Enable or disable extensions
60 | # See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
61 | #EXTENSIONS = {
62 | # 'scrapy.extensions.telnet.TelnetConsole': None,
63 | #}
64 |
65 | # Configure item pipelines
66 | # See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
67 | #ITEM_PIPELINES = {
68 | # 'myproject.pipelines.SomePipeline': 300,
69 | #}
70 |
71 | # Enable and configure the AutoThrottle extension (disabled by default)
72 | # See http://doc.scrapy.org/en/latest/topics/autothrottle.html
73 | #AUTOTHROTTLE_ENABLED = True
74 | # The initial download delay
75 | #AUTOTHROTTLE_START_DELAY = 5
76 | # The maximum download delay to be set in case of high latencies
77 | #AUTOTHROTTLE_MAX_DELAY = 60
78 | # The average number of requests Scrapy should be sending in parallel to
79 | # each remote server
80 | #AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
81 | # Enable showing throttling stats for every response received:
82 | #AUTOTHROTTLE_DEBUG = False
83 |
84 | # Enable and configure HTTP caching (disabled by default)
85 | # See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
86 | #HTTPCACHE_ENABLED = True
87 | #HTTPCACHE_EXPIRATION_SECS = 0
88 | #HTTPCACHE_DIR = 'httpcache'
89 | #HTTPCACHE_IGNORE_HTTP_CODES = []
90 | #HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
91 |
--------------------------------------------------------------------------------
/chapter_6/6-3/myproject/spiders/hanbit.py:
--------------------------------------------------------------------------------
1 | from scrapy.spiders import SitemapSpider
2 |
3 | class HanbitSpider(SitemapSpider):
4 | name = "hanbit"
5 | allowed_domains = ["hanbit.co.kr"]
6 | # XML 사이트맵을 지정합니다.
7 | # robots.txt에서 Sitemap 디렉티브를 사용하고 있다면
8 | # robots.txt의 링크를 지정해도 됩니다.
9 | sitemap_urls = [
10 | "http://hanbit.co.kr/sitemap.xml",
11 | ]
12 | # 사이트맵 디렉티브에서 순회할 링크의 정규 표현식을 지정합니다.
13 | # sitemap_follow를 지정하지 않으면 모든 링크를 순회합니다.
14 | sitemap_follow = [
15 | r'post-2015-',
16 | ]
17 | # 사이트맵에 포함돼 있는 URL을 처리할 콜백을 지정합니다.
18 | # 규칙은 (<정규 표현식>, <처리할 콜백 함수>) 형태의 튜플을 지정합니다.
19 | # sitemap_rules를 지정하지 않으면 모든 URL을 parse() 메서드에 전달합니다.
20 | sitemap_rules = [
21 | (r'/2015/\d\d/\d\d/', 'parse_book'),
22 | ]
23 |
24 | def parse_post(self, response):
25 | # 책 페이지에서 제목을 추출합니다.
26 | yield {
27 | 'title': response.css('.store_product_info_box h3::text').extract_first(),
28 | }
--------------------------------------------------------------------------------
/chapter_6/6-3/myproject/spiders/news_crawl.py:
--------------------------------------------------------------------------------
1 | from scrapy.spiders import CrawlSpider, Rule
2 | from scrapy.linkextractors import LinkExtractor
3 |
4 | # Item의 Headline 클래스를 읽어 들입니다.
5 | from myproject.items import Headline
6 |
7 | class NewsSpider(scrapy.Spider):
8 | name = 'news'
9 | # 크롤링 대상 도메인 리스트
10 | allowed_domains = ['engadget.com']
11 | # 크롤링을 시작할 URL 리스트
12 | start_urls = ['http://engadget.com/']
13 | # 링크 순회를 위한 규칙 리스트
14 | rules = [
15 | # 토픽 페이지를 추출한 뒤 응답을 parse_topics() 메서드에 전달합니다.
16 | Rule(LinkExtractor(allow=r'/\d{4}/\d{2}/\d{2}/.+$'), callback='parse_topics'),
17 | ]
18 |
19 | def parse_topics(self, response):
20 | item = Headline()
21 | item['title'] = response.css('head title::text').extract_first()
22 | item['body'] = " ".join(response.css('.o-article_block p')\
23 | .xpath('string()')\
24 | .extract())
25 | yield item
--------------------------------------------------------------------------------
/chapter_6/6-3/scrapy.cfg:
--------------------------------------------------------------------------------
1 | # Automatically created by: scrapy startproject
2 | #
3 | # For more information about the [deploy] section see:
4 | # https://scrapyd.readthedocs.org/en/latest/deploy.html
5 |
6 | [settings]
7 | default = myproject.settings
8 |
9 | [deploy]
10 | #url = http://localhost:6800/
11 | project = myproject
12 |
--------------------------------------------------------------------------------
/chapter_6/6-4/pipelines.py:
--------------------------------------------------------------------------------
1 | from scrapy.exceptions import DropItem
2 |
3 | from pymongo import MongoClient
4 | import MySQLdb
5 |
6 |
7 | class ValidationPipeline(object):
8 | """
9 | Item을 검증하는 Pipeline
10 | """
11 | def process_item(self, item, spider):
12 | if not item['title']:
13 | # title 필드가 추출되지 않으면 제거합니다.
14 | # DropItem()의 매개변수로 제거 이유를 나타내는 메시지를 입력합니다.
15 | raise DropItem('Missing title')
16 | # title 필드가 제대로 추출된 경우
17 | return item
18 |
19 | class MongoPipeline(object):
20 | """
21 | Itemd을 MongoDB에 저장하는 Pipeline
22 | """
23 | def open_spider(self, spider):
24 | """
25 | Spider를 시작할 때 MongoDB에 접속합니다.
26 | """
27 | # 호스트와 포트를 지정해서 클라이언트를 생성합니다.
28 | self.client = MongoClient('localhost', 27017)
29 | # scraping-book 데이터베이스를 추출합니다.
30 | self.db = self.client['scraping-book']
31 | # items 콜렉션을 추출합니다.
32 | self.collection = self.db['items']
33 |
34 | def close_spider(self, spider):
35 | """
36 | Spider가 종료될 때 MongoDB 접속을 끊습니다.
37 | """
38 | self.client.close()
39 | def process_item(self, item, spider):
40 | """
41 | Item을 콜렉션에 추가합니다.
42 | """
43 | # insert_one()의 매개변수에는 item을 깊은 복사를 통해 전달합니다.
44 | self.collection.insert_one(dict(item))
45 | return item
46 |
47 | class MySQLPipeline(object):
48 | """
49 | Item을 MySQL에 저장하는 Pipeline
50 | """
51 |
52 | def open_spider(self, spider):
53 | """
54 | Spider를 시작할 때 MySQL 서버에 접속합니다.
55 | items 테이블이 존재하지 않으면 생성합니다.
56 | """
57 | # settings.py에서 설정을 읽어 들입니다.
58 | settings = spider.settings
59 | params = {
60 | 'host': settings.get('MYSQL_HOST', 'localhost'),
61 | 'db': settings.get('MYSQL_DATABASE', 'scraping'),
62 | 'user': settings.get('MYSQL_USER', ''),
63 | 'passwd': settings.get('MYSQL_PASSWORD', ''),
64 | 'charset': settings.get('MYSQL_CHARSET', 'utf8mb4'),
65 | }
66 | # MySQL 서버에 접속합니다.
67 | self.conn = MySQLdb.connect(**params)
68 | # 커서를 추출합니다.
69 | self.c = self.conn.cursor()
70 | # items 테이블이 존재하지 않으면 생성합니다.
71 | self.c.execute('''
72 | CREATE TABLE IF NOT EXISTS items (
73 | id INTEGER NOT NULL AUTO_INCREMENT,
74 | title CHAR(200) NOT NULL,
75 | PRIMARY KEY (id)
76 | )
77 | ''')
78 | # 변경을 커밋합니다.
79 | self.conn.commit()
80 |
81 | def close_spider(self, spider):
82 | """
83 | Spider가 종료될 때 MySQL 서버와의 접속을 끊습니다.
84 | """
85 | self.conn.close()
86 | def process_item(self, item, spider):
87 | """
88 | Item을 items 테이블에 삽입합니다.
89 | """
90 | self.c.execute('INSERT INTO items (title) VALUES (%(title)s)', dict(item))
91 | self.conn.commit()
92 | return item
--------------------------------------------------------------------------------
/chapter_6/6-7/myproject/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wikibook/python-for-web-scraping/ee720e5453456650e67febc3cb7ce2bdc21b46d6/chapter_6/6-7/myproject/__init__.py
--------------------------------------------------------------------------------
/chapter_6/6-7/myproject/items.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Define here the models for your scraped items
4 | #
5 | # See documentation in:
6 | # http://doc.scrapy.org/en/latest/topics/items.html
7 |
8 | import scrapy
9 |
10 |
11 | class Restaurant(scrapy.Item):
12 | """
13 | 서울 음식점 정보
14 | """
15 | name = scrapy.Field()
16 | address = scrapy.Field()
17 | phone = scrapy.Field()
18 | station = scrapy.Field()
19 | latitude = scrapy.Field()
20 | longitude = scrapy.Field()
21 |
22 | class Page(scrapy.Item):
23 | """
24 | Web 페이지
25 | """
26 | url = scrapy.Field()
27 | title = scrapy.Field()
28 | content = scrapy.Field()
29 |
30 | def __repr__(self):
31 | """
32 | 로그에 출력할 때 너무 길게 출력하지 않게
33 | content를 생략합니다.
34 | """
35 | # 해당 페이지를 복제합니다.
36 | p = Page(self)
37 | if len(p['content']) > 100:
38 | # 100자 이후의 내용은 생략합니다.
39 | p['content'] = p['content'][:100] + '...'
40 | # 복제한 Page를 문자열로 만들어서 반환합니다.
41 | return super(Page, p).__repr__()
--------------------------------------------------------------------------------
/chapter_6/6-7/myproject/pipelines.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Define your item pipelines here
4 | #
5 | # Don't forget to add your pipeline to the ITEM_PIPELINES setting
6 | # See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html
7 |
8 |
9 | class MyprojectPipeline(object):
10 | def process_item(self, item, spider):
11 | return item
12 |
--------------------------------------------------------------------------------
/chapter_6/6-7/myproject/settings.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Scrapy settings for myproject project
4 | #
5 | # For simplicity, this file contains only settings considered important or
6 | # commonly used. You can find more settings consulting the documentation:
7 | #
8 | # http://doc.scrapy.org/en/latest/topics/settings.html
9 | # http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
10 | # http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
11 |
12 | BOT_NAME = 'myproject'
13 |
14 | SPIDER_MODULES = ['myproject.spiders']
15 | NEWSPIDER_MODULE = 'myproject.spiders'
16 |
17 |
18 | # Crawl responsibly by identifying yourself (and your website) on the user-agent
19 | #USER_AGENT = 'myproject (+http://www.yourdomain.com)'
20 |
21 | # Obey robots.txt rules
22 | ROBOTSTXT_OBEY = True
23 |
24 | # Configure maximum concurrent requests performed by Scrapy (default: 16)
25 | #CONCURRENT_REQUESTS = 32
26 |
27 | # Configure a delay for requests for the same website (default: 0)
28 | # See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay
29 | # See also autothrottle settings and docs
30 | DOWNLOAD_DELAY = 1
31 | # The download delay setting will honor only one of:
32 | #CONCURRENT_REQUESTS_PER_DOMAIN = 16
33 | #CONCURRENT_REQUESTS_PER_IP = 16
34 |
35 | # Disable cookies (enabled by default)
36 | #COOKIES_ENABLED = False
37 |
38 | # Disable Telnet Console (enabled by default)
39 | #TELNETCONSOLE_ENABLED = False
40 |
41 | # Override the default request headers:
42 | #DEFAULT_REQUEST_HEADERS = {
43 | # 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
44 | # 'Accept-Language': 'en',
45 | #}
46 |
47 | # Enable or disable spider middlewares
48 | # See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
49 | #SPIDER_MIDDLEWARES = {
50 | # 'myproject.middlewares.MyCustomSpiderMiddleware': 543,
51 | #}
52 |
53 | # Enable or disable downloader middlewares
54 | # See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
55 | #DOWNLOADER_MIDDLEWARES = {
56 | # 'myproject.middlewares.MyCustomDownloaderMiddleware': 543,
57 | #}
58 |
59 | # Enable or disable extensions
60 | # See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
61 | #EXTENSIONS = {
62 | # 'scrapy.extensions.telnet.TelnetConsole': None,
63 | #}
64 |
65 | # Configure item pipelines
66 | # See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
67 | #ITEM_PIPELINES = {
68 | # 'myproject.pipelines.SomePipeline': 300,
69 | #}
70 |
71 | # Enable and configure the AutoThrottle extension (disabled by default)
72 | # See http://doc.scrapy.org/en/latest/topics/autothrottle.html
73 | #AUTOTHROTTLE_ENABLED = True
74 | # The initial download delay
75 | #AUTOTHROTTLE_START_DELAY = 5
76 | # The maximum download delay to be set in case of high latencies
77 | #AUTOTHROTTLE_MAX_DELAY = 60
78 | # The average number of requests Scrapy should be sending in parallel to
79 | # each remote server
80 | #AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
81 | # Enable showing throttling stats for every response received:
82 | #AUTOTHROTTLE_DEBUG = False
83 |
84 | # Enable and configure HTTP caching (disabled by default)
85 | # See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
86 | #HTTPCACHE_ENABLED = True
87 | #HTTPCACHE_EXPIRATION_SECS = 0
88 | #HTTPCACHE_DIR = 'httpcache'
89 | #HTTPCACHE_IGNORE_HTTP_CODES = []
90 | #HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
91 |
--------------------------------------------------------------------------------
/chapter_6/6-7/myproject/spiders/broad.py:
--------------------------------------------------------------------------------
1 | import scrapy
2 | from myproject.items import Page
3 | from myproject.utils import get_content
4 |
5 | class BroadSpider(scrapy.Spider):
6 | name = "broad"
7 | start_urls = (
8 | # 하테나 북마크 엔트리 페이지
9 | 'http://b.hatena.ne.jp/entrylist',
10 | )
11 |
12 | def parse(self, response):
13 | """
14 | 하테나 북마크의 엔트리 페이지를 파싱합니다.
15 | """
16 | # 각각의 웹 페이지 링크를 추출합니다.
17 | for url in response.css('a.entry-link::attr("href")').extract():
18 | # parse_page() 메서드를 콜백 함수로 지정합니다.
19 | yield scrapy.Request(url, callback=self.parse_page)
20 | # of 뒤의 숫자를 두 자리로 지정해 5페이지(첫 페이지, 20, 40, 60, 80)만 추출하게 합니다.
21 | url_more = response.css('a::attr("href")').re_first(r'.*\?of=\d{2}$')
22 | if url_more:
23 | # url_more의 값은 /entrylist로 시작하는 상대 URL이므로
24 | # response.urljoiin() 메서드를 사용해 절대 URL로 변경합니다.
25 | # 콜백 함수를 지정하지 않았으므로 응답은 기본적으로
26 | # parse() 메서드에서 처리하게 됩니다.
27 | yield scrapy.Request(response.urljoin(url_more))
28 |
29 | def parse_page(self, response):
30 | """
31 | 각 페이지를 파싱합니다.
32 | """
33 | # utils.py에 정의돼 있는 get_content() 함수로 타이틀과 본문을 추출합니다.
34 | title, content = get_content(response.text)
35 | # Page 객체로 반환합니다.
36 | yield Page(url=response.url, title=title, content=content)
--------------------------------------------------------------------------------
/chapter_6/6-7/myproject/spiders/visitseoul.py:
--------------------------------------------------------------------------------
1 | import re
2 | from scrapy.spiders import CrawlSpider, Rule
3 | from scrapy.linkextractors import LinkExtractor
4 | from myproject.items import Restaurant
5 |
6 | class VisitSeoulSpider(CrawlSpider):
7 | name = "visitseoul"
8 | allowed_domains = ["korean.visitseoul.net"]
9 | start_urls = ['http://korean.visitseoul.net/eat?curPage=1']
10 | rules = [
11 | # 9페이지까지 순회합니다.
12 | # 정규 표현식 \d를 \d+로 지정하면 모든 페이지를 순회합니다.
13 | Rule(LinkExtractor(allow=r'/eat\?curPage=\d$')),
14 | # 음식점 상세 페이지를 분석합니다.
15 | Rule(LinkExtractor(allow=r'/eat/\w+/\d+'),
16 | callback='parse_restaurant'),
17 | ]
18 |
19 | def parse_restaurant(self, response):
20 | """
21 | 음식점 정보 페이지를 파싱합니다.
22 | """
23 | # 정보를 추출합니다.
24 | name = response.css("#pageheader h3")\
25 | .xpath("string()").extract_first().strip()
26 | address = response.css("dt:contains('주소') + dd")\
27 | .xpath("string()").extract_first().strip()
28 | phone = response.css("dt:contains('전화번호') + dd")\
29 | .xpath("string()").extract_first().strip()
30 | station = response.css("th:contains('지하철') + td")\
31 | .xpath("string()").extract_first().strip()
32 |
33 | # 위도 경도를 추출합니다.
34 | try:
35 | scripts = response.css("script:contains('var lat')").xpath("string()").extract_first()
36 | latitude = re.findall(r"var lat = '(.+)'", scripts)[0]
37 | longitude = re.findall(r"var lng = '(.+)'", scripts)[0]
38 | except Exception as exception:
39 | print("예외 발생")
40 | print(exception)
41 | print()
42 |
43 | # 음식점 객체를 생성합니다.
44 | item = Restaurant(
45 | name=name,
46 | address=address,
47 | phone=phone,
48 | latitude=latitude,
49 | longitude=longitude,
50 | station=station
51 | )
52 | yield item
--------------------------------------------------------------------------------
/chapter_6/6-7/myproject/utils.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import lxml.html
3 | import readability
4 | # Readability의 DEBUG/INFO 수준의 로그를 출력하지 않게 합니다.
5 | # Spider를 실행할 때 Readability의 로그가 많이 출력되므로
6 | # 출력이 보기 힘들어지는 것을 막는 것입니다.
7 | logging.getLogger('readability.readability').setLevel(logging.WARNING)
8 | def get_content(html):
9 | """
10 | HTML 문자열에서 (<제목>, <본문>) 형태의 튜플을 찾은 뒤 반환합니다.
11 | """
12 | document = readability.Document(html)
13 | content_html = document.summary()
14 | # HTM 태그를 제거하고 텍스트만 추출합니다.
15 | content_text = lxml.html.fromstring(content_html).text_content().strip()
16 | short_title = document.short_title()
17 |
18 | return short_title, content_text
--------------------------------------------------------------------------------
/chapter_6/6-7/scrapy.cfg:
--------------------------------------------------------------------------------
1 | # Automatically created by: scrapy startproject
2 | #
3 | # For more information about the [deploy] section see:
4 | # https://scrapyd.readthedocs.org/en/latest/deploy.html
5 |
6 | [settings]
7 | default = myproject.settings
8 |
9 | [deploy]
10 | #url = http://localhost:6800/
11 | project = myproject
12 |
--------------------------------------------------------------------------------
/chapter_6/6-8/extract_faces.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 | import cv2
4 |
5 | try:
6 | # 얼굴 검출 전용 특징량 파일의 경로
7 | cascade_path = sys.argv[1]
8 | except IndexError:
9 | # 명령어 매개변수가 부족한 경우에는 사용법을 출력하고 곧바로 종료합니다.
10 | print('Usage: python extract_faces.py CASCADE_PATH IMAGE_PATH...', file=sys.stderr)
11 | exit(1)
12 |
13 | # 얼굴 이미지 출력 대상 디렉터리가 존재하지 않으면 생성해 둡니다.
14 | output_dir = 'faces'
15 | if not os.path.exists(output_dir):
16 | os.makedirs(output_dir)
17 |
18 | # 특징량 파일이 존재하는지 확인합니다.
19 | assert os.path.exists(cascade_path)
20 | # 특징량 파일의 경로를 지정해 분석 객체를 생성합니다.
21 | classifier = cv2.CascadeClassifier(cascade_path)
22 |
23 | # 두 번째 이후의 매개변수 파일 경로를 반복 처리합니다.
24 | for image_path in sys.argv[2:]:
25 | print('Processing', image_path, file=sys.stderr)
26 |
27 | # 명령어 매개변수에서 얻은 경로의 이미지 파일을 읽어 들입니다.
28 | image = cv2.imread(image_path)
29 | # 얼굴 검출을 빠르게 할 수 있게 이미지를 그레이스케일로 변환합니다.
30 | gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
31 | # 얼굴을 검출합니다.
32 | faces = classifier.detectMultiScale(gray_image)
33 |
34 | # 이미지 파일 이름의 확장자를 제거합니다.
35 | image_name = os.path.splitext(os.path.basename(image_path))[0]
36 |
37 | # 추출된 얼굴의 리스트를 반복 처리합니다.
38 | # i는 0부터 시작되는 순번입니다.
39 | for i, (x, y, w, h) in enumerate(faces):
40 | # 얼굴 부분만 자릅니다.
41 | face_image = image[y:y + h, x: x + w]
42 | # 출력 대상 파일 경로를 생성합니다.
43 | output_path = os.path.join(output_dir, '{0}_{1}.jpg'.format(image_name, i))
44 | # 얼굴 이미지를 저장합니다.
45 | cv2.imwrite(output_path, face_image)
--------------------------------------------------------------------------------
/chapter_6/6-8/myproject/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wikibook/python-for-web-scraping/ee720e5453456650e67febc3cb7ce2bdc21b46d6/chapter_6/6-8/myproject/__init__.py
--------------------------------------------------------------------------------
/chapter_6/6-8/myproject/items.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Define here the models for your scraped items
4 | #
5 | # See documentation in:
6 | # http://doc.scrapy.org/en/latest/topics/items.html
7 |
8 | import scrapy
9 |
10 |
11 | class MyprojectItem(scrapy.Item):
12 | # define the fields for your item here like:
13 | # name = scrapy.Field()
14 | pass
15 |
--------------------------------------------------------------------------------
/chapter_6/6-8/myproject/pipelines.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Define your item pipelines here
4 | #
5 | # Don't forget to add your pipeline to the ITEM_PIPELINES setting
6 | # See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html
7 |
8 |
9 | class MyprojectPipeline(object):
10 | def process_item(self, item, spider):
11 | return item
12 |
--------------------------------------------------------------------------------
/chapter_6/6-8/myproject/settings.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Scrapy settings for myproject project
4 | #
5 | # For simplicity, this file contains only settings considered important or
6 | # commonly used. You can find more settings consulting the documentation:
7 | #
8 | # http://doc.scrapy.org/en/latest/topics/settings.html
9 | # http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
10 | # http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
11 |
12 | BOT_NAME = 'myproject'
13 |
14 | SPIDER_MODULES = ['myproject.spiders']
15 | NEWSPIDER_MODULE = 'myproject.spiders'
16 |
17 |
18 | # Crawl responsibly by identifying yourself (and your website) on the user-agent
19 | #USER_AGENT = 'myproject (+http://www.yourdomain.com)'
20 |
21 | # Obey robots.txt rules
22 | ROBOTSTXT_OBEY = True
23 |
24 | # Configure maximum concurrent requests performed by Scrapy (default: 16)
25 | #CONCURRENT_REQUESTS = 32
26 |
27 | # Configure a delay for requests for the same website (default: 0)
28 | # See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay
29 | # See also autothrottle settings and docs
30 | DOWNLOAD_DELAY = 1
31 | # The download delay setting will honor only one of:
32 | #CONCURRENT_REQUESTS_PER_DOMAIN = 16
33 | #CONCURRENT_REQUESTS_PER_IP = 16
34 |
35 | # Disable cookies (enabled by default)
36 | #COOKIES_ENABLED = False
37 |
38 | # Disable Telnet Console (enabled by default)
39 | #TELNETCONSOLE_ENABLED = False
40 |
41 | # Override the default request headers:
42 | #DEFAULT_REQUEST_HEADERS = {
43 | # 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
44 | # 'Accept-Language': 'en',
45 | #}
46 |
47 | # Enable or disable spider middlewares
48 | # See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
49 | #SPIDER_MIDDLEWARES = {
50 | # 'myproject.middlewares.MyCustomSpiderMiddleware': 543,
51 | #}
52 |
53 | # Enable or disable downloader middlewares
54 | # See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
55 | #DOWNLOADER_MIDDLEWARES = {
56 | # 'myproject.middlewares.MyCustomDownloaderMiddleware': 543,
57 | #}
58 |
59 | # Enable or disable extensions
60 | # See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
61 | #EXTENSIONS = {
62 | # 'scrapy.extensions.telnet.TelnetConsole': None,
63 | #}
64 |
65 | # Configure item pipelines
66 | # See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
67 | #ITEM_PIPELINES = {
68 | # 'myproject.pipelines.SomePipeline': 300,
69 | #}
70 |
71 | # Enable and configure the AutoThrottle extension (disabled by default)
72 | # See http://doc.scrapy.org/en/latest/topics/autothrottle.html
73 | #AUTOTHROTTLE_ENABLED = True
74 | # The initial download delay
75 | #AUTOTHROTTLE_START_DELAY = 5
76 | # The maximum download delay to be set in case of high latencies
77 | #AUTOTHROTTLE_MAX_DELAY = 60
78 | # The average number of requests Scrapy should be sending in parallel to
79 | # each remote server
80 | #AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
81 | # Enable showing throttling stats for every response received:
82 | #AUTOTHROTTLE_DEBUG = False
83 |
84 | # Enable and configure HTTP caching (disabled by default)
85 | # See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
86 | #HTTPCACHE_ENABLED = True
87 | #HTTPCACHE_EXPIRATION_SECS = 0
88 | #HTTPCACHE_DIR = 'httpcache'
89 | #HTTPCACHE_IGNORE_HTTP_CODES = []
90 | #HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
91 |
92 | # ダウンロードした画像ファイルの保存場所
93 | FILES_STORE = 'images'
94 | # SpiderでyieldしたItemを処理するパイプライン
95 | ITEM_PIPELINES = {
96 | 'scrapy.pipelines.files.FilesPipeline': 1
97 | }
98 |
--------------------------------------------------------------------------------
/chapter_6/6-8/myproject/spiders/flickr.py:
--------------------------------------------------------------------------------
1 | import os
2 | from urllib.parse import urlencode
3 | import scrapy
4 | class FlickrSpider(scrapy.Spider):
5 | name = "flickr"
6 | # Files Pipeline으로 다운로드하는 이미지 파일은 allowed_domains에
7 | # 제한을 받으므로 allowed_domains에 'staticflickr.com'을 추가해야 합니다
8 | allowed_domains = ["api.flickr.com"]
9 |
10 | # 키워드 매개변수로 Spider 매개변수 값을 받습니다.
11 | def __init__(self, text='sushi'):
12 | # 부모 클래스의 __init__()을 실행합니다.
13 | super().__init__()
14 | # 환경변수와 Spider 매개변수 값을 사용해 start_urls를 조합합니다.
15 | # urlencode() 함수는 매개변수로 지정한 dict의 키와 값을 URI 인코드해서
16 | # key1=value1&key2=value2라는 문자열로 반환해 줍니다.
17 | self.start_urls = [
18 | 'https://api.flickr.com/services/rest/?' + urlencode({
19 | 'method': 'flickr.photos.search',
20 | 'api_key': os.environ['FLICKR_API_KEY'],
21 | 'text': text,
22 | 'sort': 'relevance',
23 | # CC BY 2.0, CC BY-SA 2.0, CC0를 지정합니다.
24 | 'license': '4,5,9',
25 | }),
26 | ]
27 | def parse(self, response):
28 | """
29 | API의 응답을 파싱해서 file_urls라는 키를 포함한 dict를 생성하고 yield합니다.
30 | """
31 | for photo in response.css('photo'):
32 | yield {'file_urls': [flickr_photo_url(photo)]}
33 |
34 | def flickr_photo_url(photo):
35 | """
36 | 플리커 사진 URL을 조합합니다.
37 | 참고: https://www.flickr.com/services/api/misc.urls.html
38 | """
39 | # 이 경우는 XPath가 CSS 선택자보다 쉬우므로 XPath를 사용하겠습니
40 | return 'https://farm{farm}.staticflickr.com/{server}/{id}_{secret}_{size}.jpg'.format(
41 | farm=photo.xpath('@farm').extract_first(),
42 | server=photo.xpath('@server').extract_first(),
43 | id=photo.xpath('@id').extract_first(),
44 | secret=photo.xpath('@secret').extract_first(),
45 | size='b',
46 | )
--------------------------------------------------------------------------------
/chapter_6/6-8/myproject/utils.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | import lxml.html
4 | import readability
5 |
6 |
7 | logging.getLogger('readability.readability').setLevel(logging.WARNING)
8 |
9 |
10 | def get_content(html):
11 | document = readability.Document(html)
12 | content_html = document.summary()
13 | content_text = lxml.html.fromstring(content_html).text_content().strip()
14 | short_title = document.short_title()
15 |
16 | return short_title, content_text
17 |
--------------------------------------------------------------------------------
/chapter_6/6-8/scrapy.cfg:
--------------------------------------------------------------------------------
1 | # Automatically created by: scrapy startproject
2 | #
3 | # For more information about the [deploy] section see:
4 | # https://scrapyd.readthedocs.org/en/latest/deploy.html
5 |
6 | [settings]
7 | default = myproject.settings
8 |
9 | [deploy]
10 | #url = http://localhost:6800/
11 | project = myproject
12 |
--------------------------------------------------------------------------------
/chapter_7/crawl.py:
--------------------------------------------------------------------------------
1 | import time
2 | import re
3 | import sys
4 |
5 | import requests
6 | import lxml.html
7 | from pymongo import MongoClient
8 | from redis import Redis
9 | from rq import Queue
10 |
11 | def main():
12 | """
13 | 크롤러의 메인 처리
14 | """
15 | q = Queue(connection=Redis())
16 | # 로컬 호스트의 MongoDB에 접속
17 | client = MongoClient('localhost', 27017)
18 | # scraping 데이터베이스의 ebook_htmls 콜렉션을 추출합니다.
19 | collection = client.scraping.ebook_htmls
20 | # key로 빠르게 검색할 수 있게 유니크 인덱스를 생성합니다.
21 | collection.create_index('key', unique=True)
22 |
23 | session = requests.Session()
24 | # 목록 페이지를 추출합니다.
25 | response = requests.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
26 | # 상세 페이지의 URL 목록을 추출합니다.
27 | urls = scrape_list_page(response)
28 | for url in urls:
29 | # URL로 키를 추출합니다.
30 | key = extract_key(url)
31 | # MongoDB에서 key에 해당하는 데이터를 검색합니다.
32 | ebook_html = collection.find_one({'key': key})
33 | # MongoDB에 존재하지 않는 경우에만 상세 페이지를 크롤링합니다.
34 | if not ebook_html:
35 | time.sleep(1)
36 | print('Fetching {0}'.format(url), file=sys.stderr)
37 | # 상세 페이지를 추출합니다.
38 | response = session.get(url)
39 | # HTML을 MongoDB에 저장합니다.
40 | collection.insert_one({
41 | 'url': url,
42 | 'key': key,
43 | 'html': response.content,
44 | })
45 | # 큐에 잡을 주가합니다.
46 | # result_ttl=0을 매개변수로 지정해서
47 | # 태스크의 반환값이 저장되지 않게 합니다.
48 | q.enqueue('scraper_tasks.scrape', key, result_ttl=0)
49 |
50 | def scrape_list_page(response):
51 | """
52 | 목록 페이지의 Response에서 상세 페이지의 URL을 추출합니다.
53 | """
54 | root = lxml.html.fromstring(response.content)
55 | root.make_links_absolute(response.url)
56 | for a in root.cssselect('.view_box .book_tit a'):
57 | url = a.get('href')
58 | yield url
59 |
60 | def extract_key(url):
61 | """
62 | URL에서 키(URL 끝의 p_code)를 추출합니다.
63 | """
64 | m = re.search(r"p_code=(.+)", url)
65 | return m.group(1)
66 |
67 | if __name__ == '__main__':
68 | main()
--------------------------------------------------------------------------------
/chapter_7/crawl_images.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import time
3 | import requests
4 | import lxml.html
5 | import boto3
6 |
7 | # S3 버킷 이름[자신이 생성한 버킷 이름으로 변경해 주세요]
8 | S3_BUCKET_NAME = 'scraping-book'
9 |
10 | def main():
11 | # Wikimedia Commons 페이지에서 이미지 URL을 추출합니다.
12 | image_urls = get_image_urls('https://commons.wikimedia.org/wiki/Category:Mountain_glaciers')
13 | # S3 Bucket 객체를 추출합니다.
14 | s3 = boto3.resource('s3')
15 | bucket = s3.Bucket(S3_BUCKET_NAME)
16 |
17 | for image_url in image_urls:
18 | # 2초 동안 대기합니다.
19 | time.sleep(2)
20 |
21 | # 이미지 파일을 내려받습니다.
22 | print('Downloading', image_url, file=sys.stderr)
23 | response = requests.get(image_url)
24 |
25 | # URL을 기반으로 파일 이름을 추출합니다.
26 | _, filename = image_url.rsplit('/', maxsplit=1)
27 |
28 | # 다운로드한 파일을 S3에 저장합니다.
29 | print('Putting', filename, file=sys.stderr)
30 | bucket.put_object(Key=filename, Body=response.content)
31 |
32 | def get_image_urls(page_url):
33 | """
34 | 매개변수로 전달된 페이지에 출력되고 있는 섬네일 이미지의 원래 URL을 추출합니다.
35 | """
36 | response = requests.get(page_url)
37 | html = lxml.html.fromstring(response.text)
38 |
39 | image_urls = []
40 | for img in html.cssselect('.thumb img'):
41 | thumbnail_url = img.get('src')
42 | image_urls.append(get_original_url(thumbnail_url))
43 |
44 | return image_urls
45 |
46 | def get_original_url(thumbnail_url):
47 | """
48 | 섬네일 URL에서 원래 이미지 URL을 추출합니다.
49 | """
50 | # /로 잘라서 디렉터리에 대응하는 부분의 URL을 추출합니다.
51 | directory_url, _ = thumbnail_url.rsplit('/', maxsplit=1)
52 | # /thumb/을 /로 변경해서 원래 이미지 URL을 추출합니다.
53 | original_url = directory_url.replace('/thumb/', '/')
54 | return original_url
55 |
56 | if __name__ == '__main__':
57 | main()
--------------------------------------------------------------------------------
/chapter_7/crawl_with_aiohttp.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import asyncio
3 |
4 | import aiohttp
5 | import feedparser
6 | from bs4 import BeautifulSoup
7 |
8 | # 최대 동시 다운로드 수를 3개로 제한하기 위한 세마포어를 생성합니다.
9 | semaphore = asyncio.Semaphore(3)
10 |
11 | async def main():
12 | # 인기 항목 RSS에서 URL 목록을 추출합니다
13 | d = feedparser.parse('http://www.reddit.com/r/python/.rss')
14 | urls = [entry.link for entry in d.entries]
15 | # 세션 객체를 생성합니다.
16 | with aiohttp.ClientSession() as session:
17 | # URL 개수만큼 코루틴을 생성합니다.
18 | coroutines = []
19 | for url in urls:
20 | coroutine = fetch_and_scrape(session, url)
21 | coroutines.append(coroutine)
22 | # 코루틴을 완료한 뒤 반복합니다.
23 | for coroutine in asyncio.as_completed(coroutines):
24 | # 코루틴 결과를 출력합니다: 간단하게 출력을 보여드리고자 가공했습니다.
25 | output = await coroutine
26 | output['url'] = output['url'].replace('https://www.reddit.com/r/Python/comments', '')
27 | print(output)
28 |
29 | async def fetch_and_scrape(session, url):
30 | """
31 | 매개변수로 지정한 URL과 제목을 포함한 dict를 반환합니다.
32 | """
33 | # 세마포어 락이 풀릴 때까지 대기합니다.
34 | with await semaphore:
35 | print('Start downloading',
36 | url.replace('https://www.reddit.com/r/Python/comments', ''),
37 | file=sys.stderr)
38 | # 비동기로 요청을 보내고 응답 헤더를 추출합니다.
39 | response = await session.get(url)
40 | # 응답 본문을 비동기적으로 추출합니다.
41 | soup = BeautifulSoup(await response.read(), 'lxml')
42 | return {
43 | 'url': url,
44 | 'title': soup.title.text.strip(),
45 | }
46 |
47 | if __name__ == '__main__':
48 | # 이벤트 루프를 추출합니다.
49 | loop = asyncio.get_event_loop()
50 | # 이벤트 루프로 main()을 실행하고 종료할 때까지 대기합니다.
51 | loop.run_until_complete(main())
--------------------------------------------------------------------------------
/chapter_7/crawl_with_multi_thread.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from concurrent.futures import ThreadPoolExecutor
3 | import feedparser
4 | import requests
5 | from bs4 import BeautifulSoup
6 |
7 | def main():
8 | # URL을 추출합니다.
9 | d = feedparser.parse('http://www.aladin.co.kr/rss/special_new/351')
10 | urls = [entry.link for entry in d.entries]
11 | # 최대 3개의 스레드로 병렬 처리하는 Executor를 생성합니다.
12 | executer = ThreadPoolExecutor(max_workers=3)
13 | # Future 객체를 저장할 리스트를 선언합니다.
14 | futures = []
15 | for url in urls:
16 | # 함수의 실행을 스케줄링하고, Future 객체를 저장합니다.
17 | # submit()의 두 번째 이후 매개변수는 getch_and_scrape() 함수의 매개변수로써 전달됩니다.
18 | future = executer.submit(fetch_and_scrape, url)
19 | futures.append(future)
20 |
21 | for future in futures:
22 | # Future 객체의 결과를 출력합니다.
23 | print(future.result())
24 |
25 | def fetch_and_scrape(url):
26 | """
27 | 매개변수에 지정된 URL 페이지를 추출합니다.
28 | URL와 타이틀을 추출해서 dict 자료형으로 반환합니다.
29 | """
30 | # RSS 링크를 분석합니다.
31 | print('Parse Link', url.split('itemId=')[-1], file=sys.stderr)
32 | response_a = requests.get(url)
33 | soup_a = BeautifulSoup(response_a.content, 'lxml')
34 | book_url = soup_a.select_one('noscript').text.strip().split('\n')[-1]
35 | # 책 링크에 들어갑니다. 알라딘 사이트의 RSS가 이상하게 구성돼 있어서
36 | # 이러한 형태로 타고 들어가도록 코드를 구성했습니다.
37 | print('Parse Book Link', book_url.split('ISBN=')[-1], file=sys.stderr)
38 | response_b = requests.get(book_url)
39 | soup_b = BeautifulSoup(response_b.content, 'lxml')
40 | return {
41 | 'url': url,
42 | 'title': soup_b.title.text.strip(),
43 | }
44 |
45 | if __name__ == '__main__':
46 | main()
--------------------------------------------------------------------------------
/chapter_7/enqueue.py:
--------------------------------------------------------------------------------
1 | from redis import Redis
2 | from rq import Queue
3 | from tasks import add
4 |
5 | # localhost의 TCP 포트 6379에 있는 Redis에 접속합니다.
6 | # 이러한 매개변수는 기본값이므로 생략해도 됩니다.
7 | conn = Redis('localhost', 6379)
8 |
9 | # default라는 이름의 Queue 객체를 추출합니다.
10 | # 이 이름도 기본값이므로 생략해도 됩니다
11 | q = Queue('default', connection=conn)
12 |
13 | # 함수와 매개변수를 지정하고 잡을 추가합니다.
14 | q.enqueue(add, 3, 4)
--------------------------------------------------------------------------------
/chapter_7/scraper_tasks.py:
--------------------------------------------------------------------------------
1 | import re
2 | import lxml.html
3 | from pymongo import MongoClient
4 |
5 | def scrape(key):
6 | """
7 | 워커로 실행할 대상
8 | """
9 | # 로컬 호스트의 MongoDB에 접속합니다.
10 | client = MongoClient('localhost', 27017)
11 |
12 | # scraping 데이터베이스의 ebook_htmls 콜렉션을 추출합니다.
13 | html_collection = client.scraping.ebook_htmls
14 |
15 | # MongoDB에서 key에 해당하는 데이터를 찾습니다.
16 | ebook_html = html_collection.find_one({'key': key})
17 | ebook = scrape_detail_page(key, ebook_html['url'], ebook_html['html'])
18 |
19 | # ebooks 콜렉션을 추출합니다.
20 | ebook_collection = client.scraping.ebooks
21 |
22 | # key로 빠르게 검색할 수 있게 유니크 인덱스를 생성합니다.
23 | ebook_collection.create_index('key', unique=True)
24 |
25 | # ebook을 저장합니다.
26 | ebook_collection.insert_one(ebook)
27 |
28 | def scrape_detail_page(key, url, html):
29 | """
30 | 상세 페이지의 Response에서 책 정보를 dict로 추출하기
31 | """
32 | root = lxml.html.fromstring(html)
33 | ebook = {
34 | 'url': response.url,
35 | 'key': key,
36 | 'title': root.cssselect('.store_product_info_box h3')[0].text_content(),
37 | 'price': root.cssselect('.pbr strong')[0].text_content(),
38 | 'content': [normalize_spaces(p.text_content())
39 | for p in root.cssselect('#tabs_3 .hanbit_edit_view p')
40 | if normalize_spaces(p.text_content()) != ""]
41 | }
42 | return ebook
43 |
44 | def normalize_spaces(s):
45 | """
46 | 연결돼 있는 공백을 하나의 공백으로 변경합니다.
47 | """
48 | return re.sub(r'\s+', ' ', s).strip()
--------------------------------------------------------------------------------
/chapter_7/slow_jobs_async.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | async def slow_job(n):
4 | """
5 | 매개변수로 지정한 시간 만큼 시간이 걸리는 처리를
6 | 비동기적으로 수행하는 코루틴입니다.
7 | asyncio.sleep()을 사용해 시간이 걸리는 처리를 비슷하게 재현해 봤습니다.
8 | """
9 | print('Job {0} will take {0} seconds'.format(n))
10 | # n초 동안 정지
11 | # await는 처리가 끝날 때까지 대기하는 구문입니다.
12 | await asyncio.sleep(n)
13 | print('Job {0} finished'.format(n))
14 |
15 | # 이벤트 루프 추출
16 | loop = asyncio.get_event_loop()
17 | # 3개의 코루틴을 생성합니다. 코루틴은 현재 시점에서 실행되는 것이 아닙니다.
18 | coroutines = [slow_job(1), slow_job(2), slow_job(3)]
19 | # 이벤트 루프로 3개의 코루틴을 실행합니다. 모두 종료될 때까지 이 줄에서 대기합니다.
20 | loop.run_until_complete(asyncio.wait(coroutines))
--------------------------------------------------------------------------------
/chapter_7/slow_jobs_sync.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | def slow_job(n):
4 | """
5 | 매개변수로 지정한 시간 만큼 시간이 걸리는 처리를 수행하는 함수입니다.
6 | time.sleep()을 사용해 시간이 걸리는 처리를 비슷하게 재현해 봤습니다.
7 | """
8 | print('Job {0} will take {0} seconds'.format(n))
9 | # n초 대기합니다.
10 | time.sleep(n)
11 | print('Job {0} finished'.format(n))
12 |
13 | slow_job(1)
14 | slow_job(2)
15 | slow_job(3)
--------------------------------------------------------------------------------
/chapter_7/tasks.py:
--------------------------------------------------------------------------------
1 | def add(x, y):
2 | print(x + y)
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # 파이썬을 이용한 웹 크롤링과 스크레이핑
2 |
3 | 『파이썬을 이용한 웹 크롤링과 스크레이핑』의 예제 파일입니다.
--------------------------------------------------------------------------------
| |