Хрень (из прошлого) скребок


Я слушаю подкаст называется хрень из прошлого. Это радио-шоу, которое выходит еженедельно в Миннеаполисе и специализируется на воспроизведении музыки из моего детства. Шоу было на воздухе в течение ~26 лет. Иногда я хочу знать, если они когда-либо играли ту или иную песню, но поиск предполагает использование одной из 26 статических веб-страниц. (Или, вы знаете, просить Google.) Я решил написать свой первый реальный Свифт программу, чтобы очистить свои страницы и ставлю спектакли в CSV-файл, я мог бы посмотреть в цифрах.

//
//  main.swift
//  CrapScraper
//

import Foundation

func getPage(address: URL) -> String {
    let ephemeralConfiguration  = URLSessionConfiguration.ephemeral
    let ephemeralSession = URLSession(configuration: ephemeralConfiguration, delegate:nil, delegateQueue:nil)
    var done = false;
    var result = ""

    let task = ephemeralSession.dataTask(with: address) { (data, response, error) in
        if let error = error {
            print ("error: \(error)")
        }
        else if let data = data,
            let string = String(data: data, encoding: .utf8) {
            result = string
        }
        done = true;
    }
    task.resume()

    // Busy wait until we get a result
    while (!done){
    }

    return result
}

func escapeString(str : inout String)
{
    // Because Numbers and Excel don't actually handle all of RFC4180, we can't just
    // put quotes around the string and escape the inner quotes to handle commas. We
    // need to actually replace them. This changes the data which is not great, but
    // we don't have a lot of choice here.
    str = str.replacingOccurrences(of: ",", with: ";", options: .regularExpression)
}


func extractDate(scanner: Scanner) -> String
{
    var urlPrefix = "http://www.archive.org/details/cftp-"
    let scannerStart = scanner.scanLocation
    var nsScannedData : NSString?
    if !scanner.scanUpTo(urlPrefix, into: &nsScannedData) {
        return ""
    }

    if scanner.isAtEnd {
        scanner.scanLocation = scannerStart
        let secureURLPrefix = "https://www.archive.org/details/cftp-"
        urlPrefix = secureURLPrefix
        if !scanner.scanUpTo(urlPrefix, into: &nsScannedData) {
            return ""
        }
    }
    if !scanner.scanString(urlPrefix, into: nil) {
        return ""
    }

    var dateStr = ""
    var nsDateStr : NSString?
    if !scanner.scanUpTo("\"", into: &nsDateStr) {
        return ""
    }

    dateStr = nsDateStr!.substring(from: 0)

    escapeString(str: &dateStr)

    return dateStr
}

func extractShowName(scanner: Scanner) -> String
{
    // The show name may be blank in some cases
    // If there is a title, it usually starts with ": " which
    // we want to remove
    let anchorEnd = "</a>"
    if !scanner.scanUpTo(anchorEnd, into: nil) {
        return ""
    }

    if !scanner.scanString(anchorEnd, into: nil) {
        return ""
    }

    let headingEnd = "</h2>"
    var nsShowName : NSString?
    if !scanner.scanUpTo(headingEnd, into: &nsShowName) {
        return ""
    }

    var showName = nsShowName!.substring(from: 0)
    if showName.starts(with: ": ") {
        showName.removeFirst(2)
    }
    escapeString(str: &showName)

    return showName;
}

func extractSongs(scanner: Scanner) -> Array<String>
{
    var songs : Array<String> = []
    let indent = "<p class=\"indent\">"
    if !scanner.scanUpTo(indent, into: nil) {
        return songs
    }

    if !scanner.scanString(indent, into: nil) {
        return songs
    }

    var nsSongInfo : NSString?
    let paragraphEnd = "</p>"
    if !scanner.scanUpTo(paragraphEnd, into: &nsSongInfo) {
        return songs
    }

    if !scanner.scanString(paragraphEnd, into: nil) {
        return songs
    }

    let songInfo = nsSongInfo!.substring(from: 0)
    let songScanner = Scanner(string: songInfo)
    let breakTag = "<br>"
    while !songScanner.isAtEnd {
        var nsNextSong : NSString?
        if !songScanner.scanUpTo(breakTag, into: &nsNextSong) {
            return songs
        }

        var nextSong = nsNextSong!.substring(from: 0)
        escapeString(str: &nextSong)
        songs.append(nextSong)

        if !songScanner.scanString(breakTag, into: nil) {
            return songs
        }
    }

    return songs
}

func analyzePage(page: String) -> String {
    let scanner = Scanner(string: page)

    // Sanity check - make sure we've at least got an HTML body tag
    let body  = "<body>"

    if !scanner.scanUpTo(body, into: nil) {
        return ""
    }

    if !scanner.scanString(body, into: nil) {
        return ""
    }

    // Start scanning for the <h2> sections
    let h2  = "<h2>"
    var csv = ""
    while (scanner.scanUpTo(h2, into: nil)) {
        // Read past the <h2>. Note that at the end, scanUpTo() will
        // return true if it reads to the end of the file and then
        // scanString() will fail.
        if !scanner.scanString(h2, into: nil) {
            if scanner.isAtEnd {
                return csv
            }

            return ""
        }

        let dateStr = extractDate(scanner: scanner)

        let showName = extractShowName(scanner: scanner)

        let songs = extractSongs(scanner: scanner)

        let songList = songs.joined(separator: ", ")

        csv.append(dateStr)
        csv.append(", ")
        csv.append(showName)
        csv.append(", ")
        csv.append(songList)
        csv.append("\n")
    }

    return csv
}

var year : Int
print ("\"date\", \"Show Name\", \"Song Titles\"\n")
for year in 1992...2018 {
    let nextAddress = URL(string: "http://crapfromthepast.com/playlists/\(year).htm")
    let nextPage = getPage(address: nextAddress!)
    if (nextPage != "") {
        let analyzedPage = analyzePage(page: nextPage)
        if analyzedPage != "" {
            print ("\(analyzedPage)")
        }
    }
}

Обратите внимание, что бег-это на самом деле будет захватить страниц с сайта, поэтому, пожалуйста, будьте уважительны и не молотком их!



140
2
задан 12 февраля 2018 в 02:02 Источник Поделиться
Комментарии
1 ответ

Я рассмотрю только один аспект вашей программы: как серия
сетевые запросы обрабатываются.

Этот “опрос”

// Busy wait until we get a result
while (!done){
}

это плохо, потому что он тратит циклы процессора. В моем тесте это вызвало почти 100% использование
одно ядро процессора, в то время как запрос сети.

Сетевые запросы являются асинхронными в природе, и есть лучше (и менее
ресурсоемкий) способы справиться с этим.

Первые изменения getPage() функции обратного вызова вместо того, чтобы возвращать
результат. Этого также достаточно, чтобы создать URLSession после:

let ephemeralConfiguration = URLSessionConfiguration.ephemeral
let ephemeralSession = URLSession(configuration: ephemeralConfiguration,
delegate: nil, delegateQueue: nil)

func getPage(address: URL, callback: @escaping (_ page: String) -> Void) {

let task = ephemeralSession.dataTask(with: address) { (data, response, error) in
if let error = error {
print("error: \(error)")
callback("")
} else if let data = data,
let string = String(data: data, encoding: .utf8) {
callback(string)
} else {
print("Bad data")
callback("")
}
}
task.resume()
}

Одним из возможных подходов было бы сейчас начать первому требованию, и в обработчик завершения
запустите следующий запрос, пока все страницы были получены.

Другой подход заключается в использовании “отправление групп” сеть сетевые запросы.
Это позволяет практически сохранить структуру основной цикл:

let group = DispatchGroup()
for year in 1992...2018 {
group.enter()
let nextAddress = URL(string: "http://crapfromthepast.com/playlists/\(year).htm")!
getPage(address: nextAddress) { (page) in

// ... Analyze and print page ...

group.leave()
}
group.wait()
}

Диспетчерская группа имеет “счетчик”, который увеличивается на group.enter()
и снизился на group.leave(). group.wait() ждет (почти с
без использования процессора) до тех пор, пока счетчик не достигнет нуля. В нашем случае это
механизм используется для запуска следующего запроса только после предыдущего
один был завершен.

Другой вариант заключается в использовании “семафор”:

let sema = DispatchSemaphore(value: 0)
for year in 1992...2018 {
let nextAddress = URL(string: "http://crapfromthepast.com/playlists/\(year).htm")!
getPage(address: nextAddress) { (page) in

// ... Analyze and print page ...

sema.signal()
}
sema.wait()
}

Здесь sema.signal() увеличивает значение семафора, делая
это положительное, что sema.wait() ждет.

2
ответ дан 12 февраля 2018 в 08:02 Источник Поделиться