Skip to content

sunny4381/spring-boot-integration-crawler-sample

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Spring Boot と Spring Integration を使用したクローラ

Spring Integration 4 から XML が不要になりました。 Spring Boot と Spring Integration を使用して XML を使用せずに Java Config のみを使って単純なクローラを作ってみます。

必須

以下のソフトウェアが必要です。あらかじめインストールしておいてください。

  • JDK 6 or later
  • Maven 3.0 or later

概要

開発するクローラーは、Wikipedia のダンプリスト をスクレ―ピングして、 次のような情報を取得します。

timestamp id ref status
2014-05-31 01:57:31 nowiki nowiki/20140530 Dump in progress
2014-05-31 01:57:32 trwiki trwiki/20140530 Dump in progress
2014-05-30 14:38:08 anwiki anwiki/20140530 Dump complete
2014-05-30 14:30:29 viwiktionary viwiktionary/20140530 Dump complete
2014-05-30 13:02:57 ckbwiki ckbwiki/20140530 Dump complete
... ... ... ...

pom.xml

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.0.M2</version>
        <relativePath/>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-integration</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.7.3</version>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <url>http://repo.spring.io/milestone/</url>
        </repository>
    </repositories>

スクレ―ピングする際に jsoup を使用するので pom.xml に組み込んでいます。

Downloader

Downloader は、定期的に指定された URL から HTML をダウンロードします。

@MessageEndpoint
public class Downloader {
	@Autowired
	private CrawlerConfig config;

	@Autowired
	private RestTemplate template;

	@InboundChannelAdapter(value = "channel1", poller = @Poller("downloadTrigger"))
	public ResponseEntity<String> download() {
		String url = config.getUrl();
		ResponseEntity<String> entity = template.getForEntity(url, String.class);
		return entity;
	}
}

ダウンロードした HTML は、ResponseEntity<String> クラスのインスタンスで、 channel1 に送ります。 どんな間隔でダウンロードするかは、外部の downloadTrigger bean で定義します。

Scraper

Scraper は、HTML から目的の部分を抽出します。

@MessageEndpoint
public class Scraper {
	private final Pattern patter = Pattern.compile("^<li>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\S+");

	@Splitter(inputChannel = "channel1", outputChannel = "channel2")
	public List<Element> scrape(ResponseEntity<String> payload) {
		String html = payload.getBody();
		final Document htmlDoc = Jsoup.parse(html);
		final Elements anchorNodes = htmlDoc.select("body").select("ul").select("li");

		final List<Element> anchorList = new ArrayList<Element>();
		anchorNodes.traverse(new NodeVisitor() {
			@Override
			public void head(org.jsoup.nodes.Node node, int depth) {
				if (node instanceof org.jsoup.nodes.Element) {
					Element e = (Element)node;
					anchorList.add(e);
				}
			}

			@Override
			public void tail(Node node, int depth) {
			}
		});

		return anchorList;
	}

	@Filter(inputChannel = "channel2", outputChannel = "channel3")
	public boolean filter(Element payload) {
		Matcher m = patter.matcher(payload.toString());
		return m.find();
	}

	@Transformer(inputChannel = "channel3", outputChannel = "channel4")
	public DumpEntry convert(Element payload) throws ParseException {
		String dateStr = payload.ownText().substring(0, 19);

		DateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
		format.setTimeZone(TimeZone.getTimeZone("GMT"));

		Date timestamp = format.parse(dateStr);

		Elements list = payload.select("a");
		String id;
		String ref;
		if (list.size() > 0) {
			Element a = list.get(0);
			id = a.ownText();
			ref = a.attr("href");
		} else {
			id = "private data";
			ref = null;
		}

		Element span = payload.select("span").get(0);
		String status = span.ownText();

		return new DumpEntry(timestamp, id, ref, status);
	}
}

channel1 から受け取った HTML から body/ul/li 要素を抽出し、必要な li 要素を選別し、li 要素を DompEntry に変換し、 channel4 に送ります。

DompEntry

DompEntry は、目的の部分を表すエンティティです。

public class DumpEntry implements Serializable {
	private Date timestamp;
	private String id;
	private String ref;
	private String status;

	public DumpEntry(Date timestamp, String id, String ref, String status) {
		this.timestamp = timestamp;
		this.id = id;
		this.ref = ref;
		this.status = status;
	}

	public Date getTimestamp() {
		return timestamp;
	}

	public String getId() {
		return id;
	}

	public String getRef() {
		return ref;
	}

	public String getStatus() {
		return status;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (!(o instanceof DumpEntry)) return false;

		DumpEntry dumpEntry = (DumpEntry) o;

		if (!id.equals(dumpEntry.id)) return false;
		if (!ref.equals(dumpEntry.ref)) return false;
		if (!status.equals(dumpEntry.status)) return false;
		if (!timestamp.equals(dumpEntry.timestamp)) return false;

		return true;
	}

	@Override
	public int hashCode() {
		int result = timestamp.hashCode();
		result = 31 * result + id.hashCode();
		result = 31 * result + ref.hashCode();
		result = 31 * result + status.hashCode();
		return result;
	}

	@Override
	public String toString() {
		return "DumpEntry{" +
				"timestamp=" + timestamp +
				", id='" + id + '\'' +
				", ref='" + ref + '\'' +
				", status='" + status + '\'' +
				'}';
	}
}

CrawlerConfig

クローラの設定を表します。

@Component
@ConfigurationProperties
public class CrawlerConfig {
	private static final String DEFAULT_URL = "http://dumps.wikimedia.org/backup-index.html";
	private static final long DEFAULT_DOWNLOAD_INTERVAL = TimeUnit.HOURS.toMillis(1);
	private String url = DEFAULT_URL;
	private long downloadInterval = DEFAULT_DOWNLOAD_INTERVAL;

	public String getUrl() {
		return url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	public long getDownloadInterval() {
		return downloadInterval;
	}

	public void setDownloadInterval(long downloadInterval) {
		this.downloadInterval = downloadInterval;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;

		CrawlerConfig that = (CrawlerConfig) o;

		if (downloadInterval != that.downloadInterval) return false;
		if (!url.equals(that.url)) return false;

		return true;
	}

	@Override
	public int hashCode() {
		int result = url.hashCode();
		result = 31 * result + (int) (downloadInterval ^ (downloadInterval >>> 32));
		return result;
	}

	@Override
	public String toString() {
		return "CrawlerConfig{" +
				"url='" + url + '\'' +
				", downloadInterval=" + downloadInterval +
				'}';
	}
}

既定では、1時間ごとに "http://dumps.wikimedia.org/backup-index.html" から HTML をダウンロードします。

CrawlerApp

最後にアプリケーションクラスです。

@Configuration
@ComponentScan
@EnableAutoConfiguration
@EnableConfigurationProperties
public class CrawlerApp {
	private static Logger LOG = LoggerFactory.getLogger(CrawlerApp.class);

	public static void main(String[] args) throws Exception {
		ApplicationContext ctx = SpringApplication.run(CrawlerApp.class, args);
		System.in.read();
		Runtime.getRuntime().exit(SpringApplication.exit(ctx));
	}

	@Autowired
	private CrawlerConfig config;

	@PostConstruct
	public void postConstruct() {
		LOG.info("starting crawler with config={}", config);
	}

	@MessageEndpoint
	public static class Endpoint {
		@ServiceActivator(inputChannel="channel4")
		public void log(DumpEntry payload) {
			LOG.info("entry={}", payload);
		}
	}

	@Bean
	public RestTemplate restTemplate() {
		return new RestTemplate();
	}

	@Bean
	public PollerMetadata downloadTrigger() {
		PeriodicTrigger trigger = new PeriodicTrigger(config.getDownloadInterval());
		trigger.setFixedRate(true);
		PollerMetadata pollerMetadata = new PollerMetadata();
		pollerMetadata.setTrigger(trigger);
		pollerMetadata.setMaxMessagesPerPoll(1);
		return pollerMetadata;
	}

	@Bean
	public MessageChannel channel1() {
		return new QueueChannel(10);
	}

	@Bean
	public MessageChannel channel2() {
		return new DirectChannel();
	}

	@Bean
	public MessageChannel channel3() {
		return new DirectChannel();
	}

	@Bean
	public MessageChannel channel4() {
		return new QueueChannel(10);
	}

	// <int:poller id="poller" default="true" fixed-rate="10"/>
	@Bean(name = PollerMetadata.DEFAULT_POLLER)
	public PollerMetadata poller() {
		PeriodicTrigger trigger = new PeriodicTrigger(10);
		trigger.setFixedRate(true);
		PollerMetadata pollerMetadata = new PollerMetadata();
		pollerMetadata.setTrigger(trigger);
		return pollerMetadata;
	}
}

CrawlerApp では downloadTrigger にダウンロード間隔を定義したり、 channel1 から channel4 の各チャンネルを定義しています。

また、CrawlerApp では、channel4 から受信した DumpEntry をログに出力しています。 実際にはファイルに書いたり、DB に保存したり、MQ に送るなどして外部のシステムと連携します。

実行方法&実行例

次のように実行します。

mvn package
java -jar target/spring-boot-integration-crawler-sample-1.0.jar

Complete Source Code

git clone https://github.com/sunny4381/spring-boot-integration-crawler-sample.git

About

a spring boot + spring integration crawler sample.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages