|
| 1 | +# WeRead — Auto Reading |
| 2 | + |
| 3 | +Field-tested against weread.qq.com on 2026-05-06. |
| 4 | +Requires WeChat login for reading features. |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +## Core Flow |
| 9 | + |
| 10 | +``` |
| 11 | +Step 1: Check login status |
| 12 | + ├── Not logged in → Prompt user to scan QR code → Wait for login completion |
| 13 | + └── Logged in → Proceed to Step 2 |
| 14 | +
|
| 15 | +Step 2: Check reading progress |
| 16 | + ├── No progress or finished → Pick first book from New Book ranking → Record status → Proceed to Step 3 |
| 17 | + └── Unfinished book → Resume reading → Proceed to Step 3 |
| 18 | +
|
| 19 | +Step 3: Auto reading (1 minute per session) |
| 20 | + ├── Every 3-5s scroll down 100px |
| 21 | + ├── See "Next Chapter" → Click, continue reading |
| 22 | + ├── See "Book Complete" + "Mark as Finished" → Click mark as finished → Record status as finished |
| 23 | + └── Time's up → Save progress, wait for next session |
| 24 | +``` |
| 25 | + |
| 26 | +--- |
| 27 | + |
| 28 | +## URL Patterns |
| 29 | + |
| 30 | +| Page | URL | |
| 31 | +|------|-----| |
| 32 | +| Home | `https://weread.qq.com/` | |
| 33 | +| Rising | `https://weread.qq.com/web/category/rising` | |
| 34 | +| Hot Search | `https://weread.qq.com/web/category/hot_search` | |
| 35 | +| New Book | `https://weread.qq.com/web/category/newbook` | |
| 36 | +| Book Detail | `https://weread.qq.com/web/bookDetail/{BOOK_ID}` | |
| 37 | +| Reader | `https://weread.qq.com/web/reader/{BOOK_ID}k{CHAPTER_HASH}` | |
| 38 | + |
| 39 | +### Reader URL Pattern |
| 40 | + |
| 41 | +``` |
| 42 | +https://weread.qq.com/web/reader/{BOOK_ID}k{CHAPTER_HASH} |
| 43 | +``` |
| 44 | + |
| 45 | +- `BOOK_ID` — unique book identifier (e.g., `ee0320b053b925ee0519857`) |
| 46 | +- `CHAPTER_HASH` — chapter hash value (e.g., `08432c902c4084b6fbb18c9`) |
| 47 | +- Directly accessing the URL jumps to the specified chapter |
| 48 | + |
| 49 | +--- |
| 50 | + |
| 51 | +## Step 1: Login Flow |
| 52 | + |
| 53 | +### Login Detection |
| 54 | + |
| 55 | +```python |
| 56 | +# Check if login is needed |
| 57 | +login_needed = js(""" |
| 58 | + const loginBtn = document.querySelector('[class*=login], [class*=Login]'); |
| 59 | + const qrCode = document.querySelector('[class*=qrcode], [class*=QRCode]'); |
| 60 | + return !!(loginBtn || qrCode); |
| 61 | +""") |
| 62 | +``` |
| 63 | + |
| 64 | +### Login Process |
| 65 | + |
| 66 | +1. Navigate to `https://weread.qq.com/` |
| 67 | +2. Page shows a QR code login prompt |
| 68 | +3. User scans the QR code with the WeChat mobile app |
| 69 | +4. Page auto-redirects to the home page after successful scan |
| 70 | +5. Login state is persisted — no need to re-login |
| 71 | + |
| 72 | +### Wait For Login Completion |
| 73 | + |
| 74 | +```python |
| 75 | +# Wait for login to complete |
| 76 | +import time |
| 77 | + |
| 78 | +def wait_for_login(timeout=120): |
| 79 | + start = time.time() |
| 80 | + while time.time() - start < timeout: |
| 81 | + # Check if still on login page |
| 82 | + on_login = js(""" |
| 83 | + const loginBtn = document.querySelector('[class*=login], [class*=Login]'); |
| 84 | + return !!loginBtn; |
| 85 | + """) |
| 86 | + if not on_login: |
| 87 | + return True |
| 88 | + time.sleep(2) |
| 89 | + return False |
| 90 | +``` |
| 91 | + |
| 92 | +--- |
| 93 | + |
| 94 | +## Step 2: Reading Progress Management |
| 95 | + |
| 96 | +### progress.json Structure |
| 97 | + |
| 98 | +```json |
| 99 | +{ |
| 100 | + "book": { |
| 101 | + "title": "book title", |
| 102 | + "author": "author", |
| 103 | + "url": "current chapter URL" |
| 104 | + }, |
| 105 | + "progress": { |
| 106 | + "status": "reading | finished", |
| 107 | + "currentChapter": "chapter name", |
| 108 | + "completedChapters": ["list of chapters"], |
| 109 | + "lastReadTime": "2026-05-06" |
| 110 | + } |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +### Load Progress |
| 115 | + |
| 116 | +```python |
| 117 | +import json |
| 118 | +import os |
| 119 | + |
| 120 | +PROGRESS_FILE = "progress.json" |
| 121 | + |
| 122 | +def load_progress(): |
| 123 | + if not os.path.exists(PROGRESS_FILE): |
| 124 | + return None |
| 125 | + with open(PROGRESS_FILE, "r", encoding="utf-8") as f: |
| 126 | + return json.load(f) |
| 127 | +``` |
| 128 | + |
| 129 | +### Save Progress |
| 130 | + |
| 131 | +```python |
| 132 | +def save_progress(book_title, author, url, chapter, completed_chapters, status="reading"): |
| 133 | + progress = { |
| 134 | + "book": { |
| 135 | + "title": book_title, |
| 136 | + "author": author, |
| 137 | + "url": url |
| 138 | + }, |
| 139 | + "progress": { |
| 140 | + "status": status, |
| 141 | + "currentChapter": chapter, |
| 142 | + "completedChapters": completed_chapters, |
| 143 | + "lastReadTime": time.strftime("%Y-%m-%d") |
| 144 | + } |
| 145 | + } |
| 146 | + with open(PROGRESS_FILE, "w", encoding="utf-8") as f: |
| 147 | + json.dump(progress, f, ensure_ascii=False, indent=2) |
| 148 | +``` |
| 149 | + |
| 150 | +### Check Reading Status |
| 151 | + |
| 152 | +```python |
| 153 | +def should_pick_new_book(): |
| 154 | + progress = load_progress() |
| 155 | + if progress is None: |
| 156 | + return True # never read before |
| 157 | + if progress["progress"]["status"] == "finished": |
| 158 | + return True # already finished |
| 159 | + return False # has an unfinished book |
| 160 | +``` |
| 161 | + |
| 162 | +### Pick Book From New Book Ranking |
| 163 | + |
| 164 | +```python |
| 165 | +def pick_book_from_new_ranking(): |
| 166 | + # Navigate to New Book ranking |
| 167 | + new_tab("https://weread.qq.com/web/category/newbook") |
| 168 | + wait_for_load() |
| 169 | + time.sleep(2) |
| 170 | + |
| 171 | + # Scroll to top |
| 172 | + js("window.scrollTo(0, 0)") |
| 173 | + time.sleep(1) |
| 174 | + |
| 175 | + # Click the first book |
| 176 | + first_book = js(""" |
| 177 | + const allElements = document.querySelectorAll("[class*=title]"); |
| 178 | + for (const el of allElements) { |
| 179 | + const text = el.textContent.trim(); |
| 180 | + if (text.length > 3 && text.length < 50 && !text.includes("榜")) { |
| 181 | + const rect = el.getBoundingClientRect(); |
| 182 | + return { |
| 183 | + title: text, |
| 184 | + x: rect.x + rect.width / 2, |
| 185 | + y: rect.y + rect.height / 2 |
| 186 | + }; |
| 187 | + } |
| 188 | + } |
| 189 | + return null; |
| 190 | + """) |
| 191 | + |
| 192 | + if first_book: |
| 193 | + click_at_xy(first_book["x"], first_book["y"]) |
| 194 | + wait_for_load() |
| 195 | + time.sleep(1) |
| 196 | + |
| 197 | + return first_book["title"] if first_book else None |
| 198 | +``` |
| 199 | + |
| 200 | +--- |
| 201 | + |
| 202 | +## Step 3: Auto Reading |
| 203 | + |
| 204 | +### Scroll Reading |
| 205 | + |
| 206 | +```python |
| 207 | +import random |
| 208 | + |
| 209 | +def scroll_reading(duration=360): |
| 210 | + start_time = time.time() |
| 211 | + chapters_read = [] |
| 212 | + |
| 213 | + while time.time() - start_time < duration: |
| 214 | + # Scroll down 100px |
| 215 | + js("window.scrollBy(0, 100)") |
| 216 | + |
| 217 | + # Random wait 3-5 seconds |
| 218 | + wait_time = random.uniform(3, 5) |
| 219 | + time.sleep(wait_time) |
| 220 | + |
| 221 | + # Check if "Next Chapter" button is visible |
| 222 | + next_chapter = find_next_chapter_button() |
| 223 | + if next_chapter and next_chapter["visible"]: |
| 224 | + # Record current chapter |
| 225 | + current = get_current_chapter() |
| 226 | + chapters_read.append(current) |
| 227 | + # Click next chapter |
| 228 | + click_at_xy(next_chapter["x"], next_chapter["y"]) |
| 229 | + wait_for_load() |
| 230 | + time.sleep(1) |
| 231 | + continue |
| 232 | + |
| 233 | + # Check if "Book Complete" and "Mark as Finished" |
| 234 | + finished = check_book_finished() |
| 235 | + if finished: |
| 236 | + click_mark_finished() |
| 237 | + return chapters_read, True # True = book finished |
| 238 | + |
| 239 | + return chapters_read, False # False = time's up, book not finished |
| 240 | +``` |
| 241 | + |
| 242 | +### Find Next Chapter Button |
| 243 | + |
| 244 | +```python |
| 245 | +def find_next_chapter_button(): |
| 246 | + buttons = js(""" |
| 247 | + const items = []; |
| 248 | + const elements = document.querySelectorAll("button, a, [role=button], [class*=next], [class*=Next]"); |
| 249 | + elements.forEach(el => { |
| 250 | + const text = el.textContent.trim(); |
| 251 | + if (text.includes("下一章")) { |
| 252 | + const rect = el.getBoundingClientRect(); |
| 253 | + items.push({ |
| 254 | + text: text, |
| 255 | + x: rect.x + rect.width/2, |
| 256 | + y: rect.y + rect.height/2, |
| 257 | + visible: rect.top < window.innerHeight && rect.bottom > 0 |
| 258 | + }); |
| 259 | + } |
| 260 | + }); |
| 261 | + return items; |
| 262 | + """) |
| 263 | + return buttons[0] if buttons else None |
| 264 | +``` |
| 265 | + |
| 266 | +### Get Current Chapter |
| 267 | + |
| 268 | +```python |
| 269 | +def get_current_chapter(): |
| 270 | + return js(""" |
| 271 | + const title = document.title; |
| 272 | + const parts = title.split(" - "); |
| 273 | + return parts.length > 1 ? parts[1] : "未知章节"; |
| 274 | + """) |
| 275 | +``` |
| 276 | + |
| 277 | +### Check Book Finished |
| 278 | + |
| 279 | +```python |
| 280 | +def check_book_finished(): |
| 281 | + return js(""" |
| 282 | + const elements = document.querySelectorAll("[class*=finish], [class*=complete], [class*=end]"); |
| 283 | + for (const el of elements) { |
| 284 | + const text = el.textContent.trim(); |
| 285 | + if (text.includes("全书完") || text.includes("已读完")) { |
| 286 | + return true; |
| 287 | + } |
| 288 | + } |
| 289 | + return false; |
| 290 | + """) |
| 291 | +``` |
| 292 | + |
| 293 | +### Click Mark As Finished |
| 294 | + |
| 295 | +```python |
| 296 | +def click_mark_finished(): |
| 297 | + button = js(""" |
| 298 | + const elements = document.querySelectorAll("button, [role=button]"); |
| 299 | + for (const el of elements) { |
| 300 | + const text = el.textContent.trim(); |
| 301 | + if (text.includes("标记读完") || text.includes("标记已读")) { |
| 302 | + const rect = el.getBoundingClientRect(); |
| 303 | + return {x: rect.x + rect.width/2, y: rect.y + rect.height/2}; |
| 304 | + } |
| 305 | + } |
| 306 | + return null; |
| 307 | + """) |
| 308 | + if button: |
| 309 | + click_at_xy(button["x"], button["y"]) |
| 310 | + wait_for_load() |
| 311 | + time.sleep(1) |
| 312 | +``` |
| 313 | + |
| 314 | +--- |
| 315 | + |
| 316 | +## Gotchas |
| 317 | + |
| 318 | +- **Persistent login** — No need to re-login after first scan, unless browser data is cleared. |
| 319 | +- **"Next Chapter" button position** — The button is at the bottom of the page; scroll it into the viewport before clicking. |
| 320 | +- **Scroll interval** — 3-5s random interval simulates real reading; scrolling too fast may trigger detection. |
| 321 | +- **Chapter URL changes** — Each chapter has a unique URL hash; saving the full URL allows precise position restoration. |
| 322 | +- **Book finished detection** — Some books may lack a "Book Complete" prompt; adjust detection logic as needed. |
| 323 | +- **New Book ranking first pick** — Ranking order may change; always fetch the first book in real time. |
0 commit comments