Django에서 Scrapy 커스텀 익스텐션 사용하기

기본

Scrapy는 웹 크롤링을 쉽게 해주는 파인썬 라이브러리 입니다. 이 크롤러 라이브러리는 기본적으로 독립적인 프로젝트로 돌아가게끔 설계가 돼있지만 장고 모델을 사용해 크롤링 한 데이터를 저장할 수 있는 방법도 있고, 장고에서 스크래피를 실행시키는 방법도 문서에 잘 나와있습니다.

다음으로 에러났을 때 메일을 받고 싶어서 확장 포인트를 찾다가 custom extension을 작성하는 방법도 문서에서 찾았죠. 그런데 이걸 스크래피 기본 설정에서 어떻게 읽게 하는지에 대해선 관련 문서를 찾기가 어려웠습니다. (삽질의 시작..)

그래서 간단하게 정리해 보는 스크래피 커스텀 익스텐션 사용하기 3 스텝:
1. scrapy_setting.py 를 하나 작성합니다. 어떤 설정을 할 수 있는지에 대해 문서화가 잘 돼있습니다. 저는 덜렁 4줄..

BOT_NAME = 'zibit_spider'

SPIDER_MODULES = ['zibit.spiders']
EXTENSIONS = {
'zibit.spiders.extension.ReportError': 0,
}

2. 해당 설정 파일을 읽을 수 있도록 manage.py에 한 줄 추가.

os.environ['SCRAPY_SETTINGS_MODULE'] = 'zibit.scrapy_settings'

3. extension 작성. 작성 시 주의점은 스크래피가 기본적으로 APP_NAME/spiders/APP_NAME_spider.py 를 꼭 찾는다는 점입니다. 저처럼 삽질하지 마시고 왠만하면 저 패키지에 파일을 작성하세요. 어떤 시그널을 받을 수 있는지도 문서화가 잘돼있네요.

from scrapy import signals
import logging

logger = logging.getLogger(__name__)

class ReportError(object):

def __init__(self, crawler):
self.crawler = crawler
crawler.signals.connect(self.spider_opened, signal=signals.spider_opened)
crawler.signals.connect(self.report_error, signal=signals.spider_error)
crawler.signals.connect(self.spider_closed, signal=signals.spider_closed)

@classmethod
def from_crawler(cls, crawler):
return cls(crawler)

def report_error(self, failure, response, spider):
logger.debug('spider error!!')
logger.error('error message: ' + failure.getErrorMessage())
logger.error('traceback: ' + failure.getTraceback())

def spider_opened(self, spider):
logger.debug('spider opened!!')

def spider_closed(self, spider):
logger.debug('spider closed!!')

signals.connect 부분만 설정해 주면 콜백이 들어오더라고요. 굿.

스크래피 익스텐션 테스트

반응형 웹 개발할 때 레이아웃 px과 이미지 크기

기본

사이트를 반응형으로 개편하면서 한가지 궁금한 점이 생겼습니다. 맥의 레티나 디스플레이나 요새 모바일은 물리적인 화면은 작아도 해상도는 2배-3배인 경우가 있기 때문에 미디어 쿼리의 max-width < 760px 같은 수식이 정말 모바일을 구별 할 수 있을까? 하는 점이죠.

원리는 의의로 간단했습니다. 안드로이드의 dp 개념처럼 웹브라우저도 density 값을 가지고 해상도 / density를 한 값을 미디어 쿼리가 사용합니다. 그래서 해상도가 높아도 모바일과 데스크탑을 구분하는게 가능합니다.

http://uhb.kr/book/wiki/rwd/resolution/

위 사이트에서 브라우저가 계산하는 실제 해상도 크기를 볼 수 있죠.

정리해보면 density가 1인 모니터에서 작업을 할 때 디자인 가이드라인의 px 값은 그대로 사용하고(desity가 다른 환경에서도 동일한 크기로 보여질 테니 말이죠.) 이미지들은 2배-3배 크기가 따로 필요합니다. (아니면 density가 높은 환경에서 이미지를 늘여서 표시해야 하니까요.)

hamcrest 대신 FEST Assert 를 사용해 보자

기본

hamcrest 의 단점 중 하나는 많은 수의 static import 가 필요해 이클립스에서 자동완성 기능을 사용하기 어렵다는 것이죠. Intellij는 메소드 자동완성도 가능하지만 여전히 어떤 이름의 메소드가 있는지 모르거나 해당 매처가 어떤 타입을 지원하는지 모르면 사용하기 어렵긴 매한가지 입니다.

FEST Assert의 경우 빌더 패턴을 통해 이 문제를 매우 운치있게 풀어나갔습니다.

// String specific assertions
assertThat(frodo.getName()).startsWith("Fro").endsWith("do")
                           .isEqualToIgnoringCase("frodo");

// collection specific assertions
assertThat(fellowshipOfTheRing).hasSize(9)
                               .contains(frodo, sam)
                               .excludes(sauron);

hamcrest 나 jUnit assertThat() 과는 다르게 assertThat() 에 검증할 파라미터만 넘기고 매처는 메소드 체이닝으로 뙇! 넘긴 파라미터의 타입에 따라 매처가 뙇! static import 를 딱 하나만 해주면 되는거죠. 괜찮지 않나요? 여기에 2.x 버전 대라 그런지 hamcrest 로 할 수 있는 것 + 타입에 따라 다양하게 제공되는 매처들이 많아 편하네요. 뿐만 아니라 Joda Time, Guava 용 모듈이 따로 있어 최적화된 테스트가 가능합니다.

한 가지 문제점은 FEST Assert 가 기존 jUnit 의 assertThat() 과 같은 이름의 static method 를 사용하기 때문에 같이 사용한다기 보단 대체한다는 생각으로 접근해야 합니다.  그래서 그런지 jUnit 에서 마이그레이션 하는 방법도 만들어 놨더군요.

안 써본 분들은 한 번 써보시길..🙂

Spring Framework 3.2 – Themes And Trends 일부 정리

기본

Spring Framework 3.2 Webinar 인 Themes And Trends 영상을 보고 Spring Framework 3.2 에 대한 내용 중 일부를 정리한 포스트입니다:

Meta 어노테이션

빈을 주입하거나 @Bean 메소드에서 @Autowired, @Value 어노테이션을 메타 어노테이션으로 사용할 수 있습니다. 즉, 커스텀 어노테이션을 만들면서 @Value, @Autowired, 그리고 커스텀 어노테이션을 추가하면 해당 기능이 추가됩니다. 그리고 @Lazy 등을 이용해 기능을 더 추가할 수도 있다고 하네요.

비동기 지원

request.startAsync()

request.startAsync()

코드를 사용해서 명시적으로 비동기 코드를 사용할 수 있습니다. 위와 같이 지정하면..

  • 컨테이너 쓰레드가 종료되고 새로운 쓰레드로 처리되며 완료 후, 다시 컨테이너로 디스패치 됩니다.

java.util.concurrent.Callable<?>

Spring MVC 핸들러에서 Callable<?>을 리턴값으로 사용 가능합니다. Callable을 리턴할 경우 새로운 쓰레드에서 실행됩니다.

@RequestMapping(&amp;quot;…&amp;quot;)
public @ResponseBody Callable callable() {
  return new Callable() {
    @Override
    public String call() throws Exception {
      // Long Running DB job, 3rd party REST API call, etc.
      return &amp;quot;Hello world&amp;quot;;
    }
  }
}

org.springframework.web.*.DeferredResult<?>

Spring MVC가 제어하지 않는 쓰레드를 통해 비동기 처리(JMS, AMQP, Redis 등)를 합니다. 먼저 DeferredResult를 저장하고, 외부 이벤트 발생 시, 실행하게 됩니다. 이때, 스프링의 AsyncTask를 사용할 수도 있습니다:

// save
@RequestMapping(&amp;quot;…&amp;quot;)
public @ResponseBody DeferredResult deferredResult() {
  DeferredResult result = new DeferredResult();
  this.responseBodyQueue.add(result);
}

// process
@Scheduled(fixedRate=2000)
public void processQueues() {
  for (DeferredResult result : this.responseBodyQueue)  {
    result.setResult(&amp;quot;Deferred result&amp;quot;);
    this.responseBodyQueue.remove(result);
  }
}

서버단 롱폴링 처리 프로세스

  1. 브라우저 요청
  2. 핸들러 메소드가 DeferredResult 리턴
  3. 서블릿 컨테이너 종료(응답은 살아있음)
  4. JMS, AMQP, Redis 같은 외부 이벤트 발생
  5. DeferredResult 세팅
  6. 요청이 서블릿 컨테이너로 돌아감
  7. 처리가 다시 진행되고 요청 완료

서블릿 3 에서 비동기 설정

서블릿과 모든 필터에 비동기 지원 플래그 추가
<async-supported>true</async-supported>

<filter-mapping> 자식 엘리먼트인 <disaptcher> 엘리먼트에 ASYNC 추가
<dispatcher>REQUEST, ASYNC, FORWARD</dispatcher>

Java Config 에서 설정을 도와주는 WebApplicationInitializer  추가됨

public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class&amp;lt;?&amp;gt;[] getRootConfigClasses() {
    return new Class&amp;lt;?&amp;gt;[] { RootConfig.class };
  }

  @Override
  protected Class&amp;lt;?&amp;gt;[] getServletConfigClasses() {
    return new Class&amp;lt;?&amp;gt;[] { WebMvcConfig.class };
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] { &amp;quot;/*&amp;quot; };
  }

  @Override
  protected void customizeRegistration(Dynamic registration) {
    registration.setAsyncSupported(true);
  }

}

컨텐츠 네고시에이션

file extension, Accept header, request parameter, default value, custom strategy 를 기반으로 컨텐츠 네고시에이션이 가능하고, 전략의 우선순위도 설정 할 수 있습니다.

REST 에러 보고

전역 @ExceptionHandler 를 지정할 수 있는 @ControllerAdvice 어노테이션이 추가됐습니다.

@ControllerAdvice
public class GlobalExceptionHandler {

  @ExceptionHandler
  public @ResponseBody String handleBusinessException(BusinessException ex) {
    return &amp;quot;Error details&amp;quot;;
  }

  // May also add globally applicable @InitBinder(e.g. Date) and @ModelAttribute methods
}

Matrix 변수

URI path에 세미콜론을 사용하면.. GET /pets/42;q=21 다음과 같은 코드로 가져올 수 있습니다.

@PathVariable String petId, @MatrixVariable int q

Matrix 변수 설정을 사용하기 위해선 RequestMappingHandlerMapping 의 removeSemicolonContent 프로퍼티의 값을 false 로 지정해야 합니다.(디폴트 true)

TestContext 프레임워크에서 Web Application 지원

// default to &amp;quot;file:src/main/webapp&amp;quot;
@WebAppConfiguration

// detects &amp;quot;WacTests-context.xml&amp;quot; in same package
// or static nested @Configuration class
@ContextConfiguration
public class WacTests {
  //...
}

Spring MVC Test Framework

발표자가 생각하는 좋은 테스트 이디엄(idiom)을 소개해 주고 있습니다.

// Server-side test
@Runwith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(&amp;quot;servlet-context.xml&amp;quot;)
public class SampleTests {
  @Autowired
  private WebApplicationContext wac;
  private MockMvc mvc;

  @Before
  public void setup() {
    this.mvc = webAppContextSetup(this.wac).build();
  }

  @Test
  public void getFoo() throws Exception {
  this.mvc.perform(get(&amp;quot;/foo&amp;quot;).accept(&amp;quot;application/json&amp;quot;))
          .andExpect(status().isOk())
          .andExpect(content().mimeType(&amp;quot;application/json&amp;quot;))
          .andExpect(jsonPath(&amp;quot;$.name&amp;quot;).value(&amp;quot;Lee&amp;quot;));
  }
}

// Client-side test
RestTemplate restTemplate = new RestTemplate();

MockRestServletServer mockServer = MockRestServiceServer.createServer(restTemplate);

mockServer.expect(requestTo(&amp;quot;/greeting&amp;quot;))
    .andRespond(withSuccess(&amp;quot;Hello world&amp;quot;, &amp;quot;text/plain&amp;quot;));

// use RestTemplate …

mockServer.verify();

postMessage를 이용한 크로스도메인간 iframe 리사이징

기본

회사에서 외주를 주고 있는 사이트가 변경돼서, <iframe>으로 해당 사이트를 임베드 해야하는 이슈가 있었습니다. <iframe> 해서 추가하는 거야 일도 아닌데 포함시킬려는 페이지의 사이즈에 맞게 사이즈를 조절해 줘야 하더군요.

바꾸기 전에는 같은 도메인이었기 때문에 iframe 안에서 부모쪽의 함수를 호출하면 되는 부분이었는데 도메인이 아예 달라져서 이렇게 호출하려고 하면 크로스도메인 관련 오류가 떨어집니다.

이 문제를 해결하기 위해 몇 가지 트릭이 존재하는데 그 중에서 제가 찾은 가장 모던한 방법은 window.postMessage를 사용하는 겁니다.

일반적으로 서로 다른 페이지의 스크립트가 통신하기 위해선 프로토콜, 포트, 호스트(도메인)가 같아야만 가능한데 window.postMessage는 이런 제약들을 우회할 수 있다고 합니다. 그러면 보안이 문제가 될 수도 있는데 이게 다른 페이지의 메소드를 호출하는건 아니고 한쪽에선 메시지를 보내고 다른쪽에서 그 메시지를 처리하는 일종의 이벤트 이고 받는 쪽도 origin을 지정할 수 있기 때문에 보안에 큰 문제는 없어 보입니다. (단, jQuery postMessage 플러그인을 사용할 경우 window.postMessage를 지원하지 않는 브라우저에서는 origin이 무시됩니다.)

문제는 IE 8+ 부터 지원한다는 건데 다행히 jQuery postMessage 플러그인을 사용하면 IE6도 커버가 됩니다. 코드도 간단합니다. ajax 요청과 크게 다르지 않네요. window.postMessage를 지원하지 않는 경우엔 window.location에 해시(#)를 붙여 메시지를 보내고 window.location.hash 값을 모니터링하는 식으로 처리합니다. 기발하네요.

톰캣 필터 순서 조심해야지

기본

뷰와 서버단 모두 UTF-8인 환경에서 GET 방식으로 넘긴 한글 파라미터가 깨져서 몇 시간 동안 삽질 했습니다 -_-;

tomcat 6에는 useBodyEncoding=”ture” 세팅이 돼있어서 처음엔 URIEncoding 속성이 없어서 그러나 싶었는데, 기본으로 사용하는 필터에서 request 객체에 UTF-8로 세팅을 해주니 이건 상관 없죠. 계속 신경쓰였던 부분은 리얼 환경에선 제대로 되는데 유독 개발 환경에서만 한글이 깨진다는 거였습니다. 피들러로 살펴봐도 헤더부터 파라미터까지 똑같고..

결국 파트장님이랑 톰캣 디버깅 하다가 겨우 원인 발견!

제가 추가한 필터 순서 때문이었네요. request 객체에 인코딩 세팅 해주는 필터보다 일찍 타서 파라미터가 깨졌던 거죠. 톰캣은 useBodyEncoding 속성이 true면 요청 헤더에서 인코딩 정보를 가져오게 되는데 jQuery의 getJSON() 요청에 관련 헤더가 없었습니다. 이 경우 URIEncoding 속성이 있으면 여기에 세팅된 값으로 인코딩이 될텐데 이 속성도 없었던 터라 결국 톰캣 기본 인코딩인 ISO-8859-1을 사용!

리모트 디버깅에 톰캣 소스도 까보고 알찬 하루였네요 ㅋ

Spring MVC 3.1에서 ObjectId를 Jackson 매퍼에서 시리얼라이즈 할 때 toString() 하게끔 하기

기본

MongoDB는 컬렉션에 넣는 데이터에 기본적으로 ObjectId를 할당합니다. 일종의 PK라고 볼 수 있죠. Java에서 ObjectId 또한 클래스이고 그 안에 여러가지 필드를 가지고 있는데요. @ResponseBody 어노테이션으로 JacksonMapper를 사용해 ObjectId가 json으로 파싱되면 MongoDB에서 사용하는 문자열 값이 아니라 ObjectId 객체의 필드들이 JSON 형태로 만들어 집니다. 원하는 건 이게 아닌데 말이죠:-/

그래서 Jackson에서 제공해 주는 @Serializer 어노테이션으로 커스텀 시리얼라이저를 만들어 봤지만 씨알도 안먹히더군요. 그래서 이 포스트를 참고해 CustomObjectMapper 도 만들어 봤지만 <annotation-driven /> 에서 Jackson 매퍼를 빈으로 등록하기 때문에 파라미터로 세팅해 줄수가 없네요.

위 포스팅에선 @PostConstruct 를 사용해 빈으로 등록된 Jackson 매퍼를 찾아서 커스텀 매퍼를 세팅해 주고 있는데요. 위 방법은 Spring 3.1 에서 동작하지 않습니다. 그 이유는 더이상 AnnotationMethodHandlerAdapter를 사용하지 않기 때문이죠. 예제에서 AnnotationMethodHandlerAdapter를 RequestMappingHandlerAdapter로 변경하면 잘 동작합니다!