(十五)把采集进度保存成任务与结果表
1. 只保存最后页码会漏掉已发现的详情
程序发现第三页时,前面的详情可能还没有保存。如果只记“已经翻到第 3 页”,重新启动后就无法知道哪些书仍缺少字段。需要分别记住页任务、详情任务与结果:
| 表 | 主要字段 | 回答的问题 |
|---|---|---|
| meta | start_url | 这份进度从哪个目录入口建立 |
| pages | seq、url、done | 哪些目录页还没有提交发现结果 |
| jobs | seq、url、done | 哪些详情 URL 还需要取得记录 |
| items | source_id、书籍字段、source_url | 哪些记录已经写入并可重新导出 |
done 为 0 的任务会在启动后重新选择,完成并提交才改为 1。URL 的 UNIQUE 约束让同一链接只产生一项任务,items 的源站编号主键则合并两个地址取得的同一本书。
2. 打开进度库并建立目录入口
下面是 state.py 的第一部分,表结构与前面 SQLite 的写法相同:
# 项目文件:crawler_project/src/catalogcrawl/state.py
# 页任务、详情任务与结果放进同一个 SQLite 文件,提交后才把任务标为完成。
import json
from pathlib import Path
import sqlite3
from bookdata.models import Book
from .errors import CrawlError
from .models import CollectedBook
SCHEMA = """
CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS pages(
seq INTEGER PRIMARY KEY, url TEXT NOT NULL UNIQUE,
done INTEGER NOT NULL DEFAULT 0 CHECK(done IN (0, 1)));
CREATE TABLE IF NOT EXISTS jobs(
seq INTEGER PRIMARY KEY, url TEXT NOT NULL UNIQUE,
done INTEGER NOT NULL DEFAULT 0 CHECK(done IN (0, 1)));
CREATE TABLE IF NOT EXISTS items(
source_id TEXT PRIMARY KEY, title TEXT NOT NULL,
finished INTEGER NOT NULL CHECK(finished IN (0, 1)),
tags TEXT NOT NULL, source_url TEXT NOT NULL);
"""
class CrawlState:
def __init__(self, path: Path, start_url=None):
if start_url is None and not path.is_file():
raise FileNotFoundError(f"还没有采集进度:{path}")
path.parent.mkdir(parents=True, exist_ok=True)
self.connection = sqlite3.connect(path)
self.connection.row_factory = sqlite3.Row
try:
self.connection.executescript(SCHEMA)
if start_url is not None:
with self.connection:
row = self.connection.execute(
"SELECT value FROM meta WHERE key='start_url'"
).fetchone()
if row is not None and row["value"] != start_url:
raise CrawlError(
"这份进度属于另一个目录入口,请为新目录选择新的进度文件"
)
self.connection.execute(
"INSERT OR IGNORE INTO meta VALUES ('start_url', ?)",
(start_url,),
)
self.connection.execute(
"INSERT OR IGNORE INTO pages(url) VALUES (?)", (start_url,)
)
except BaseException:
self.connection.close()
raise
def __enter__(self):
return self
def __exit__(self, *error):
self.connection.close()第一次启动插入目录入口;再次用同一入口打开时,INSERT OR IGNORE 保留已有状态。误把另一份目录交给同一进度文件时,入口比较会反馈具体问题。status 和 export 不提供新入口,而是打开已经存在的进度。
连接在构造时建立;如果初始化中途失败,就先关闭再继续抛出。正常运行由 with 离开时关闭。数据库连接仍只交给当前协调层,网络工作任务不各自拿着它修改。