|
7 | 7 | // |
8 | 8 |
|
9 | 9 | import Foundation |
| 10 | +import SwiftSoup |
| 11 | + |
| 12 | +struct BalanceInfo: BaseModel, Codable { |
| 13 | + var rawData: String? |
| 14 | + var gold: Int = 0 |
| 15 | + var silver: Int = 0 |
| 16 | + var bronze: Int = 0 |
| 17 | + |
| 18 | + init() {} |
| 19 | + |
| 20 | + enum CodingKeys: String, CodingKey { |
| 21 | + case gold, silver, bronze |
| 22 | + } |
| 23 | + |
| 24 | + init(from html: Element?) { |
| 25 | + guard let root = html else { return } |
| 26 | + |
| 27 | + // Strategy 1: Parse from div#money (main balance page) |
| 28 | + // HTML: <div id="money"><a href="/balance" class="balance_area">47 <img...> 28 <img...> 26 <img...></a></div> |
| 29 | + if let moneyDiv = root.pickOne("div#money a.balance_area") { |
| 30 | + parseFromMoneyDiv(moneyDiv) |
| 31 | + } |
| 32 | + |
| 33 | + // Strategy 2: Parse from table cells (alternative format) |
| 34 | + // Structure: table.data > tbody > tr > td |
| 35 | + if gold == 0 && silver == 0 && bronze == 0 { |
| 36 | + parseFromTableCells(root: root) |
| 37 | + } |
| 38 | + |
| 39 | + // Strategy 3: Parse from table rows with separate cells |
| 40 | + // Some V2EX pages show balance as: <td align="right">47</td><td>金币</td> |
| 41 | + if gold == 0 && silver == 0 && bronze == 0 { |
| 42 | + parseFromTableRows(root: root) |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + private mutating func parseFromMoneyDiv(_ element: Element) { |
| 47 | + // Parse from: "47 <img src="/static/img/gold@2x.png"> 28 <img src="/static/img/silver@2x.png"> 26 <img src="/static/img/bronze@2x.png">" |
| 48 | + let text = element.value(.text) |
| 49 | + |
| 50 | + // Split by whitespace and filter out empty strings |
| 51 | + let numbers = text.components(separatedBy: .whitespaces) |
| 52 | + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } |
| 53 | + .filter { !$0.isEmpty } |
| 54 | + .compactMap { parseNumber($0) } |
| 55 | + |
| 56 | + // Assign in order: gold, silver, bronze |
| 57 | + if numbers.count >= 1 { gold = numbers[0] } |
| 58 | + if numbers.count >= 2 { silver = numbers[1] } |
| 59 | + if numbers.count >= 3 { bronze = numbers[2] } |
| 60 | + } |
| 61 | + |
| 62 | + private mutating func parseFromTableCells(root: Element) { |
| 63 | + // Try to parse from cells that contain balance information |
| 64 | + // V2EX balance page typically shows: "XX 金币", "XX 银币", "XX 铜币" |
| 65 | + let cells = root.pickAll("table.data tbody tr td") |
| 66 | + |
| 67 | + for cell in cells { |
| 68 | + let text = cell.value(.text) |
| 69 | + |
| 70 | + if text.contains("金币") { |
| 71 | + gold = extractNumberBefore(keyword: "金币", from: text) |
| 72 | + } else if text.contains("银币") { |
| 73 | + silver = extractNumberBefore(keyword: "银币", from: text) |
| 74 | + } else if text.contains("铜币") { |
| 75 | + bronze = extractNumberBefore(keyword: "铜币", from: text) |
| 76 | + } |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + private mutating func parseFromTableRows(root: Element) { |
| 81 | + // Try alternative parsing method for table rows |
| 82 | + let rows = root.pickAll("table.data tbody tr") |
| 83 | + |
| 84 | + for row in rows { |
| 85 | + let cells = row.pickAll("td") |
| 86 | + if cells.count >= 2 { |
| 87 | + let valueCell = cells[0].value(.text).trimmingCharacters(in: .whitespacesAndNewlines) |
| 88 | + let labelCell = cells[1].value(.text).trimmingCharacters(in: .whitespacesAndNewlines) |
| 89 | + |
| 90 | + if let value = parseNumber(valueCell) { |
| 91 | + if labelCell.contains("金币") { |
| 92 | + gold = value |
| 93 | + } else if labelCell.contains("银币") { |
| 94 | + silver = value |
| 95 | + } else if labelCell.contains("铜币") { |
| 96 | + bronze = value |
| 97 | + } |
| 98 | + } |
| 99 | + } |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + private func parseNumber(_ text: String) -> Int? { |
| 104 | + // Remove commas and parse the number |
| 105 | + // Example: "2,025,101,344" -> 2025101344 |
| 106 | + // Example: "47" -> 47 |
| 107 | + let cleaned = text.replacingOccurrences(of: ",", with: "") |
| 108 | + .trimmingCharacters(in: .whitespacesAndNewlines) |
| 109 | + return Int(cleaned) |
| 110 | + } |
| 111 | + |
| 112 | + private func extractNumberBefore(keyword: String, from text: String) -> Int { |
| 113 | + // Extract number before the keyword, handling commas |
| 114 | + // Example: "47 金币" -> 47 |
| 115 | + // Example: "2,025 金币" -> 2025 |
| 116 | + let components = text.components(separatedBy: keyword) |
| 117 | + if let firstPart = components.first { |
| 118 | + let cleaned = firstPart.trimmingCharacters(in: .whitespacesAndNewlines) |
| 119 | + return parseNumber(cleaned) ?? 0 |
| 120 | + } |
| 121 | + return 0 |
| 122 | + } |
| 123 | + |
| 124 | + func isValid() -> Bool { |
| 125 | + return gold > 0 || silver > 0 || bronze > 0 |
| 126 | + } |
| 127 | +} |
10 | 128 |
|
11 | 129 | struct AccountInfo: Codable { |
12 | 130 | var username: String |
13 | 131 | var avatar: String |
| 132 | + var balance: BalanceInfo? |
14 | 133 |
|
15 | 134 | func isValid() -> Bool { |
16 | 135 | return notEmpty(username, avatar) |
|
0 commit comments