phpcfdi / cfdi-sat-scraper
从SAT网页上抓取电子发票
Requires
- php: >=7.3
- ext-curl: *
- ext-dom: *
- ext-fileinfo: *
- ext-json: *
- ext-mbstring: *
- ext-openssl: *
- eclipxe/enum: ^0.2.0
- eclipxe/micro-catalog: ^0.1.3
- guzzlehttp/guzzle: ^7.0
- guzzlehttp/promises: ^2.0
- phpcfdi/credentials: ^1.1
- phpcfdi/image-captcha-resolver: ^0.2.3
- psr/http-message: ^1.1 || ^2.0
- symfony/css-selector: ^5.4 || ^6.0 || ^7.0
- symfony/dom-crawler: ^5.4 || ^6.0 || ^7.0
Requires (Dev)
- ext-iconv: *
- fakerphp/faker: ^1.13
- phpunit/phpunit: ^9.5
- symfony/dotenv: ^5.4 || ^6.0 || ^7.0
README
通过网页抓取从SAT页面发出的、接收的、有效的和已取消的发票。可下载的资源包括CFDI的XML文件和表示打印、取消请求和取消收据的PDF文件。
通过composer安装
composer require phpcfdi/cfdi-sat-scraper
功能
SAT提供的CFDI下载服务位于https://portalcfdi.facturaelectronica.sat.gob.mx/,需要使用RFC、CIEC密钥和解决验证码,或者使用FIEL证书和私钥。
进入网站后,可以查询发出的和接收的发票。无论是通过UUID还是通过过滤器。
-
标准
- 类型:发出的或接收的。
- 过滤器:UUID或查询。
-
发出的查询
- 发行日期和时间。
- 接收日期和时间。
- 接收者RFC。
- 凭证状态(任何状态、有效或已取消)。
- 凭证类型(如果包含特定补充)。
- 第三方账户RFC。
-
接收的查询
- 发行日期。
- 开始时间和结束时间(发行日期内)。
- 发行者RFC。
- 凭证状态(任何状态、有效或已取消)。
- 凭证类型(如果包含特定补充)。
- 第三方账户RFC。
搜索服务返回一个包含信息的表格,每个查询最多返回500条记录(即使有更多,也只显示500)。
有了清单后,网站提供链接以下载CFDI的XML文件。
在库中实现网站功能
主要工作对象是名为SatScraper
的对象,可以使用它进行日期范围或特定UUID的查询并获取结果。通过UUID(一个或多个)的查询使用listByUuids
方法执行,结果是MetadataList
。通过过滤器的查询称为QueryByFilters
,使用listByPeriod
和listByDateTime
方法执行,结果是MetadataList
。
为了生成MetadataList
的结果,库有一个分割策略。如果是通过CFDI过滤器的查询,则自动按天分割。如果查询的期间内有500条或更多记录,则搜索将细分到不同的期间,直到达到最小1秒查询。然后再次将结果合并。
一旦有了MetadataList
清单,就可以应用过滤器以获取一个新清单,其中仅包含UUID相匹配的Metadata
对象;或者使用其他过滤器,例如仅包含特定可下载资源的对象。
一旦有了MetadataList
结果,可以请求下载到特定文件夹或通过一个handler对象。下载过程允许进行多个同时下载。
可以下载的文件类型有
- CFDI文件(XML)。
- CFDI打印表示(PDF)。
- 取消请求(PDF)。
- 取消收据(PDF)。
执行元数据下载的方法是
- 通过UUID:
SatScraper::listByUuids(string[] $uuids, DownloadType $type): MetadataList
- 通过完整日期的过滤器:
SatScraper::listByPeriod(Query $query): MetadataList
- 通过精确日期的过滤器:
SatScraper::listByDateTime(Query $query): MetadataList
一旦有了MetadataList
对象,就创建一个资源下载器对象ResourceDownloader
,并要求它执行按资源类型进行的下载。
- 创建:
SatScraper::resourceDownloader(ResourceType $resourceType, MetadataList $list = null, int $concurrency = 10): ResourceDownloader
- 保存到文件夹:
ResourceDownloader::saveTo(string $destination): void
- 使用处理器保存:
ResourceDownloader::download(ResourceDownloadHandlerInterface $handler): void
如果达到1秒的最小查询时间并且获取了500或更多记录,那么还会调用一个可选的回调来报告这一事件。
搜索始终需要创建一个日期范围,默认情况下,搜索由CSDI发出,任何补充和任何状态(有效或已取消)。然而,你可以在发送处理之前更改搜索。
这个库基于Guzzle,因此你可以根据需要配置客户端,如设置代理或调试HTTP调用。多亏了这个库,我们可以提供XML的同时下载,并且使通信过程比使用完整浏览器更快。
认证
这个库允许使用两种机制之一在SAT进行身份验证:CIEC密钥或FIEL。
FIEL认证
要使用FIEL进行身份验证,需要使用会话处理器FielSessionManager
,相应的证书、私钥和私钥密码。
这种方法的优点是不需要解密。缺点是使用FIEL存在风险。
警告:除非是自己的FIEL,否则不要使用此机制。墨西哥的FIEL由“高级电子签名法”规定。它的使用范围广泛,不仅限于SAT,还可以进行多项法律操作。在PhpCfdi中,我们不推荐存储或使用第三方的FIEL。
CIEC密钥认证
要使用CIEC进行身份验证,需要使用会话处理器CiecSessionManager
,包括RFC、CIEC密钥和验证码解析器。
这种方法的优点是不需要FIEL。缺点是需要验证码解析器。
我们没有自己的方法来解析验证码,但可以使用外部服务,如Anti-Captcha。对于测试或本地实现,可以使用`eclipxe/captcha-local-resolver,其中你自己将解析验证码,这三种实现都已创建。
验证码解析是通过phpcfdi/image-captcha-resolver
库来执行的。如果你使用的是未实现的服务,可以检查此项目的文档,并在支持的客户中集成服务。
查询构建示例
<?php declare(strict_types=1); use PhpCfdi\CfdiSatScraper\QueryByFilters; use PhpCfdi\CfdiSatScraper\Filters\Options\ComplementsOption; use PhpCfdi\CfdiSatScraper\Filters\DownloadType; use PhpCfdi\CfdiSatScraper\Filters\Options\StatesVoucherOption; use PhpCfdi\CfdiSatScraper\Filters\Options\RfcOnBehalfOption; use PhpCfdi\CfdiSatScraper\Filters\Options\RfcOption; // se crea con un rango de fechas específico $query = new QueryByFilters(new DateTimeImmutable('2019-03-01'), new DateTimeImmutable('2019-03-31')); $query ->setDownloadType(DownloadType::recibidos()) // en lugar de emitidos ->setStateVoucher(StatesVoucherOption::vigentes()) // en lugar de todos ->setRfc(new RfcOption('EKU9003173C9')) // de este RFC específico ->setComplement(ComplementsOption::reciboPagoSalarios12()) // que incluya este complemento ->setRfcOnBehalf(new RfcOnBehalfOption('AAA010101AAA')) // con este RFC A cuenta de terceros ;
按日期范围下载示例
<?php declare(strict_types=1); use PhpCfdi\CfdiSatScraper\QueryByFilters; use PhpCfdi\CfdiSatScraper\ResourceType; use PhpCfdi\CfdiSatScraper\SatScraper; use PhpCfdi\CfdiSatScraper\Sessions\Ciec\CiecSessionManager; use PhpCfdi\ImageCaptchaResolver\CaptchaResolverInterface; /** @var CaptchaResolverInterface $captchaResolver */ $satScraper = new SatScraper(CiecSessionManager::create('rfc', 'ciec', $captchaResolver)); $query = new QueryByFilters(new DateTimeImmutable('2019-03-01'), new DateTimeImmutable('2019-03-31')); $list = $satScraper->listByPeriod($query); // impresión de cada uno de los metadata foreach ($list as $cfdi) { echo 'UUID: ', $cfdi->uuid(), PHP_EOL; echo 'Emisor: ', $cfdi->get('rfcEmisor'), ' - ', $cfdi->get('nombreEmisor'), PHP_EOL; echo 'Receptor: ', $cfdi->get('rfcReceptor'), ' - ', $cfdi->get('nombreReceptor'), PHP_EOL; echo 'Fecha: ', $cfdi->get('fechaEmision'), PHP_EOL; echo 'Tipo: ', $cfdi->get('efectoComprobante'), PHP_EOL; echo 'Estado: ', $cfdi->get('estadoComprobante'), PHP_EOL; } // descarga de cada uno de los CFDI, reporta los descargados en $downloadedUuids $downloadedUuids = $satScraper->resourceDownloader(ResourceType::xml(), $list) ->setConcurrency(50) // cambiar a 50 descargas simultáneas ->saveTo('/storage/downloads'); // ejecutar la instrucción de descarga echo json_encode($downloadedUuids);
按UUID列表下载示例
<?php declare(strict_types=1); use PhpCfdi\CfdiSatScraper\Filters\DownloadType; use PhpCfdi\CfdiSatScraper\SatScraper; use PhpCfdi\CfdiSatScraper\Sessions\Ciec\CiecSessionManager; use PhpCfdi\ImageCaptchaResolver\CaptchaResolverInterface; /** @var CaptchaResolverInterface $captchaResolver */ $satScraper = new SatScraper(CiecSessionManager::create('rfc', 'ciec', $captchaResolver)); $uuids = [ '5cc88a1a-8672-11e6-ae22-56b6b6499611', '5cc88c4a-8672-11e6-ae22-56b6b6499612', '5cc88d4e-8672-11e6-ae22-56b6b6499613' ]; $list = $satScraper->listByUuids($uuids, DownloadType::recibidos()); echo json_encode($list);
元数据下载通知
SAT提供的服务有限制,例如,在日期范围内不能获取超过500条记录。这个库试图缩小范围,以便创建一个查询,每秒获取所有数据,但如果出现这种情况,则可以使用处理器MetadataMessageHandler
来记录这种情况。
MetadataMessageHandler
是一个接口,可以接收不同的消息
resolved(DateTimeImmutable $since, DateTimeImmutable $until, int $count): void
:当在一天内的两个时刻之间解决查询时发生,总是少于500条记录。date(DateTimeImmutable $since, DateTimeImmutable $until, int $count): void
:当解决指定日期的查询时发生。有一个初始时间和一个结束时间,因为小时可能不是00:00:00
和23:59:59
。divide(DateTimeImmutable $since, DateTimeImmutable $until): void
: 当在一定时间段内找到500条记录时发生。将查询分割以尝试下载完整内容。maximum(DateTimeImmutable $moment): void
: 当在单一秒内找到500条记录时发生。
如果创建SatScraper
对象时未设置处理程序或将其设置为null
,则将使用NullMetadataMessageHandler
实例,正如其名称所示,它不会执行任何操作。
以下代码示例展示了在找到500条记录问题时显示消息。
<?php declare(strict_types=1); use PhpCfdi\CfdiSatScraper\NullMetadataMessageHandler; use PhpCfdi\CfdiSatScraper\QueryByFilters; use PhpCfdi\CfdiSatScraper\SatHttpGateway; use PhpCfdi\CfdiSatScraper\SatScraper; use PhpCfdi\CfdiSatScraper\Sessions\SessionManager; /** * @var SessionManager $sessionManager * @var SatHttpGateway $httpGateway */ // se define el controlador de mensajes $handler = new class () extends NullMetadataMessageHandler { public function maximum(DateTimeImmutable $date): void { echo 'Se encontraron más de 500 CFDI en el segundo: ', $date->format('c'), PHP_EOL; } }; // se crea el scraper usando el controlador de mensajes $satScraper = new SatScraper($sessionManager, $httpGateway, $handler); $query = new QueryByFilters(new DateTimeImmutable('2019-03-01'), new DateTimeImmutable('2019-03-31')); $list = $satScraper->listByPeriod($query); echo json_encode($list);
MaximumRecordsHandler
接口和NullMaximumRecordsHandler
对象自3.3.0版本以来已弃用。这两个符号将从4.0.0版本中删除。
将CFDIS下载到文件夹
执行saveTo
方法返回实际已下载的UUID数组。
如果下载过程中出现错误,将忽略该错误。
<?php declare(strict_types=1); use PhpCfdi\CfdiSatScraper\QueryByFilters; use PhpCfdi\CfdiSatScraper\ResourceType; use PhpCfdi\CfdiSatScraper\SatScraper; use PhpCfdi\CfdiSatScraper\Sessions\Ciec\CiecSessionManager; use PhpCfdi\ImageCaptchaResolver\CaptchaResolverInterface; /** @var CaptchaResolverInterface $captchaResolver */ $satScraper = new SatScraper(CiecSessionManager::create('rfc', 'ciec', $captchaResolver)); $query = new QueryByFilters(new DateTimeImmutable('2019-03-01'), new DateTimeImmutable('2019-03-31')); $list = $satScraper->listByPeriod($query); // $downloadedUuids contiene un listado de UUID que fueron procesados correctamente, 50 descargas simultáneas $downloadedUuids = $satScraper->resourceDownloader(ResourceType::xml(), $list, 50) ->saveTo('/storage/downloads', true, 0777); echo json_encode($downloadedUuids);
默认情况下,文件存储在文件夹中
- CFDI:
uuid
+.xml
。 - 打印表示:
uuid
+.pdf
。 - 取消请求:
uuid
+-cancel-request.pdf
。 - 取消收据:
uuid
+-cancel-voucher.pdf
。
要更改文件名,创建一个实现接口\PhpCfdi\CfdiSatScraper\Contracts\ResourceFileNamerInterface
的实现,并使用方法ResourceDownloader::setResourceFileNamer()
配置资源下载器。
自定义处理每个CFDI下载
执行ResourceDownloader::download
方法返回实际已下载的UUID数组。它允许配置下载事件和错误处理。
如果要忽略错误,可以简单地指定无内容的ResourceDownloadHandlerInterface::onError()
方法,那么错误仅会丢失。无论如何,由于download
方法返回一个包含已实际下载的UUID数组的数组,因此可以过滤MetadataList
对象以提取未下载的项。
查看类PhpCfdi\CfdiSatScraper\Internal\ResourceDownloadStoreInFolder
作为实现接口ResourceDownloadHandlerInterface
的示例。
<?php declare(strict_types=1); use PhpCfdi\CfdiSatScraper\Contracts\ResourceDownloadHandlerInterface; use PhpCfdi\CfdiSatScraper\Exceptions\ResourceDownloadError; use PhpCfdi\CfdiSatScraper\Exceptions\ResourceDownloadResponseError; use PhpCfdi\CfdiSatScraper\Exceptions\ResourceDownloadRequestExceptionError; use PhpCfdi\CfdiSatScraper\QueryByFilters; use PhpCfdi\CfdiSatScraper\ResourceType; use PhpCfdi\CfdiSatScraper\SatScraper; use PhpCfdi\CfdiSatScraper\Sessions\Ciec\CiecSessionManager; use PhpCfdi\ImageCaptchaResolver\CaptchaResolverInterface; use Psr\Http\Message\ResponseInterface; /** @var CaptchaResolverInterface $captchaResolver */ $satScraper = new SatScraper(CiecSessionManager::create('rfc', 'ciec', $captchaResolver)); $query = new QueryByFilters(new DateTimeImmutable('2019-03-01'), new DateTimeImmutable('2019-03-31')); $list = $satScraper->listByPeriod($query); $myHandler = new class implements ResourceDownloadHandlerInterface { public function onSuccess(string $uuid, string $content, ResponseInterface $response): void { $filename = '/storage/' . $uuid . '.xml'; echo 'Saving ', $uuid, PHP_EOL; file_put_contents($filename, (string) $response->getBody()); } public function onError(ResourceDownloadError $error) : void { if ($error instanceof ResourceDownloadRequestExceptionError) { echo "Error getting {$error->getUuid()} from {$error->getReason()->getRequest()->getUri()}\n"; } elseif ($error instanceof ResourceDownloadResponseError) { echo "Error getting {$error->getUuid()}, invalid response: {$error->getMessage()}\n"; $response = $error->getReason(); // reason is a ResponseInterface print_r(['headers' => $response->getHeaders(), 'body' => $response->getBody()]); } else { // ResourceDownloadError echo "Error getting {$error->getUuid()}, reason: {$error->getMessage()}\n"; print_r(['reason' => $error->getReason()]); } } }; // $downloadedUuids contiene un listado de UUID que fueron procesados correctamente $downloadedUuids = $satScraper->resourceDownloader(ResourceType::xml(), $list)->download($myHandler); echo json_encode($downloadedUuids);
使用Anti-Captcha服务
<?php declare(strict_types=1); use PhpCfdi\CfdiSatScraper\SatScraper; use PhpCfdi\CfdiSatScraper\Sessions\Ciec\CiecSessionManager; use PhpCfdi\ImageCaptchaResolver\Resolvers\AntiCaptchaResolver; $captchaResolver = AntiCaptchaResolver::create('anticaptcha-client-key'); $satScraper = new SatScraper(CiecSessionManager::create('rfc', 'ciec', $captchaResolver));
不进行查询验证数据认证
以下示例展示了如何使用方法SatScraper::confirmSessionIsAlive
来验证会话数据是否正确(或继续正确)。Scraper的内部工作原理是:如果会话之前未初始化,则将尝试进行认证过程,并检查会话(cookie)是否有效。
执行两个步骤以避免不必要地消耗Captcha解析服务。
<?php declare(strict_types=1); use PhpCfdi\CfdiSatScraper\Exceptions\LoginException; use PhpCfdi\CfdiSatScraper\SatScraper; use PhpCfdi\CfdiSatScraper\Sessions\Ciec\CiecSessionManager; use PhpCfdi\ImageCaptchaResolver\CaptchaResolverInterface; /** @var CaptchaResolverInterface $captchaResolver */ $satScraper = new SatScraper(CiecSessionManager::create('rfc', 'ciec', $captchaResolver)); try { $satScraper->confirmSessionIsAlive(); } catch (LoginException $exception) { echo 'ERROR: ', $exception->getMessage(), PHP_EOL; return; }
FIEL认证示例
以下示例使用FIEL,其中证书和私钥文件已加载到内存中且有效。有关如何在该项目中创建凭证的更多信息,请参阅phpcfdi/credentials
项目。
要创建凭证,需要证书、私钥和密码。如果证书和私钥内容在内存中,则使用方法Credential::create()
。如果证书和私钥在文件中,则使用方法Credential::openFiles()
。
<?php declare(strict_types=1); use PhpCfdi\CfdiSatScraper\SatScraper; use PhpCfdi\CfdiSatScraper\Sessions\Fiel\FielSessionManager; use PhpCfdi\CfdiSatScraper\Sessions\Fiel\FielSessionData; use PhpCfdi\Credentials\Credential; /** * @var string $certificate Contenido del certificado * @var string $privateKey Contenido de la llave privada * @var string $passPhrase Contraseña de la llave privada */ // crear la credencial // se puede usar Credential::openFiles(certificateFile, privateKeyFile, passphrase) si la FIEL está en archivos $credential = Credential::create($certificate, $privateKey, $passPhrase); if (! $credential->isFiel()) { throw new Exception('The certificate and private key is not a FIEL'); } if (! $credential->certificate()->validOn()) { throw new Exception('The certificate and private key is not valid at this moment'); } // crear el objeto scraper usando la FIEL $satScraper = new SatScraper(FielSessionManager::create($credential));
移除SAT证书验证
如果用于HTTPS的SAT证书失败,可以取消对它们的验证。这可以通过创建带有禁用verify
选项的Guzzle客户端来实现。
这不是推荐的做法,但在SAT面临的问题下可能需要。考虑到这可能会大大简化攻击(中间人攻击),从而导致您的CIEC密钥丢失。
注意:我们不推荐这种做法,只是因为它经常出现SAT的故障而提出。
<?php declare(strict_types=1); use GuzzleHttp\Client; use GuzzleHttp\RequestOptions; use PhpCfdi\CfdiSatScraper\SatHttpGateway; use PhpCfdi\CfdiSatScraper\SatScraper; use PhpCfdi\CfdiSatScraper\Sessions\SessionManager; $insecureClient = new Client([ RequestOptions::VERIFY => false ]); $gateway = new SatHttpGateway($insecureClient); /** @var SessionManager $sessionManager */ $scraper = new SatScraper($sessionManager, $gateway);
SAT连接问题
根据系统的一般配置,经常遇到这个问题。
cURL error 35: error:141A318A:SSL routines:tls_process_ske_dhe:dh key too small (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for https://cfdiau.sat.gob.mx/...
这个问题是由于处理SAT请求的服务器配置引起的。
仅针对此库解决问题的一种方法是在创建SatScraper
时,在SatHttpGateway
客户端设置cURL配置。
<?php declare(strict_types=1); use GuzzleHttp\Client; use PhpCfdi\CfdiSatScraper\SatHttpGateway; use PhpCfdi\CfdiSatScraper\SatScraper; use PhpCfdi\CfdiSatScraper\Sessions\SessionManager; $client = new Client([ 'curl' => [CURLOPT_SSL_CIPHER_LIST => 'DEFAULT@SECLEVEL=1'], ]); /** @var SessionManager $sessionManager */ $scraper = new SatScraper($sessionManager, new SatHttpGateway($client));
另一种解决方案是降低OpenSSL的一般安全性,一些指令可以在https://askubuntu.com/questions/1250787/when-i-try-to-curl-a-website-i-get-ssl-error中看到。
兼容性
此库将保持与最新PHP支持版本的兼容性。
我们还使用语义版本2.0.0,因此您可以放心使用此库而不用担心破坏您的应用程序。
贡献
欢迎贡献。请阅读CONTRIBUTING获取更多详细信息,并记得检查待办事项文件和变更日志文件。
开发文档
版权和许可
phpcfdi/cfdi-sat-scraper
库版权所有© PhpCfdi,许可在MIT许可(MIT)下使用。请参阅LICENSE获取更多信息。