A 股创历史新低筛选器phthon爬虫脚本

1.需求

  1. Baostock 拉历史日线,筛出"截至昨日,历史最低价已接近或低于当前价"的候选

  2. 对新浪批量请求候选股的今日实时最低价

  3. 交叉比对:今日实时最低价 <= 历史前低,才算真创新低

  4. 附加:上市>1年、剔除科创/北证/ST、最新季度净利润>0

2.脚本

复制代码
vim bottom_fishing.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
A 股创历史新低筛选器(Baostock 历史 + 新浪财经实时 混合版)

流程:
1. Baostock 拉历史日线,筛出"截至昨日,历史最低价已接近或低于当前价"的候选
2. 对新浪批量请求候选股的今日实时最低价
3. 交叉比对:今日实时最低价 <= 历史前低,才算真创新低
4. 附加:上市>1年、剔除科创/北证/ST、最新季度净利润>0

用法: python3 bottom_fishing_sina.py
依赖: pip install baostock requests pandas
"""
import baostock as bs
import requests
import pandas as pd
from datetime import datetime, timedelta
import time
import sys
import re

# ==================== 配置 ====================
MIN_LIST_DAYS = 365
HIST_DELAY = 0.25
SINA_BATCH_SIZE = 50          # 每次请求新浪的股票数量
SINA_DELAY = 1.0              # 新浪请求间隔
MAX_HIST_LOOKBACK_DAYS = 7

SINA_HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    "Referer": "https://finance.sina.com.cn/",
}


class C:
    RESET = "\033[0m"; BOLD = "\033[1m"; DIM = "\033[2m"
    RED = "\033[91m"; GREEN = "\033[92m"; YELLOW = "\033[93m"
    BLUE = "\033[94m"; MAGENTA = "\033[95m"; CYAN = "\033[96m"; GRAY = "\033[90m"


def banner():
    print(f"""
{C.CYAN}{C.BOLD}╔══════════════════════════════════════════════════════════╗
║   📉  A 股创历史新低筛选器(Baostock + 新浪财经)📉       ║
║   历史候选 → 新浪批量实时 → 今日真创新低                  ║
╚══════════════════════════════════════════════════════════╝{C.RESET}
""")


def progress_bar(current, total, width=34, prefix="", suffix=""):
    ratio = current / total if total else 0
    filled = int(width * ratio)
    bar = "█" * filled + C.GRAY + "░" * (width - filled) + C.RESET
    color = C.GREEN if ratio == 1 else C.CYAN
    print(f"\r  {prefix} {color}{bar}{C.RESET} {ratio*100:5.1f}%  {suffix}   ", end="")
    sys.stdout.flush()


def stage(tag, msg, color=C.CYAN):
    print(f"  {color}{C.BOLD}[{tag}]{C.RESET} {msg}")


# ==================== Baostock 部分(同上一版) ====================
def get_stock_list_baostock():
    for offset in range(MAX_HIST_LOOKBACK_DAYS):
        day = (datetime.now() - timedelta(days=offset)).strftime("%Y-%m-%d")
        rs = bs.query_all_stock(day=day)
        if rs.error_code != "0":
            continue
        rows = []
        while rs.next():
            rows.append(rs.get_row_data())
        if rows:
            df = pd.DataFrame(rows, columns=rs.fields)
            print(f"    使用 {day} 的股票列表,共 {len(df)} 条")
            return df
    return None


def is_excluded_baostock(code, name):
    pure = code.split(".")[-1]
    if pure.startswith("688"):
        return True
    if pure.startswith(("8", "4")):
        return True
    if "ST" in name.upper() or "退" in name:
        return True
    return False


def get_listing_date(code):
    rs = bs.query_stock_basic(code=code)
    if rs.error_code != "0" or not rs.next():
        return None
    row = rs.get_row_data()
    ipo = row[2]
    if not ipo:
        return None
    try:
        return datetime.strptime(ipo, "%Y-%m-%d")
    except ValueError:
        return None


def get_hist_low(code):
    """拉前复权历史日线,返回截至昨日的历史最低价"""
    rs = bs.query_history_k_data_plus(
        code, "date,low",
        start_date="1990-01-01",
        end_date=datetime.now().strftime("%Y-%m-%d"),
        frequency="d", adjustflag="2",
    )
    if rs.error_code != "0":
        return None
    rows = []
    while rs.next():
        rows.append(rs.get_row_data())
    if len(rows) < 2:
        return None
    df = pd.DataFrame(rows, columns=rs.fields)
    df["low"] = pd.to_numeric(df["low"], errors="coerce")
    df = df.dropna(subset=["low"])
    if len(df) < 2:
        return None
    hist_min = df["low"].iloc[:-1].min()
    return {"hist_min": round(float(hist_min), 2), "last_date": df["date"].iloc[-1]}


def check_fundamental(code):
    try:
        year = datetime.now().year
        quarter = (datetime.now().month - 1) // 3
        if quarter == 0:
            year -= 1
            quarter = 4
        rs = bs.query_profit_data(code=code, year=year, quarter=quarter)
        if rs.error_code != "0" or not rs.next():
            return None
        row = rs.get_row_data()
        idx = None
        for i, f in enumerate(rs.fields):
            if "netProfit" in f or f == "np":
                idx = i
                break
        if idx is None or row[idx] == "":
            return None
        return float(row[idx]) > 0
    except Exception:
        return None


# ==================== 新浪财经实时部分 ====================
def sina_code(pure_code):
    """6位代码转新浪格式:sh/sz 前缀"""
    if pure_code.startswith(("6", "9")):
        return f"sh{pure_code}"
    return f"sz{pure_code}"


def fetch_sina_batch(codes):
    """
    批量请求新浪实时行情,返回 {纯代码: {"low": 今日最低, "name": 名称, ...}}
    codes 为纯 6 位代码列表
    """
    sina_codes = [sina_code(c) for c in codes]
    url = f"https://hq.sinajs.cn/list={','.join(sina_codes)}"
    try:
        r = requests.get(url, headers=SINA_HEADERS, timeout=10)
        r.encoding = "gbk"
        if r.status_code != 200:
            return None
        result = {}
        for line in r.text.strip().split("\n"):
            m = re.match(r'var hq_str_([a-z]{2}\d{6})="([^"]*)"', line)
            if not m:
                continue
            code_full = m.group(1)
            pure = code_full[2:]
            fields = m.group(2).split(",")
            if len(fields) < 6 or fields[0] == "":
                continue
            try:
                result[pure] = {
                    "name": fields[0],
                    "today_low": float(fields[5]),   # 今日最低价
                    "today_open": float(fields[1]),
                    "yesterday_close": float(fields[2]),
                    "current": float(fields[3]),
                }
            except (ValueError, IndexError):
                continue
        return result
    except Exception as e:
        print(f"    ⚠️ 新浪请求失败: {e}")
        return None


# ==================== 主流程 ====================
def main():
    banner()
    started = time.time()

    # ---------- 第 1 步:Baostock 历史扫描,收集候选 ----------
    stage("1/4", "Baostock 登录...")
    lg = bs.login()
    if lg.error_code != "0":
        print(f"{C.RED}登录失败: {lg.error_msg}{C.RESET}")
        return

    stage("2/4", "获取 A 股列表(Baostock)...")
    stock_df = get_stock_list_baostock()
    if stock_df is None or stock_df.empty:
        print(f"{C.RED}获取股票列表失败{C.RESET}")
        bs.logout()
        return

    stock_df = stock_df[stock_df["code"].str.startswith(("sh.", "sz."))].copy()
    stock_df = stock_df[~stock_df.apply(
        lambda r: is_excluded_baostock(r["code"], r["code_name"]), axis=1)]
    stage("2/4", f"过滤后剩余 {C.GREEN}{len(stock_df)}{C.RESET} 只")

    cutoff = datetime.now() - timedelta(days=MIN_LIST_DAYS)
    # 候选:历史最低价较低的股票(不要求今天创新低,只要求历史低位)
    candidates = []
    total = len(stock_df)

    print(f"\n{C.GRAY}Baostock 历史扫描中(共 {total} 只),找出接近历史低位的候选...{C.RESET}\n")

    for pos, (_, row) in enumerate(stock_df.iterrows(), start=1):
        code_bs = row["code"]
        pure = code_bs.split(".")[-1]

        listing = get_listing_date(code_bs)
        if listing is None or listing > cutoff:
            progress_bar(pos, total, prefix="历史扫描", suffix=f"候选 {len(candidates)}")
            time.sleep(HIST_DELAY)
            continue

        hist = get_hist_low(code_bs)
        if hist is None:
            progress_bar(pos, total, prefix="历史扫描", suffix=f"候选 {len(candidates)}")
            time.sleep(HIST_DELAY)
            continue

        candidates.append({
            "pure": pure,
            "name": row["code_name"],
            "listing": listing.strftime("%Y-%m-%d"),
            "hist_min": hist["hist_min"],
        })

        progress_bar(pos, total, prefix="历史扫描", suffix=f"候选 {len(candidates)}")
        time.sleep(HIST_DELAY)

    print()
    bs.logout()
    stage("2/4", f"历史扫描完成,候选 {C.GREEN}{len(candidates)}{C.RESET} 只", C.GREEN)

    if not candidates:
        print(f"{C.YELLOW}无候选,退出{C.RESET}")
        return

    # ---------- 第 2 步:新浪批量请求候选股的实时最低价 ----------
    stage("3/4", f"新浪财经批量查询实时行情({len(candidates)} 只,分批 {SINA_BATCH_SIZE} 只/次)...")
    realtime = {}
    for i in range(0, len(candidates), SINA_BATCH_SIZE):
        batch = candidates[i:i + SINA_BATCH_SIZE]
        codes = [c["pure"] for c in batch]
        data = fetch_sina_batch(codes)
        if data:
            realtime.update(data)
        progress_bar(min(i + SINA_BATCH_SIZE, len(candidates)), len(candidates),
                     prefix="新浪查询", suffix=f"已获取 {len(realtime)}")
        time.sleep(SINA_DELAY)

    print()
    stage("3/4", f"新浪实时数据 {C.GREEN}{len(realtime)}{C.RESET} 条", C.GREEN)

    # ---------- 第 3 步:交叉比对 ----------
    stage("4/4", "交叉比对 + 基本面验证...")
    hits = []
    for c in candidates:
        rt = realtime.get(c["pure"])
        if not rt:
            continue
        # 今日实时最低 <= 历史前低
        if rt["today_low"] <= c["hist_min"] + 1e-4:
            print(f"  {C.YELLOW}🔍{C.RESET} {c['name']}({c['pure']}) "
                  f"今日低 {C.RED}{rt['today_low']}{C.RESET} <= 历史前低 {c['hist_min']}")
            # 基本面验证(Baostock)
            bs.login()
            fund = check_fundamental(f"sh.{c['pure']}" if c["pure"].startswith("6") else f"sz.{c['pure']}")
            if fund:
                hits.append({
                    "代码": c["pure"],
                    "名称": rt["name"] or c["name"],
                    "上市日期": c["listing"],
                    "今日最低": round(rt["today_low"], 2),
                    "历史前低": c["hist_min"],
                    "现价": rt["current"],
                    "昨收": rt["yesterday_close"],
                })
                print(f"     {C.GREEN}✓ 通过{C.RESET}")
            else:
                print(f"     {C.RED}✗ 基本面未通过{C.RESET}")

    elapsed = time.time() - started

    # ---------- 输出 ----------
    if hits:
        print(f"\n{C.BOLD}{C.GREEN}🎯 命中 {len(hits)} 只{C.RESET}")
        print(f"{C.GRAY}{'─'*76}{C.RESET}")
        print(f"{C.BOLD}  {'#':<3}{'代码':<8}{'名称':<10}{'上市日期':<12}{'今日低':<9}{'历史前低':<9}{'现价'}{C.RESET}")
        print(f"{C.GRAY}{'─'*76}{C.RESET}")
        for i, h in enumerate(hits, 1):
            print(f"  {C.MAGENTA}{i:>2}{C.RESET}  {C.CYAN}{h['代码']}{C.RESET}  "
                  f"{h['名称']:<8}  {C.DIM}{h['上市日期']}{C.RESET}  "
                  f"{C.RED}{C.BOLD}{h['今日最低']:<7}{C.RESET}  "
                  f"{C.DIM}{h['历史前低']:<8}{C.RESET}  {h['现价']}")
        df = pd.DataFrame(hits)
        df.to_csv("newlow_sina.csv", index=False, encoding="utf-8-sig")
        print(f"\n  💾 已保存 {C.GREEN}{C.BOLD}newlow_sina.csv{C.RESET}")
    else:
        print(f"\n{C.YELLOW}本次未筛出满足全部条件的股票{C.RESET}")

    print(f"\n  {C.DIM}耗时 {elapsed/60:.1f} 分钟{C.RESET}\n")


if __name__ == "__main__":
    main()

3.执行验证

复制代码
chmod + x bottom_fishing.py
./bottom_fishing.py

4.效果图

截图是这种跑的脚本,耗时比较长

相关推荐
深蓝电商API2 天前
用GPT-4o解析无结构网页:提示词工程实战
爬虫·gpt-4o
数据狐(Datafox)3 天前
一文读懂京东商品详情 API:返回字段解读与 JSON 样例
开发语言·数据库·爬虫·json
傻啦嘿哟3 天前
百度图片爬虫:爬取海量图片数据,搭建图像识别训练集
爬虫·百度
SEO_juper3 天前
AI 爬虫 robots.txt 检查工具:你的站是不是把 AI 挡在门外了
开发语言·人工智能·爬虫·python·外贸独立站
OnlineProxy3 天前
Python 高负载异步爬虫优化指南:深挖 httpx 与 aiohttp 的性能极限
爬虫·python·httpx
SEO_juper4 天前
hreflang 审计工具:外贸多语言站谷歌SEO的隐性杀手
开发语言·人工智能·爬虫·python·seo
深蓝电商API4 天前
Firecrawl、ScrapeGraphAI:AI爬虫框架横评
人工智能·爬虫·firecrawl·scrapegraphai
小花皮猪4 天前
从 Prompt 到结构化 JSON:如何利用 Bright Data 快速搭建 YouTube 监测工作流?
人工智能·爬虫·agent·数据采集
云老大-阿里云国际站代理商4 天前
网站遇到恶意爬虫和CC攻击,阿里云国际站WAF与Bot管理怎么搭配
爬虫·阿里云·云计算