Last active
March 26, 2020 04:15
-
-
Save Kyome22/9525f59f3bdc8322cf18d0d1633575cb to your computer and use it in GitHub Desktop.
Qiitaに投稿してある記事を全てMarkDownファイルとしてダウンロードするSwiftスクリプト
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
// | |
// qiita_gyotaku.swift | |
// QiitaGyotaku | |
// | |
// Created by Takuto Nakamura on 2020/03/26. | |
// Copyright © 2020 Takuto Nakamura. All rights reserved. | |
// | |
// ★★★ How to Use? ★★★ | |
// Open Terminal and run this script. | |
// $ swift qiita_gyotaku.swift [username] | |
import Foundation | |
class Article { | |
let title: String | |
var body: String | |
init(_ title: String, _ body: String) { | |
self.title = title | |
self.body = body | |
} | |
var fileName: String { | |
var name = title | |
while name.hasPrefix(".") { | |
name.removeFirst() | |
} | |
name = name.replacingOccurrences(of: ":", with: ":") | |
name = name.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) | |
return name + ".md" | |
} | |
} | |
class Gyotaku { | |
private static func createDirectory() { | |
var dir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) | |
dir.appendPathComponent("Articles") | |
if !FileManager.default.fileExists(atPath: dir.path) { | |
try? FileManager.default.createDirectory(at: dir, | |
withIntermediateDirectories: true, | |
attributes: nil) | |
} | |
dir.appendPathComponent("Images") | |
if !FileManager.default.fileExists(atPath: dir.path) { | |
try? FileManager.default.createDirectory(at: dir, | |
withIntermediateDirectories: true, | |
attributes: nil) | |
} | |
} | |
private static func getItemsCount(userName: String) -> (urlStr: String, itemsCount: Int)? { | |
let semaphore = DispatchSemaphore(value: 0) | |
let urlStr = "https://qiita.com/api/v2/users/\(userName)" | |
guard let url = URL(string: urlStr) else { return nil } | |
var itemsCount: Int = 0 | |
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in | |
defer { semaphore.signal() } | |
if let error = error { | |
Swift.print("Failur: \(error.localizedDescription)") | |
return | |
} | |
guard | |
let data = data, | |
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any], | |
let count = json["items_count"] as? Int | |
else { | |
return | |
} | |
itemsCount = count | |
} | |
task.resume() | |
semaphore.wait() | |
if itemsCount == 0 { | |
Swift.print("Failur: \(userName) has no article.") | |
return nil | |
} else if itemsCount == 1 { | |
Swift.print("\(userName) has \(itemsCount) article") | |
} else { | |
Swift.print("\(userName) has \(itemsCount) articles") | |
} | |
return (urlStr, itemsCount) | |
} | |
private static func getArticle(_ urlStr: String, _ page: Int) -> [Article] { | |
let semaphore = DispatchSemaphore(value: 0) | |
var articles = [Article]() | |
guard var components = URLComponents(string: "\(urlStr)/items") else { return articles } | |
components.queryItems = [ | |
URLQueryItem(name: "page", value: String(page)), | |
URLQueryItem(name: "per_page", value: "100") | |
] | |
let task = URLSession.shared.dataTask(with: components.url!) { (data, response, error) in | |
defer { semaphore.signal() } | |
if let error = error { | |
Swift.print("Failur: \(error.localizedDescription)") | |
return | |
} | |
guard | |
let data = data, | |
let items = try? JSONSerialization.jsonObject(with: data, options: []) as? NSArray else { | |
return | |
} | |
articles = items.compactMap { (item) -> Article? in | |
guard | |
let obj = item as? [String : Any], | |
let title = obj["title"] as? String, | |
let body = obj["body"] as? String | |
else { | |
return nil | |
} | |
return Article(title, body) | |
} | |
} | |
task.resume() | |
semaphore.wait() | |
return articles | |
} | |
static func renewalImages(body: inout String) { | |
guard let regex = try? NSRegularExpression(pattern: "!\\[.+\\]\\((.+)\\)") else { | |
return | |
} | |
let matches = regex.matches(in: body, range: NSRange(location: 0, length: body.count)) | |
matches.reversed().forEach { (result) in | |
let nsbody = NSString(string: body) | |
let sentence = nsbody.substring(with: result.range) | |
let imageURL = nsbody.substring(with: result.range(at: 1)) | |
if Gyotaku.getImage(urlStr: imageURL) { | |
let url = URL(string: imageURL)! | |
let newImageURL = "./Images/\(url.lastPathComponent)" | |
let newSentence = sentence.replacingOccurrences(of: imageURL, with: newImageURL) | |
body = nsbody.replacingCharacters(in: result.range, with: newSentence) | |
} | |
} | |
} | |
static func getImage(urlStr: String) -> Bool { | |
guard let url = URL(string: urlStr) else { return false } | |
let semaphore = DispatchSemaphore(value: 0) | |
var imageData: Data? | |
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in | |
defer { semaphore.signal() } | |
if let error = error { | |
Swift.print("Failur: \(error.localizedDescription)") | |
return | |
} | |
imageData = data | |
} | |
task.resume() | |
semaphore.wait() | |
guard let data = imageData else { return false } | |
let saveUrl = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) | |
.appendingPathComponent("Articles", isDirectory: true) | |
.appendingPathComponent("Images", isDirectory: true) | |
.appendingPathComponent(url.lastPathComponent) | |
guard let _ = try? data.write(to: saveUrl, options: []) else { | |
return false | |
} | |
return true | |
} | |
private static func saveArticles(_ articles: [Article]) { | |
let dir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) | |
.appendingPathComponent("Articles", isDirectory: true) | |
articles.forEach { (article) in | |
let saveURL = dir.appendingPathComponent(article.fileName) | |
try? article.body.write(to: saveURL, atomically: true, encoding: .utf8) | |
} | |
Swift.print("Complete. Check Articles directory.") | |
} | |
static func main() { | |
guard CommandLine.arguments.count == 2 else { | |
Swift.print("$ swift qiita_gyotaku.swift [username]") | |
return | |
} | |
guard let response = Gyotaku.getItemsCount(userName: CommandLine.arguments[1]) else { | |
return | |
} | |
let articles = (0 ... Int(response.itemsCount / 100)).flatMap { (i) -> [Article] in | |
return Gyotaku.getArticle(response.urlStr, i + 1) | |
} | |
Gyotaku.createDirectory() | |
articles.forEach { (article) in | |
Gyotaku.renewalImages(body: &article.body) | |
} | |
Gyotaku.saveArticles(articles) | |
} | |
} | |
Gyotaku.main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment