
简介“批量获取网站标题1.3”是一款面向开发者与网络数据分析人员的轻量级抓取工具核心用途是批量采集网站标题同时支持域名、IP和端口识别并能在网页多次跳转时自动跟随重定向减少人工逐个访问的繁琐操作。工具底层依赖TCP/IP协议通信与HTTP状态码解析从TCP三次握手建立连接到识别301/302重定向并更新目标地址完整呈现了网络爬虫的关键环节适合做站点巡检、SEO数据整理或爬虫入门学习的读者。资源包共13个文件压缩后仅1.6MB包括GetWebTitle.exe主程序、NPOI系列dll、SmoothProgressBar.dll进度条控件、config配置文件与示例图片附带的PDB调试文件也有助于理解程序结构整体简洁便于运行和二次开发。目前已有292人学习下载。借助该工具读者既能理解HTTP请求与响应解析、重定向处理等原理也能通过NPOI将抓取结果批量导出为Excel表格再结合进度条控件的界面反馈可以完整掌握从数据采集到结果导出的实用技能对实际开展网络数据采集和桌面工具开发有直接帮助。1. 批量获取网站标题1.3 到底解决什么问题做站点巡检、外链普查、改版核对的时候「这批域名现在还没有 title、title 是不是还挂在旧站上」往往比看首页截图更快暴露问题。批量获取网站标题这类工具核心就是给一批 URL 各发一次请求把title抽出来顺带告诉你哪些超时、哪些跳转、哪些服务器根本没回。1.3 这个版本号在同类小工具里通常意味着已经把编码、超时、重试这些兜底逻辑补全了而不是单纯加了个并发开关。适合运维、SEO 和对站群做体检的数据工程师把它接进自己的巡检脚本里按需改参数。2. 批量获取网站标题的解析原理从 HTML 到 title 的两条路径2.1title和 og:title哪个才是页面真正的标题浏览器地址栏显示的是head里的title社交平台分享卡用的却是meta propertyog:title。多数页面两者一致但并不保证。真正做批量获取网站标题时我一般把优先级定为标准title有内容就用它没有或为空再回退 og:title两者都没有才记为缺失。这样得到的字段不会因为某个 CMS 模板只写了分享卡标题而整行空缺。举个例子很多 WordPress 主题在 SEO 插件未启用时文章页的title被压成一行正文里的评论数等变量会把标题撑散而 og:title 反而是完整标题。反过来某些企业站只在title里写「首页」og:title 却写了品牌全称。到底信谁取决于业务口径做收录核对信title做分享文案核查信 og:title。脚本里把两个值都取出来让下游自己选口径比在采集端硬定规则更省事。2.2 三种解析器选型正则、lxml、BeautifulSoup标题提取看起来是「找两个标签」但现实页面的 HTML 质量参差不齐。常见做法是三个方案里选一个正则title(.*?)/title最快但遇到 title 标签里嵌套其他标签、属性带换行就会翻车只适合快速验证。lxml 的etree.HTMLParserlibxml2 的容错解析能力最强能把浏览器模式下不闭合的标签自动补全速度仅次于纯正则。BeautifulSoup html.parserAPI 友好但解析同一份文档通常比 lxml 慢一个数量级批量过万 URL 时差距非常明显。我的选择是 lxml理由只有一个解析坏 HTML 的恢复能力决定了整批任务的失败率。下面这段是最小提取函数1.3 这类版本常见做法是把解析和请求拆开方便单独加断言from lxml import etree def extract_title(html_text: str) - tuple[str, str]: 从 HTML 文本中提取标题返回 (标题内容, 来源标签) parser etree.HTMLParser(recoverTrue) tree etree.fromstring(html_text, parserparser) if tree is None: return , empty for node in tree.xpath(//title): text .join(node.itertext()).strip() if text: return text, title for node in tree.xpath(//meta[propertyog:title]): text (node.get(content) or ).strip() if text: return text, og:title return , missing这段代码有两个细节值得注意一是recoverTrue让解析器遇到标签错乱时尽量跳过而不是抛异常二是node.itertext()把 title 内部可能存在的 em、strong 等子标签文本也拼接进来避免「标题被拆成两段」的假缺失。xpath 里同时限定 property 属性是为了避开nametwitter:title这类重复字段。2.3 提取层最容易翻车的 7 个边界情况下方表格是我排查时固定对照的清单每一条都对应线上真实出现过的返回结果情况现象处理口径title为空但标签存在返回空字符串回退 og:title仍为空记 missingtitle 里套了script或style提取到一段 JS 代码过滤 script/style 子节点后再拼接title出现在body里浏览器会忽略它正则却抓得到只信任 head 内节点或用 xpath 限定位置页面是 gzip 响应但未解压解析出乱码甚至空标题在请求层强制解压见 3.3title 文本里含右尖括号正则提前截断用解析器取 text不用正则整页是 JS 渲染HTML 里没有 title永远 missing标记为 js-rendered交给无头浏览器抽查响应是 404/302 错误页抓到「404 Not Found」先按状态码分流错误页标题不写进结果第 4 行和第 7 行其实是请求层问题但排查时最容易误判成解析 bug。我的做法是先把状态码和最终 URL 记下来再把标题字段填成原始文本最后统一清洗。这样批量获取网站标题的结果数据里「无标题」和「错误页标题」可区分而不是混成一堆空值。3. 用 Python 写最小可用的批量获取网站标题脚本3.1 请求头与超时参数先定规矩再写循环批量场景和浏览器访问最大的差别是没有 cookie、没有 JS、没有用户手工等待。所以请求层要把默认行为定死。我常用的请求头只有四个字段多了反而容易被一些 WAF 识别成脚本特征HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.8,en;q0.5, }请求超时用元组分别指定连接和读取timeout(3, 8)表示 TCP 连不上 3 秒就放弃首字节 8 秒等不到就放弃。单设一个timeout8的问题是某些路由黑洞会让 connect 阶段挂满 8 秒再进入读取阶段单个 URL 总耗时翻倍。allow_redirects保留默认 True但要把session.max_redirects调小避免遇到循环跳转的站点在单个 URL 上空转几十跳。3.2 单线程版本先把正确性跑通批量获取网站标题的最小脚本我习惯先写成单线程、带状态码分流、失败不中断的形态。只有这个版本跑出来的结果和人工抽查一致才值得上并发import csv import requests HEADERS {User-Agent: Mozilla/5.0 ...} TIMEOUT (3, 8) def fetch_title(url: str, session: requests.Session | None None) - dict: 抓取单个 URL 的标题传入 session 时复用连接否则自建并关闭 result {url: url, status: 0, final_url: url, title: , source: , error: } close_session session is None if close_session: session requests.Session() try: with session.get(url, headersHEADERS, timeoutTIMEOUT, allow_redirectsTrue, streamTrue) as resp: resp.raw.read(1024 * 512) # 只读前 512KB防止大文件拖垮任务 result[status] resp.status_code result[final_url] resp.url if resp.status_code ! 200: result[error] fhttp_{resp.status_code} return result if text/html not in resp.headers.get(Content-Type, ): result[error] not_html return result result[title], result[source] extract_title(resp.text) except requests.Timeout: result[error] timeout except requests.RequestException as exc: result[error] fnetwork:{type(exc).__name__} finally: if close_session: session.close() return result这里的streamTrue配合手动读取前 512KB 是关键写法不设 stream 时 requests 会把整个响应体下载完才返回遇到一个 200MB 的安装包会让单线程脚本卡到超时边界。读完即弃连接由 with 块自动归还。状态码非 200 时直接返回不解析页面避免错误页的 title 污染数据。调用方用循环收集结果即可每个 URL 独立写成一行 CSV即使中间某个请求异常前面的结果也已经落盘with open(titles.csv, w, newline, encodingutf-8-sig) as fp: writer csv.DictWriter(fp, fieldnames[url, status, final_url, title, source, error]) writer.writeheader() for url in url_list: writer.writerow(fetch_title(url))3.3 编码乱码排在所有问题第一位的坑requests 的resp.text默认按响应头里的 charset 解码但大量老站不返回 charset或返回过期的 GB2312。此时 requests 会回退到 ISO-8859-1中文全部变乱码。解决顺序是优先用响应头的 charset没有则用apparent_encoding探测探测结果是 GB2312 时统一按 GBK 解码因为 GB2312 是 GBK 的子集按 GBK 解不会引入额外错误。if resp.encoding is None or resp.encoding.lower() in (iso-8859-1, ascii): detected resp.apparent_encoding or utf-8 if detected.upper() in (GB2312, GBK): detected gbk resp.encoding detected提示apparent_encoding对短文本和纯英文页面经常误判所以只有响应头没给 charset 或给了错误兜底值时才允许它接管。抓回来的 title 如果还有零星乱码大概率是页面本身把 GBK 字节写进了 utf-8 声明的文档里那种只能整站统一转码单请求层面救不回来。4. 并发提速与限速批量获取网站标题的吞吐量调优4.1 线程池还是协程先看目标站点数量级批量获取网站标题的耗时瓶颈绝大部分在网络 IO不在 CPU。两个主流方案concurrent.futures.ThreadPoolExecutor requests或者 httpx 的 AsyncClient asyncio。两者在千级 URL 场景差距不大差别主要在工程层面对比项ThreadPoolExecutorhttpx 异步代码改动量循环换成 map几乎不动整个请求链都要 async调试门槛报错堆栈清晰协程报错需要额外定位会话复用每线程一个 Session全局一个 AsyncClient上万 URL 吞吐够用更高但限速逻辑更难写我的习惯是五千 URL 以内用线程池代码可读性优先上两万再考虑 httpx 异步加信号量限流。不要一上来就异步异步版本一旦某个第三方站点长时间不响应排查成本比省下的那几分钟高得多。4.2 ThreadPoolExecutor 的并发抓取模板并发版本的核心改动有两点每个线程持有一个独立的 requests.Session 以复用连接结果收集放主线程避免多线程同时写 CSV。下面是一个可以直接替换单循环的模板import threading from concurrent.futures import ThreadPoolExecutor, as_completed _local threading.local() def get_session() - requests.Session: 返回当前线程自己的 Session保证连接复用且线程隔离 if not hasattr(_local, sess): _local.sess requests.Session() _local.sess.headers.update(HEADERS) return _local.sess def worker(url: str) - dict: session get_session() # 此处在 worker 线程内执行拿到的是本线程实例 return fetch_title(url, sessionsession) def run_batch(url_list: list[str], max_workers: int 8) - list[dict]: results [] with ThreadPoolExecutor(max_workersmax_workers) as pool: future_map {pool.submit(worker, url): url for url in url_list} for future in as_completed(future_map): results.append(future.result()) return results注意get_session()是在worker()内部被调用的而不是在主线程里先建好再传给线程。ThreadPoolExecutor 的 worker 线程是池内固定的所以每个执行过任务的线程只会创建一个 Session同站多次请求可以复用 TCP 连接。as_completed保证哪个先完成先收集不会因为列表靠后的慢 URL 阻塞前面结果的处理。4.3 三个必调参数max_workers、delay、重试退避并发不是越大越好。目标站点是普通企业站时常见的合理取值如下参数建议值调整依据max_workers8~16站点返回 429/503 就调小到 4同域名最小间隔0.1~0.5 秒被限流就翻倍单请求超时连接 3s 读取 8s移动端弱网环境放宽到 15s重试次数2~3 次超过 3 次收益极低重试退避0.5s 起每次乘 2加随机抖动避免同步重试重试只对网络层错误和 5xx 做4xx 一律不重试。常见做法是超时重试、连接错误重试、429/503 重试404/403 直接标记失败。重试时对整批任务加锁保护请求时间戳能让限速不随并发数失效_last_request {} _lock threading.Lock() def rate_limited_get(session, url, min_interval0.2): with _lock: now time.monotonic() netloc url.split(/)[2] gap now - _last_request.get(netloc, 0) if gap min_interval: time.sleep(min_interval - gap) _last_request[netloc] time.monotonic() return session.get(url, timeoutTIMEOUT)注意4xx 一律不重试。403 通常是请求头问题重试只会放大被封风险404 是目标自身问题重试浪费连接。只有超时、连接错误、429 和 503 才值得做指数退避重试。按域名 netloc 做粒度限速而不是全局限速是批量抓取多站点列表时的正确姿势A 站的 1000 个 URL 不会因为 B 站慢而整体降速但同一站内部不会瞬间打满并发。sleep放在锁内是为了防止多个线程同时睡醒后在同一毫秒发出请求把限速变成摆设。5. 抓完只是开始批量获取网站标题的结果校验与交付技巧5.1 落盘格式里值得追加的三个字段CSV 里除了 url、title建议至少再存final_url、status、source三个字段。final_url能反查跳转链是否指向已售域名或竞争对手页面source区分标题来自title还是 og:titlestatus用来在 Excel 里直接筛选非 200 的异常行。编码用utf-8-sig而不是utf-8这样用 Excel 打开不会出现列头乱码。5.2 用分布表和抽样复核验收结果跑完两万条之后先看三张分布状态码分布、source 分布、标题长度分布。标题长度在 50 字符左右的密集柱通常是被模板截断的标题source 里 og:title 占比超过 20%说明很多站点主标题字段为空要回头检查解析逻辑是否漏了某些 head 结构。最后随机抽 20 条手工打开比对重点看 title 来源是 og:title 和 missing 的记录这两类最可能是解析误判而不是站点真的没写。5.3 一个可复用的标题相关性探针技巧最后一个技巧把抓回来的标题和已知锚文本对比不增加任何额外请求。对外链普查场景站点 B 首页标题里通常含品牌词如果标题连外链锚文本里的核心词都不包含这条记录就需要标记为「疑似关联度低」。具体做法是在写 CSV 前加一个本地计算函数def mark_unrelated(row: dict, anchor: str) - dict: 标题不含锚文本核心词时打标记供人工复核 if anchor.lower() not in row[title].lower(): row[flag] title_unrelated else: row[flag] ok return row这个函数纯本地做子串匹配批量两万行也就几十毫秒却能让下游从两万行标题里直接挑出需要人工复核的几百行。配合前两节的状态码、来源分布批量获取网站标题1.3 这类工具就从「数据采集」迈过了「数据体检」的坎输出结果可以直接作为巡检报告交付。本文还有配套的精品资源点击获取