首页 文章 精选 留言 我的

精选列表

搜索[Python],共10000篇文章
优秀的个人博客,低调大师

Python 手工账电子化②:OCR+校对路径(百度云 accurate 实战)

问题 凭证量大、客户不愿重录时,OCR 是退而求其次的方案。但手写体、印章、竖排三栏表,识别率必然翻车。正确姿势是:OCR 只做趋势和异常扫描,关键科目必须抽校原始凭证,绝不拿 OCR 金额当勾稽依据。 示例代码 plaintext ```javascript def ocr_and_review(ocr_rows, key_accounts): # ocr_rows: OCR 识别出的凭证行; key_accounts: 必须人工抽校的科目 return [r for r in ocr_rows if r["account"] in key_accounts] key = ["库存现金", "银行存款", "应收账款", "主营业务收入"] rows = [ {"account": "库存现金", "amt": 2000}, {"account": "办公费", "amt": 300}, {"account": "银行存款", "amt": 80000}, ] print(ocr_and_review(rows, key)) ``` 关键点 - OCR 引擎优先级:百度云 accurate(已固化在 fast_read.py)优于通用免费 OCR,对手写体稍好但仍需校对。 - 关键科目(货币资金、应收、主营收入)金额必须翻原始凭证人工确认,OCR 只标"有哪些凭证"。 - 手写体、印章遮盖、竖排表格——这三类直接判"人工录入",别赌 OCR。 小结 OCR 在手工账里是"辅助扫描"不是"数据来源"。把它定位成趋势工具,关键数字交回人工,既不误事也不浪费。 --- 你用 OCR 扫手工账翻车最多的是"手写体"还是"印章遮盖"?评论区聊。 如果有用,点赞收藏关注,留下审计难题。下期预告:混合路径决策与数据校验自动化(平衡校验复用)。 会计师事务所的一名注册会计师

优秀的个人博客,低调大师

华为小米荣耀集体调价,我用 Python 写了套可长期运行的手机比价监控

早上刷到一条消息,华为、小米、荣耀多款手机最高涨了 1000 块,机构说这股压力要拖到明年。看完我关心的不是涨多少,而是通告里的涨幅有没有落到电商页面的实时标价上。官方调价、渠道补贴、以旧换新、限时券叠在一起,购物车里的成交价常常和新闻对不上。要验证,只能把几个平台的标价按天抓下来自己比。我花了一下午把这套监控写成能长期跑的脚本。下面把工程上踩过的点和对应的写法拆开讲,重点不在能跑,在于能稳定跑一个月不掉链子。 先定义数据契约很多人先写请求再想存什么,抓回来一堆字段再筛。我反过来,先用 dataclass 把一条价格记录的形状定死,后续落库、写文件、画图都顺着这个结构,不用来回改。 ``` from dataclasses import dataclass from datetime import datetime from zoneinfo import ZoneInfo CST = ZoneInfo("Asia/Shanghai") @dataclass class PhonePrice: model: str platform: str sku: str price: float currency: str captured_at: datetime # 带时区的感知时间 ``` currency 和 sku 是我后来补上的。不同平台价格单位不统一,加上 sku 才能精确定位到具体商品,而不是只靠型号名去猜。captured_at 用 zoneinfo 带时区,机器时区一变数据就乱,这个坑我踩过。价格走独立接口,不在 HTML 里电商页面是前后端分离,标价由浏览器另发请求从价接口取回。京东的价接口大概在 p.3.cn,拼上 sku id 返回 JSON。直接打接口比解析整页快得多,也绕开了页面里大量无关 DOM。 ``` import requests def fetch_raw(sku: str, session: requests.Session) -> dict: url = f"https://p.3.cn/prices/mgets?skuIds=J_{sku}" resp = session.get(url, timeout=(3, 8), headers={ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://item.jd.com/", }) resp.raise_for_status() return resp.json()[0] ``` timeout 我写成元组 (connect, read),把建连和读取分开控。空 UA 有的接口直接甩空响应,加 Referer 更接近真实浏览器轨迹。raise_for_status 让 4xx、5xx 立刻抛错,不会静默拿到半截数据继续往下走。单 IP 撑不住,代理层要独立可替换这才是真正的工程难点。本机一个 IP 高频敲同一接口,当天下午基本 403,严重的返回空数据还不报错。平台风控比前年紧,单 IP 硬刚当天出局。我把代理抽成一个独立组件,不让抓取逻辑依赖具体供应商。长期用的是亿牛云代理,企业级动态 IP,调用它的 API 拿可用 IP,按量计费,IP 池够大,掉线换一个就行。对接时把代理抽象成可迭代的源,后面换供应商只改这一处。 ``` from typing import Iterator class YiniuProxySource: """亿牛云动态代理源,按需拉取并在本地缓存一批 IP。""" def __init__(self, api: str, pool_size: int = 5) -> None: self._api = api self._pool: list[str] = [] self._pool_size = pool_size def _refill(self) -> None: resp = requests.get(self._api, timeout=5).json() # 亿牛云返回 ip 与 port 字段,按实际响应适配 self._pool = [f"{r['ip']}:{r['port']}" for r in resp["data"]] def __iter__(self) -> Iterator[str]: return self def __next__(self) -> str: if not self._pool: self._refill() return f"http://{self._pool.pop(0)}" ``` 本地攒一个小池,用完再拉,避免每条请求都打一次供应商 API。这样代理获取本身不会成为瓶颈,也不会因为频繁调用把配额耗光。网络会抖,重试和日志要分两层重试我放在两个层面。连接层用 urllib3 的 Retry 挂到 Session 上,处理瞬时抖动;业务层用 tenacity 处理代理失效、JSON 解析失败这类需要换代理的重试。 ``` import logging from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type from requests.exceptions import ProxyError, Timeout, HTTPError logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s" ) logger = logging.getLogger("price_crawler") def build_session(proxies: dict) -> requests.Session: s = requests.Session() adapter = requests.adapters.HTTPAdapter( max_retries=requests.packages.urllib3.util.retry.Retry( total=2, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503] ) ) s.mount("https://", adapter) s.proxies.update(proxies) return s @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1), retry=retry_if_exception_type((ProxyError, Timeout, HTTPError)), reraise=True, ) def fetch_price(sku: str, proxies: dict) -> float: session = build_session(proxies) raw = fetch_raw(sku, session) price = float(raw["p"]) logger.info("sku=%s price=%.2f proxy=%s", sku, price, proxies["http"]) return price ``` 指数退避比固定间隔稳。429 是限流信号,我把它塞进 status_forcelist,让连接层先退避再试。业务层只重试代理和网络类异常,价格解析出的业务错误不该被重试掩盖,该报错就报错。拿到价格先校验,再落库抓回来的数不能直接信。价接口偶尔返回 0、返回字符串占位、返回和上次差出几倍的数。落库前做一道校验,脏数据挡在外面。 ``` def validate(price: float, last: float | None) -> bool: if price 100000: return False if last is not None and abs(price - last) / last > 0.5: return False # 单日波动超五成,大概率是接口异常 return True ``` 落库我换成 SQLite,靠 (sku, captured_at) 做幂等,重跑不会插重复行。CSV 适合一次性分析,长期监控还是库省心。 ``` import sqlite3 from contextlib import closing def save(conn: sqlite3.Connection, row: PhonePrice) -> None: with closing(conn): conn.execute( "INSERT OR IGNORE INTO prices(" "model,platform,sku,price,currency,captured_at) " "VALUES(?,?,?,?,?,?)", (row.model, row.platform, row.sku, row.price, row.currency, row.captured_at.astimezone(CST).isoformat()), ) conn.commit() ``` 控制频率,别把对面打挂多 sku 循环时我在每次请求间加随机间隔,模拟人手节奏。想快可以用 ThreadPoolExecutor 开几个 worker,但并发数要压住,否则代理 IP 被你自己的请求耗光,风控也来得更快。 ``` import random, time def run_once(conn: sqlite3.Connection, source: YiniuProxySource) -> None: last: dict[str, float] = {} for model, platform, sku in WATCH_LIST: try: price = fetch_price(sku, {"http": next(source), "https": next(source)}) except Exception as exc: logger.error("跳过 sku=%s 原因=%s", sku, exc) continue if not validate(price, last.get(sku)): logger.warning("sku=%s 校验未过 price=%.2f", sku, price) continue save(conn, PhonePrice(model, platform, sku, price, "CNY", datetime.now(CST))) last[sku] = price time.sleep(random.uniform(1.5, 3.5)) ``` 数据到手能看什么SQLite 攒够一周,按 sku 分组拉出来画折线,谁在哪一刻动的价清清楚楚。新闻说最高涨 1000,你能在图上区分是全线普调还是只动了顶配。有时候通告喊涨,券后价反而更低,这种反差只有自己抓了才知道。定时跑我交给系统计划任务,每天早九晚八各扫一次。比人肉盯着稳,人会有忘的时候,脚本不会。整套下来,能跑通半天就能写完,能稳定跑一个月才是硬功夫。爬虫逻辑不值钱,代理质量和重试、校验、限频这些工程细节才决定你拿到的是干净数据还是一堆 403。这也是我长期用亿牛云的原因,IP 池和稳定性过关,剩下的都是自己的事。只想看个大概,手动查两次也行。要做长期价格档案,就让脚本替你守着。

优秀的个人博客,低调大师

被Python异步坑了三年,才搞懂await和create_task的区别

--- ## **那晚,我的爬虫又崩了** 凌晨两点,我被报警短信震醒。线上爬虫服务又挂了。 打开监控面板,CPU跑满,内存飙升,连接数异常。重启之后不到十分钟,再次崩溃。翻日志只看到一行:`TimeoutError`,没有更多信息。 这已经是一个月内的第三次了。 代码我看过无数遍,逻辑清晰,结构合理。用了`asyncio`,用了`aiohttp`,该`await`的地方都`await`了,按理说不该出问题。 直到我盯着那几行核心代码看了整整两个小时,才突然意识到一个被我反复忽略的事实: **`await`和`asyncio.create_task`,根本就不是一回事。而我,一直在把它们混着用。** --- ## **场景重现:一个看起来毫无问题的爬虫** 先给你看看我当时写的代码(简化版): ``` import asyncio import aiohttp async def fetch(url): async with aiohttp.ClientSession() as session: async with session.get(url) as response: return await response.text() async def main(): urls = ['https://api.example.com/data'] * 100 for url in urls: result = await fetch(url) # 一个个来? await save_to_db(result) # 一个个存? asyncio.run(main()) ``` 我看到这代码的第一反应是:**这跟同步有什么区别?** 于是我"优化"了一下: ``` async def main(): urls = ['https://api.example.com/data'] * 100 tasks = [] for url in urls: tasks.append(fetch(url)) for task in tasks: result = await task # 还是一个个等? await save_to_db(result) ``` 看起来创建了一堆任务对吧?但实际上,**这里连一个并发都没有**。 为什么?因为你虽然把协程对象放进了列表,但在`await task`的那一刻,程序还是老老实实等它执行完,再执行下一个`await`。整个流程依然是串行的。 这就好比你点了十份外卖,但每一份都等送到吃完才点下一份——那跟只点一份有什么区别? > 当时的我还自我安慰说"用了异步应该比同步快吧",结果压测数据打脸打得啪啪响。QPS跟同步代码几乎一样,该超时的照样超时。 --- ## **到底什么是await?** 讲清楚区别之前,得先明白这两个东西分别是什么。 `await`是一个**等待操作符**。 它的行为很简单:**“停在这里,等到这个异步操作完成,把结果给我。”** ``` data = await fetch(url) # 程序停在这里,fetch完成之前不往下走 print(data) ``` 注意,`await`不会创建任何东西。它只是"等待"一个已经存在的异步操作。 打个比方:你打电话给客服,客服说"请稍等,我帮你查一下",然后你**拿着电话等在原地**——这就是`await`。你什么都没创建,只是在等。 那么问题来了:我写了`tasks.append(fetch(url))`,那不是创建了任务吗? 不是。`fetch(url)`返回的是一个**协程对象(coroutine object)**,不是任务。协程对象就像一个"待办事项清单",上面写着"我要去请求这个URL"。但**这个清单本身并不会被执行**,除非你主动去驱动它。 你可以这样理解: - 协程对象 = 一张写了计划的纸条 - `await` = 站在原地,把纸条上的事情做完再走 - `create_task` = 把纸条交给一个助手,让他去做,你继续干别的 --- ## **那create_task又是干什么的?** `asyncio.create_task()`才是真正"创建任务"的东西。 它会把你给的协程**包装成一个Task对象**,然后**立即调度到事件循环中执行**。注意"立即"这两个字——任务从创建那一刻起就开始跑了,不需要你手动`await`。 ``` task = asyncio.create_task(fetch(url)) # 到这里,fetch(url)已经开始执行了 # 程序不会阻塞,立刻往下走 ``` 回到打电话的比喻:`create_task`不是让你拿着电话等,而是你把事情交代给助手,助手马上开始处理,你放下电话该干嘛干嘛。 **关键区别来了:** |操作|是否阻塞|是否创建任务|是否立即执行| | --- | --- | --- | --- | |await 协程|是(等结果)|否|立刻执行并等待| |create_task(协程)|否|是|立刻执行但不等待| 用`create_task`改一下前面的爬虫: ``` async def main(): urls = ['https://api.example.com/data'] * 100 tasks = [] for url in urls: task = asyncio.create_task(fetch(url)) tasks.append(task) # 到这里,所有请求已经同时发出了 # 现在可以等它们全部完成 results = await asyncio.gather(*tasks) for result in results: await save_to_db(result) ``` 这才叫真正的并发。 100个请求同时发出,等待时间从"100次网络往返"变成了"1次网络往返中最慢的那一次"。在我的测试环境里,响应时间从45秒直接降到了2.3秒。 --- ## **但事情没那么简单——create_task的陷阱** 如果你以为掌握了`create_task`就万事大吉,那就太天真了。我踩过的坑远不止这一个。 ### **陷阱一:任务创建了但不等待,直接消失了** ``` async def main(): asyncio.create_task(fetch(url)) # 创建了任务 # 但程序立刻结束了,任务根本没跑完 asyncio.run(main()) ``` `asyncio.run()`在`main()`结束后会关闭事件循环。如果还有任务没完成,它们会被直接丢弃——连报错都没有。 你以为任务在跑,实际上它已经**被悄悄杀掉了**。 解决方案:用`asyncio.gather()`或者`await task`来确保任务完成。 ``` async def main(): task = asyncio.create_task(fetch(url)) await task # 确保任务完成 ``` 或者: ``` async def main(): tasks = [asyncio.create_task(fetch(url)) for _ in range(10)] await asyncio.gather(*tasks) # 全部等待 ``` ### **陷阱二:异常被静默吞掉了** `create_task`创建的任务如果抛出了异常,而这个异常没有被捕获,它不会像普通函数那样直接报错让你看见。异常会**保存在Task对象内部**,等你`await task`或者`task.result()`的时候才会抛出来。 如果你不等待任务,异常就永远消失了。 ``` async def broken(): raise ValueError("出错了") async def main(): task = asyncio.create_task(broken()) # 这里啥也没发生,异常被吞了 await asyncio.sleep(1) # 程序安静地运行 asyncio.run(main()) # 没有任何报错 ``` 这个问题非常隐蔽。我当时那个凌晨崩溃的爬虫,就是因为某个任务抛了异常没被捕获,然后任务一直卡在那里占着资源,最后把整个事件循环拖垮了。 正确做法是给每个任务加异常处理: ``` async def safe_fetch(url): try: return await fetch(url) except Exception as e: log.error(f"请求{url}失败: {e}") return None # 或者在获取结果时捕获 results = await asyncio.gather(*tasks, return_exceptions=True) for result in results: if isinstance(result, Exception): log.error(f"任务失败: {result}") else: process(result) ``` ### **陷阱三:任务数量爆炸** `create_task`的调度成本很低,但不是零。如果你一下子创建一万个任务,事件循环的调度器会忙不过来。每个任务都要维护状态、切换上下文,开销不可忽略。 我的爬虫曾经一次性创建了5000个任务去请求不同的API,结果事件循环的切换开销比网络请求本身还大。 解决方案是控制并发数量——用`asyncio.Semaphore`或者`asyncio.gather`分批处理。 ``` sem = asyncio.Semaphore(100) # 最多同时100个 async def limited_fetch(url): async with sem: return await fetch(url) tasks = [limited_fetch(url) for url in urls] results = await asyncio.gather(*tasks) ``` --- ## **什么时候用await,什么时候用create_task?** 总结下来,规则其实不复杂: **用`await`的场景:** - 你**需要**这个操作的结果才能继续往下走 - 单个异步操作,不需要并发 - 操作本身很快,不值得额外创建任务 比如读取数据库的一行记录、调用一个API取回配置、等待用户输入——这些都是"拿到结果再做事"的场景。 **用`create_task`的场景:** - 你**不需要**立刻拿到结果,可以后面再取 - 需要**并发**执行多个异步操作 - 想让某些操作在**后台**运行,不阻塞主流程 比如批量请求多个API、同时下载多个文件、先发送通知但不等待反馈——这些都是"先发起再说,回头再取结果"的场景。 **一个简单的判断标准:** 写下`await`的时候问自己一句——“如果我不等这个结果,下面的代码能跑吗?” - 能跑 → 用`create_task` - 不能跑 → 用`await` --- ## **那晚之后,我改成了这样** 那天凌晨四点,我把代码改成了这样: ``` async def main(): urls = get_urls() # 用Semaphore控制并发数 sem = asyncio.Semaphore(50) async def fetch_with_limit(url): async with sem: try: return await fetch(url) except Exception as e: log.error(f"URL失败 {url}: {e}") return None # 创建所有任务,立即开始执行 tasks = [asyncio.create_task(fetch_with_limit(url)) for url in urls] # 等待所有任务完成,异常不抛出,而是作为结果返回 results = await asyncio.gather(*tasks, return_exceptions=True) # 处理结果,把异常的单独拎出来 success = [] failed = [] for url, result in zip(urls, results): if isinstance(result, Exception) or result is None: failed.append(url) else: success.append(result) log.info(f"成功{len(success)}条,失败{len(failed)}条") # 失败的重试 if failed: retry_failed(failed) asyncio.run(main()) ``` 改完之后跑了三天,再也没崩过。 --- ## **你可能还会踩的坑(我替你踩过了)** 除了上面说的,还有几个小坑顺带提一下,都是血泪教训: **1. 不要在循环里直接`await`异步函数。** 这等于把并发变成了串行。要么先收集所有协程对象再`gather`,要么用`create_task`创建任务列表。 **2. `asyncio.run()`和`get_event_loop()`别混用。** `asyncio.run()`每次会创建新的事件循环,如果你在里面用了`get_event_loop()`拿到的是不同的循环,任务可能调度不进去。 **3. 异步函数里别用`time.sleep()`,用`asyncio.sleep()`。** `time.sleep()`会阻塞整个线程,事件循环直接卡死。当年我因为这个bug排查了一整天。 **4. `gather`和`wait`的区别。** `gather`返回所有结果,`wait`返回完成/未完成的任务集合。简单场景用`gather`就够了,需要精细控制超时和取消时用`wait`。 --- ## **说穿了就一句话** `await`是"等结果",`create_task`是"派任务"。 前者让你停下脚步,后者让你同时做多件事。异步编程的核心就是在这两者之间找到平衡——该等的时候等,该并发的时候并发。 三年时间,我从"会写async/await语法"到"真的理解它在干什么",中间隔了无数次线上事故和凌晨排查。希望读完这篇文章的你,不用再走这些弯路。 下次写异步代码的时候,多问自己一句:**这个操作,我是该等它,还是该派出去?** 答案清楚了,代码也就对了。

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册