Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
4
Help us understand the problem. What is going on with this article?
@tikidunpon

Swift4で言語処理100本ノック 2015 第3章を途中で諦めた

More than 1 year has passed since last update.

はじめに

前回Swift4で言語処理100本ノック 2015 第1章,第2章を書いて大分日が経ってしまいました。
第3章は途中で諦めようと思いますが1、折角なので得たことをまとめておこうと思います。
今回もテストコードを書きながら問題20〜24までを進めました、リポジトリはこちらです。

得たこと、良かったことなど

JSONのデコードにCodableを利用

Swift4から新しく追加されたエンコード/デコード用のプロトコルCodableを試しました。
以下はコードの一部ですが、Codableにより簡単にJSONが扱えることを実感しました。

参考: Codableについて色々まとめた[Swift4]

// 利用したい型にCodableを採用する  
struct WikiItem: Codable {
    let title: String
    let text: String
}
/// 入力されたjsonからタイトルがイギリスのものを探し先頭の1件を返す
static func ukWikiItem(input: String) -> WikiItem {
    let lines = input.components(separatedBy: CharacterSet.newlines).filter{ !$0.isEmpty }
    let decoder = JSONDecoder()
    let ukWiki = lines.map{
        // 簡単なエンコード/デコードなら1行で出来る
        return try! decoder.decode(WikiItem.self, from: $0.data(using: .utf8)!)
        }.filter{ $0.title == "イギリス" }
        .first!
    return ukWiki
}

複数行リテラルの活用

参考: Multiline String Literals

正規表現をベタ書きする時に分かりやすいよう、複数行リテラルで改行してみました。
この例ではそこまで見やすくないですが、SQLなど状況によって活用できると思います。

let categoryNameMatches = linesHasCategory.map { (line) -> String in
            let regex =
            """
            \\[Category:\
            (\
            .+[^\\]]\
            )\
            \\]
            """
            return line.matches(regex: regex)[1]
        }

コメント内にコードサンプルを埋め込む

分かりづらい関数のコメント内にサンプルコードの利用例を追加しました。
説明の間に1行間を空け、スペース5個でインデントしたコメントは、

/// 入力された正規表現文字列にマッチした結果を配列で返す
///
///     //(例)下記のように正規表現を引数に渡して利用します。
///     let matches = "Swift Moji 9876".matches(regex: "^(.+)\\s(\\d{4})")
///
///     // 戻り値の先頭はマッチ結果の完全なキャプチャを返します。
///     matches.first // "Swift Moji 9876"
///
///     // 個別のキャプチャされたグループは[1]以降に格納されます。
///     matches[1] // "Swift Moji"
///     matches.last // "2017"
///
/// - Parameter regex: 正規表現文字列
/// - Returns: マッチ結果

このようにコード部分が整形されて表示されるようになります。

スクリーンショット 2017-11-23 17.04.30.png

答え合わせ

答え合わせにはPythonで書かれている、素人の言語処理100本ノック:まとめ 第3章: 正規表現を参考にしました。
正規表現しっかり頑張ってて偉いなと思いました。

今後

第3章は正規表現が中心となっていて、副次的な成果はあったにせよ、あまりコードに落とす意味を感じませんでした。
自然言語処理をする上でどの程度まで正規表現を理解しておくべきなんでしょうか?
時間も勿体無いのでひとまず飛ばして先に進みたいと思います。

余談(第1章の副産物)

第1章で学んだNグラムを使って2つ文字列の類似度を出すコードを書きました。

こんな感じに使います。類似度が最も低い時0.0、もっとも高い時1.0を返します。

func testSimilarity() {
    XCTAssertTrue(String.similarity("", "I") == 0.0)
    XCTAssertTrue(String.similarity("I", "I") == 1.0)
    XCTAssertTrue(String.similarity("I", "Y") == 0.0)
    XCTAssertTrue(String.similarity("I", "IY") == 0.0)
    XCTAssertTrue(String.similarity("I see", "I see") == 1.0)
    XCTAssertTrue(String.similarity("I see", "I sea") >= 0.6)
    XCTAssertTrue(String.similarity("I see", "IC") <= 0.5)
}

こちらの文字列の類似度を計算するgemにあるようにレーベンシュタイン距離を使うよりはパフォーマンスが出るとのことです。

コード全文


//
//  Chapter3.swift
//  LanguageProcessing100knock2015
//
//  Created by koichi tanaka on 2017/10/10.
//  Copyright © 2017年 koichi tanaka. All rights reserved.
//

import Foundation

struct WikiItem: Codable {
    let title: String
    let text: String
}

struct Section {
    let name: String
    let level: Int
}

struct Chapter3 {
    //20. JSONデータの読み込み
    //Wikipedia記事のJSONファイルを読み込み,「イギリス」に関する記事本文を表示せよ.問題21-29では,ここで抽出した記事本文に対して実行せよ.
    static func q20(input: String) -> WikiItem {
        return ukWikiItem(input: input)
    }

    //21. カテゴリ名を含む行を抽出
    //記事中でカテゴリ名を宣言している行を抽出せよ.
    static func q21(input: String) -> [String] {
        let ukWiki = ukWikiItem(input: input)
        let linesHasCategory = ukWiki.text.components(separatedBy: CharacterSet.newlines).filter{ $0.contains("Category") }
        return linesHasCategory
    }

    //22. カテゴリ名の抽出
    //記事のカテゴリ名を(行単位ではなく名前で)抽出せよ.
    static func q22(input: String) -> Set<String> {
        let linesHasCategory = q21(input: input)
        let categoryNameMatches = linesHasCategory.map { (line) -> String in
            let regex =
            """
            \\[Category:\
            (\
            .+[^\\]]\
            )\
            \\]
            """
            return line.matches(regex: regex)[1]
        }
        var categoryNames = [String]()
        categoryNameMatches.forEach { (s) in
            if s.contains("|") {
                categoryNames.append(contentsOf: s.components(separatedBy: "|"))
            } else {
                categoryNames.append(s)
            }
        }
        return Set(categoryNames)
    }

    //23. セクション構造
    //記事中に含まれるセクション名とそのレベル(例えば"== セクション名 =="なら1)を表示せよ.
    static func q23(input: String) -> String {
        let ukWiki = ukWikiItem(input: input)
        let lines = ukWiki.text.components(separatedBy: CharacterSet.newlines)
        let sections = lines.flatMap { (line) -> Section? in
            let matche = line.matches(regex: "^(={2,})\\s*(.+?)\\s*={2,}$")
            if !matche.isEmpty {
                return Section(name: matche[2], level: (matche.first?.count)! - 1)
            } else {
                return nil
            }
        }
        return sections.map{ "\($0.name), \($0.level)" }.joined(separator: "\n")
    }

    //24. ファイル参照の抽出
    //記事から参照されているメディアファイルをすべて抜き出せ.
    static func q24(input: String) -> String {
        let ukWiki = ukWikiItem(input: input)
        let matches = ukWiki.text.matches(regex: "(ファイル:(.+?)|File:(.+?))[|]")
        let matchedFileIndeices = stride(from: 2, to: matches.count, by: 3).map{$0}

        return matches.enumerated().reduce("") { (combined, pair) -> String in
            if matchedFileIndeices.contains(pair.offset) {
                return "\(combined)\(pair.element)\n"
            } else {
                return combined
            }
        }
    }

}

extension Chapter3 {
    /// 入力されたjsonからタイトルがイギリスのものを探し先頭の1件を返す
    static func ukWikiItem(input: String) -> WikiItem {
        let lines = input.components(separatedBy: CharacterSet.newlines).filter{ !$0.isEmpty }
        let decoder = JSONDecoder()
        let ukWiki = lines.map{
            return try! decoder.decode(WikiItem.self, from: $0.data(using: .utf8)!)
            }.filter{ $0.title == "イギリス" }
            .first!
        return ukWiki
    }
}

extension String {

    /// 入力された正規表現文字列にマッチした結果を配列で返す
    ///
    ///     //(例)下記のように正規表現を引数に渡して利用します。
    ///     let matches = "Swift Moji 9876".matches(regex: "^(.+)\\s(\\d{4})")
    ///
    ///     // 戻り値の先頭はマッチ結果の完全なキャプチャを返します。
    ///     matches.first // "Swift Moji 9876"
    ///
    ///     // 個別のキャプチャされたグループは[1]以降に格納されます。
    ///     matches[1] // "Swift Moji"
    ///     matches.last // "2017"
    ///
    /// - Parameter regex: 正規表現文字列
    /// - Returns: マッチ結果
    func matches(regex: String!) -> [String] {
        do {
            let regex = try NSRegularExpression(pattern: regex, options: [])
            let results = regex.matches(in: self,
                                        options: [],
                                        range: NSRange(location: 0, length: self.count))
            var matches = [String]()

            for result in results {
                for i in 0..<result.numberOfRanges {
                    let range = Range.init(result.range(at: i), in: self)
                    if let r = range {
                        matches.append(String(self[r]))
                    }
                }
            }

            return matches
        } catch let error as NSError {
            assertionFailure("invalid regex: \(error.localizedDescription)")
            return []
        }
    }
}


  1. 第3章は正規表現が中心となっていて、副次的な成果はあったにせよ、あまりコードに落とす意味を感じませんでした。あとwikiのjsonの謎仕様を理解していくのも時間の無駄感が否めません。 

4
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
4
Help us understand the problem. What is going on with this article?