418. I'm a teapot

おもに判例の解析をやってます

判例データの用意

テキスト解析をするにあたって、判例データを用意します。

準備

判例データを以下の構造に集めます。

/root
  ├平成N年
  │ ├平成N年M月P日
  │ │ ├123456
  │ │ │ ├info.xml
  │ │ │ └content.pdf
  │ │ └432198
  │ └平成N年M月Q日
  ├昭和M年
  └昭和O年

content.pdfは最高裁判例データベース(裁判所 | 裁判例情報)から落としてこれるpdfファイルです。

info.xmlは次の構造で、上記データベースの検索結果詳細画面のデータを抽出したものです。

<?xml version='1.0' encoding='UTF-8'?>
<sentence>
  <caseNumber>平成13(行ツ)82</caseNumber>
  <name>在外日本人選挙権剥奪違法確認等請求事件</name>
  <date>平成17年9月14日</date>
  <court>最高裁判所大法廷</court>
  <type>判決</type>
  <result>その他</result>
  <reporter>民集 第59巻7号2087頁</reporter>
  <previous>
    <court>東京高等裁判所</court>
    <number>平成11(行コ)253</number>
    <date>平成12年11月8日</date>
  </previous>
  <theme>1 公職選挙法(平成10年法律第47号による改正前のもの)が在外国民の国政選挙における投票を平成8年10月20日に施行された衆議院議員の総選挙当時全く認めていなかったことと憲法15条1項,3項,43条1項,44条ただし書
2 公職選挙法附則8項の規定のうち在外国民に国政選挙における選挙権の行使を認める制度の対象となる選挙を当分の間両議院の比例代表選出議員の選挙に限定する部分と憲法15条1項,3項,43条1項,44条ただし書
3 在外国民が次回の衆議院議員の総選挙における小選挙区選出議員の選挙及び参議院議員の通常選挙における選挙区選出議員の選挙において在外選挙人名簿に登録されていることに基づいて投票をすることができる地位にあることの確認を求める訴えの適否
4 在外国民と次回の衆議院議員の総選挙における小選挙区選出議員の選挙及び参議院議員の通常選挙における選挙区選出議員の選挙において投票をすることができる地位
5 国会議員の立法行為又は立法不作為が国家賠償法1条1項の適用上違法の評価を受ける場合
6 平成8年10月20日に施行された衆議院議員の総選挙までに国会が在外国民の国政選挙における投票を可能にするための立法措置を執らなかったことについて国家賠償請求が認容された事例</theme>
  <summary>1 平成8年10月20日に施行された衆議院議員の総選挙当時,公職選挙法(平成10年法律第47号による改正前のもの)が,国外に居住していて国内の市町村の区域内に住所を有していない日本国民が国政選挙において投票をするのを全く認めていなかったことは,憲法15条1項,3項,43条1項,44条ただし書に違反する。
2 公職選挙法附則8項の規定のうち,国外に居住していて国内の市町村の区域内に住所を有していない日本国民に国政選挙における選挙権の行使を認める制度の対象となる選挙を当分の間両議院の比例代表選出議員の選挙に限定する部分は,遅くとも,本判決言渡し後に初めて行われる衆議院議員の総選挙又は参議院議員の通常選挙の時点においては,憲法15条1項,3項,43条1項,44条ただし書に違反する。
3 国外に居住していて国内の市町村の区域内に住所を有していない日本国民が,次回の衆議院議員の総選挙における小選挙区選出議員の選挙及び参議院議員の通常選挙における選挙区選出議員の選挙において,在外選挙人名簿に登録されていることに基づいて投票をすることができる地位にあることの確認を求める訴えは,公法上の法律関係に関する確認の訴えとして適法である。
4 国外に居住していて国内の市町村の区域内に住所を有していない日本国民は,次回の衆議院議員の総選挙における小選挙区選出議員の選挙及び参議院議員の通常選挙における選挙区選出議員の選挙において,在外選挙人名簿に登録され ていることに基づいて投票をすることができる地位にある。
5 国会議員の立法行為又は立法不作為は,その立法の内容又は立法不作為が国民に憲法上保障されている権利を違法に侵害するものであることが明白な場合や,国民に憲法上保障されている権利行使の機会を確保するために所要の立法措置を執ることが必要不可欠であり,それが明白であるにもかかわらず,国会が正当な理由なく長期にわたってこれを怠る場合などには,例外的に,国家賠償法1条1項の適用上,違法の評価を受ける。
6 国外に居住していて国内の市町村の区域内に住所を有していない日本国民に国政選挙における選挙権行使の機会を確保するためには,上記国民に上記選挙権の行使を認める制度を設けるなどの立法措置を執ることが必要不可欠であったにもかかわらず,上記国民の国政選挙における投票を可能にするための法律案が廃案となった後,平成8年10月20日の衆議院議員総選挙の施行に至るまで10年以上の長きにわたって国会が上記投票を可能にするための立法措置を執らなかったことは,国家賠償法1条1項の適用上違法の評価を受けるものというべきであり,国は,上記選挙において投票をすることができなかったことにより精神的苦痛を被った上記国民に対し,慰謝料各5000円の支払義務を負う。
(1,2,4〜6につき,補足意見,反対意見がある。)</summary>
  <articles>憲法15条1項,憲法15条3項,憲法41条,憲法43条1項,憲法44条,公職選挙法第4章の2 在外選挙人名簿,公職選挙法42条,公職選挙法49条の2,公職選挙法附則8項,公職選挙法(平成12年法律第62号による改正前のもの)21条1項,公職選挙法(平成10年法律第47号による改正前のもの)42条,住民基本台帳法15条1項,行政事件訴訟法4条,国家賠償法1条1項</articles>
  <pdf>http://www.courts.go.jp/app/files/hanrei_jp/338/052338_hanrei.pdf</pdf>
</sentence>

参考:裁判所 | 裁判例情報:検索結果詳細画面

このデータを元に、処理用のデータを作成します。 なお、このデータの作成には自作のライブラリnil2013/LIlibを利用しています(宣伝)

テキストデータの抽出

pdfデータのままだと解析に利用できないので、生のテキストデータへと変換します。

最初はpdftotextを使おうかと思ったのですが、一部のpdfデータを変換することに失敗したので、抽出用のプログラムを書きました。

一部は以前に書いたLIlib/SentenceGetter.scalaを流用しています。

ついでに、上のライブラリを使って大雑把にですがページ番号を消去しています。

trait Processor {
  final val jpPattern = """((?:平成|昭和)[0-9元]+年)1?[1-9]月[1-3]?[0-9]日""".r
  final val enPattern = """(H|S)([0-9元]+)-(1?[1-9])-([1-3]?[0-9])""".r

  final def main(args: Array[String]) {
    val root = new File("data")
    if(args.size>0) {
      // 特定のフォルダだけを処理する
      val dir = args(0) match {
        case dir @ jpPattern(year) => new File(new File(root, year), dir)
        case enPattern(era, year, month, date) =>
          val y = (era match {
            case "H" => "平成"
            case "S" => "昭和"
          }) + year + "年"
          new File(new File(root, y), s"$y${month}月${date}日")
      }
      foreach(dir)
    } else {
      // すべてのフォルダのファイルを処理する
      root.listFiles foreach { year =>
        if(year.isDirectory())
          year.listFiles foreach foreach
      }
    }
  }

  final def foreach(dir: File) {
    if(dir.isDirectory()) {
      dir.listFiles() foreach {
        case dir if dir.isDirectory() => process(dir)
        case _ =>
      }
    }
  }
  
  def process(dir: File): Unit
}

object Pdf2txt extends Processor {
  def process(dir: File) {
    if(dir.isDirectory()) {
      println(dir.getAbsolutePath)
      val pdf = new File(dir, "content.pdf")
      if(pdf.exists()) {
        val txt = new File(dir, "content.txt")
    
        getTextFromPdfByPages(pdf) match {
          case None => throw new IllegalArgumentException(s"Couldn't parse as pdf file '$pdf'")
          case Some(pages) =>
            try {
              val raw = pages.map{x =>
                x.lines.toList match {
                  case lines if lines.size>0 =>DefaultSentenceGetter.removePageNumber(lines)
                  case lines => lines
                }
              }.reduce(_++_)
              using(new PrintWriter(new FileOutputStream(txt))) {out =>
                raw foreach out.println
              }
            } catch {
              case e: Exception =>
                e.printStackTrace()
                println(pages)
            }
        }
      }
    }
  }
  def getTextFromPdfByPages(file: File): Option[Seq[String]] = {
    def strategy = new JapaneseTextExtractionStrategy
    using(new com.itextpdf.text.pdf.PdfReader(new FileInputStream(file))) {
      reader =>
        try{
          val parser = new PdfReaderContentParser(reader)
          val ret = (1 to reader.getNumberOfPages()).map { i => TextUtil.powerTrim(parser.processContent(i, strategy).getResultantText) }
          Some(ret)
        } catch {
          case e: Exception => None
        }
    }
  }
  def using[A, R <: { def close() }](r : R)(f : R => A) : A =
    try {
      f(r)
    } finally {
      r.close()
    }
}

これで、各フォルダにcontent.txtという名前で、生のテキストデータが生成されます。

中身はこんな感じ。

主    文
1 原判決を次のとおり変更する。
第1審判決を次のとおり変更する。
(1) 本件各確認請求に係る訴えのうち,違法確認請求に係る各訴えをいずれも却
下する。
(2) 別紙当事者目録1記載の上告人らが,次回の衆議院議員の総選挙における小
......

分かち書きされたデータを作成する

分かち書きされたデータがあると単語が半角スペースで区切られた文たち を入力として受け取るシステムにそのまま流用できて便利なので、それも用意しておきます。

まず、MeCabScalaから使うためのラッパー

sealed abstract class MeCabOption {
  def toArg: Seq[String]
}
case object Wakati extends MeCabOption {
  override def toArg = Seq("-Owakati")
}
case object ChaSen extends MeCabOption {
  override def toArg = Seq("-Ochasen")
}
case class Output(file: File) extends MeCabOption {
  override def toArg = Seq("-o", file.getAbsolutePath)
}

object MeCab {
  private def EOL = "\n"
}

case class MeCab(options: MeCabOption*) {
  import MeCab._

  def builder = options match {
    case ops if ops.size > 0 => Process("mecab" +: ops.map(_.toArg).reduce(_ ++ _))
    case _ => Process("mecab")
  }

  def apply() = builder

  def apply(input: String): ProcessBuilder = builder #< new ByteArrayInputStream(
    (if (input.endsWith(EOL)) { input } else { input + EOL }).getBytes)

  def apply(file: File): ProcessBuilder = builder #< new FileInputStream(file)

  def apply(is: InputStream): ProcessBuilder = builder #< is

}

つぎに、これを使ってcontent.txtから分かち書きされたsplitted.txtを生成するプログラム

object TextSplit extends Processor {
  val mecab = MeCab(Wakati)
  override def process(dir: File) {
    if(dir.isDirectory()) {
      new File(dir, "content.txt") match {
        case raw if !raw.exists() => System.err.println(s"file not found : ${raw.getAbsolutePath}")
        case raw if raw.exists() =>
          val txt = using(new BufferedReader(new FileReader(raw))) { br =>
            var line = ""
            val sb = new StringBuilder
            while({line=br.readLine(); line != null}) {
              sb.append({
                val r = """[ \s]+""".r
                r.replaceAllIn(line, " ")
              }).append("\n")
            }
            sb.toString()
          }
          val splitted = new File(dir, "splitted.txt")
          mecab(txt) #> splitted run
      }
    }
  }
}

処理の際、同時に全角スペースを処理しています。

同様に中身はこんな感じ。

主 文 
1 原 判決 を 次 の とおり 変更 する 。 
第 1 審 判決 を 次 の とおり 変更 する 。 
( 1 ) 本件 各 確認 請求 に 係る 訴え の うち , 違法 確認 請求 に 係る 各 訴え を いずれ も 却 
下 する 。 
( 2 ) 別紙 当事者 目録 1 記載 の 上告 人 ら が , 次回 の 衆議院 議員 の 総 選挙 における 小 

こんな感じで処理をした結果、一番上に載せたディレクトリの構造は以下の様になりました。

/root
  ├平成N年
  │ ├平成N年M月P日
  │ │ ├123456
  │ │ │ ├info.xml
  │ │ │ ├content.pdf
  │ │ │ ├content.txt
  │ │ │ └splitted.txt
  │ │ └432198
  │ └平成N年M月Q日
  ├昭和M年
  └昭和O年