资讯动态

PHP股票采集整站源码解析:从数据抓取到定时更新

发布时间:2026/9/10 17:03:49 来源:尧图企业网站定制
简介一份基于PHP的股票网站整站源码带采集功能面向PHP初学者、全栈开发者以及希望快速搭建股票数据展示与采集系统的技术人群可学习从后端逻辑、数据库设计到前端可视化的完整实现路径也适用于课程设计与毕业设计参考。压缩包共2000个文件包含340个PHP后端脚本、278个htm及17个html页面大量gif/png/jpg图片用于界面素材还有js、css、xml、sql等文件整体仅7.95MB目录结构紧凑。已有274人学习或下载。源码提供了股票信息展示、行情数据采集等参考实现涉及PHP语法与安全实践、cURL抓取与DOM解析、数据库表设计与查询优化、第三方API接口调用以及ECharts等图表展示技术适合按模块拆解研究也便于二次开发为个人股票数据平台帮助提升全栈开发能力。1. 股票网站最贵的是数据不是页面如果有人把行情页直接存成静态 HTML那这个站第二天就废了——股票数据按秒在变。一套基于 PHP 的股票网整站真正的价值在于它自带的采集逻辑定时从公开行情源拉数据、清洗、入库再由前端展示。把基于PHP的股票网整站(带采集)源码.zip解压后通常是一套前后台齐全的 PHP 工程加独立采集脚本适合想快速搭行情站、积累历史数据、给量化分析喂数据的人。别指望解压就能跑通先花半小时把目录和数据库结构摸清楚。下文按我搭这类项目的路径把结构、采集、增量更新和部署校验讲透。2. 整站结构从入口文件开始梳理 PHP 项目的运行路径解压 zip 之后先别急着配域名。常见做法是先打开目录清单判断这套源码是原生 PHP 还是老框架。很多股票采集整站都是原生 PHP MySQL 的老架构好处是不依赖 Composer扔到 PHP 5.6 就能跑坏处是代码风格杂容易藏着不该有的后台入口。所以第一步不是看代码是看结构。2.1 先看目录布局判断采集逻辑放在哪一层如果你在本地已经准备了宝塔面板或者 phpStudy直接把压缩包上传到站点目录然后在命令行里跑一次 tree会看到一个类似这样的结构tree -L 2 stock输出可能长这样stock/ ├── index.php ├── admin/ │ ├── login.php │ └── dashboard.php ├── include/ │ ├── config.php │ ├── db.php │ └── functions.php ├── collect/ │ ├── spider.php │ ├── parser.php │ └── task.php ├── static/ │ ├── css/ │ └── js/ └── install/ └── stock.sql这个结构比较标准include放公共配置collect放采集逻辑install放数据库初始化脚本。看到admin目录说明带后台管理。用 tree 了解之后优先读include/config.php和install/stock.sql因为数据库连接方式和表结构是后面所有调试的锚点。有些源码把采集逻辑直接埋在后台admin/collect.php里目录结构会扁平一些。不用按教科书重构它只要找到采集入口在哪、能改参数就够了。2.2 行情数据表实时报价和历史 K 线要分开落库股票数据写入频繁、查询频繁但实时查询和历史查询模式完全不同。常见设计方案会拆成至少三张表股票代码表、当日/历史行情表、采集日志表。下面是 MySQL DDL 的常见情况CREATE TABLE stock_code ( code varchar(10) NOT NULL COMMENT 股票代码如 600000, name varchar(30) NOT NULL COMMENT 股票名称, market tinyint NOT NULL DEFAULT 0 COMMENT 0A股 1港股 2美股, is_active tinyint DEFAULT 1 COMMENT 是否启用, PRIMARY KEY (code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE stock_quote ( code varchar(10) NOT NULL, trade_date date NOT NULL, open decimal(10,3) DEFAULT NULL, high decimal(10,3) DEFAULT NULL, low decimal(10,3) DEFAULT NULL, close decimal(10,3) DEFAULT NULL, volume bigint DEFAULT NULL, amount decimal(16,2) DEFAULT NULL, updated_at timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (code,trade_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里把code和trade_date设为联合主键作用很关键同一个股票同一天只能有一行采集脚本重复跑也不会产生重复行情。open/high/low/close用decimal(10,3)而不是 float因为浮点数在累计计算时精度会偏移A 股价格带三位小数10 位整数位足够。volume用 bigint沪深单日成交量动辄上亿int 存不下。另外需要给trade_date建一个普通索引按时间范围查涨跌幅时会走索引避免全表扫描。如果业务只看最近一个月可以考虑按月分区这是进阶优化。三张表的核心关系可以归纳如下表名核心字段用途stock_codecode, name, market维护股票基本信息和启停状态stock_quotecode, trade_date, OHLC存储行情快照联合主键防重collect_logsource, code, last_date记录采集断点支撑增量更新表与表之间靠code关联日志表单独存在不参与前端查询。2.3 入口文件先加载配置再路由别把业务全塞进 index.php很多整站的index.php只有几十行拉配置、连数据库、根据a参数跳转页面。下面是这类写法的典型骨架?php // index.php define(APP_ROOT, __DIR__); require APP_ROOT . /include/config.php; require APP_ROOT . /include/db.php; require APP_ROOT . /include/functions.php; // 取得动作参数没传则回首页 $action isset($_GET[a]) ? $_GET[a] : home; switch ($action) { case detail: $code preg_match(/^\d{6}$/, isset($_GET[code]) ? $_GET[code] : ) ? $_GET[code] : ; include APP_ROOT . /templates/detail.php; break; default: include APP_ROOT . /templates/home.php; }入口文件的关键不是路由本身而是它强迫你先加载config.php和db.php。调试采集脚本时也建议复用同一个db.php里的getDb()方法不要另写数据库连接。这样可以避免出现“前台能连库、采集脚本报密码错误”的奇葩问题。看到这里你应该已经明白整站是壳真正的核心在collect目录。下一章进入采集模块讲抓取和解析的完整套路。3. 采集模块从抓取到解析避开正则幻觉股票采集最常见的任务是从公开财经页面抓历史日线数据。不要一上来就写正则去抠 HTML页面结构一变就全废。通用做法是分两层先做好 HTTP 请求的健壮性再做结构化 DOM 解析。3.1 用 cURL 抓取时先设定身份和超时避免“裸奔”请求第一个坑是直接用file_get_contents()去拿远端页面。这个函数默认不带 User-Agent很多行情接口会直接拒绝或者返回验证页。应该用 cURL 并显式设置请求头?php function fetchUrl($url, $timeout 10) { $ch curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER true, CURLOPT_FOLLOWLOCATION true, CURLOPT_CONNECTTIMEOUT $timeout, CURLOPT_TIMEOUT $timeout, CURLOPT_SSL_VERIFYPEER false, CURLOPT_USERAGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36, CURLOPT_REFERER https://finance.sina.com.cn/, CURLOPT_HTTPHEADER [ Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, ], ]); $body curl_exec($ch); $httpCode curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode ! 200) { throw new RuntimeException(HTTP {$httpCode}); } return $body; }参数说明CURLOPT_FOLLOWLOCATION跟随 302 跳转有些行情源会在请求时先跳一次CURLOPT_CONNECTTIMEOUT是连接超时CURLOPT_TIMEOUT是整体超时都建议设置否则源站慢时会卡死整个采集队列。CURLOPT_SSL_VERIFYPEER设为 false是因为部分源站证书链不完整本地测试时省事生产环境如果对证书有要求应该保留校验。整个函数的返回条件是 HTTP 200其余状态抛异常由上层决定是重试还是跳过。3.2 用 DOMXPath 解析表格比用正则死磕可靠得多历史行情数据通常会放在一个table里表头是日期、开盘、最高、最低、收盘、成交量后面跟着几十行。用 DOMDocument 把 HTML 装进去再用 XPath 查询 tr/td代码更接近人的表格式读法?php function parseTable($html) { $doc new DOMDocument(); // 忽略 HTML 不规范导致的警告 libxml_use_internal_errors(true); $doc-loadHTML(?xml encodingUTF-8 . $html); libxml_clear_errors(); $xpath new DOMXPath($doc); // 选取 class 包含 table 的表格里的所有 tr $rows $xpath-query(//table[contains(class,table)]//tr); $data []; foreach ($rows as $row) { $cells $row-getElementsByTagName(td); if ($cells-length 6) { continue; } $data[] [ date trim($cells-item(0)-textContent), open trim($cells-item(1)-textContent), high trim($cells-item(2)-textContent), low trim($cells-item(3)-textContent), close trim($cells-item(4)-textContent), volume trim($cells-item(5)-textContent), ]; } return $data; }为什么加?xml encodingUTF-8这是为了让 DOMDocument 按 UTF-8 解析否则中文股票名和表头会乱码。libxml_use_internal_errors(true)的作用是压制不规范 HTML 在 php 命令行下刷 Notice。用contains(class,table)而不是精确匹配classtable是因为页面上的 class 经常带前缀后缀精确匹配反而漏行。每个字段都trim去掉\n、\t和空格。解析出来的$data是二维数组但别急着入库还需要清洗和去重。清洗逻辑放下一章。3.3 遭遇 412 响应时按“身份-频率-载荷”三步排查采集时最常遇到的非 200 状态码是 412 Precondition Failed。网上很多结论说换 IP但大多数场景并不是 IP 被封而是请求头不完整或者频率太高。这就是常见的防采集拦截逻辑。我一般这样处理第一步检查请求头。先把CURLOPT_HTTPHEADER补成和浏览器一致的集合至少带上Accept-Language和Referer。不少反爬逻辑发现缺了某个头直接返回 412。第二步检查请求频率。连续两次抓取之间至少隔 1 到 2 秒用下面的 sleep 控制usleep(1200000); // 间隔 1.2 秒usleep比sleep灵活单位是微秒1200000微秒就是 1.2 秒。抓几百只股票时这个间隔不会太慢但能明显降低触发限频的概率。第三步检查返回内容。有时候 HTTP 200 但页面里全是验证脚本这种“软封禁”比 412 更隐蔽。抓完页面后做一次strpos($body, 验证码)检查命中就停掉当前批次并写错误日志。这三步的优先级和常见处理如下步骤检查内容常用处理1请求头完整性补 User-Agent、Referer、Accept-Language2请求频率两次请求间隔 1~2 秒3返回内容载荷用strpos检查验证码关键字并告警把这三步做成一个函数包在fetchUrl外面采集脚本的鲁棒性会明显提升。4. 数据清洗与增量更新先采集再清洗避免重复数据采集只是第一步数据治理要先采集再清洗。把抓下来的字符串直接存库后面做图表时会遇到格式错乱、重复行、空值等一堆问题。4.1 用联合主键和 ON DUPLICATE KEY UPDATE 实现幂等写入上一章建表时用了PRIMARY KEY (code, trade_date)现在写入库语句正好利用它做幂等。MySQL 遇到主键冲突时不会报错而是执行后面的更新?php function insertQuote(PDO $db, array $row) { $sql INSERT INTO stock_quote (code, trade_date, open, high, low, close, volume) VALUES (:code, :date, :open, :high, :low, :close, :volume) ON DUPLICATE KEY UPDATE open VALUES(open), high VALUES(high), low VALUES(low), close VALUES(close), volume VALUES(volume), updated_at CURRENT_TIMESTAMP; $stmt $db-prepare($sql); $stmt-execute([ :code $row[code], :date $row[date], :open $row[open], :high $row[high], :low $row[low], :close $row[close], :volume $row[volume], ]); }ON DUPLICATE KEY UPDATE是这套方案的核心第一次采集写入第二次跑到同一天时只更新数值不会增加新记录。这样配合定时任务天然支持增量更新。你不必先查一次“今天的数据在不在”直接写就行。注意MySQL 8.0 中将VALUES()函数标记为 deprecated但依然可用如果使用 MariaDB 10.3 也支持。真正要关注的是updated_at它会在每次冲突时变化可以用它判断某一天的真实刷新时间。全量采集和增量采集的取舍也反映在日志和任务写法上采集方式请求量重复概率适用场景全量大高首次初始化或重建数据增量按日期小低每日定时任务增量拉取首次部署时可以先全量跑一次之后全部切增量。4.2 清洗流程去分隔符、规范化数字、校验日期从页面解析出的字符串千奇百怪常见情况有数字带逗号1,234.50、日期是2024/1/5、成交量带“万手”“亿”等单位。清洗函数要把这些变成统一格式?php function cleanNumber($value) { $value trim($value); // 去掉逗号、全角逗号和空格 $value str_replace([,, , ], , $value); if (strpos($value, 万) ! false) { return round(floatval($value) * 10000, 2); } if (strpos($value, 亿) ! false) { return round(floatval($value) * 100000000, 2); } return floatval($value); } function cleanDate($value) { $value trim($value); $value str_replace(/, -, $value); $d date_parse($value); return sprintf(%04d-%02d-%02d, $d[year], $d[month], $d[day]); }清洗的原则宁可函数啰嗦也不要放过异常格式。调用时在入库前执行$row[open] cleanNumber($cell[1]); $row[high] cleanNumber($cell[2]); $row[low] cleanNumber($cell[3]); $row[close] cleanNumber($cell[4]); $row[volume] cleanNumber($cell[5]); $row[date] cleanDate($cell[0]);2024/1/5 15:00会先被str_replace换成2024-1-5 15:00再由date_parse拆出日期。如果页面给的是空字符串floatval()会变成 0这不是好行为但至少不会让数据库报错。更严格的写法是在清洗函数里判断$value 时返回 null让上游知道这行数据不完整。4.3 用采集日志实现断点续采和失败重试几十只股票采到一半挂了下次再跑不能从头开始。常见的做法是用一张日志表记录每个代码最近一次成功的日期断点续采只需要读这张表CREATE TABLE collect_log ( id int AUTO_INCREMENT PRIMARY KEY, source varchar(20) NOT NULL DEFAULT sina, code varchar(10) NOT NULL, last_date date DEFAULT NULL, status tinyint NOT NULL DEFAULT 0, message varchar(255) DEFAULT NULL, created_at timestamp DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_source_code (source, code) );采集开始之前先查一次日志$query SELECT code, last_date FROM collect_log WHERE source sina;拿到last_date后请求历史数据时只请求该日期之后的数据这就是增量更新。如果某次解析抛异常在catch里更新status1和message下次跑的时候把失败代码单独拎出来重试不用重跑全部批次。status字段的状态机可以这样约定status含义后续动作0采集成功无需处理1解析失败下次重试2连续失败停止并人工检查状态为 2 的情况一般在连续失败超过三次时由脚本写入防止对一个死源无限重试。5. 定时任务与 Docker 镜像把采集脚本跑成自动化服务采集脚本写好后需要放置到服务器上。很多源码包里只给一个spider.php定时任务需要自己配。这里讲两个层面的部署crontab 和 Docker 打包最后加一个数据校验技巧。5.1 crontab 里加 flock防止上一次任务没跑完采集任务最怕的不是跑不完而是定时任务重叠。上一次跑了 10 分钟cron 每 5 分钟触发一次的话第二个进程会和第一个同时写库。用 flock 锁文件*/5 * * * * /usr/bin/flock -xn /tmp/stock_collect.lock -c cd /var/www/stock php collect/spider.php /dev/null 21flock -xn的意思是非阻塞获取独占锁拿不到锁就直接退出本次执行。-c后面是实际命令行。这样能保证任何时刻最多只有一个采集进程在跑。如果连续丢任务优先检查锁文件是否残留必要时手动删除后重跑。5.2 用 Docker 打包采集镜像绕过本机 PHP 环境差异本机 PHP 7.0、服务器 PHP 8.1 时采集脚本里的语法差异可能直接致命。常见做法是把采集逻辑单独打成镜像FROM php:8.1-cli WORKDIR /app RUN docker-php-ext-install pdo_mysql COPY collect/ collect/ COPY include/ include/ CMD [php, collect/spider.php]镜像里只需要 CLI 版本不需要 Apache/FPM。构建和运行docker build -t stock-collector . docker run --rm --network host stock-collector--network host是为了连接宿主机上的 MySQL。日志可以挂目录出来docker run --rm --network host -v /var/log/stock:/app/log stock-collectordocker-php-ext-install pdo_mysql是官方镜像提供扩展安装命令比手动编译省事。如果采集脚本还依赖 Redis再加装 redis 扩展。5.3 用一条 SQL 校验今日采集是否完整部署完先别跑先把校验手段准备好。采集完成后执行这条 SQL能立刻看出今天有多少只启用的股票还没有当日行情SELECT COUNT(*) AS missing_count FROM stock_code c LEFT JOIN stock_quote q ON q.code c.code AND q.trade_date CURDATE() WHERE c.is_active 1 AND q.code IS NULL;如果missing_count不为 0说明有股票没采到。把这句 SQL 放进check.php在定时任务的最后一步运行发现缺数时向标准错误输出missing 3之类的信息$missing $db-query($sql)-fetchColumn(); if ($missing 0) { fwrite(STDERR, missing . $missing . \n); }配合 cron 的邮件或日志监控就能及时抓到异常。这个校验脚本建议固化到采集流程最后一步。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价