Last active
August 29, 2015 14:16
-
-
Save mike-neck/bbc69c32924f35e291ec to your computer and use it in GitHub Desktop.
@sue445 さんのccc_privacy_botのpdfパーサーをgroovyで書きなおしてみた
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Grab('org.jsoup:jsoup:1.8.1') | |
@Grab('com.itextpdf:itextpdf:5.5.5') | |
@Grab('org.bouncycastle:bcpkix-jdk15on:1.51') | |
@Grab('org.bouncycastle:bcprov-jdk15on:1.51') | |
import org.jsoup.* | |
import com.itextpdf.text.pdf.* | |
import com.itextpdf.text.pdf.parser.* | |
def base = 'http://qa.tsite.jp' | |
def url = "${base}/faq/show/25129" | |
// 個人情報提供先企業名の一覧pdfのurlを取得 | |
def con = Jsoup.connect(url) | |
def doc = con.get() | |
def link = doc.select("#okw_center div div div div dd a")[0] | |
// 左シフトで最初に入ったオブジェクトだけ保持するクラス | |
class Reference<T> { | |
private T value | |
private boolean empty = true | |
void leftShift(T v) { | |
if(empty) { | |
value = v | |
empty = false | |
} | |
} | |
T get(){value} | |
} | |
import static java.lang.Math.round | |
// テキスト情報(テキスト, 左の座標, 上からの座標) | |
class Text { | |
final String text | |
final int left | |
final int bottom | |
Text(String t, float l, float b) { | |
this.text = t | |
this.left = round(l) | |
this.bottom = round(b) | |
} | |
} | |
// パーサーから渡される細切れのテキストを再構成する | |
class TextBuilder { | |
// X座標だけ取得するための単位ベクトル | |
static final Vector X = new Vector(1,0,0) | |
// Y座標だけ取得するための単位ベクトル | |
static final Vector Y = new Vector(0,1,0) | |
private final StringWriter sb = new StringWriter() | |
private Reference<Vector> start = new Reference() | |
private Vector end | |
// レフトシフトでビルダーにテキスト情報を渡すことができる | |
void leftShift(TextRenderInfo info) { | |
sb << info.text | |
start << info.baseline.startPoint | |
end = info.baseline.endPoint | |
} | |
// Textオブジェクトを構築 | |
Text get() { | |
new Text(sb.toString(), start.get().dot(X), end.dot(Y)) | |
} | |
} | |
//JSON形式にしたいだけ | |
class Organization { | |
static final String DELIM = ',' | |
static final String Q = '"' | |
final int number | |
final String name | |
final String since | |
final String type | |
final List<String> users | |
Organization(List<Text> list) { | |
def l = list.collect{it.text} | |
number = l[0] as int | |
name = l[1] | |
since = l[2] | |
def tmp = l[3].split(':') | |
type = tmp[0] | |
users = tmp[1].contains(DELIM) ? Arrays.asList(tmp[1].split(DELIM)):[tmp[1]] | |
} | |
@Override String toString() { | |
def l = [] | |
l << "${Q}number${Q}: ${number}" | |
l << "${Q}organization_name${Q}: ${Q}${name}${Q}" | |
l << "${Q}since${Q}: ${Q}${since}${Q}" | |
l << "${Q}type${Q}: ${Q}${type}${Q}" | |
def u = users.collect{"${Q}${it}${Q}"}.join(',') | |
l << "${Q}${Q}: [$u]" | |
"{${l.join(',')}}" | |
} | |
} | |
// パーサーから呼び出されるレンダリングオブジェクト | |
class Listener implements RenderListener { | |
//テキストビルダー | |
private TextBuilder tb | |
//Textオブジェクトを保持 | |
private List<Text> list = [] | |
//テキストブロック開始 | |
void beginTextBlock(){tb = new TextBuilder()} | |
//テキストのレンダリング | |
void renderText(TextRenderInfo info){tb << info} | |
//テキストブロック終了 | |
void endTextBlock(){list << tb.get()} | |
//イメージのレンダリング(今回は使わない) | |
void renderImage(ImageRenderInfo info){} | |
@Override String toString() { | |
//エクセルで上揃えとかを統一しない人がいるので | |
//座標をある程度の範囲でグルーピングするための | |
//ギミック | |
//see https://twitter.com/mike_neck/status/573486572202409984 | |
int row = -1 | |
def map = [:] | |
def tmp = list.collect{it.bottom}.sort{-it}.each { | |
if(row == -1) { | |
map.put(it, it) | |
} else if (row - 4 < it) { | |
map.put(it, map[row]) | |
} else { | |
map.put(it, it) | |
} | |
row = it | |
} | |
def body = list.groupBy{ | |
//上からの座標でグルーピング | |
map[it.bottom] | |
}.sort{ | |
//上からの座標で並べ替え | |
-it.key | |
}.each{ | |
//内部を左からの座標でソート | |
it.value.sort{it.left} | |
}.findAll{ | |
//企業の名前のみ取得 | |
it.value.size() == 4 && it.value[0].text ==~ /\d+/ | |
}.collect{ | |
//JSONにする | |
new Organization(it.value).toString() | |
}.join(',') | |
"[$body]" | |
} | |
} | |
// PdfReaderContentParserのイテレーターがPdfReaderだったのが、ちょっとアレなので、クラスを拡張 | |
PdfReaderContentParser.metaClass.define { | |
each = {Closure c -> | |
def p = delegate | |
def r = delegate.reader | |
int i = 1 | |
while(i <= r.numberOfPages) { | |
c(p, i++) | |
} | |
return p | |
} | |
} | |
//PdfReaderインスタンスをURLから取得 | |
def rd = new PdfReader(new URL("${base}${link.attr('href')}")) | |
//PdfReaderContentParserのインスタンスを取得 | |
def ps = new PdfReaderContentParser(rd) | |
//各ページごとに処理 | |
ps.each {parser, page -> | |
println (parser.processContent(page, new Listener())) | |
} | |
rd.close() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment