夜雨聆风学习资料网

ARTICLE · 1340963

出口收入汇率提取自动化(附源码)

出口收入汇率提取自动化(附源码)

1. 项目背景

依据增值税法第十八条规定,纳税人以人民币以外的货币结算销售额的,应当折合成人民币计算。依据增值税法实施条例第十七条规定,折合率可以选择「销售额发生的当日或者当月 1 日有效的人民币汇率中间价」。纳税人确定折合率后,12 个月内不得变更。

根据该增值税新规,我司选择以「当月 1 日有效的人民币汇率中间价」作为折合率,本脚本用于自动获取当月 1 日有效的人民币汇率中间价(1 美元对人民币),自动获取每月适用的汇率,用于当月出货 / 开票折算人民币金额,并静默打印公告页存成 PDF 凭证。

「当月 1 日有效」在本脚本中的落地口径

  1. 1. 当月 1 日央行公布了中间价(1 日即当月第一个工作日)→ 直接取当月 1 日的汇率。
  2. 2. 当月 1 日没有公布中间价(当月第一个工作日不是 1 日,例如 1 日是周末或节假日)→ 1 日有效的是最近一次公布的中间价,即上月最后一个工作日的汇率。
    • • 例:11 月第一个工作日为 11 月 2 日,则采用 10 月 30 日(10 月最后一个工作日)的汇率作为 11 月出货汇率。

说明:工作日以央行官网公告列表中实际存在的日期为准,不用日历或节假日表推算。原因:央行只在外汇市场交易日发布中间价公告,公告列表本身就是最可靠的工作日清单。

等价简化(代码实际用的判定)

判断「当月第一个工作日是不是 1 日」,等价于判断「当月 1 日有没有公告」:

  • • 1 日有公告 → 取 1 日;
  • • 1 日无公告 → 当月第一个工作日必然晚于 1 日 → 取上月最后一个工作日(即所有早于 1 日的公告日期中最大的那个)。

数据源

中国人民银行货币政策司 → 人民币汇率 → 人民币汇率中间价公告:https://camlmac.pbc.gov.cn/zhengcehuobisi/125207/125217/125925/index.html

公告正文如:

中国人民银行授权中国外汇交易中心公布,2026年10月30日银行间外汇市场人民币汇率中间价为1美元对人民币6.7138元,……

脚本只提取其中「1美元对人民币…元」的数值,保留官网原始精度,不做四舍五入,想提取其他币种,只需要修改下代码中匹配规则即可。

实测样例

目标月份
判定
采用公告日期
汇率
2026-11
11 月 1 日无公告 → 取上月最后一个工作日
2026-10-30
6.7138
2026-06
6 月 1 日有公告 → 取当月 1 日
2026-06-01
6.8167

2. 交付物与运行效果

汇率/├── pbc_rate.py         主脚本└── README.md           本说明

PDF 凭证输出到 print_to_pdf() 中指定的目录(见 5.1),文件名如:2026-11_2026-10-30_USD6.7138.pdf

  • • stdout(标准输出)只有汇率数值,例如 6.7138,方便被其他程序、管道或 > 重定向直接读取。
  • • stderr(标准错误) 输出提示信息,不污染 stdout:
[浏览器] Microsoft Edge[依据] 当月 1 日无公告(第一个工作日非 1 日)→ 取上月最后一个工作日:2026-10-30[公告] https://camlmac.pbc.gov.cn/.../2026103008584376327/index.html[PDF ] C:\...\汇率凭证\2026-11_2026-10-30_USD6.7138.pdf
  • • 取不到目标公告 / 提取不到汇率时,stderr 明确报错、退出码 1,绝不猜测数值。

参数表

参数
说明
month
(位置参数)
目标月份 YYYY-MM,省略则取当前月份
-b
 / --browser
edge
(默认)/ chrome,选择用哪个浏览器
--no-pdf
只返回汇率,不生成 PDF
-h
显示帮助

3. 快速开始

python pbc_rate.py                          # 当前月份,默认 Edge,生成 PDFpython pbc_rate.py 2026-11                  # 指定月份python pbc_rate.py 2026-11 -b chrome        # 用 Chromepython pbc_rate.py 2026-11 --browser edge   # -b 的长写法同样可用python pbc_rate.py 2026-11 --no-pdf         # 只要汇率python pbc_rate.py 2026-11 > rate.txt       # rate.txt 里只有 6.7138

预期 stdout:

6.7138

3.1 直接运行就是当月

命令行不带任何参数、或在 IDLE 里直接按 F5(Run Module),取的就是当月——不需要手动填月份,也不需要改代码。

决定月份的是 main() 里这一行:

year, month = parse_month(args.month) if args.month else (dt.date.today().year, dt.date.today().month)

不传 month 参数时走 else 分支,值来自 datetime.date.today(),也就是电脑系统日期的年月,不是网站日期、也不是交易日。所以只要电脑系统日期正确,月份就正确;系统日期不准会取错月,这是唯一要注意的前提。

3.2 如何设置别的月份

方式 A:不改代码,传参数(推荐)

python pbc_rate.py 2026-10

IDLE 里传参有两种办法:

  • • 部分 IDLE 版本的 Run → Run Customized… 可以填命令行参数,填 2026-10 即可(也可以填 -b chrome --no-pdf)。

方式 B:改代码,固定默认月份

如果希望「什么都不传」时固定取某个月,改 main() 里的两处:

ap.add_argument("month", nargs="?", default="2026-11", help="目标月份 YYYY-MM")...year, month = parse_month(args.month)

这样不带参数固定取 2026-11,同时仍可用 python pbc_rate.py 2026-10 临时覆盖,比写死更灵活。

如果要把月份彻底锁死、连参数都不接受,把那一行直接换成:

year, month = (2026, 11)

注意:这样位置参数 month 会被忽略,且以后每个月都要手动改代码,一般不建议。

同类的一行改法:把默认浏览器改成 Chrome 见 6.2。

4. 运行需要装什么

按下面清单逐项确认即可。只需把 pbc_rate.py 拷过去;PDF 输出目录在首次运行时自动创建,具体路径写在 print_to_pdf() 里,换电脑需按 5.1 改成该机实际路径。

序号
项目
要求 / 命令
说明
1
Python
≥ 3.10
(本机实测 3.13.14)
python --version
 确认
2
Playwright
python -m pip install playwright
本机版本 1.65.0,会自动带 greenlet 3.5.6
3
浏览器
本机已装 Microsoft Edge 或 Google Chrome
二选一即可;默认用 Edge
4
浏览器内核不需要playwright install
脚本用 channel 调用本机浏览器,不下载 Chromium

安装后验证:

python -c "import playwright; print('ok')"python pbc_rate.py --no-pdf

第二条能打印出 6.7138 就说明环境就绪(月份按运行时当前月变化)。

说明:PDF 输出目录由 print_to_pdf() 中的绝对路径决定,与从哪个目录启动脚本无关;换电脑后请按 5.1 改成该机实际路径。

5. 自定义需要修改哪些地方(含改法)

5.1 PDF 保存路径(最常改)

在 print_to_pdf() 中(约 203–206 行):

out_dir = Path(r"路径")  # 目录out_dir.mkdir(exist_ok=True)out_path = out_dir / f"{year}-{month:02d}_{target.isoformat()}_USD{rate}.pdf"# 文件名out_path.write_bytes(base64.b64decode(result["data"]))     # 真正落盘
  • • 改目录:把 out_dir 那行的绝对路径换成本机实际目录,例如
out_dir = Path(r"D:\财务\汇率")

注意 mkdir(exist_ok=True) 只创建最后一级,父目录必须已存在;需要连父目录一起建时改成 mkdir(parents=True, exist_ok=True)。

  • • 改文件名:改 out_path 的 f-string,例如去掉汇率:
out_path = out_dir / f"{year}-{month:02d}_{target.isoformat()}.pdf"
  • • 加 --no-pdf 会跳过整个函数,不产生文件。

5.2 浏览器安装路径(仅非标准安装才需要改)

常量区(约 50–55 行):

EDGE_FALLBACK = r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"CHROME_FALLBACKS = [r"C:\Program Files\Google\Chrome\Application\chrome.exe",r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",str(Path.home() / "AppData/Local/Google/Chrome/Application/chrome.exe"),]

脚本优先用 Playwright 的 channel="msedge" / "chrome" 自动定位,只有 channel 失败才回退到这些路径,所以一般不用动。若浏览器装在自定义目录,把实际路径追加进列表即可:

CHROME_FALLBACKS.append(r"D:\Tools\Chrome\chrome.exe")

5.3 查更早月份 / 官网记录数变多

常量 MAX_PAGES = 24(约 60 行)。脚本按列表页倒序翻页,直到覆盖「目标月 1 日 + 整个上月」。查很久以前的月份时报[错误] 连续翻了 24 页仍未覆盖…,把值调大即可:

MAX_PAGES = 60

官网当前约 3021 条 / 152 页,24 页约覆盖近两年,正常用不到改。

5.4 不要改的地方

print_to_pdf() 里必须先 Page.printToPDF 生成 PDF,再调用 window.print()。--kiosk-printing 下先调 window.print() 会让页面立刻进入打印状态,随后 CDP 会报Page is already being printed,PDF 生成失败。顺序颠倒过一次,已修正,别再调回去。

6. 如何更改浏览器(chrome&microsoft edge)

6.1 首选:不改代码,运行时选

python pbc_rate.py 2026-11            # 默认 Edgepython pbc_rate.py 2026-11 -b edge    # 明确 Edgepython pbc_rate.py 2026-11 -b chrome  # 切 Chrome

-b 只接受 edge / chrome,填别的会由参数校验直接拒绝(退出码 2)。两条链路都已实测通过:Chrome 完整链路(含 PDF)= 6.7138、Chrome 规则① 2026-06 = 6.8167、Edge 回归 = 6.7138。

新电脑如果只装了 Chrome,必须带 -b chrome;不带参数会走默认 Edge 并报[错误] 未找到 Microsoft Edge。

6.2 想让默认变成 Chrome:改一行

main() 里的参数定义(约 218–219 行):

ap.add_argument("-b", "--browser", choices=("edge", "chrome"), default="chrome",help="用哪个浏览器抓取:chrome(默认)/ edge")

只是把 default="edge" 改成 default="chrome",-b edge 依旧可用。

6.3 写死只用 Edge(Edge 精简版)

需要动 5 处,其余约 200 行不变:

① 常量区:删掉 CHROME_FALLBACKS 和整个 BROWSERS 字典,只留

EDGE_FALLBACK = r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"MAX_PAGES = 24

② 启动函数:把 launch_browser(pw, which) 整个替换为

deflaunch_edge(pw):"""用本机 Edge 启动;--kiosk-printing 让 window.print() 不弹打印预览框。"""    kwargs = {"headless": True,"args": ["--kiosk-printing", "--disable-gpu"]}try:return pw.chromium.launch(channel="msedge", **kwargs)except PWError as exc:print(f"[提示] channel=msedge 启动失败,改用 Edge 可执行文件:{exc}", file=sys.stderr)ifnot Path(EDGE_FALLBACK).is_file():            fail("未找到 Microsoft Edge,请安装后重试")return pw.chromium.launch(executable_path=EDGE_FALLBACK, **kwargs)

③ main():删掉 -b/--browser 那两行参数定义。

④ main():browser = launch_browser(pw, args.browser) 改为 browser = launch_edge(pw)。

⑤ main():删掉 print(f"[浏览器] {BROWSERS[args.browser][1]}", ...) 这行(或改成固定文字)。

另外文件开头 docstring 里的 -b chrome 示例行删掉、依赖说明改回「使用本机 Microsoft Edge」。

6.4 写死只用 Chrome(Chrome精简版)

同样 5 处,方向与 6.3 对应:

① 常量区:删除EDGE_FALLBACK 和 BROWSERS,保留CHROME_FALLBACKS。

② 启动函数:替换为

deflaunch_chrome(pw):"""用本机 Chrome 启动;--kiosk-printing 让 window.print() 不弹打印预览框。"""    kwargs = {"headless": True,"args": ["--kiosk-printing", "--disable-gpu"]}try:return pw.chromium.launch(channel="chrome", **kwargs)except PWError as exc:print(f"[提示] channel=chrome 启动失败,改用 Chrome 可执行文件:{exc}", file=sys.stderr)for path in CHROME_FALLBACKS:if Path(path).is_file():return pw.chromium.launch(executable_path=path, **kwargs)        fail("未找到 Google Chrome,请安装后重试")

③ main():删掉 -b/--browser 参数定义。④ main():改为 browser = launch_chrome(pw)。⑤ main():删掉 [浏览器] 那行输出。docstring 同步改成「使用本机 Google Chrome」,并删掉 -b 示例。

两种精简版都比现在少约 20 行,行为与「带对应 -b 参数」完全一致;如果不是必须锁死一种浏览器,保留现有的 -b 开关更省事。

7. 常见问题 / 排错

现象
原因与处理
[错误] 未找到 Google Chrome / Microsoft Edge
该浏览器未安装,或装在非标准路径(按 5.2 加路径);也可换 -b 用另一个
[提示] channel=... 启动失败,改用…可执行文件
不是错误,是自动回退提示;后面能正常出汇率就不用管
net::ERR_NAME_NOT_RESOLVED
DNS 瞬时解析失败,重试一次通常即恢复;当前脚本未内置自动重试
stderr 中文乱码
脚本已对 stdout / stderr 做 UTF-8 reconfigure;个别老旧终端需切到 UTF-8 代码页

8. 完整代码(pbc_rate.py)

以下为脚本全文。print_to_pdf() 中的 PDF 输出目录等请改为自己实际路径。

# -*- coding: utf-8 -*-"""月度美元对人民币汇率取数 —— 中国人民银行「人民币汇率中间价公告」取值规则(增值税新规口径,每月只取一个汇率):  1. 当月 1 日有中间价公告(1 日即当月第一个工作日)→ 取当月 1 日汇率;  2. 当月 1 日没有公告(当月第一个工作日不是 1 日)→ 取上月最后一个工作日汇率。  等价判定:只需看「当月 1 日有没有公告」,不必用日历推算第一个工作日是哪天。  「工作日」一律以官网公告列表中实际存在的日期为准。用法:  python pbc_rate.py                            取当前月份(默认用 Edge)  python pbc_rate.py 2026-11                    取指定月份  python pbc_rate.py 2026-11 -b chrome          指定用 Chrome  python pbc_rate.py 2026-11 --browser edge     指定用 Edge  python pbc_rate.py 2026-11 --no-pdf           只返回汇率,不生成 PDF输出:  stdout 只输出汇率数值,方便被其他程序 / 管道直接读取;  浏览器、依据日期、公告地址、PDF 路径等提示信息一律写到 stderr。  取不到目标公告或提取不到汇率时,stderr 明确报错、退出码 1,绝不猜测数值。依赖:  pip install playwright  使用本机已安装的 Microsoft Edge 或 Google Chrome(-b/--browser 切换),  无需执行 playwright install 下载浏览器。"""import argparseimport base64import datetime as dtimport reimport sysfrom pathlib import Pathfrom urllib.parse import urljoinfrom playwright.sync_api import Error as PWErrorfrom playwright.sync_api import sync_playwrightfor _stream in (sys.stdout, sys.stderr):try:        _stream.reconfigure(encoding="utf-8")except Exception:passBASE = "https://camlmac.pbc.gov.cn"LIST_DIR = "/zhengcehuobisi/125207/125217/125925/"INDEX_URL = BASE + LIST_DIR + "index.html"PAGE_URL_FALLBACK = BASE + LIST_DIR + "17105-{page}.html"EDGE_FALLBACK = r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"CHROME_FALLBACKS = [r"C:\Program Files\Google\Chrome\Application\chrome.exe",r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",str(Path.home() / "AppData/Local/Google/Chrome/Application/chrome.exe"),]BROWSERS = {"edge": ("msedge", "Microsoft Edge", [EDGE_FALLBACK]),"chrome": ("chrome", "Google Chrome", CHROME_FALLBACKS),}MAX_PAGES = 24ANCHOR_SELECTOR = 'a[title*="中国外汇交易中心受权公布人民币汇率中间价公告"]'TITLE_RE = re.compile(r"^(\d{4})年(\d{1,2})月(\d{1,2})日中国外汇交易中心受权公布人民币汇率中间价公告$")RATE_RE = re.compile(r"1美元对人民币\s*(\d+(?:\.\d+)?)\s*元")deffail(message):print(f"[错误] {message}", file=sys.stderr)    sys.exit(1)defparse_month(text):    m = re.fullmatch(r"(\d{4})-(\d{1,2})", text)ifnot m:        fail(f"月份格式应为 YYYY-MM,收到:{text!r}")    year, month = int(m.group(1)), int(m.group(2))ifnot1 <= month <= 12:        fail(f"月份必须在 1-12 之间:{text!r}")return year, monthdeflaunch_browser(pw, which):"""按 -b/--browser 启动本机 Edge 或 Chrome。    优先用 Playwright 的 channel 自动定位浏览器;找不到时回退到常见安装路径。    --kiosk-printing 让 window.print() 不弹打印预览框。    """    channel, label, fallbacks = BROWSERS[which]    kwargs = {"headless": True,"args": ["--kiosk-printing", "--disable-gpu"],    }try:return pw.chromium.launch(channel=channel, **kwargs)except PWError as exc:print(f"[提示] channel={channel} 启动失败,改用 {label} 可执行文件:{exc}", file=sys.stderr)for path in fallbacks:if Path(path).is_file():return pw.chromium.launch(executable_path=path, **kwargs)        other = "chrome"if which == "edge"else"edge"        fail(f"未找到 {label},请安装后重试,或改用 -b {other}")defgoto(page, url):try:        resp = page.goto(url, wait_until="domcontentloaded", timeout=30000)except PWError as exc:        fail(f"打开页面失败:{url}\n{exc}")if resp isNoneornot resp.ok:        status = "无响应"if resp isNoneelse resp.status        fail(f"打开页面返回异常状态 {status}:{url}")defparse_list_page(page):"""返回本页 {日期: 详情URL} 以及最旧日期、下一页 URL。"""    found = {}for anchor in page.locator(ANCHOR_SELECTOR).all():        title = (anchor.get_attribute("title") or"").strip()        href = anchor.get_attribute("href") or""        m = TITLE_RE.match(title)ifnot m:continue# 自动排除「更正」「补充」等非标准公告        day = dt.date(int(m.group(1)), int(m.group(2)), int(m.group(3)))        found[day] = urljoin(BASE, href)ifnot found:        fail("列表页未解析到标准格式的汇率公告,官网结构可能已改版")return found, min(found)defnext_page_url(page, page_no):"""优先取「下一页」链接上的 tagname 属性,避免依赖硬编码的栏目 ID。"""for anchor in page.locator("a[tagname]").all():if (anchor.inner_text() or"").strip() == "下一页":            tag = anchor.get_attribute("tagname")if tag:return urljoin(BASE, tag)return PAGE_URL_FALLBACK.format(page=page_no + 1)defresolve_target(page, year, month):"""按规则确定应该采用哪一天的公告,返回 (日期, URL, 依据说明)。"""    first = dt.date(year, month, 1)    prev_month_last = first - dt.timedelta(days=1)    prev_month_first = prev_month_last.replace(day=1)if first > dt.date.today().replace(day=1):        fail(f"{year}-{month:02d} 尚未到来,官网不可能已有公告")    announcements = {}    url = INDEX_URLfor page_no inrange(1, MAX_PAGES + 1):        goto(page, url)        found, oldest = parse_list_page(page)        announcements.update(found)if oldest < prev_month_first:break# 已覆盖目标月 1 日和整个上月        url = next_page_url(page, page_no)else:        fail(f"连续翻了 {MAX_PAGES} 页仍未覆盖 {year}-{month:02d} 所需的日期范围")if first in announcements:return first, announcements[first], "当月 1 日有公告(当月第一个工作日为 1 日)→ 取当月 1 日"    earlier = [d for d in announcements if d < first]ifnot earlier:        fail(f"{year}-{month:02d} 之前未找到任何公告,无法确定上月最后一个工作日")    target = max(earlier)if (target.year, target.month) != (prev_month_last.year, prev_month_last.month):        fail(f"上月({prev_month_last.year}-{prev_month_last.month:02d})没有任何公告,取到的最近日期为 {target}")return target, announcements[target], "当月 1 日无公告(第一个工作日非 1 日)→ 取上月最后一个工作日"defextract_rate(page, url, target):    goto(page, url)    title = page.title() or""    expected = f"{target.year}年{target.month}月{target.day}日"if expected notin title:        fail(f"公告页标题与预期日期不符:预期 {expected},实际标题 {title!r}")try:        body = page.locator("#zoom").inner_text(timeout=10000)except PWError as exc:        fail(f"未找到公告正文(#zoom),官网结构可能已改版:{exc}")    m = RATE_RE.search(body)ifnot m:        fail(f"在 {url} 正文中未提取到「1美元对人民币…元」")return m.group(1)  # 保留官网原始精度,不做四舍五入defprint_to_pdf(context, page, year, month, target, rate):"""把当前公告页静默存成 PDF,并触发页面自带的 window.print()。    --kiosk-printing 让 window.print() 不再弹打印预览框。顺序不能颠倒:    headless + kiosk 下 window.print() 会立即让页面进入打印状态,若先调用,    Page.printToPDF 会报 "Page is already being printed" 而无法产出 PDF。    """    cdp = context.new_cdp_session(page)    result = cdp.send("Page.printToPDF",        {"printBackground": True, "landscape": False, "preferCSSPageSize": False},    )    out_dir = Path(r"路径")    out_dir.mkdir(exist_ok=True)    out_path = out_dir / f"{year}-{month:02d}_{target.isoformat()}_USD{rate}.pdf"    out_path.write_bytes(base64.b64decode(result["data"]))try:        page.evaluate("window.print()")except PWError as exc:print(f"[提示] window.print() 调用被忽略:{exc}", file=sys.stderr)return out_pathdefmain():    ap = argparse.ArgumentParser(description="按增值税新规口径获取月度 1 美元对人民币汇率")    ap.add_argument("month", nargs="?", help="目标月份 YYYY-MM,缺省为当前月份")    ap.add_argument("-b", "--browser", choices=("edge", "chrome"), default="edge",help="用哪个浏览器抓取:edge(默认)/ chrome")    ap.add_argument("--no-pdf", action="store_true", help="只返回汇率,不生成 PDF")    args = ap.parse_args()    year, month = parse_month(args.month) if args.month else (dt.date.today().year, dt.date.today().month)try:with sync_playwright() as pw:            browser = launch_browser(pw, args.browser)try:                context = browser.new_context(viewport={"width": 1280, "height": 900}, locale="zh-CN")                page = context.new_page()                page.set_default_timeout(30000)                target, url, reason = resolve_target(page, year, month)                rate = extract_rate(page, url, target)                pdf_path = Noneifnot args.no_pdf:                    pdf_path = print_to_pdf(context, page, year, month, target, rate)finally:                browser.close()except SystemExit:raiseexcept Exception as exc:        fail(f"执行失败:{type(exc).__name__}: {exc}")print(rate)  # stdout 只有汇率print(f"[浏览器] {BROWSERS[args.browser][1]}", file=sys.stderr)print(f"[依据] {reason}:{target.isoformat()}", file=sys.stderr)print(f"[公告] {url}", file=sys.stderr)if pdf_path:print(f"[PDF ] {pdf_path}", file=sys.stderr)if __name__ == "__main__":    main()

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料