律师助手(国家法律法规数据库爬虫部分的自动更新功能及日志)

法律数据爬虫进阶:日志系统、多模式爬取与守护进程的实现

在本次开发工作中,我对原有法律数据爬虫进行了全面升级,新增日志记录、守护进程模式、全量爬取、增量爬取以及指定内容爬取等功能,为后续将其深度嵌入项目整体框架奠定基础。需强调的是,以下内容仅用于技术学习与交流。

一、新增日志系统:让爬取过程有迹可循

本次优化的首要任务是为爬虫添加日志功能,通过详细记录爬取过程中的关键信息,实现操作的可追溯性与问题的快速定位。

# 日志配置
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        RotatingFileHandler('law_crawler.log', maxBytes=10*1024*1024, backupCount=5, encoding='utf-8'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

上述代码配置了日志系统,将日志信息同时输出到控制台和文件中。日志文件采用滚动策略,当单个文件大小达到 10MB 时,自动生成新文件,并保留最近 5 个历史文件。在数据库初始化、不同类型数据爬取、错误发生以及爬取结束等关键节点,都会输出详细日志,便于后续管理与问题排查。

同时,在数据库中创建了crawl_history表,专门用于记录法律法规及后续诉讼案件的爬取过程。

CREATE TABLE IF NOT EXISTS crawl_history (
    id INT AUTO_INCREMENT PRIMARY KEY,
    crawl_type VARCHAR(20) NOT NULL,
    start_time DATETIME NOT NULL,
    end_time DATETIME,
    new_items INT DEFAULT 0,
    updated_items INT DEFAULT 0,
    status VARCHAR(20) DEFAULT 'running',
    error_message TEXT
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4

该表记录了爬取任务的类型、开始与结束时间、新增和更新的数据条目数、任务状态以及可能出现的错误信息,为全面监控和分析爬取任务提供了数据支持。

二、数据库设计优化:支持增量爬取的关键

为实现增量爬取功能,对law表的结构进行了优化,新增了create_timeupdate_time字段。

CREATE TABLE law (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    title VARCHAR(255) NOT NULL COMMENT '法律标题',
    enacting_authority VARCHAR(100) NOT NULL COMMENT '制定机关',
    legal_nature VARCHAR(50) NOT NULL COMMENT '法律性质',
    validity_status VARCHAR(20) NULL COMMENT '时效性(有效/失效)',
    publish_date DATE NULL COMMENT '公布日期',
    content TEXT NOT NULL COMMENT '法律正文内容',
    file_content LONGBLOB COMMENT '文件内容',
    file_type VARCHAR(10) COMMENT '文件类型',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    expiry_date DATE NULL COMMENT '到期/废止日期',
    `type` VARCHAR(10) NOT NULL COMMENT '文件类型',
    INDEX idx_title (title),
    INDEX idx_authority (enacting_authority),
    INDEX idx_expiry_date (expiry_date),
    INDEX idx_update_time (update_time)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '法律信息表';

通过比较网站数据的更新时间与数据库中对应类型法律法规的最晚更新时间,能够准确识别出新增或更新的数据,并进行针对性爬取,有效提升了爬取效率,避免了重复数据的获取。

三、多模式爬取与守护进程实现

1. 守护进程模式

利用schedule库实现了爬虫的守护进程模式,设置为每 6 小时自动执行一次爬取任务,并且每分钟检查一次是否有任务需要执行。

def run_scheduler(interval_hours=6, incremental=True):
    """运行定时任务"""
    schedule.every(interval_hours).hours.do(lambda: scheduled_crawl(incremental))
    
    logger.info(f"定时爬虫服务已启动,每{interval_hours}小时执行一次")
    while True:
        schedule.run_pending()
        time.sleep(60)  # 每分钟检查一次

在定时任务执行过程中,会生成详细的日志记录,并在发现新数据时发送提示信息,确保能够及时掌握数据更新情况。

2. 定时爬取任务

def scheduled_crawl(incremental=True):
    """定时爬取任务"""
    logger.info(f"开始定时爬取任务: {datetime.now()}")
    page_size = 10
    total_new = 0
    
    try:
        # 爬取各类数据
        for t in types:
            new_items = crawl_type(t, page_size, incremental)
            total_new += new_items
            if new_items > 0:
                send_notification(f"发现{new_items}条新的{t}类型法律")
        
        # 宪法数据
        new_items = crawl_xf(incremental)
        total_new += new_items
        if new_items > 0:
            send_notification(f"发现{new_items}条新的宪法数据")
        
        logger.info(f"定时爬取完成,共发现{total_new}条新记录")
        return total_new
        
    except Exception as e:
        logger.error(f"定时爬取失败: {str(e)}")
        send_notification(f"定时爬取失败: {str(e)}")
        raise

该函数负责协调各类数据的爬取工作,在增量爬取模式下,精准获取新增数据,并在任务结束后汇总统计新发现的数据条目数。

3. 命令行交互

为方便手动运行和调试,添加了命令行参数解析功能,支持多种运行模式:

if __name__ == "__main__":
    # 初始化数据库
    init_database()
    
    # 命令行参数解析
    parser = argparse.ArgumentParser(description='法律数据爬虫')
    parser.add_argument('--incremental', action='store_true', help='增量爬取模式')
    parser.add_argument('--full', action='store_true', help='全量爬取模式')
    parser.add_argument('--daemon', action='store_true', help='以守护进程模式运行定时任务')
    parser.add_argument('--interval', type=int, default=6, help='定时任务执行间隔(小时)')
    parser.add_argument('--type', type=str, help='指定爬取的类型')
    args = parser.parse_args()
    
    if args.daemon:
        # 守护进程模式
        logger.info("启动守护进程模式...")
        run_scheduler(args.interval, not args.full)
    elif args.type:
        # 爬取指定类型
        if args.type == 'xf':
            crawl_xf(not args.full)
        elif args.type in types:
            crawl_type(args.type, incremental=not args.full)
        else:
            logger.error(f"未知类型: {args.type}")
    elif args.full:
        # 全量爬取
        logger.info("启动全量爬取模式...")
        page_size = 10
        for t in types:
            crawl_type(t, page_size, incremental=False)
        crawl_xf(incremental=False)
    else:
        # 默认增量爬取
        logger.info("启动增量爬取模式...")
        page_size = 10
        for t in types:
            crawl_type(t, page_size, incremental=True)
        crawl_xf(incremental=True)

用户可以通过命令行灵活选择增量爬取、全量爬取、守护进程模式,或者指定特定类型进行爬取,极大地提高了爬虫使用的灵活性和便捷性。

四、完整代码展示

import requests
import time
import math
import pymysql
import json
from datetime import datetime
from urllib.parse import urljoin
import random
import argparse
import schedule
import logging
from logging.handlers import RotatingFileHandler

# 日志配置
log_file_path='D:\lawyer-helper\log\law_crawler.log'
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        RotatingFileHandler(log_file_path, maxBytes=10*1024*1024, backupCount=5, encoding='utf-8'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# 数据库配置
DB_CONFIG = {
# 数据库配置
}

# 请求配置
headers = {
# 请求配置
}

base_url = 'https://flk.npc.gov.cn/api/'
types = ['flfg', 'xzfg', 'jcfg', 'sfjs', 'dfxfg']
MAX_RETRIES = 5  # 最大重试次数
BASE_DELAY = 3   # 基础延迟时间(秒)
TIMEOUT = 30     # 请求超时时间
MAX_FILE_SIZE = 50 * 1024 * 1024  # 50MB

def get_db_connection():
    """获取数据库连接"""
    try:
        return pymysql.connect(**DB_CONFIG)
    except pymysql.Error as e:
        logger.error(f"数据库连接失败: {e}")
        raise

def init_database():
    """初始化数据库表结构和索引"""
    conn = None
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS law  (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    title VARCHAR(255) NOT NULL COMMENT '法律标题',
    enacting_authority VARCHAR(100) NOT NULL COMMENT '制定机关',
    legal_nature VARCHAR(50) NOT NULL COMMENT '法律性质',
    validity_status VARCHAR(20) NULL COMMENT '时效性(有效/失效)',
    publish_date DATE NULL COMMENT '公布日期',
    file_content LONGBLOB COMMENT '文件内容',
    file_type VARCHAR(10) COMMENT '文件类型',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    expiry_date DATE NULL COMMENT '到期/废止日期',
    `type` VARCHAR(10) NOT NULL COMMENT '文件类型',
    INDEX idx_title (title),
    INDEX idx_authority (enacting_authority),
    INDEX idx_expiry_date (expiry_date),
    INDEX idx_update_time (update_time)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '法律信息表'


            """)
            # 新增表用于记录爬取历史
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS crawl_history ( 
                    id INT AUTO_INCREMENT PRIMARY KEY,
                    crawl_type VARCHAR(20) NOT NULL,
                    start_time DATETIME NOT NULL,
                    end_time DATETIME,
                    new_items INT DEFAULT 0,
                    updated_items INT DEFAULT 0,
                    status VARCHAR(20) DEFAULT 'running',
                    error_message TEXT
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
            """)
        conn.commit()
        logger.info("数据库初始化完成")
    except Exception as e:
        logger.error(f"数据库初始化失败: {e}")
        if conn:
            conn.rollback()
        raise
    finally:
        if conn:
            conn.close()

def safe_request(url, method='get', data=None, params=None, retry=0):
    """
    带重试机制的请求函数
    解决JSON解析错误问题
    """
    try:
        if method.lower() == 'get':
            response = requests.get(
                url,
                headers=headers,
                params=params,
                timeout=TIMEOUT
            )
        else:
            headers_post = headers.copy()
            headers_post['Content-Type'] = 'application/x-www-form-urlencoded'
            response = requests.post(
                url,
                headers=headers_post,
                data=data,
                timeout=TIMEOUT
            )
        
        # 检查响应内容是否为JSON
        if 'application/json' not in response.headers.get('Content-Type', '').lower():
            raise ValueError("响应不是JSON格式")
        
        response.raise_for_status()
        return response.json()
    
    except (requests.exceptions.RequestException, ValueError, json.JSONDecodeError) as e:
        if retry < MAX_RETRIES:
            wait_time = BASE_DELAY * (2 ** retry) + random.uniform(0, 1)
            logger.warning(f"请求失败,{wait_time:.1f}秒后重试... (错误: {str(e)})")
            time.sleep(wait_time)
            return safe_request(url, method, data, params, retry + 1)
        logger.error(f"请求失败,已达最大重试次数: {str(e)}")
        raise

def get_status_mapping(code):
    """根据状态码返回状态文本"""
    status_map = {
        "1": "有效",
        "5": "已修改",
        "9": "已废止",
        "3": "尚未生效"
    }
    return status_map.get(code, "未知状态")

def get_type_mapping(code):
    """根据类型码返回类型文本"""
    type_map = {
        "xf":"宪法",
        "flfg": "法律",
        "xzfg": "行政法规",
        "jcfg": "经济法规",
        "sfjs": "司法解释",
        "dfxfg": "地方性法规"
    }
    return type_map.get(code, "未知类型")
def download_file_with_retry(url, max_size=MAX_FILE_SIZE):
    """
    带重试机制的文件下载
    返回文件内容和文件类型
    """
    for attempt in range(MAX_RETRIES):
        try:
            response = requests.get(
                url,
                headers=headers,
                stream=True,
                timeout=TIMEOUT
            )
            response.raise_for_status()
            
            # 检查文件大小
            file_size = int(response.headers.get('Content-Length', 0))
            if file_size > max_size:
                raise ValueError(f"文件过大({file_size}字节),超过{max_size}字节限制")
            
            # 分块读取内容
            file_content = b''
            for chunk in response.iter_content(chunk_size=8192):
                file_content += chunk
                if len(file_content) > max_size:
                    raise ValueError(f"文件超过大小限制({max_size}字节)")
            
            # 确定文件类型
            content_type = response.headers.get('Content-Type', '').lower()
            if 'pdf' in content_type:
                file_type = 'pdf'
            elif 'word' in content_type or 'msword' in content_type:
                file_type = 'word'
            else:
                # 根据URL后缀判断
                ext = url.split('.')[-1].lower()
                file_type = ext if ext in ['pdf', 'docx', 'doc'] else 'unknown'
            
            return file_content, file_type
            
        except requests.exceptions.RequestException as e:
            if attempt == MAX_RETRIES - 1:
                logger.error(f"文件下载失败: {str(e)}")
                raise
            wait_time = BASE_DELAY * (attempt + 1)
            logger.warning(f"文件下载失败,{wait_time:.1f}秒后重试... (错误: {str(e)})")
            time.sleep(wait_time)
    
    raise Exception("文件下载失败")

def save_to_database(data,t):
    """保存法律数据到数据库"""
    conn = None
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            # 使用ON DUPLICATE KEY UPDATE实现更新已有记录
            sql = """
            INSERT INTO law (title, enacting_authority, legal_nature, 
                            validity_status, publish_date, 
                            file_content, file_type, expiry_date, `type`)
            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
            ON DUPLICATE KEY UPDATE
                enacting_authority = VALUES(enacting_authority),
                legal_nature = VALUES(legal_nature),
                validity_status = VALUES(validity_status),
                file_content = VALUES(file_content),
                file_type = VALUES(file_type),
                update_time = CURRENT_TIMESTAMP,
                expiry_date = VALUES(expiry_date),
                `type` = VALUES(`type`)
            """
            cursor.execute(sql, (
                data.get('title', '无标题'),
                data.get('office', '未知机关'),
                data.get('type', '未知类型'),
                data.get('status', '未知状态'),
                data.get('publish'),  # 可能是 NULL 或有效日期
                data.get('file_content'),
                data.get('file_type', 'unknown'),
                data.get('expiry_date','未知时间'),
                get_type_mapping(t)
            ))
        conn.commit()
        logger.info(f"成功保存/更新: {data.get('title')}")
        return True
    except pymysql.Error as e:
        logger.error(f"数据库错误: {e}")
        if conn:
            conn.rollback()
        return False
    finally:
        if conn:
            conn.close()

def get_last_crawl_time(t):
    """获取指定类型上次爬取时间"""
    conn = None
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute("""
                SELECT MAX(update_time) FROM law 
                WHERE `type` = %s
            """, (get_type_mapping(t),))
            result = cursor.fetchone()
            return result[0] if result[0] else ''
    except Exception as e:
        logger.error(f"获取上次爬取时间失败: {e}")
        return datetime(1900, 1, 1)
    finally:
        if conn:
            conn.close()

def get_total_pages(t, size=10, last_crawl_time=None):
    """获取总页数(增强错误处理)"""
    params = {
        'type': t,
        'searchType': 'title;vague',
        'sortTr': 'f_bbrq_s;desc',
        'gbrqStart': last_crawl_time.strftime('%Y-%m-%d') if last_crawl_time else '',
        'gbrqEnd': '',
        'sxrqStart': '',
        'sxrqEnd': '',
        'sort': 'true',
        'page': '1',
        'size': str(size),
        '_': str(int(time.time() * 1000))
    }
    
    try:
        result = safe_request(base_url, params=params)
        total_sizes = result['result']['totalSizes']
        pages = math.ceil(total_sizes / size)
        logger.info(f"开始时间: { last_crawl_time.strftime('%Y-%m-%d') if last_crawl_time else ''}")
        logger.info(f"结束时间: {datetime.now().strftime('%Y-%m-%d')}")
        logger.info(f"{t}类型总页数: {pages}")
        return pages
    except Exception as e:
        logger.error(f"获取{t}类型总页数失败: {e}")
        return -1

def process_law_item(item,t):
    """处理单个法律条目"""
    try:
        # 获取详情
        detail_url = urljoin(base_url, 'detail')
        detail_data = {'id': item['id']}
        
        try:
            detail_json = safe_request(detail_url, method='post', data=detail_data)
        except Exception as e:
            logger.error(f"获取详情失败: {str(e)}")
            return False
        
        if not detail_json.get('result', {}).get('body'):
            logger.warning(f"{item.get('title')} 无详情内容")
            return False
            
        body = detail_json['result']['body'][0]
        file_path = body.get('path', '')
        
        if not file_path:
            logger.warning(f"{item.get('title')} 无文件路径")
            return False
            
        file_url = urljoin('https://wb.flk.npc.gov.cn', file_path)
        
        # 下载文件
        try:
            file_content, file_type = download_file_with_retry(file_url)
        except Exception as e:
            logger.error(f"下载文件失败: {str(e)}")
            return False
            
        # 准备数据
        law_data = {
            'title': item.get('title', '无标题'),
            'office': item.get('office', '未知机关'),
            'publish': item.get('publish'),
            'type': item.get('type', '未知类型'),
            'expiry_date':item.get('expiry'),
            'status': get_status_mapping(item.get('status', '')),
            'file_content': file_content,
            'file_type': file_type,
        }
        
        # 保存到数据库
        if not save_to_database(law_data,t):
            return False
            
        # 随机延迟防止被封
        time.sleep(random.uniform(1, 3))
        return True
        
    except Exception as e:
        logger.error(f"处理条目失败: {str(e)}")
        return False

def crawl_type(t, size=10, incremental=True):
    """爬取指定类型数据"""
    conn = None
    crawl_id = None
    try:
        # 记录爬取开始
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute("""
                INSERT INTO crawl_history 
                (crawl_type, start_time, status)
                VALUES (%s, %s, %s)
            """, (t, datetime.now(), 'running'))
            crawl_id = cursor.lastrowid
        conn.commit()
        
        last_crawl_time = get_last_crawl_time(t) if incremental else None
        total_pages = get_total_pages(t, size, last_crawl_time)
        
        if total_pages == -1:
            logger.warning(f"{t}类型获取页数失败,跳过")
            return 0
        if total_pages == 0:
            logger.warning(f"{t}暂无更新数据,跳过")
            return 0
            
        logger.info(f"开始爬取{t}类型,共{total_pages}页...")
        new_items = 0
        
        for page in range(1, total_pages + 1):
            logger.info(f"正在处理第{page}/{total_pages}页...")
            
            params = {
                'type': t,
                'searchType': 'title;vague',
                'sortTr': 'f_bbrq_s;desc',
                'gbrqStart': last_crawl_time.strftime('%Y-%m-%d') if last_crawl_time else '',
                'gbrqEnd': datetime.now().strftime('%Y-%m-%d'),
                'sxrqStart': '',
                'sxrqEnd': '',
                'sort': 'true',
                'page': str(page),
                'size': str(size),
                '_': str(int(time.time() * 1000))
            }
            
            try:
                # 获取列表数据
                result = safe_request(base_url, params=params)
                data = result['result']['data']
                
                # 处理每个条目
                for item in data:
                    if process_law_item(item,t):
                        new_items += 1
                        
            except Exception as e:
                logger.error(f"第{page}页处理失败: {str(e)}")
                continue
        
        # 更新爬取记录
        with conn.cursor() as cursor:
            cursor.execute("""
                UPDATE crawl_history 
                SET end_time = %s, new_items = %s, status = 'completed'
                WHERE id = %s
            """, (datetime.now(), new_items, crawl_id))
        conn.commit()
        
        logger.info(f"{t}类型爬取完成,新增{new_items}条记录")
        return new_items
        
    except Exception as e:
        logger.error(f"{t}类型爬取过程中发生错误: {str(e)}")
        if conn and crawl_id:
            with conn.cursor() as cursor:
                cursor.execute("""
                    UPDATE crawl_history 
                    SET end_time = %s, status = 'failed', error_message = %s
                    WHERE id = %s
                """, (datetime.now(), str(e), crawl_id))
            conn.commit()
        raise
    finally:
        if conn:
            conn.close()

def crawl_xf(incremental=True):
    """爬取宪法数据"""
    conn = None
    crawl_id = None
    try:
        # 记录爬取开始
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute("""
                INSERT INTO crawl_history 
                (crawl_type, start_time, status)
                VALUES (%s, %s, %s)
            """, ('xf', datetime.now(), 'running'))
            crawl_id = cursor.lastrowid
        conn.commit()
        
        logger.info("开始爬取宪法数据...")
        url = urljoin(base_url, 'xf')
        new_items = 0
        
        try:
            result = safe_request(url)
            data = result['result']['data']
            
            # 如果是增量模式,获取上次爬取时间
            last_crawl_time = get_last_crawl_time('xf') if incremental else None
            if last_crawl_time=='':
                last_crawl_time = datetime.strptime('1900-01-01', '%Y-%m-%d')
            for item in data[:7]:  # 只取前7条
                # 检查是否是新数据
                if incremental and item.get('publish'):
                    publish_date = datetime.strptime(item['publish'], '%Y-%m-%d %H:%M:%S')
                    if publish_date < last_crawl_time:
                        continue
                
                # 获取详情
                detail_url = urljoin(base_url, 'detail')
                detail_data = {'id': item['id']}
                
                try:
                    detail_json = safe_request(detail_url, method='post', data=detail_data)
                except Exception as e:
                    logger.error(f"获取宪法详情失败: {str(e)}")
                    continue
                    
                if not detail_json.get('result', {}).get('body'):
                    logger.warning(f"{item.get('title')} 无详情内容")
                    continue
                    
                body = detail_json['result']['body'][0]
                file_path = body.get('path', '')
                
                if not file_path:
                    logger.warning(f"{item.get('title')} 无文件路径")
                    continue
                    
                file_url = urljoin('https://wb.flk.npc.gov.cn', file_path)
                
                # 下载文件
                try:
                    file_content, file_type = download_file_with_retry(file_url)
                except Exception as e:
                    logger.error(f"下载宪法文件失败: {str(e)}")
                    continue
                    
                # 准备数据
                law_data = {
                    'title': item.get('title', '无标题'),
                    'office': '全国人民代表大会',
                    'publish': item.get('publish'),
                    'type': '宪法',
                    'status': '有效',
                    'file_content': file_content,
                    'file_type': file_type,
                    'expiry_date': item.get('expiry'),
                }
                if save_to_database(law_data,'xf'):
                    new_items += 1
                time.sleep(random.uniform(1, 3))
                    
        except Exception as e:
            logger.error(f"获取宪法数据失败: {str(e)}")
            raise
        
        # 更新爬取记录
        with conn.cursor() as cursor:
            cursor.execute("""
                UPDATE crawl_history 
                SET end_time = %s, new_items = %s, status = 'completed'
                WHERE id = %s
            """, (datetime.now(), new_items, crawl_id))
        conn.commit()
        
        logger.info(f"宪法数据爬取完成,新增{new_items}条记录")
        return new_items
        
    except Exception as e:
        logger.error(f"宪法数据爬取过程中发生错误: {str(e)}")
        if conn and crawl_id:
            with conn.cursor() as cursor:
                cursor.execute("""
                    UPDATE crawl_history 
                    SET end_time = %s, status = 'failed', error_message = %s
                    WHERE id = %s
                """, (datetime.now(), str(e), crawl_id))
            conn.commit()
        raise
    finally:
        if conn:
            conn.close()

def send_notification(message):
    """发送通知(可集成邮件、短信等)"""
    logger.info(f"发送通知: {message}")
    # 这里可以添加实际的邮件或短信发送逻辑

def scheduled_crawl(incremental=True):
    """定时爬取任务"""
    logger.info(f"开始定时爬取任务: {datetime.now()}")
    page_size = 10
    total_new = 0
    
    try:
        # 爬取各类数据
        for t in types:
            new_items = crawl_type(t, page_size, incremental)
            total_new += new_items
            if new_items > 0:
                send_notification(f"发现{new_items}条新的{t}类型法律")
        
        # 宪法数据
        new_items = crawl_xf(incremental)
        total_new += new_items
        if new_items > 0:
            send_notification(f"发现{new_items}条新的宪法数据")
        
        logger.info(f"定时爬取完成,共发现{total_new}条新记录")
        return total_new
        
    except Exception as e:
        logger.error(f"定时爬取失败: {str(e)}")
        send_notification(f"定时爬取失败: {str(e)}")
        raise

def run_scheduler(interval_hours=6, incremental=True):
    """运行定时任务"""
    schedule.every(interval_hours).hours.do(lambda: scheduled_crawl(incremental))
    
    logger.info(f"定时爬虫服务已启动,每{interval_hours}小时执行一次")
    while True:
        schedule.run_pending()
        time.sleep(60)  # 每分钟检查一次

if __name__ == "__main__":
    # 初始化数据库
    init_database()
    
    # 命令行参数解析
    parser = argparse.ArgumentParser(description='法律数据爬虫')
    parser.add_argument('--incremental', action='store_true', help='增量爬取模式')
    parser.add_argument('--full', action='store_true', help='全量爬取模式')
    parser.add_argument('--daemon', action='store_true', help='以守护进程模式运行定时任务')
    parser.add_argument('--interval', type=int, default=6, help='定时任务执行间隔(小时)')
    parser.add_argument('--type', type=str, help='指定爬取的类型')
    args = parser.parse_args()
    
    if args.daemon:
        # 守护进程模式
        logger.info("启动守护进程模式...")
        run_scheduler(args.interval, not args.full)
    elif args.type:
        # 爬取指定类型
        if args.type == 'xf':
            crawl_xf(not args.full)
        elif args.type in types:
            crawl_type(args.type, incremental=not args.full)
        else:
            logger.error(f"未知类型: {args.type}")
    elif args.full:
        # 全量爬取
        logger.info("启动全量爬取模式...")
        page_size = 10
        for t in types:
            crawl_type(t, page_size, incremental=False)
        crawl_xf(incremental=False)
    else:
        # 默认增量爬取
        logger.info("启动增量爬取模式...")
        page_size = 10
        for t in types:
            crawl_type(t, page_size, incremental=True)
        crawl_xf(incremental=True)
        

完整代码涵盖了从数据库连接、请求处理、数据爬取到多种模式运行的全部逻辑,实现了功能完备且可灵活配置的法律数据爬虫。

五、测试与展望

经过初步测试,本次升级后的爬虫在各项功能上均表现稳定,暂未发现明显问题。后续,我将进一步优化爬虫性能,探索如何突破更多网站的反爬机制,并深入研究数据存储和管理策略,以更好地应对大规模数据的挑战。希望本次分享能对大家的技术学习和实践有所启发,欢迎交流讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值