Skip to content

Instantly share code, notes, and snippets.

@Kyome22
Last active March 26, 2020 04:15
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kyome22/9525f59f3bdc8322cf18d0d1633575cb to your computer and use it in GitHub Desktop.
Save Kyome22/9525f59f3bdc8322cf18d0d1633575cb to your computer and use it in GitHub Desktop.
Qiitaに投稿してある記事を全てMarkDownファイルとしてダウンロードするSwiftスクリプト
//
// 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