2013년 12월 4일 수요일

[python] Scrapy - 네이버 영화 파싱


Scrapy is a fast high-level screen scraping and web crawling framework, used to crawl websites and extract structured data from their pages. It can be used for a wide range of purposes, from data mining to monitoring and automated testing.

Scrapy documentation : http://doc.scrapy.org/en/0.20/


공식 페이지의 설명에서 볼 수 있듯이 Scrapy는 python 기반의 파싱 프레임워크다.

Scrapy를 알기 전에는 urllib2와 beatifulsoup로 파싱을 해 왔으나, Scrapy를 쓰면 훨씬 빠른 속도로 crawling을 할 수 있었다.


자세한 정보는 공식 튜토리얼을 통해서 얻을 수 있으며, Scrapy를 사용하기 위해서는 lxml, OpenSSL이 필요하다.

Installation : http://doc.scrapy.org/en/latest/intro/install.html

Tutorial : http://doc.scrapy.org/en/latest/intro/tutorial.html#crawling


간단하게 프로젝트를 시작하는 방법을 설명해 보면,

scrapy startproject scrapy_sample

cd scrapy_sample
vi scrapy_sample/spiders/spider.py
로 새로운 프로젝트를 하나 만들고, spider.py 파일을 만든다.


Scrapy 공식 홈페이지에 나와있는 example 소스는 아래와 같다.

from scrapy.spider import BaseSpider
from scrapy.selector import HtmlXPathSelector
from scrapy.item import Item, Field

class Website(Item): // parse 된 정보를 저장할 class
    name = Field() // Field()에는 숫자와 string 모두 저장할 수 있음
    description = Field()
    url = Field()

class DmozSpider(BaseSpider):
    name = "dmoz" // spider의 이름을 나타낸다
    allowed_domains = ["dmoz.org"] // dmoz.org 이외의 redirect는 무시된다.
    start_urls = [
        "http://www.dmoz.org/Computers/Programming/Languages/Python/Books/",
        "http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/",
    ] // start_urls 안에 들어있는 url이 parse 된다

    def parse(self, response):
        hxs = HtmlXPathSelector(response)

        // css selector 문법을 통해 dom element 를 찾을 수 있다.
        sites = hxs.select('//ul[@class="directory-url"]/li') 
        items = []

        for site in sites:
            item = Website()
            item['name'] = site.select('a/text()').extract()
            item['url'] = site.select('a/@href').extract()
            item['description'] = site.select('text()').re('-\s([^\n]*?)\\n')
            items.append(item)
            
        return items

이후 project 의 root 디렉토리에서 아래와 같은 명령어를 입력하면 된다.

scrapy crwal dmoz // spiders 폴더 내에 있는 소스 코드 중 dmoz를 이름으로 가지고 있는 spider를 실행
scrapy crawl dmoz -o some.json -t json 2> result.txt // item을 json형태로 txt 파일에 저장
 
rm result.txt // 크롤 하기 전에 result.txt는 매번 지워주어야 함
 
scrapy shell spider.py // scrapy를 shell 형태로 실행
scrapy shell "http://www.dmoz.org/Computers/Programming/Languages/Python/Books/"

selector 에 대한 정보는

http://doc.scrapy.org/en/latest/topics/selectors.html

여기서 찾을 수 있다.


Scrapy에 대한 한국어 문서가 없어서 제대로된 사용법을 찾는데 시간이 조금 걸렸지만,

다른분들은 이 글을 읽으시고 시간낭비를 하지 않으시길 바란다.


아래의 예제는 네이버 영화 정보를 파싱하는 spider다.

영화 제목과 개봉시기, 장르 등의 정보를 파싱하며 위에 나온 명령어들을 사용하면 쉽게 json 파일을 얻을 수 있을 것이다.


__author__ = 'carpedm20'
__date__ = '2013.11.14'

from scrapy.spider import BaseSpider
from scrapy.selector import HtmlXPathSelector
from scrapy.http.request import Request
from scrapy.item import Item, Field

import sys, os

class ScrapyItem(Item):
    title = Field()
    url = Field()
    year = Field()
    open1 = Field() # 2013
    open2 = Field() # 06.12
    country = Field()
    genre = Field()
    form = Field()
    grade = Field()

YEAR = "2015"
START_PAGE = 10 # 0
START_PAGE = 0 # 0
MAX_LOOP = 4 #-1
MAX_LOOP = -1 #-1
PMOD = True
PMOD = False

print "==================="
print YEAR
print "==================="

url = "http://movie.naver.com/movie/sdb/browsing/bmovie.nhn?year="+YEAR+"&page=" + str(START_PAGE)

index = start_page = START_PAGE
old_index = -1
old_title = ""
loop = 0
pmod = PMOD
max_loop = MAX_LOOP

class ScrapyOrgSpider(BaseSpider):
    name = "naver"
    allowed_domains = ["movie.naver.com"]
    start_urls = [url]

    def parse(self, response):
        global start_page, index, loop, old_title, pmod, max_loop

        hxs = HtmlXPathSelector(response)
        items = []

        loop += 1

        next_page = ["http://movie.naver.com/movie/sdb/browsing/bmovie.nhn?year="+YEAR+"&page="+str(loop + start_page)]

        if max_loop != -1:
          if loop >= max_loop:
              next_page = []

        posts = hxs.select("//ul[@class='directory_list']/li")
        title = posts[0].select("a/text()")[0].extract()

        if old_title == title and loop > 2:
          next_page = []
        else:
          old_title = title

        if not not next_page:
            yield Request(next_page[0], self.parse)

        #posts = hxs.select("//tr")

        count = 0
        print "[ " + str(loop) + " ] index : " + str(index) + ", len(posts) : " + str(len(posts))

        for post in posts:
            try:
              title = post.select("a/text()")[0].extract()
              if pmod: print " [ " + str(count) + " ] TITLE : " + title
              url = post.select("a/@href")[0].extract()
              if pmod: print " [ " + str(count) + " ] URL : " + url
              year = post.select("ul[@class='detail']/li/a")[0].select("b/text()")[0].extract()
              if pmod: print " [ " + str(count) + " ] YEAR : " + year

              open1 = ""
              open2 = ""
              country = ""
              genre = ""
              form = ""
              grade = ""

              open_count = 0
              lis = post.select("ul[@class='detail']/li/a")

              for li in lis:
                h = li.select('@href')[0].extract()
                href = h[h.find('&')+1:h.rfind('=')]
                if pmod: print " [*] HREF : " + href

                if href == '?year':
                  if pmod: print "  [-] HREF SKIP : " + h
                  continue

                if href == 'open' and open_count == 0:
                  open1 = li.select('text()')[0].extract()
                  open_count += 1
                  if pmod: print " [*] open1 : " + open1
                elif  href == 'open' and open_count == 1:
                  try:
                    open2 = li.select('text()')[0].extract()
                    if pmod: print " [*] open2 : " + open2
                  except:
                    z = 123
                elif href == 'nation':
                  country = li.select('text()')[0].extract()
                  if pmod: print " [*] country : " + country
                elif href == 'genre':
                  genre = li.select('text()')[0].extract()
                  if pmod: print " [*] genre : " + genre
                elif href == 'form':
                  form = li.select('text()')[0].extract()
                  if pmod: print " [*] form : " + form
                elif href == 'grade':
                  grade = li.select('text()')[0].extract()
                  if pmod: print " [*] grade : " + grade
                else:
                  print " [^] Found NEW href : " + h
                  return

              count += 1
            except Exception as e:
              #for frame in traceback.extract_tb(sys.exc_info()[2]):
              #  fname,lineno,fn,text = frame
              #  print "Error in %s on line %d" % (fname, lineno)
              exc_type, exc_obj, exc_tb = sys.exc_info()
              fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
              print(exc_type, fname, exc_tb.tb_lineno)
              #for e in sys.exc_info():
              #  print e
              continue

            item = ScrapyItem()
            item["title"] = title
            item["url"] = url
            item["year"] = year
            item["open1"] = open1
            item["open2"] = open2
            item["country"] = country
            item["genre"] = genre
            item["form"] = form
            item["grade"] = grade
            items.append(item)

        for item in items:
            yield item

        old_index = index
        index += count

ps.


2013년 12월 3일 화요일

왓챠 (Watcha) api 분석


왓챠는 유저의 평가를 바탕으로 영화를 추천해주는 웹 서비스다.

왓챠를 분석하게 된 이유는 유한개의 DB를 가지고 있는 서비스에서

모든 영화를 평가했을때 어떻게 될까 라는 단순한 호기심이 생겼었기 때문이다.

사실 웹 사이트 api 분석이라기 보다는 json 둘러보기 정도가 더 정확한듯 하다.


MySQL을 사용하고는 있으나 웹 프레임 워크(루비온레일즈)를 사용하고 있기 때문에 sql injection를 기대하진 않았다.

또한 왓챠의 멤버 중에서 보안 실력이 뛰어난 분이 계시기 때문에 큰 문제는 없을거란걸 알았다.


가장 핵심인 '영화 추천' 부분은 무한 스크롤 기능을 통해 서버에 request를

http://watcha.net/api/movies/recommend?ref=wall&count=8&more=true

와 같은 주소로 전송하고 json 형태로 받아오게 된다.



count를 증가시키면 데이터를 더 받아올 수는 있지만 위의 request로는 20개가 최대인것 같다.

하지만

http://watcha.net/api/movies?type=eval&count=900&more=true&page=

와 같은 형태로 request를 보내면 900개의 데이터를 볼 수 있었다.




그리고 구글링을 통해서

http://watcha.net/movies/detail/18681‎

와 같은 형태로 영화 정보를 얻을 수 있다는 사실을 알 수 있었다.

이를 통해 watcha에 저장된 모든 영화의 id와 hash된 id를 얻을 수 있었다.


다행이도 지금은 위의 url이 막힌것처럼 보이며,

http://watcha.net/api/movie/mp0dx6

와 같이 숫자가 아닌 hash화된 id를 통해서 영화 정보를 얻어온다.


그리고 비슷한 영화를 추천할 때는

http://watcha.net/api/movies/similar/mvf3t1?count=10

와 같은 request를 보내게 된다.


영화 검색시에는

http://watcha.net/search.json?query=%20&page=1

와 같이 request를 보내며 javascript 로 공백을 검색하는것을 막아 뒀지만,


http://watcha.net/search.json?query=+ 와 같은 형태로 우회할 수 있다.


그래서 watcha가 가지고 있는 영화 DB의 갯수가 대략 6만개 정도라고 예상했다.


네이버의 DB가 약 9만개인 점을 생각한다면 적다고 생각했지만, 네이버와 달리

드라마 정보가 적기 때문이라고 생각했다.


대충 소스를 짜서 돌려본 결과 아래와 같은 계정을 만들 수 있었고,


무한 로딩


추천하는 영화가 없음


위와 같이 평소에 보지 못했던 새로운 메세지를 볼 수 있었다 :)

( 하지만 페이지 로딩 시간은 상당히 길어졌다 )


쿼리를 바꿔보면 새로운 쿼리를 찾을 수 도 있을것 같지만, 호기심은 해결되었으니 더 둘러보지는 않았다.


나는 이 글을 통해 왓챠가 보안에 취약하다는 것을 말하려고 하는 것은 아니다.

단지, 평소에 즐겨 쓰던 서비스였기 때문에 애정이 많이 가지만,

DB가 생각했던것보다 많이 공개되어 있어서 조금 조심할 필요가 있다고 생각된다.


사실 웹 개발에 대한 지식이 부족해서 어떻게 개선할 수 있을지는 잘 모르겠다..


마지막으로 다른분들은 이런 잉여짓을 하지 않기를 바란다...

[Python] Twitter hacking


The title is somewhat attractive but I'm not (maybe can't) going to talk about hacking the Twitter server.

As you know, Twitter is a well known social network and lots of people use Twitter openly and share his or others information through Twitter.

But some people use their account privately, which means they use Twitter to communicate with his friends.

These people tends to make mistake like twit his personal information on Twitter.




So, I want to show you how easily people can be hacked through Twitter.

I took this experiment few months ago and I could get 1560 different phone numbers in one week...


DO NOT USE THIS FOR REAL HACKING!

Just be careful when you use SNS or internet.


  1. # -*- coding:utf-8 -*-
  2. import time
  3. from twitter import *
  4. import re
  5. import MySQLdb
  6. DB_NAME = 'twitter'
  7. DB_TABLE = 'phone'
  8. # tid bigint(20), name varchar(20), text text, number bigint(20)
  9. DB_ID = 'carpedm20'
  10. DB_PASS = ' '
  11. db = MySQLdb.connect(host="", user=DB_ID, db=DB_NAME, passwd=DB_PASS, port=3306)
  12. cur = db.cursor()
  13. OAUTH_TOKEN = ''
  14. OAUTH_SECRET = ''
  15. CONSUMER_KEY = ''
  16. CONSUMER_SECRET = ''
  17. = Twitter( auth=OAuth(OAUTH_TOKEN, OAUTH_SECRETCONSUMER_KEY, CONSUMER_SECRET)
  18. findNum = re.compile(r'regular expression for phone number')
  19. while 1:
  20.         print "= start at " + time.ctime() + " ="
  21.         output = t.search.tweets(q="010",lang='ko',count='100')
  22.         print output['search_metadata']['count']
  23.         for s in output['statuses']:
  24.                 tid =  s['id']
  25.                 name = s['user']['name'].encode('utf-8')
  26.                 text = s['text'].replace("'","").encode('utf-8')
  27.                 number = findNum.findall(s['text'])
  28.                 upload_time = s['created_at'].encode('utf-8')
  29.                 if number is []:
  30.                         continue
  31.                 query = "SELECT * FROM "+DB_TABLE+" WHERE tid="+str(tid)
  32.                 cur.execute(query)
  33.                 if cur.fetchone() != None:
  34.                         continue
  35.                 for n in number:
  36.                         query = "INSERT INTO "+DB_TABLE+" (`tid`,`name`,`text`,`number`,`time`) VALUES("+str(tid)+",'"+name+"','"+text+"','"+str(n)+"','"+upload_time+"');"
  37.                         print query
  38.                         try:
  39.                                 cur.execute(query)
  40.                                 db.commit()
  41.                         except:
  42.                                 print "ERROR : " + query
  43.         time.sleep(30)

2013년 12월 2일 월요일

컴공아 일하자 (Comgong Job)

Comgong Job


What is Comgong Job

This is a python code for Comgong Job bot.
This is a program that uploads newest internship and recruit announcements to facebook automatically.

Links

Copyright

Copyright (c) 2013 Kim Tae Hoon

Screenshots


* 2015.04.17 *



* 2014.05.31 *



* 2013.12.05 *