<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: python</title>
    <description>The latest articles tagged 'python' on DEV Community.</description>
    <link>https://dev.to/t/python</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tag/python"/>
    <language>en</language>
    <item>
      <title>Como aprendi Apache Spark — Parte 10: Client Mode vs. Cluster Mode — onde o Spark realmente executa</title>
      <dc:creator>renanpyd</dc:creator>
      <pubDate>Fri, 06 Nov 2026 19:50:43 +0000</pubDate>
      <link>https://dev.to/renanpyd/como-aprendi-apache-spark-parte-10-client-mode-vs-cluster-mode-onde-o-spark-realmente-executa-5b1e</link>
      <guid>https://dev.to/renanpyd/como-aprendi-apache-spark-parte-10-client-mode-vs-cluster-mode-onde-o-spark-realmente-executa-5b1e</guid>
      <description>&lt;p&gt;&lt;em&gt;Nota de atualização (2026): este artigo revisita conceitos da minha jornada de aprendizado de Apache Spark e os conecta aos ambientes modernos de implantação. Algumas opções de execução, integrações e recomendações surgiram ou evoluíram depois dos primeiros anos do projeto.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Nos nove artigos anteriores, exploramos a arquitetura do Apache Spark por diferentes perspectivas.&lt;/p&gt;

&lt;p&gt;Aprendemos que uma aplicação Spark possui um &lt;strong&gt;Driver&lt;/strong&gt;, que coordena o processamento, e &lt;strong&gt;Executors&lt;/strong&gt;, responsáveis por executar Tasks sobre partições de dados.&lt;/p&gt;

&lt;p&gt;Também vimos que o Spark organiza o trabalho em Jobs, Stages e Tasks, utiliza shuffle para redistribuir dados, gerencia memória, mantém lineage para recuperação de falhas e oferece mecanismos como Broadcast Variables e Accumulators.&lt;/p&gt;

&lt;p&gt;Mas existe uma pergunta que ainda não respondemos completamente:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Onde o Driver realmente executa quando enviamos uma aplicação Spark para um cluster?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;E uma segunda pergunta igualmente importante:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quem inicia, gerencia e encerra os Executors?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Essas questões nos levam a três conceitos fundamentais:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deployment Mode.&lt;/li&gt;
&lt;li&gt;Cluster Manager.&lt;/li&gt;
&lt;li&gt;Application Submission.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vamos entender como eles se relacionam e por que essa distinção é essencial para desenvolver, implantar e operar aplicações Spark em produção.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. O que acontece quando executamos uma aplicação Spark?
&lt;/h2&gt;

&lt;p&gt;Considere uma aplicação PySpark:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SparkSession&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql.functions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;

&lt;span class="n"&gt;spark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;SparkSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VendasAnalytics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;vendas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/data/vendas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;resultado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;vendas&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ano&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;estado&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;resultado&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O código parece simples.&lt;/p&gt;

&lt;p&gt;Mas, dependendo de onde executamos a aplicação, existe uma infraestrutura por trás:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  APPLICATION
                       |
                       v
                     DRIVER
                       |
                       v
                 CLUSTER MANAGER
                       |
             +---------+---------+
             |         |         |
             v         v         v
         EXECUTOR 1 EXECUTOR 2 EXECUTOR 3
             |         |         |
           TASKS     TASKS     TASKS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O Driver coordena o processamento.&lt;/p&gt;

&lt;p&gt;O Cluster Manager participa da alocação e do gerenciamento dos recursos necessários.&lt;/p&gt;

&lt;p&gt;Os Executors realizam o trabalho distribuído.&lt;/p&gt;

&lt;p&gt;A localização e o ciclo de vida desses componentes dependem do ambiente de implantação.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Driver, Executor e Cluster Manager não são a mesma coisa
&lt;/h2&gt;

&lt;p&gt;Antes de falar sobre Client Mode e Cluster Mode, precisamos separar três responsabilidades.&lt;/p&gt;

&lt;h3&gt;
  
  
  Driver
&lt;/h3&gt;

&lt;p&gt;O Driver é o processo que coordena a aplicação Spark.&lt;/p&gt;

&lt;p&gt;Entre suas responsabilidades estão:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Executar o programa principal.&lt;/li&gt;
&lt;li&gt;Construir e coordenar operações Spark.&lt;/li&gt;
&lt;li&gt;Planejar Jobs e Stages.&lt;/li&gt;
&lt;li&gt;Agendar Tasks.&lt;/li&gt;
&lt;li&gt;Acompanhar resultados e falhas.&lt;/li&gt;
&lt;li&gt;Interagir com os Executors.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Executor
&lt;/h3&gt;

&lt;p&gt;Os Executors são processos associados à aplicação Spark que executam Tasks e armazenam dados intermediários ou persistidos conforme necessário.&lt;/p&gt;

&lt;p&gt;Eles utilizam recursos como CPU, memória, disco e rede.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cluster Manager
&lt;/h3&gt;

&lt;p&gt;O Cluster Manager gerencia recursos computacionais do ambiente.&lt;/p&gt;

&lt;p&gt;Dependendo da plataforma, ele pode ser responsável por alocar containers, Pods ou processos para os componentes Spark.&lt;/p&gt;

&lt;p&gt;Uma visão simplificada:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Driver
  |
  | Solicita recursos e coordena execução
  |
  v
Cluster Manager
  |
  | Disponibiliza recursos
  |
  v
Executors
  |
  | Executam Tasks
  |
  v
Distributed Processing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;É importante lembrar que o Cluster Manager não substitui o scheduler interno do Spark.&lt;/p&gt;

&lt;p&gt;O Spark continua responsável por organizar e distribuir suas Tasks entre os Executors disponíveis.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. O que é Deployment Mode?
&lt;/h2&gt;

&lt;p&gt;Deployment Mode define, principalmente, &lt;strong&gt;onde o processo Driver será executado em relação ao ambiente que submete a aplicação&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Os dois modos mais conhecidos são:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client Mode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;e:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cluster Mode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essa distinção parece pequena, mas afeta diretamente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conectividade.&lt;/li&gt;
&lt;li&gt;Logs.&lt;/li&gt;
&lt;li&gt;Disponibilidade do Driver.&lt;/li&gt;
&lt;li&gt;Empacotamento da aplicação.&lt;/li&gt;
&lt;li&gt;Operação em produção.&lt;/li&gt;
&lt;li&gt;Diagnóstico de falhas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vamos começar pelo Client Mode.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Client Mode
&lt;/h2&gt;

&lt;p&gt;No Client Mode, o Driver executa no ambiente de onde a aplicação é submetida.&lt;/p&gt;

&lt;p&gt;Imagine que estamos utilizando uma máquina de desenvolvimento ou um servidor de borda para executar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spark-submit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--deploy-mode&lt;/span&gt; client &lt;span class="se"&gt;\&lt;/span&gt;
  app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Em um ambiente de cluster compatível, a arquitetura conceitual é:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;       MÁQUINA DE SUBMISSÃO
       +-------------------+
       |                   |
       |      DRIVER       |
       |                   |
       +---------+---------+
                 |
                 | Comunicação
                 |
                 v
          CLUSTER MANAGER
                 |
       +---------+---------+
       |         |         |
       v         v         v
   EXECUTOR 1 EXECUTOR 2 EXECUTOR 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O Driver permanece no processo de submissão.&lt;/p&gt;

&lt;p&gt;Os Executors podem executar em outros nós.&lt;/p&gt;

&lt;p&gt;Isso significa que o Driver precisa conseguir se comunicar adequadamente com os Executors e com os serviços relevantes do cluster.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Por que Client Mode é útil?
&lt;/h2&gt;

&lt;p&gt;Client Mode costuma ser conveniente em cenários como:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Desenvolvimento interativo.&lt;/li&gt;
&lt;li&gt;Depuração.&lt;/li&gt;
&lt;li&gt;Sessões de notebooks.&lt;/li&gt;
&lt;li&gt;Execução a partir de servidores de borda.&lt;/li&gt;
&lt;li&gt;Aplicações nas quais desejamos acompanhar diretamente a saída do Driver.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Imagine que estamos investigando um pipeline.&lt;/p&gt;

&lt;p&gt;Queremos observar mensagens como:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Starting Spark application...
Loading dataset...
Executing aggregation...
Writing output...
Application finished.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No Client Mode, a saída do Driver normalmente fica associada ao ambiente de submissão.&lt;/p&gt;

&lt;p&gt;Isso facilita determinados fluxos de desenvolvimento e troubleshooting.&lt;/p&gt;

&lt;p&gt;Mas também traz responsabilidades.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. A máquina cliente faz parte da execução
&lt;/h2&gt;

&lt;p&gt;Um erro comum é imaginar:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Enviei a aplicação para o cluster. Agora minha máquina não importa mais.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Em Client Mode, isso não é verdade.&lt;/p&gt;

&lt;p&gt;O Driver continua executando no ambiente cliente.&lt;/p&gt;

&lt;p&gt;Se esse processo for encerrado, a aplicação poderá ser interrompida.&lt;/p&gt;

&lt;p&gt;Considere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer Laptop
       |
       v
     Driver
       |
       v
    Cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se o laptop for desligado ou o processo Driver terminar, não devemos assumir que a aplicação continuará normalmente.&lt;/p&gt;

&lt;p&gt;O comportamento exato depende do ambiente e de seus mecanismos de supervisão, mas o princípio permanece:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;O Driver é parte ativa da aplicação.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Client Mode e conectividade
&lt;/h2&gt;

&lt;p&gt;Outra consequência importante é a rede.&lt;/p&gt;

&lt;p&gt;Os Executors precisam se comunicar com o Driver.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer Laptop
       |
       | VPN / Firewall / NAT
       |
       v
   Spark Cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se os Executors não conseguem alcançar o endereço anunciado pelo Driver, a aplicação pode falhar mesmo que exista CPU e memória disponíveis.&lt;/p&gt;

&lt;p&gt;Problemas comuns incluem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Endereço incorreto do Driver.&lt;/li&gt;
&lt;li&gt;DNS.&lt;/li&gt;
&lt;li&gt;Firewall.&lt;/li&gt;
&lt;li&gt;Portas bloqueadas.&lt;/li&gt;
&lt;li&gt;NAT.&lt;/li&gt;
&lt;li&gt;Instabilidade de VPN.&lt;/li&gt;
&lt;li&gt;Interfaces de rede configuradas incorretamente.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Em ambientes corporativos, essa é uma das razões para utilizar servidores de borda ou plataformas de notebooks integradas ao cluster.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Cluster Mode
&lt;/h2&gt;

&lt;p&gt;Agora vamos mudar a localização do Driver.&lt;/p&gt;

&lt;p&gt;No Cluster Mode, o Driver é executado dentro do ambiente gerenciado pelo cluster, conforme os mecanismos da plataforma utilizada.&lt;/p&gt;

&lt;p&gt;Conceitualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;       MÁQUINA DE SUBMISSÃO
       +-------------------+
       |                   |
       |   spark-submit    |
       |                   |
       +---------+---------+
                 |
                 | Submete aplicação
                 |
                 v
          CLUSTER MANAGER
                 |
       +---------+---------+
       |                   |
       v                   v
     DRIVER             EXECUTORS
       |                   |
       +---------+---------+
                 |
                 v
          DISTRIBUTED JOB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A diferença central é:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;O Driver não precisa permanecer no processo cliente que iniciou a submissão.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Isso torna o Cluster Mode adequado a muitos cenários de execução automatizada.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Cluster Mode é melhor para produção?
&lt;/h2&gt;

&lt;p&gt;Frequentemente, Cluster Mode é uma escolha conveniente para Jobs batch de produção.&lt;/p&gt;

&lt;p&gt;Mas não devemos transformar isso em uma regra absoluta.&lt;/p&gt;

&lt;p&gt;Existem plataformas gerenciadas e arquiteturas específicas em que o ciclo de vida da aplicação é controlado de outras maneiras.&lt;/p&gt;

&lt;p&gt;O importante é compreender os requisitos.&lt;/p&gt;

&lt;p&gt;Por exemplo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pipeline noturno
       |
       v
Scheduler
       |
       v
Application Submission
       |
       v
Cluster-managed Driver
       |
       v
Executors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nesse cenário, não queremos depender de uma sessão interativa de desenvolvimento para manter a aplicação ativa.&lt;/p&gt;

&lt;p&gt;Cluster Mode ajuda a separar o ambiente de submissão do ambiente de execução.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Client Mode vs. Cluster Mode
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Característica&lt;/th&gt;
&lt;th&gt;Client Mode&lt;/th&gt;
&lt;th&gt;Cluster Mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Localização do Driver&lt;/td&gt;
&lt;td&gt;Ambiente de submissão&lt;/td&gt;
&lt;td&gt;Ambiente gerenciado pelo cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uso frequente&lt;/td&gt;
&lt;td&gt;Desenvolvimento e sessões interativas&lt;/td&gt;
&lt;td&gt;Jobs automatizados e batch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs do Driver&lt;/td&gt;
&lt;td&gt;Próximos ao processo cliente&lt;/td&gt;
&lt;td&gt;Normalmente obtidos pela plataforma&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependência do cliente&lt;/td&gt;
&lt;td&gt;Driver permanece no cliente&lt;/td&gt;
&lt;td&gt;Submissão pode terminar sem encerrar necessariamente a aplicação&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conectividade&lt;/td&gt;
&lt;td&gt;Executors precisam alcançar o Driver cliente&lt;/td&gt;
&lt;td&gt;Driver executa na rede do cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disponibilidade&lt;/td&gt;
&lt;td&gt;Depende do processo Driver cliente&lt;/td&gt;
&lt;td&gt;Depende do cluster e da supervisão&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Melhor escolha&lt;/td&gt;
&lt;td&gt;Depende do cenário&lt;/td&gt;
&lt;td&gt;Depende do cenário&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;O ponto principal é:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Client Mode e Cluster Mode descrevem a localização do Driver, não a quantidade de dados processados nem o grau de paralelismo da aplicação.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  11. Deployment Mode não é Cluster Manager
&lt;/h2&gt;

&lt;p&gt;Agora precisamos evitar outra confusão.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployment Mode&lt;/strong&gt; responde:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Onde o Driver executa?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Cluster Manager&lt;/strong&gt; responde:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Qual sistema gerencia os recursos do ambiente?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;São dimensões diferentes.&lt;/p&gt;

&lt;p&gt;Por exemplo, dependendo do suporte da plataforma, podemos encontrar combinações como:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;YARN + Client Mode
YARN + Cluster Mode

Kubernetes + Client Mode
Kubernetes + Cluster Mode

Standalone + Client Mode
Standalone + Cluster Mode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Entretanto, a disponibilidade de cada combinação depende do Cluster Manager, da linguagem e da versão do Spark.&lt;/p&gt;

&lt;p&gt;Em particular, o suporte a aplicações Python em Standalone Cluster Mode possui restrições documentadas em versões do Spark. Portanto, não devemos presumir que todas as combinações funcionam igualmente.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. Spark Standalone
&lt;/h2&gt;

&lt;p&gt;Spark Standalone é o gerenciador de cluster próprio do Apache Spark.&lt;/p&gt;

&lt;p&gt;Sua arquitetura clássica envolve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             STANDALONE MASTER
                     |
           +---------+---------+
           |         |         |
           v         v         v
        WORKER 1  WORKER 2  WORKER 3
           |         |         |
           v         v         v
        EXECUTOR  EXECUTOR  EXECUTOR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O Master coordena a disponibilidade dos recursos do cluster Standalone.&lt;/p&gt;

&lt;p&gt;Os Workers disponibilizam recursos computacionais.&lt;/p&gt;

&lt;p&gt;O Driver e os Executors são iniciados conforme o modo de implantação e os recursos disponíveis.&lt;/p&gt;

&lt;p&gt;Esse modelo é útil para compreender a arquitetura do Spark sem adicionar toda a complexidade de um gerenciador externo.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. Spark em YARN
&lt;/h2&gt;

&lt;p&gt;YARN significa:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yet Another Resource Negotiator.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ele faz parte do ecossistema Hadoop e gerencia recursos para aplicações distribuídas.&lt;/p&gt;

&lt;p&gt;Uma visão simplificada:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  YARN
                   |
             ResourceManager
                   |
        +----------+----------+
        |          |          |
        v          v          v
     NodeManager NodeManager NodeManager
        |          |          |
        v          v          v
     Containers Containers Containers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O Spark pode utilizar esses recursos para executar seus componentes.&lt;/p&gt;

&lt;p&gt;No YARN, existe também o conceito de &lt;strong&gt;ApplicationMaster&lt;/strong&gt;, cujo papel e relacionamento com o Driver variam conforme o deployment mode.&lt;/p&gt;




&lt;h2&gt;
  
  
  14. YARN Client Mode
&lt;/h2&gt;

&lt;p&gt;No YARN Client Mode, o Driver executa no ambiente cliente.&lt;/p&gt;

&lt;p&gt;Conceitualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        CLIENT MACHINE
        +------------+
        |   DRIVER   |
        +-----+------+
              |
              v
        YARN CLUSTER
              |
        ApplicationMaster
              |
       +------+------+
       |             |
       v             v
   Executor 1    Executor 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O ApplicationMaster participa do gerenciamento dos recursos da aplicação no YARN.&lt;/p&gt;

&lt;p&gt;O Driver permanece no cliente.&lt;/p&gt;

&lt;p&gt;Esse detalhe é importante ao interpretar logs e falhas.&lt;/p&gt;




&lt;h2&gt;
  
  
  15. YARN Cluster Mode
&lt;/h2&gt;

&lt;p&gt;No YARN Cluster Mode, o Driver executa no cluster, dentro do contexto da aplicação gerenciada pelo YARN.&lt;/p&gt;

&lt;p&gt;Uma visão conceitual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;       SUBMISSION CLIENT
               |
               v
         YARN CLUSTER
               |
               v
        ApplicationMaster
        +--------------+
        |    DRIVER    |
        +------+-------+
               |
        +------+------+
        |             |
        v             v
    Executor 1    Executor 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nesse modo, o Driver não depende de continuar executando na máquina que fez a submissão.&lt;/p&gt;

&lt;p&gt;Para workloads batch, isso frequentemente simplifica a operação.&lt;/p&gt;




&lt;h2&gt;
  
  
  16. Spark em Kubernetes
&lt;/h2&gt;

&lt;p&gt;Kubernetes utiliza uma arquitetura baseada em recursos como Pods, Deployments, Services e outros objetos.&lt;/p&gt;

&lt;p&gt;No Spark, podemos executar o Driver e os Executors em Pods.&lt;/p&gt;

&lt;p&gt;Uma visão típica de Cluster Mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             spark-submit
                  |
                  v
          KUBERNETES API
                  |
                  v
              DRIVER POD
                  |
                  v
          EXECUTOR PODS
          /     |     \
         v      v      v
       Exec 1 Exec 2 Exec 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O Driver solicita os recursos necessários por meio da integração Spark com Kubernetes.&lt;/p&gt;

&lt;p&gt;Os Executors executam em Pods associados à aplicação.&lt;/p&gt;

&lt;p&gt;Isso facilita a integração com práticas modernas de infraestrutura containerizada.&lt;/p&gt;




&lt;h2&gt;
  
  
  17. Kubernetes não elimina a arquitetura do Spark
&lt;/h2&gt;

&lt;p&gt;É importante compreender:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Kubernetes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;não substitui:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Spark DAG Scheduler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;nem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Spark Task Scheduler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O Kubernetes gerencia recursos e o ciclo de vida dos Pods.&lt;/p&gt;

&lt;p&gt;O Spark continua responsável por:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Construir planos de execução.&lt;/li&gt;
&lt;li&gt;Organizar Jobs e Stages.&lt;/li&gt;
&lt;li&gt;Distribuir Tasks.&lt;/li&gt;
&lt;li&gt;Coordenar Executors.&lt;/li&gt;
&lt;li&gt;Gerenciar shuffle e recomputação.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Podemos pensar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Kubernetes
    |
    v
Resource Orchestration
    |
    v
Spark Application
    |
    v
Distributed Data Processing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;São responsabilidades complementares.&lt;/p&gt;




&lt;h2&gt;
  
  
  18. E o modo local?
&lt;/h2&gt;

&lt;p&gt;Antes de utilizar um cluster real, podemos executar Spark localmente.&lt;/p&gt;

&lt;p&gt;Por exemplo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;spark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;SparkSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;master&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local[*]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SparkLocal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O parâmetro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local[*]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;indica que o Spark pode utilizar os núcleos lógicos disponíveis na máquina local.&lt;/p&gt;

&lt;p&gt;Também podemos utilizar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local[2]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;para configurar dois threads de execução local.&lt;/p&gt;

&lt;p&gt;Exemplo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;spark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;SparkSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;master&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local[2]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SparkLocal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isso é útil para desenvolvimento e experimentação.&lt;/p&gt;

&lt;p&gt;Mas precisamos lembrar que uma execução local não reproduz integralmente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Latência de rede entre nós.&lt;/li&gt;
&lt;li&gt;Comportamento de um cluster real.&lt;/li&gt;
&lt;li&gt;Falhas distribuídas.&lt;/li&gt;
&lt;li&gt;Restrições de containers.&lt;/li&gt;
&lt;li&gt;Escalabilidade de armazenamento.&lt;/li&gt;
&lt;li&gt;Dinâmica de alocação de recursos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Portanto, testes locais são importantes, mas não substituem validação no ambiente de destino.&lt;/p&gt;




&lt;h2&gt;
  
  
  19. O que é &lt;code&gt;spark-submit&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;spark-submit&lt;/code&gt; é a ferramenta de submissão de aplicações Spark.&lt;/p&gt;

&lt;p&gt;Ela permite informar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Aplicação.&lt;/li&gt;
&lt;li&gt;Cluster Manager.&lt;/li&gt;
&lt;li&gt;Deployment Mode.&lt;/li&gt;
&lt;li&gt;Recursos.&lt;/li&gt;
&lt;li&gt;Configurações.&lt;/li&gt;
&lt;li&gt;Dependências.&lt;/li&gt;
&lt;li&gt;Argumentos da aplicação.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A estrutura geral é:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spark-submit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--master&lt;/span&gt; &amp;lt;MASTER&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--deploy-mode&lt;/span&gt; &amp;lt;MODE&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; &amp;lt;&lt;span class="nv"&gt;KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;VALUE&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Os valores exatos dependem do ambiente.&lt;/p&gt;




&lt;h2&gt;
  
  
  20. Exemplo local com &lt;code&gt;spark-submit&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Vamos criar uma aplicação simples.&lt;/p&gt;

&lt;p&gt;Arquivo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conteúdo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SparkSession&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql.functions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;

&lt;span class="n"&gt;spark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;SparkSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ExemploSparkSubmit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;dados&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1_000_001&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;resultado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dados&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Quantidade de números pares:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;resultado&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Executamos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spark-submit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--master&lt;/span&gt; &lt;span class="s2"&gt;"local[*]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resultado esperado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Quantidade de números pares: 500000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esse exemplo é suficiente para compreender a estrutura básica de uma submissão.&lt;/p&gt;




&lt;h2&gt;
  
  
  21. Exemplo de submissão para YARN
&lt;/h2&gt;

&lt;p&gt;Em um ambiente YARN configurado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spark-submit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--master&lt;/span&gt; yarn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--deploy-mode&lt;/span&gt; cluster &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--driver-memory&lt;/span&gt; 4g &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--executor-memory&lt;/span&gt; 8g &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--executor-cores&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--num-executors&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aqui estamos solicitando uma aplicação com determinados recursos.&lt;/p&gt;

&lt;p&gt;Mas atenção:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Esses valores são ilustrativos, não uma recomendação universal de dimensionamento.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;O tamanho adequado depende de:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Volume de dados.&lt;/li&gt;
&lt;li&gt;Tipo de operação.&lt;/li&gt;
&lt;li&gt;Quantidade de partições.&lt;/li&gt;
&lt;li&gt;Shuffle.&lt;/li&gt;
&lt;li&gt;Memória.&lt;/li&gt;
&lt;li&gt;Concorrência.&lt;/li&gt;
&lt;li&gt;Limites do cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esse raciocínio conecta diretamente a Parte 7.&lt;/p&gt;




&lt;h2&gt;
  
  
  22. Exemplo de submissão para Kubernetes
&lt;/h2&gt;

&lt;p&gt;Em um ambiente Kubernetes com Spark configurado, uma submissão pode seguir este formato:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spark-submit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--master&lt;/span&gt; k8s://https://K8S_API_SERVER:6443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--deploy-mode&lt;/span&gt; cluster &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; spark-example &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.kubernetes.container.image&lt;span class="o"&gt;=&lt;/span&gt;REGISTRY/SPARK_IMAGE:TAG &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.executor.instances&lt;span class="o"&gt;=&lt;/span&gt;3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;local&lt;/span&gt;:///opt/spark/app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Os valores são placeholders e precisam ser substituídos pelos recursos reais do ambiente.&lt;/p&gt;

&lt;p&gt;Observe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;k8s://
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;que identifica a integração com Kubernetes.&lt;/p&gt;

&lt;p&gt;E:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local:///opt/spark/app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;que, nesse exemplo, indica um arquivo disponível dentro da imagem/container utilizado na execução.&lt;/p&gt;

&lt;p&gt;A imagem precisa conter a aplicação nesse caminho, além de estar preparada para executar Spark corretamente.&lt;/p&gt;




&lt;h2&gt;
  
  
  23. &lt;code&gt;--master&lt;/code&gt; e &lt;code&gt;--deploy-mode&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Esses dois parâmetros merecem atenção especial.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;--master&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Indica o ambiente de execução ou o gerenciador de cluster.&lt;/p&gt;

&lt;p&gt;Exemplos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local[*]
spark://HOST:7077
yarn
k8s://https://HOST:6443
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;--deploy-mode&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Indica a localização do Driver.&lt;/p&gt;

&lt;p&gt;Exemplos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client
cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Portanto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--master&lt;/span&gt; yarn &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--deploy-mode&lt;/span&gt; cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;significa:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Utilizar YARN como Cluster Manager e executar o Driver no ambiente do cluster.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;São decisões relacionadas, mas distintas.&lt;/p&gt;




&lt;h2&gt;
  
  
  24. O que acontece depois do &lt;code&gt;spark-submit&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;Uma sequência conceitual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. spark-submit
       |
       v
2. Application Submission
       |
       v
3. Driver Initialization
       |
       v
4. SparkSession / SparkContext
       |
       v
5. Resource Allocation
       |
       v
6. Executors Available
       |
       v
7. Action Triggered
       |
       v
8. Job Created
       |
       v
9. Stages Planned
       |
       v
10. Tasks Scheduled
       |
       v
11. Distributed Execution
       |
       v
12. Results / Writes
       |
       v
13. Application Completion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A ordem exata de alguns eventos depende do Cluster Manager e do modo de implantação.&lt;/p&gt;

&lt;p&gt;Mas essa sequência ajuda a conectar submissão e execução distribuída.&lt;/p&gt;




&lt;h2&gt;
  
  
  25. O Driver precisa acessar os dados?
&lt;/h2&gt;

&lt;p&gt;Essa pergunta merece cuidado.&lt;/p&gt;

&lt;p&gt;O Driver normalmente coordena o processamento, mas a leitura distribuída de grandes datasets é realizada pelos Executors.&lt;/p&gt;

&lt;p&gt;Ainda assim, dependendo do código e das fontes utilizadas, o Driver pode precisar acessar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Metadados.&lt;/li&gt;
&lt;li&gt;Catálogos.&lt;/li&gt;
&lt;li&gt;Configurações.&lt;/li&gt;
&lt;li&gt;Endpoints de serviços.&lt;/li&gt;
&lt;li&gt;Informações de schema.&lt;/li&gt;
&lt;li&gt;Caminhos e credenciais.&lt;/li&gt;
&lt;li&gt;Recursos necessários para planejamento.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Não devemos assumir que apenas os Executors precisam de conectividade com os sistemas externos.&lt;/p&gt;

&lt;p&gt;A arquitetura precisa considerar o acesso de cada componente.&lt;/p&gt;




&lt;h2&gt;
  
  
  26. O erro clássico de caminhos locais
&lt;/h2&gt;

&lt;p&gt;Imagine uma aplicação:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/usuario/dados.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ela funciona no notebook local.&lt;/p&gt;

&lt;p&gt;Depois enviamos para um cluster.&lt;/p&gt;

&lt;p&gt;Agora os Executors estão em outras máquinas.&lt;/p&gt;

&lt;p&gt;O caminho:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/home/usuario/dados.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pode não existir nesses nós.&lt;/p&gt;

&lt;p&gt;O problema não está necessariamente no código de leitura.&lt;/p&gt;

&lt;p&gt;Está na arquitetura de armazenamento.&lt;/p&gt;

&lt;p&gt;Em produção, geralmente utilizamos fontes acessíveis aos componentes relevantes do cluster, como:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HDFS
S3
ADLS
GCS
Distributed Storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ou volumes compartilhados adequadamente configurados.&lt;/p&gt;

&lt;p&gt;Essa é uma das diferenças mais importantes entre desenvolvimento local e execução distribuída.&lt;/p&gt;




&lt;h2&gt;
  
  
  27. Código local não é necessariamente código distribuído
&lt;/h2&gt;

&lt;p&gt;Considere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/usuario/config.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;arquivo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;configuracao&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arquivo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se isso acontecer no Driver, o arquivo precisa estar acessível ao Driver.&lt;/p&gt;

&lt;p&gt;Agora imagine que uma função executada pelos workers tente abrir esse mesmo caminho.&lt;/p&gt;

&lt;p&gt;Os Executors podem estar em máquinas ou containers diferentes.&lt;/p&gt;

&lt;p&gt;Então precisamos perguntar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Onde o arquivo existe?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;E:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Qual processo precisa acessá-lo?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esse raciocínio se conecta à Parte 9 sobre closures, serialização e variáveis distribuídas.&lt;/p&gt;




&lt;h2&gt;
  
  
  28. Empacotamento da aplicação
&lt;/h2&gt;

&lt;p&gt;Em projetos reais, uma aplicação Spark raramente é apenas um arquivo.&lt;/p&gt;

&lt;p&gt;Podemos ter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spark-project/
|
+-- app.py
+-- src/
|   +-- transformations/
|   +-- validations/
|   +-- connectors/
|
+-- config/
+-- tests/
+-- requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Em produção, precisamos garantir que o código e suas dependências estejam disponíveis onde serão executados.&lt;/p&gt;

&lt;p&gt;Isso pode envolver:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pacotes Python.&lt;/li&gt;
&lt;li&gt;Arquivos distribuídos.&lt;/li&gt;
&lt;li&gt;Imagens de container.&lt;/li&gt;
&lt;li&gt;Bibliotecas JVM.&lt;/li&gt;
&lt;li&gt;Dependências nativas.&lt;/li&gt;
&lt;li&gt;Configuração de ambiente.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Uma aplicação que funciona localmente pode falhar em produção simplesmente porque uma dependência não foi disponibilizada aos Executors.&lt;/p&gt;




&lt;h2&gt;
  
  
  29. Python e ambiente de execução
&lt;/h2&gt;

&lt;p&gt;Em PySpark, precisamos lembrar que existe execução de código Python associada aos workers.&lt;/p&gt;

&lt;p&gt;Isso significa que versões e dependências Python importam.&lt;/p&gt;

&lt;p&gt;Por exemplo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Driver
  |
  +-- Python Environment
  |
Executors
  |
  +-- Python Workers
      |
      +-- Required Libraries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se uma biblioteca existe apenas na máquina cliente, mas não está disponível no ambiente dos workers, determinadas operações podem falhar.&lt;/p&gt;

&lt;p&gt;Em ambientes containerizados, uma estratégia comum é construir imagens que contenham as dependências necessárias.&lt;/p&gt;

&lt;p&gt;Em outras plataformas, podemos distribuir ambientes ou pacotes conforme os mecanismos suportados.&lt;/p&gt;




&lt;h2&gt;
  
  
  30. Logs: onde procurar?
&lt;/h2&gt;

&lt;p&gt;Agora chegamos a uma questão prática.&lt;/p&gt;

&lt;p&gt;Quando uma aplicação falha, onde estão os logs?&lt;/p&gt;

&lt;p&gt;A resposta depende do ambiente.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client Mode
&lt;/h3&gt;

&lt;p&gt;Os logs do Driver normalmente estão associados ao processo de submissão.&lt;/p&gt;

&lt;p&gt;Os logs dos Executors ficam no ambiente onde eles executam.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cluster Mode
&lt;/h3&gt;

&lt;p&gt;Os logs do Driver normalmente precisam ser consultados por meio dos mecanismos da plataforma.&lt;/p&gt;

&lt;p&gt;Em Kubernetes, por exemplo, podemos investigar os logs do Driver Pod.&lt;/p&gt;

&lt;p&gt;Em YARN, podemos utilizar os mecanismos de logs da aplicação.&lt;/p&gt;

&lt;p&gt;O ponto principal é:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Saber onde o Driver executa ajuda a saber onde procurar o erro.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  31. Spark UI
&lt;/h2&gt;

&lt;p&gt;A Spark UI continua sendo uma das ferramentas mais importantes de observabilidade.&lt;/p&gt;

&lt;p&gt;Ela permite investigar informações como:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Jobs
Stages
Tasks
Executors
Storage
SQL Queries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dependendo do ambiente e da configuração, podemos acessar a interface diretamente ou por meio de proxies, túneis e interfaces da plataforma.&lt;/p&gt;

&lt;p&gt;A Spark UI normalmente utiliza a porta &lt;code&gt;4040&lt;/code&gt; como ponto inicial para aplicações em execução, mas a porta pode variar quando já está ocupada ou quando a plataforma utiliza mecanismos próprios.&lt;/p&gt;

&lt;p&gt;Não devemos assumir que a interface estará diretamente exposta na rede.&lt;/p&gt;




&lt;h2&gt;
  
  
  32. Spark History Server
&lt;/h2&gt;

&lt;p&gt;Existe uma diferença importante entre observar uma aplicação em execução e investigar uma aplicação que já terminou.&lt;/p&gt;

&lt;p&gt;Durante a execução, a Spark UI oferece informações do estado atual.&lt;/p&gt;

&lt;p&gt;Para análise posterior, podemos utilizar o &lt;strong&gt;Spark History Server&lt;/strong&gt;, quando o registro de eventos está habilitado e configurado.&lt;/p&gt;

&lt;p&gt;Conceitualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Spark Application
       |
       v
Event Logs
       |
       v
Persistent Storage
       |
       v
Spark History Server
       |
       v
Post-execution Analysis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isso é especialmente útil em ambientes de produção.&lt;/p&gt;

&lt;p&gt;Imagine um pipeline que executa às 3h da manhã.&lt;/p&gt;

&lt;p&gt;Se ele falhar, precisamos conseguir investigar o que aconteceu depois.&lt;/p&gt;




&lt;h2&gt;
  
  
  33. Falha de Executor vs. falha de Driver
&lt;/h2&gt;

&lt;p&gt;Vamos conectar com a Parte 8.&lt;/p&gt;

&lt;h3&gt;
  
  
  Executor Failure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Executor 2
    |
    v
Failure
    |
    v
Lost Tasks / Data
    |
    v
Retry / Recomputation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Em determinados cenários, Spark consegue recuperar o trabalho.&lt;/p&gt;

&lt;h3&gt;
  
  
  Driver Failure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Driver
   |
   v
Failure
   |
   v
Application Coordination Lost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A falha do Driver possui consequências diferentes.&lt;/p&gt;

&lt;p&gt;O reinício ou a recuperação da aplicação depende dos mecanismos de implantação, supervisão e tolerância a falhas disponíveis.&lt;/p&gt;

&lt;p&gt;Por isso, escolher Client Mode ou Cluster Mode também influencia a estratégia operacional.&lt;/p&gt;




&lt;h2&gt;
  
  
  34. Cluster Mode não significa alta disponibilidade automática
&lt;/h2&gt;

&lt;p&gt;Esse é um ponto importante.&lt;/p&gt;

&lt;p&gt;Executar o Driver dentro do cluster não garante, por si só, que ele seja automaticamente recuperado de qualquer falha.&lt;/p&gt;

&lt;p&gt;Podemos ter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cluster Mode
     |
     v
Driver Pod
     |
     v
Failure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A recuperação depende de mecanismos como:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supervisão da aplicação.&lt;/li&gt;
&lt;li&gt;Políticas de reinício.&lt;/li&gt;
&lt;li&gt;Recursos do Cluster Manager.&lt;/li&gt;
&lt;li&gt;Orquestração externa.&lt;/li&gt;
&lt;li&gt;Persistência de estado.&lt;/li&gt;
&lt;li&gt;Reexecução do Job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Portanto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cluster Mode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;não é sinônimo de:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exactly-once execution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;nem de:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Automatic complete recovery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essas garantias precisam ser analisadas separadamente.&lt;/p&gt;




&lt;h2&gt;
  
  
  35. Orquestração de Jobs Spark
&lt;/h2&gt;

&lt;p&gt;Em ambientes reais, aplicações Spark frequentemente fazem parte de pipelines maiores.&lt;/p&gt;

&lt;p&gt;Por exemplo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Airflow / Orchestrator
        |
        v
spark-submit
        |
        v
Spark Application
        |
        v
Data Lake
        |
        v
Validation
        |
        v
Analytics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O orquestrador controla aspectos como:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agendamento.&lt;/li&gt;
&lt;li&gt;Dependências entre Jobs.&lt;/li&gt;
&lt;li&gt;Retentativas.&lt;/li&gt;
&lt;li&gt;Alertas.&lt;/li&gt;
&lt;li&gt;Parâmetros.&lt;/li&gt;
&lt;li&gt;Monitoramento do workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;O Spark executa o processamento distribuído.&lt;/p&gt;

&lt;p&gt;São responsabilidades complementares.&lt;/p&gt;




&lt;h2&gt;
  
  
  36. Uma arquitetura de produção
&lt;/h2&gt;

&lt;p&gt;Imagine uma plataforma de Engenharia de Dados:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 ORCHESTRATOR
                      |
                      v
                 SPARK JOB
                      |
                      v
                 CLUSTER MANAGER
                      |
             +--------+--------+
             |        |        |
             v        v        v
          DRIVER   EXECUTOR  EXECUTOR
             |        |        |
             +--------+--------+
                      |
                      v
                  DATA LAKE
                      |
                      v
               DATA QUALITY
                      |
                      v
                 ANALYTICS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Na prática, o Driver coordena os Executors, e o Cluster Manager administra os recursos necessários à aplicação.&lt;/p&gt;

&lt;p&gt;Esse tipo de arquitetura exige decisões sobre:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deployment Mode.&lt;/li&gt;
&lt;li&gt;Recursos.&lt;/li&gt;
&lt;li&gt;Armazenamento.&lt;/li&gt;
&lt;li&gt;Dependências.&lt;/li&gt;
&lt;li&gt;Logs.&lt;/li&gt;
&lt;li&gt;Segurança.&lt;/li&gt;
&lt;li&gt;Observabilidade.&lt;/li&gt;
&lt;li&gt;Recuperação.&lt;/li&gt;
&lt;li&gt;Custos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Não basta que o código funcione.&lt;/p&gt;

&lt;p&gt;Precisamos que ele seja operável.&lt;/p&gt;




&lt;h2&gt;
  
  
  37. Um checklist antes de implantar
&lt;/h2&gt;

&lt;p&gt;Antes de enviar uma aplicação Spark para produção, gosto de responder:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Qual Cluster Manager será utilizado?&lt;/li&gt;
&lt;li&gt;O Driver executará em Client ou Cluster Mode?&lt;/li&gt;
&lt;li&gt;Onde os Executors serão iniciados?&lt;/li&gt;
&lt;li&gt;Driver e Executors conseguem se comunicar?&lt;/li&gt;
&lt;li&gt;Os dados estão acessíveis ao cluster?&lt;/li&gt;
&lt;li&gt;As dependências Python estão disponíveis?&lt;/li&gt;
&lt;li&gt;As credenciais estão configuradas com segurança?&lt;/li&gt;
&lt;li&gt;Qual é a política de recursos?&lt;/li&gt;
&lt;li&gt;Onde ficam os logs do Driver?&lt;/li&gt;
&lt;li&gt;Onde ficam os logs dos Executors?&lt;/li&gt;
&lt;li&gt;Os event logs estão habilitados?&lt;/li&gt;
&lt;li&gt;Como a aplicação será monitorada?&lt;/li&gt;
&lt;li&gt;Como falhas serão tratadas?&lt;/li&gt;
&lt;li&gt;O Job pode ser reexecutado com segurança?&lt;/li&gt;
&lt;li&gt;Como serão controlados custos e consumo de recursos?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Essas perguntas transformam uma aplicação experimental em uma aplicação operacionalmente preparada.&lt;/p&gt;




&lt;h2&gt;
  
  
  38. Problemas comuns de implantação
&lt;/h2&gt;

&lt;p&gt;Alguns erros aparecem repetidamente.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A aplicação funciona localmente, mas falha no cluster.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Possíveis causas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dependências ausentes.&lt;/li&gt;
&lt;li&gt;Arquivos locais indisponíveis.&lt;/li&gt;
&lt;li&gt;Credenciais incorretas.&lt;/li&gt;
&lt;li&gt;Diferenças de versão.&lt;/li&gt;
&lt;li&gt;Configuração de rede.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Executors não conseguem se conectar ao Driver.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Possíveis causas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Firewall.&lt;/li&gt;
&lt;li&gt;DNS.&lt;/li&gt;
&lt;li&gt;Endereço anunciado incorretamente.&lt;/li&gt;
&lt;li&gt;Portas.&lt;/li&gt;
&lt;li&gt;Configuração de rede.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A aplicação é encerrada após fechar a sessão cliente.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Verifique se o Driver está executando em Client Mode e como o processo é supervisionado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;O Job termina, mas não conseguimos investigar sua execução.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Verifique event logs, retenção e configuração do History Server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;O Driver funciona, mas Tasks falham com erro de biblioteca.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Verifique as dependências nos ambientes dos workers.&lt;/p&gt;

&lt;p&gt;Esses problemas não são necessariamente erros de lógica de transformação.&lt;/p&gt;

&lt;p&gt;Muitas vezes são erros de implantação.&lt;/p&gt;




&lt;h2&gt;
  
  
  39. O que muda em plataformas gerenciadas?
&lt;/h2&gt;

&lt;p&gt;Em plataformas gerenciadas de Spark, muitos detalhes de infraestrutura são abstraídos.&lt;/p&gt;

&lt;p&gt;Podemos encontrar interfaces para:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Criar clusters.&lt;/li&gt;
&lt;li&gt;Definir Jobs.&lt;/li&gt;
&lt;li&gt;Configurar recursos.&lt;/li&gt;
&lt;li&gt;Consultar logs.&lt;/li&gt;
&lt;li&gt;Executar notebooks.&lt;/li&gt;
&lt;li&gt;Agendar pipelines.&lt;/li&gt;
&lt;li&gt;Gerenciar dependências.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Isso simplifica a operação.&lt;/p&gt;

&lt;p&gt;Mas não elimina os fundamentos.&lt;/p&gt;

&lt;p&gt;Continuamos tendo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Driver
Executors
Tasks
Partitions
Shuffle
Memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A plataforma pode esconder parte da complexidade operacional.&lt;/p&gt;

&lt;p&gt;Ela não elimina a física do processamento distribuído.&lt;/p&gt;




&lt;h2&gt;
  
  
  40. A relação com custos
&lt;/h2&gt;

&lt;p&gt;Agora imagine duas aplicações.&lt;/p&gt;

&lt;h3&gt;
  
  
  Aplicação A
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Driver
  |
  v
4 Executors
  |
  v
Job completes in 10 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Aplicação B
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Driver
  |
  v
20 Executors
  |
  v
Job completes in 9 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A aplicação B utiliza muito mais recursos para economizar apenas um minuto.&lt;/p&gt;

&lt;p&gt;Dependendo do modelo de cobrança, isso pode ser economicamente pior.&lt;/p&gt;

&lt;p&gt;Mais Executors não significam automaticamente melhor eficiência.&lt;/p&gt;

&lt;p&gt;Precisamos avaliar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution Time
      +
Resource Consumption
      +
Infrastructure Cost
      +
Operational Requirements
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esse raciocínio conecta performance tuning e FinOps.&lt;/p&gt;




&lt;h2&gt;
  
  
  41. O modelo mental completo
&lt;/h2&gt;

&lt;p&gt;Agora podemos conectar os dez primeiros capítulos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  SPARK APPLICATION
                         |
                         v
                     SUBMISSION
                         |
                         v
                   DEPLOYMENT MODE
                         |
              +----------+----------+
              |                     |
              v                     v
           CLIENT                 CLUSTER
              |                     |
              +----------+----------+
                         |
                         v
                       DRIVER
                         |
                         v
                    SPARK PLAN
                         |
                         v
                       JOBS
                         |
                         v
                      STAGES
                         |
                         v
                       TASKS
                         |
                         v
                     EXECUTORS
                         |
              +----------+----------+
              |          |          |
              v          v          v
           MEMORY      SHUFFLE     CPU
              |          |          |
              +----------+----------+
                         |
                         v
                       RESULT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essa visão mostra que a arquitetura do Spark possui duas dimensões complementares.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A dimensão de implantação:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Where does it run?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A dimensão de execução:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How does it process data?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para trabalhar com Spark em produção, precisamos compreender as duas.&lt;/p&gt;




&lt;h2&gt;
  
  
  42. A principal lição deste capítulo
&lt;/h2&gt;

&lt;p&gt;Se eu tivesse que resumir Client Mode vs. Cluster Mode em uma única ideia, seria:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;O Deployment Mode define onde o Driver executa; o Cluster Manager administra os recursos; e o Spark continua responsável por coordenar o processamento distribuído.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Parece uma distinção simples.&lt;/p&gt;

&lt;p&gt;Mas ela influencia:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Arquitetura.&lt;/li&gt;
&lt;li&gt;Rede.&lt;/li&gt;
&lt;li&gt;Disponibilidade.&lt;/li&gt;
&lt;li&gt;Observabilidade.&lt;/li&gt;
&lt;li&gt;Troubleshooting.&lt;/li&gt;
&lt;li&gt;Segurança.&lt;/li&gt;
&lt;li&gt;Operação.&lt;/li&gt;
&lt;li&gt;Custos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quando sabemos onde cada componente executa, muitos erros deixam de parecer misteriosos.&lt;/p&gt;




&lt;h2&gt;
  
  
  43. Como penso hoje
&lt;/h2&gt;

&lt;p&gt;Quando recebo um problema como:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A aplicação Spark funciona no notebook, mas falha em produção.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Não começo necessariamente pelo DataFrame.&lt;/p&gt;

&lt;p&gt;Primeiro quero entender:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Qual é o ambiente?
        |
        v
Qual é o Cluster Manager?
        |
        v
Client ou Cluster Mode?
        |
        v
Onde está o Driver?
        |
        v
Onde estão os Executors?
        |
        v
Os dados são acessíveis?
        |
        v
As dependências estão disponíveis?
        |
        v
Existe conectividade?
        |
        v
Onde estão os logs?
        |
        v
Qual componente falhou?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Só depois avanço para o plano físico, memória, shuffle e operações distribuídas, quando esses forem os pontos relevantes.&lt;/p&gt;

&lt;p&gt;Essa abordagem reduz o tempo de diagnóstico e evita alterar configurações que não têm relação com a causa real.&lt;/p&gt;




&lt;h2&gt;
  
  
  Próximo artigo
&lt;/h2&gt;

&lt;p&gt;Agora sabemos como uma aplicação Spark é submetida e onde seus componentes executam.&lt;/p&gt;

&lt;p&gt;Mas existe outro desafio importante:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Como configurar corretamente os recursos de uma aplicação Spark?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No próximo capítulo vamos aprofundar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;spark.executor.instances&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;spark.executor.cores&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;spark.executor.memory&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;spark.driver.memory&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Dynamic Allocation.&lt;/li&gt;
&lt;li&gt;Paralelismo.&lt;/li&gt;
&lt;li&gt;Número de partições.&lt;/li&gt;
&lt;li&gt;Resource sizing.&lt;/li&gt;
&lt;li&gt;Relação entre CPU, memória e shuffle.&lt;/li&gt;
&lt;li&gt;Dimensionamento para workloads batch.&lt;/li&gt;
&lt;li&gt;Trade-offs entre desempenho e custo.&lt;/li&gt;
&lt;li&gt;Estratégias de benchmarking.&lt;/li&gt;
&lt;li&gt;Como evitar configurações copiadas sem diagnóstico.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vamos conectar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Workload
   |
   v
Data Volume
   |
   v
Partitions
   |
   v
Parallelism
   |
   v
Executor Resources
   |
   v
Execution Time
   |
   v
Infrastructure Cost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Porque configurar recursos não é apenas escolher números.&lt;/p&gt;

&lt;p&gt;É compreender como a aplicação utiliza o cluster.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Série:&lt;/strong&gt; Como aprendi Apache Spark&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;← Parte 9:&lt;/strong&gt; Broadcast, Accumulators e Closures&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Próximo:&lt;/strong&gt; Parte 11 — Dimensionamento e configuração de recursos no Apache Spark&lt;/p&gt;

</description>
      <category>bigdata</category>
      <category>spark</category>
      <category>apache</category>
      <category>python</category>
    </item>
    <item>
      <title>Wie man zwei Versionen eines PDF-Vertrags in Python Wort für Wort vergleicht</title>
      <dc:creator>Hanako H.</dc:creator>
      <pubDate>Fri, 06 Nov 2026 19:50:01 +0000</pubDate>
      <link>https://dev.to/bestaitools-de/wie-man-zwei-versionen-eines-pdf-vertrags-in-python-wort-fur-wort-vergleicht-9cc</link>
      <guid>https://dev.to/bestaitools-de/wie-man-zwei-versionen-eines-pdf-vertrags-in-python-wort-fur-wort-vergleicht-9cc</guid>
      <description>&lt;p&gt;Ein PDF hat keine Änderungsverfolgung. Wenn ein Vertrag als PDF „mit ein paar kleinen Anpassungen“ zurückkommt, steckt die einzige Information darüber, was sich geändert hat, im Unterschied zwischen zwei Dateien. Es gibt drei naheliegende Wege, diesen Unterschied zu berechnen: die gerenderten Seiten Pixel für Pixel vergleichen, den extrahierten Text Zeile für Zeile vergleichen oder ihn Wort für Wort vergleichen. Auf demselben Dateipaar liefern sie sehr unterschiedliche Ergebnisse, und nur einer davon übersteht es, wenn die Textverarbeitung der Gegenseite den Text neu umbricht.&lt;/p&gt;

&lt;p&gt;Unten stehen Messungen aller drei Ansätze an einem echten PDF-Paar und danach ein Vergleich auf Wortebene in rund 120 Zeilen Python (PyMuPDF plus &lt;code&gt;difflib&lt;/code&gt; aus der Standardbibliothek), der die Boxen jedes geänderten Worts ermittelt und die Änderungen als Kommentare zurück ins PDF schreibt. Der PDF Editor in der Desktop-App von &lt;a href="https://osfoundry.io/de" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; funktioniert genauso: Sein Dialog „Dateien vergleichen“ erklärt, dass der Text Wort für Wort verglichen und die Unterschiede in der neuen Datei angezeigt werden, und bei diesem Paar meldet er dieselben fünf Änderungen wie das Skript unten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Das Testpaar
&lt;/h2&gt;

&lt;p&gt;Die Dateien sind ein frei erfundener einseitiger Vertrag über Designleistungen (Example), erzeugt mit PyMuPDF, damit die Textebene echt ist. Die zurückgeschickte Version enthält fünf Änderungen:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Klausel&lt;/th&gt;
&lt;th&gt;Verschickt&lt;/th&gt;
&lt;th&gt;Zurückgekommen&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3. Payment&lt;/td&gt;
&lt;td&gt;within 30 days. Late payments add a 2% fee each month.&lt;/td&gt;
&lt;td&gt;within 60 days.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Revisions&lt;/td&gt;
&lt;td&gt;two rounds of revisions&lt;/td&gt;
&lt;td&gt;unlimited rounds of revisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Ownership&lt;/td&gt;
&lt;td&gt;the final logo once the fee is paid in full.&lt;/td&gt;
&lt;td&gt;the final logo and all drafts once work begins.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Um zu simulieren, dass die Software der Gegenseite den Text neu setzt, enthält eine dritte Datei exakt dieselben Wörter der zurückgeschickten Version, neu gesetzt in 12 pt auf einer 300 pt breiten Spalte statt in 11.5 pt auf 468 pt. Gleicher Text, andere Zeilenumbrüche.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pixel, Zeilen oder Wörter
&lt;/h2&gt;

&lt;p&gt;Der Pixel-Diff rendert jeweils die erste Seite beider Dateien mit 72 dpi in Graustufen und zählt die Zeilen, in denen irgendein Pixel um mehr als 40 Stufen abweicht; ein „Band“ ist eine zusammenhängende Folge solcher Zeilen. Der Zeilen-Diff lässt &lt;code&gt;difflib.SequenceMatcher&lt;/code&gt; über &lt;code&gt;page.get_text()&lt;/code&gt; laufen, aufgeteilt an Zeilenumbrüchen. Der Wort-Diff ist das Skript weiter unten.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Neue Datei&lt;/th&gt;
&lt;th&gt;Pixel-Diff&lt;/th&gt;
&lt;th&gt;Zeilen-Diff&lt;/th&gt;
&lt;th&gt;Wort-Diff&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;So wie zurückgeschickt&lt;/td&gt;
&lt;td&gt;30 von 792 Zeilen, 3 Bänder&lt;/td&gt;
&lt;td&gt;3 Hunks, 3 Zeilen&lt;/td&gt;
&lt;td&gt;5 Änderungen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gleiche Wörter, neu gesetzt&lt;/td&gt;
&lt;td&gt;284 von 792 Zeilen, 20 Bänder&lt;/td&gt;
&lt;td&gt;6 Hunks, 9 Zeilen&lt;/td&gt;
&lt;td&gt;5 Änderungen&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Bei der Datei, so wie sie zurückkam, findet der Pixel-Diff drei Bereiche und der Zeilen-Diff drei Zeilen. Beides stimmt, aber keiner von beiden sagt, was sich geändert hat: Der Zeilen-Diff meldet Klausel 3 als eine ersetzte Zeile, und „30“ gegenüber „60“ musst du immer noch selbst finden. Bei der neu gesetzten Kopie leuchten im Pixel-Diff 20 einzelne Bänder über fast den ganzen Text auf, und der Zeilen-Diff verdoppelt sich auf 9 betroffene Zeilen, weil sich der Umbruch verschoben hat. Der Wort-Diff liefert bei beiden Dateien dieselben fünf Änderungen, weil Leerraum und Zeilenumbrüche bei ihm gar nicht erst ankommen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zeichen samt Boxen extrahieren
&lt;/h2&gt;

&lt;p&gt;Um Änderungen zu finden, reicht ein einfacher &lt;code&gt;get_text()&lt;/code&gt;-String, aber um sie auf der Seite zu markieren, braucht man Koordinaten. &lt;code&gt;get_text("rawdict")&lt;/code&gt; liefert jedes Zeichen mit seiner Bounding Box. Die folgende Funktion macht daraus eine flache Liste in Lesereihenfolge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;difflib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;char_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;All characters of a PDF in reading order, NFKC-normalised, each with its page and box.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;chars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pymupdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rawdict&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;blocks&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lines&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;spans&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;chars&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;NFKC&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;c&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
                            &lt;span class="n"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bbox&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])))&lt;/span&gt;
                &lt;span class="n"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# a line break is only whitespace
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chars&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zwei Details sind wichtig. &lt;code&gt;sort=True&lt;/code&gt; sortiert die Blöcke von oben nach unten und von links nach rechts, und genau das will man bei einem einspaltigen Vertrag (zweispaltige Layouts brauchen eine eigene Sortierung). NFKC führt ähnlich aussehende Zeichen zusammen, sodass ein auf der einen Seite getipptes vollbreites „６０“ und ein „60“ auf der anderen nicht als Änderung auftauchen. Jedes normalisierte Zeichen behält die Box des Zeichens, aus dem es entstanden ist, damit die Liste deckungsgleich mit der Seite bleibt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tokens: Wörter, und dort, wo es keine Leerzeichen gibt, jeweils ein Zeichen
&lt;/h2&gt;

&lt;p&gt;Der Diff arbeitet auf Tokens, nicht auf Zeichen. Ein Token ist hier eine Folge lateinischer Buchstaben und Ziffern oder ein beliebiges anderes einzelnes sichtbares Zeichen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[0-9A-Za-z][0-9A-Za-z.,%$&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;’-]*(?&amp;lt;![.,])|\S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finditer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Für Englisch ergibt das Wörter, wobei ein abschließender Punkt abgetrennt wird, damit „days.“ und „days“ trotzdem übereinstimmen. Zahlen bleiben am Stück, eine Änderung erscheint also als „30“ → „60“ und nicht als „3“ → „6“.&lt;/p&gt;

&lt;p&gt;Der Teil mit dem „beliebigen anderen einzelnen Zeichen“ ist für Sprachen ohne Leerzeichen zwischen den Wörtern gedacht. Teilt man eine japanische Version desselben Vertrags an Leerraum auf, was PyMuPDFs &lt;code&gt;get_text("words")&lt;/code&gt; macht, bekommt man ein „Wort“ pro Satz, und der Diff meldet drei komplette Sätze als ersetzt. Mit einem Token pro Zeichen findet er die Änderungen innerhalb der Sätze, allerdings als 8 Fragmente, weil gemeinsame Zeichen wie の eine Änderung in Stücke zerteilen („2“ gelöscht, „まで“ → „数“, „制限なく“ eingefügt). Führt man Änderungen zusammen, die höchstens ein unverändertes Zeichen trennt, sind es nur noch 5, also dieselben fünf Änderungen wie in der englischen Datei. Im Englischen greift das Zusammenführen nie, denn die kürzeste Lücke zwischen zwei Änderungen („once“) ist vier Zeichen lang.&lt;/p&gt;

&lt;h2&gt;
  
  
  Der Diff
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;PUNCT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.。．!?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hunks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old_toks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_toks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gap_chars&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;old_toks&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;new_toks&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j2&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;difflib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SequenceMatcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autojunk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get_opcodes&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;equal&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;delete&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;insert&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;delete&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PUNCT&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PUNCT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;i1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;pi1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pi2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pj1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pj2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pi2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;gap_chars&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pj2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;j1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;gap_chars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pi1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pj1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;i1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Die Verschiebeschleife fängt eine Eigenheit ab, die jeder Diff-Algorithmus hat. Der gelöschte Satz steht zwischen „days“ und „4. Revisions“, und der Text drumherum lautet auf der einen Seite „. Late payments add a 2% fee each month.“ und auf der anderen „.“. „. Late … month“ zu löschen und „Late … month.“ zu löschen ist gleich minimal, und &lt;code&gt;SequenceMatcher&lt;/code&gt; hat sich für Ersteres entschieden. Schiebt man eine reine Löschung oder Einfügung so lange über die wiederholten Tokens, bis sie auf einem Satzzeichen endet, erhält man die Variante, die ein Mensch markieren würde.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;autojunk=False&lt;/code&gt; ist Absicht; dazu weiter unten mehr. Auf die beiden Dateien angewendet, gibt das vollständige Skript (&lt;code&gt;contract_diff.py&lt;/code&gt;) Folgendes aus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5 changes
1. replaced  '30' -&amp;gt; '60'
2. deleted   'Late payments add a 2% fee each month.' -&amp;gt; ''
3. replaced  'two' -&amp;gt; 'unlimited'
4. inserted  '' -&amp;gt; 'and all drafts'
5. replaced  'the fee is paid in full' -&amp;gt; 'work begins'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;und für die neu gesetzte Kopie exakt dieselben fünf Zeilen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Die Änderungen als Kommentare zurückschreiben
&lt;/h2&gt;

&lt;p&gt;Jedes Token trägt die Boxen seiner Zeichen mit sich, also besteht das Markieren einer Änderung in der neuen Datei aus einer Hervorhebungsannotation über diesen Boxen, zeilenweise zusammengefasst, mit altem und neuem Text als Kommentar. Bei einer Löschung bleibt nichts zum Hervorheben übrig, deshalb bekommt sie ein Notizsymbol direkt hinter dem Text, der ihr vorausging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_pdf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_pdf&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_pdf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;by_page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;boxes&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;rects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;by_page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rects&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;y0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# same line: one box, not one per character
&lt;/span&gt;                &lt;span class="n"&gt;rects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;rects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pymupdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;replaced&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Replaced: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; with &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deleted&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Deleted: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;inserted&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Inserted: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;}[&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;kind&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rects&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;by_page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pno&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;kind&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deleted&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;annot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_text_annot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;tr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Comment&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;annot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_highlight_annot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rects&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;annot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;annot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;compare&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;annot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_pdf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgu4ox6625jda1yi16sdv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgu4ox6625jda1yi16sdv.jpg" alt="Der zurückgeschickte Vertrag mit den vier geänderten Formulierungen hervorgehoben und einem Kommentarsymbol an der Stelle, wo der Satz zur Verzugsgebühr gelöscht wurde"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Das sind ganz normale PDF-Annotationen, also zeigen gängige PDF-Viewer sie an, und wer die Änderungen geschickt hat, sieht genau, worauf sich deine Rückfrage bezieht.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lange Verträge: autojunk und Klausel für Klausel
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SequenceMatcher&lt;/code&gt; hat eine Heuristik namens autojunk: Hat die zweite Sequenz 200 oder mehr Elemente, wird jedes Element, das mehr als 1% davon ausmacht, als Ankerpunkt für Übereinstimmungen ignoriert. Gedacht ist das für Dinge wie Leerzeilen in Quellcode. In Vertragstext sind die häufigen Tokens Wörter wie „the“ oder im Japanischen の und 甲 (die Bezeichnung des Vertrags für eine der Parteien). Bei der einseitigen japanischen Datei (238 Tokens in der neuen Version) hat sie 18 verschiedene Zeichen aussortiert, darunter 甲, 乙, の und 。, und das Ergebnis änderte sich von 8 Hunks auf 6. In diesem Fall nicht schlechter, aber anders, und so etwas will man bei einem Dokument, das man gleich unterschreibt, nicht einer Heuristik überlassen.&lt;/p&gt;

&lt;p&gt;Das Abschalten hat seinen Preis. Im schlimmsten Fall ist der Diff ungefähr quadratisch, und ein 40-seitiger Vertrag ist weit länger als eine Seite. Zum Messen wurde der japanische Vertrag 10- und 100-mal wiederholt und nur die letzte Kopie bearbeitet, auf einem M1-Mac mit Python 3.12:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;th&gt;autojunk=False&lt;/th&gt;
&lt;th&gt;autojunk=True&lt;/th&gt;
&lt;th&gt;Klausel für Klausel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2,550&lt;/td&gt;
&lt;td&gt;12 ms&lt;/td&gt;
&lt;td&gt;3 ms&lt;/td&gt;
&lt;td&gt;1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25,500&lt;/td&gt;
&lt;td&gt;1,142–1,564 ms&lt;/td&gt;
&lt;td&gt;163–209 ms&lt;/td&gt;
&lt;td&gt;7–10 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Die Werte für 25,500 Tokens sind die Spanne aus drei Läufen; gleichzeitig lasteten andere Jobs die CPU aus, und zwei frühere Läufe unter höherer Last brauchten ohne autojunk bis zu 4.2 Sekunden.&lt;/p&gt;

&lt;p&gt;Die letzte Spalte ist die Lösung. Verträge sind ohnehin in nummerierte Klauseln gegliedert, also teilt man beide Texte an den Klauselüberschriften auf, ordnet die Klauseln mit einem einzigen &lt;code&gt;SequenceMatcher&lt;/code&gt; über die Klauselliste einander zu (identische Klauseln passen als ganze Elemente, das kostet kaum etwas) und lässt den Token-Diff nur innerhalb der Paare laufen, die sich unterscheiden. So bleibt &lt;code&gt;autojunk=False&lt;/code&gt; erhalten, und es ist trotzdem die schnellste der drei Varianten. In &lt;code&gt;measure.py&lt;/code&gt; erfolgt die Aufteilung beim japanischen Text per Regex auf 第N条; für Englisch würde ein Muster auf Zeilen wie „3. Payment“ dasselbe leisten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wo das an seine Grenzen kommt
&lt;/h2&gt;

&lt;p&gt;Alles oben liest die Textebene. Ein eingescannter Vertrag hat keine und braucht zuerst OCR, danach tauchen OCR-Fehler als Änderungen auf. Tabellen, die in falscher Lesereihenfolge extrahiert werden, erzeugen verrauschte Diffs. Und eine Klausel, die verschoben statt bearbeitet wurde, erscheint als eine Löschung plus eine Einfügung; um Verschiebungen zu erkennen, braucht es einen zweiten Durchgang, der gelöschte und eingefügte Blöcke miteinander abgleicht.&lt;/p&gt;

&lt;p&gt;Die Skripte (&lt;code&gt;contract_diff.py&lt;/code&gt;, &lt;code&gt;measure.py&lt;/code&gt; und die Generatoren für die Testdateien) laufen mit &lt;code&gt;uvx --with pymupdf python&lt;/code&gt;. Der PDF Editor mit denselben Schritten zum Vergleichen, Markieren als Kommentare und Erstellen eines Berichts gehört zur kostenlosen Desktop-App auf der &lt;a href="https://osfoundry.io/de/download" rel="noopener noreferrer"&gt;Download-Seite von osFoundry&lt;/a&gt; und läuft wie der Rest der &lt;a href="https://osfoundry.io/de/platform/local-first" rel="noopener noreferrer"&gt;Local-first-Plattform von osFoundry&lt;/a&gt; auf deinem eigenen Rechner.&lt;/p&gt;

</description>
      <category>python</category>
      <category>pdf</category>
      <category>difflib</category>
      <category>vertraege</category>
    </item>
    <item>
      <title>Notas offline con SQLite: guardado idempotente y recuperación tras reiniciar</title>
      <dc:creator> AppInitDev</dc:creator>
      <pubDate>Fri, 06 Nov 2026 19:46:54 +0000</pubDate>
      <link>https://dev.to/appinitdev/notas-offline-con-sqlite-guardado-idempotente-y-recuperacion-tras-reiniciar-4j85</link>
      <guid>https://dev.to/appinitdev/notas-offline-con-sqlite-guardado-idempotente-y-recuperacion-tras-reiniciar-4j85</guid>
      <description>&lt;h2&gt;
  
  
  Notas offline con SQLite: guardado idempotente y recuperación tras reiniciar
&lt;/h2&gt;

&lt;p&gt;Una nota es útil si sobrevive al cierre de la aplicación. Antes de agregar colores, tareas y recordatorios, conviene responder una pregunta: ¿qué ocurre si el proceso se destruye después de escribir?&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema y el alcance del ejemplo
&lt;/h2&gt;

&lt;p&gt;La ficha de &lt;strong&gt;Notepad: Bloc de notas tareas&lt;/strong&gt; resume su función así: «Apunta ideas, crea listas de tareas y organiza tus recordatorios diarios.» Notepad presenta notas, listas y recordatorios. Para ilustrar el almacenamiento local, construiremos una &lt;strong&gt;nota con actualización idempotente&lt;/strong&gt; en SQLite. El esquema es didáctico, no una declaración sobre la base de datos usada por Notepad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo reproducir la idea
&lt;/h2&gt;

&lt;p&gt;Necesitas Python 3. Guarda este archivo como &lt;code&gt;notas.py&lt;/code&gt;; al ejecutarlo se creará &lt;code&gt;notas_demo.sqlite3&lt;/code&gt; en la carpeta actual.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;notas_demo.sqlite3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;CREATE TABLE IF NOT EXISTS notas (
        id INTEGER PRIMARY KEY,
        contenido TEXT NOT NULL,
        modificado TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    )&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO notas (id, contenido) VALUES (?, ?)
        ON CONFLICT(id) DO UPDATE SET
            contenido=excluded.contenido,
            modificado=CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Comprar fruta y revisar tareas&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;notas_demo.sqlite3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;nota&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SELECT contenido FROM notas WHERE id=?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,)).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Nota recuperada:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nota&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nota&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;no existe&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Qué demuestra el código
&lt;/h2&gt;

&lt;p&gt;La instrucción &lt;code&gt;ON CONFLICT(id) DO UPDATE&lt;/code&gt; evita crear duplicados cuando se guarda repetidamente la misma nota con un identificador conocido. El bloque &lt;code&gt;with&lt;/code&gt; confirma los cambios al salir sin errores, y la segunda conexión comprueba la persistencia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pruebas recomendadas
&lt;/h2&gt;

&lt;p&gt;Ejecuta dos veces: el contenido debe seguir siendo una sola nota. Cambia el texto de la instrucción &lt;code&gt;INSERT&lt;/code&gt; y ejecuta de nuevo: se reemplaza el contenido de la nota &lt;code&gt;1&lt;/code&gt;. Abre el archivo SQLite con un visor si quieres comprobar las filas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Límites que conviene reconocer
&lt;/h2&gt;

&lt;p&gt;No hay cifrado, búsquedas, adjuntos, conflictos entre dispositivos, backups ni gestión de notas borradas. La marca &lt;code&gt;CURRENT_TIMESTAMP&lt;/code&gt; usa una precisión limitada; para sincronización suelen necesitarse identificadores estables, revisiones y reglas explícitas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué aprendemos
&lt;/h2&gt;

&lt;p&gt;Persistir no es simplemente «guardar una cadena»: es &lt;strong&gt;definir identidad, consistencia y recuperación&lt;/strong&gt; después de que la interfaz desaparece.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contexto y enlaces del autor
&lt;/h2&gt;

&lt;p&gt;El tema se relaciona con las funciones declaradas públicamente de &lt;strong&gt;Notepad: Bloc de notas tareas&lt;/strong&gt;, una aplicación Android de AppInitDev. &lt;strong&gt;El código anterior es un ejemplo independiente&lt;/strong&gt;: no se ha inspeccionado el repositorio privado ni se afirma que sea la implementación interna de la app. La referencia aparece al final para ofrecer contexto, no como requisito para completar el tutorial.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Play:&lt;/strong&gt; &lt;a href="https://play.google.com/store/apps/details?id=com.appinitdev.notes.premium" rel="noopener noreferrer"&gt;Notepad: Bloc de notas tareas&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Página de la aplicación:&lt;/strong&gt; &lt;a href="https://notes-premium.appinitdev.com/" rel="noopener noreferrer"&gt;https://notes-premium.appinitdev.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sitio del desarrollador:&lt;/strong&gt; &lt;a href="https://appinitdev.com/" rel="noopener noreferrer"&gt;AppInitDev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8wi3atn84e5ekyxwoeku.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8wi3atn84e5ekyxwoeku.png" alt="Image APPS" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Referencia técnica
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.sqlite.org/lang_UPSERT.html" rel="noopener noreferrer"&gt;SQLite: UPSERT&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sqlite</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Método de bisección con Python: convergencia y condiciones que no debes omitir</title>
      <dc:creator> AppInitDev</dc:creator>
      <pubDate>Fri, 06 Nov 2026 19:45:25 +0000</pubDate>
      <link>https://dev.to/appinitdev/metodo-de-biseccion-con-python-convergencia-y-condiciones-que-no-debes-omitir-jo5</link>
      <guid>https://dev.to/appinitdev/metodo-de-biseccion-con-python-convergencia-y-condiciones-que-no-debes-omitir-jo5</guid>
      <description>&lt;h2&gt;
  
  
  Método de bisección con Python: convergencia y condiciones que no debes omitir
&lt;/h2&gt;

&lt;p&gt;Un método numérico no es únicamente una fórmula. También necesita una condición de entrada, un criterio de parada y una forma de avisar cuando no puede ofrecer una solución fiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema y el alcance del ejemplo
&lt;/h2&gt;

&lt;p&gt;La ficha de &lt;strong&gt;Numerical Methods: Cálculo&lt;/strong&gt; resume su función así: «Resuelve raíces, interpolación y mínimos cuadrados con iteraciones paso a paso» Numerical Methods incluye búsqueda de raíces y visualización de iteraciones según su descripción pública. Vamos a reproducir &lt;strong&gt;bisección&lt;/strong&gt; para una ecuación continua, con validación del intervalo y tolerancia explícita. Este ejemplo no es código de la app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo reproducir la idea
&lt;/h2&gt;

&lt;p&gt;Necesitas Python 3. Guarda &lt;code&gt;biseccion.py&lt;/code&gt; y ejecútalo sin dependencias externas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;biseccion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tolerancia&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_iter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;fa&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fa&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fb&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fa&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;fb&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;El intervalo no tiene cambio de signo&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;paso&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_iter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="n"&gt;fm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;paso&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: x=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, f(x)=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fm&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;tolerancia&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;tolerancia&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fa&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;fm&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fm&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fm&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;No convergió dentro del límite&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;raiz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;biseccion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Raíz aproximada:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raiz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Qué demuestra el código
&lt;/h2&gt;

&lt;p&gt;Cada paso conserva un intervalo con cambio de signo. La implementación muestra los valores de la función y permite ver cómo el ancho del intervalo disminuye. Bisección es robusta bajo sus hipótesis, aunque puede converger más despacio que otros métodos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pruebas recomendadas
&lt;/h2&gt;

&lt;p&gt;El resultado debe aproximarse a &lt;code&gt;1.52138&lt;/code&gt;. Cambia el intervalo por &lt;code&gt;[3, 4]&lt;/code&gt; para observar el error de validación. Reduce &lt;code&gt;max_iter&lt;/code&gt; a &lt;code&gt;2&lt;/code&gt; y verifica que el algoritmo no finge haber encontrado una respuesta exacta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Límites que conviene reconocer
&lt;/h2&gt;

&lt;p&gt;El método exige continuidad en el intervalo y un cambio de signo apropiado; existen raíces sin cambio de signo que esta verificación no detecta. No incorpora análisis de error de redondeo ni métodos para funciones discontinuas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué aprendemos
&lt;/h2&gt;

&lt;p&gt;Un programa educativo debe hacer visibles &lt;strong&gt;las hipótesis y los criterios de parada&lt;/strong&gt;. Una tabla de iteraciones puede enseñar más que un número final.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contexto y enlaces del autor
&lt;/h2&gt;

&lt;p&gt;El tema se relaciona con las funciones declaradas públicamente de &lt;strong&gt;Numerical Methods: Cálculo&lt;/strong&gt;, una aplicación Android de AppInitDev. &lt;strong&gt;El código anterior es un ejemplo independiente&lt;/strong&gt;: no se ha inspeccionado el repositorio privado ni se afirma que sea la implementación interna de la app. La referencia aparece al final para ofrecer contexto, no como requisito para completar el tutorial.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Play:&lt;/strong&gt; &lt;a href="https://play.google.com/store/apps/details?id=com.appinitdev.methods.premium" rel="noopener noreferrer"&gt;Numerical Methods: Cálculo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Página de la aplicación:&lt;/strong&gt; &lt;a href="https://methods-premium.appinitdev.com/" rel="noopener noreferrer"&gt;https://methods-premium.appinitdev.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sitio del desarrollador:&lt;/strong&gt; &lt;a href="https://appinitdev.com/" rel="noopener noreferrer"&gt;AppInitDev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdslx9uqbp4f0uayjt9yz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdslx9uqbp4f0uayjt9yz.png" alt="Image APPS" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Referencia técnica
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.root_scalar.html" rel="noopener noreferrer"&gt;SciPy: root_scalar&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>algorithms</category>
      <category>mathematics</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Fotos y privacidad: cómo generar una copia sin EXIF antes de compartir</title>
      <dc:creator> AppInitDev</dc:creator>
      <pubDate>Fri, 06 Nov 2026 19:43:56 +0000</pubDate>
      <link>https://dev.to/appinitdev/fotos-y-privacidad-como-generar-una-copia-sin-exif-antes-de-compartir-2kbj</link>
      <guid>https://dev.to/appinitdev/fotos-y-privacidad-como-generar-una-copia-sin-exif-antes-de-compartir-2kbj</guid>
      <description>&lt;h2&gt;
  
  
  Fotos y privacidad: cómo generar una copia sin EXIF antes de compartir
&lt;/h2&gt;

&lt;p&gt;Una fotografía puede revelar más que su contenido visual. La información EXIF puede contener la hora, el dispositivo y, en algunos casos, coordenadas GPS. Compartir una imagen no debería implicar compartir metadatos por accidente.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema y el alcance del ejemplo
&lt;/h2&gt;

&lt;p&gt;La ficha de &lt;strong&gt;Gallery: Álbum de Fotos Seguro&lt;/strong&gt; resume su función así: «Galería de fotos con caja fuerte privada para álbumes, imágenes y vídeos.» Gallery presenta una galería con opciones de privacidad y eliminación de EXIF en su ficha pública. Este laboratorio explica la idea de &lt;strong&gt;crear una copia nueva sin reutilizar los metadatos del original&lt;/strong&gt;. No supone que la app utilice Pillow ni este procedimiento.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo reproducir la idea
&lt;/h2&gt;

&lt;p&gt;Instala Pillow con &lt;code&gt;python -m pip install Pillow&lt;/code&gt;. Coloca una fotografía de prueba llamada &lt;code&gt;origen.jpg&lt;/code&gt; junto al script. Utiliza un archivo de prueba, no una foto sensible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;PIL&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ImageOps&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;origen.jpg&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;foto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Aplica la orientación EXIF a los píxeles antes de retirarla.
&lt;/span&gt;    &lt;span class="n"&gt;orientada&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ImageOps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exif_transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;foto&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;RGB&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Lienzo nuevo: no copiamos el diccionario EXIF del origen.
&lt;/span&gt;    &lt;span class="n"&gt;salida&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;RGB&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orientada&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;salida&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paste&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orientada&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;salida&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;para_compartir.png&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PNG&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;para_compartir.png&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;verificacion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;EXIF de salida:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verificacion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getexif&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Qué demuestra el código
&lt;/h2&gt;

&lt;p&gt;La copia se reconstruye a partir de píxeles sin pasar &lt;code&gt;exif=&lt;/code&gt; a &lt;code&gt;save&lt;/code&gt;. Usamos PNG por simplicidad y para evitar pérdida adicional por JPEG. La rotación se aplica antes porque, de otro modo, una foto tomada en vertical podría aparecer girada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pruebas recomendadas
&lt;/h2&gt;

&lt;p&gt;Inspecciona ambos archivos con un visor de metadatos y comprueba si la copia tiene coordenadas. Observa también el tamaño final: PNG puede ser considerablemente más grande que JPEG para fotografías.&lt;/p&gt;

&lt;h2&gt;
  
  
  Límites que conviene reconocer
&lt;/h2&gt;

&lt;p&gt;Eliminar EXIF &lt;strong&gt;no anonimiza el contenido visual&lt;/strong&gt;: rostros, matrículas, reflejos o información dibujada en la imagen permanecen visibles. El ejemplo convierte a RGB, por lo que no conserva transparencia ni todos los perfiles de color.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué aprendemos
&lt;/h2&gt;

&lt;p&gt;La privacidad de una galería depende tanto de permisos y almacenamiento como del &lt;strong&gt;contenido que se entrega al compartir&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contexto y enlaces del autor
&lt;/h2&gt;

&lt;p&gt;El tema se relaciona con las funciones declaradas públicamente de &lt;strong&gt;Gallery: Álbum de Fotos Seguro&lt;/strong&gt;, una aplicación Android de AppInitDev. &lt;strong&gt;El código anterior es un ejemplo independiente&lt;/strong&gt;: no se ha inspeccionado el repositorio privado ni se afirma que sea la implementación interna de la app. La referencia aparece al final para ofrecer contexto, no como requisito para completar el tutorial.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Play:&lt;/strong&gt; &lt;a href="https://play.google.com/store/apps/details?id=com.appinitdev.gallery" rel="noopener noreferrer"&gt;Gallery: Álbum de Fotos Seguro&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Página de la aplicación:&lt;/strong&gt; &lt;a href="https://gallery.appinitdev.com/" rel="noopener noreferrer"&gt;https://gallery.appinitdev.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sitio del desarrollador:&lt;/strong&gt; &lt;a href="https://appinitdev.com/" rel="noopener noreferrer"&gt;AppInitDev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11wtqiqdkt3dl8uh3voz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11wtqiqdkt3dl8uh3voz.png" alt="Image APPS" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Referencia técnica
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pillow.readthedocs.io/en/stable/reference/ImageOps.html#PIL.ImageOps.exif_transpose" rel="noopener noreferrer"&gt;Pillow: ExifTranspose&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>privacy</category>
      <category>android</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building Healthcare Provider Lists From the Free NPI Registry (and Getting Past Its 1,200-Result Cap)</title>
      <dc:creator>Ben Barazandeh</dc:creator>
      <pubDate>Fri, 06 Nov 2026 19:38:48 +0000</pubDate>
      <link>https://dev.to/dedicated_crest/building-healthcare-provider-lists-from-the-free-npi-registry-and-getting-past-its-1200-result-8a</link>
      <guid>https://dev.to/dedicated_crest/building-healthcare-provider-lists-from-the-free-npi-registry-and-getting-past-its-1200-result-8a</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I'm Ben, and I built the Apify Actor mentioned later in this post. I charge per result for it. Everything in the tutorial part works with free tools too.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you sell to dentists, clinics, chiropractors or therapists, or you need to verify a list of providers, the best free source in the US is the &lt;strong&gt;NPI Registry&lt;/strong&gt; (NPPES), run by CMS. Every HIPAA-covered provider has a National Provider Identifier, about 8 million of them, and the registry has a public API with no key.&lt;/p&gt;

&lt;p&gt;Each record has the NPI, name, credential, taxonomies (specialties), license, practice and mailing addresses with &lt;strong&gt;phone and fax&lt;/strong&gt;, the date the NPI was issued, and for organizations the &lt;strong&gt;authorized official&lt;/strong&gt; (often the owner or CEO) with title and phone.&lt;/p&gt;

&lt;p&gt;What it doesn't have: &lt;strong&gt;email addresses&lt;/strong&gt;. Plan around that from the start.&lt;/p&gt;

&lt;p&gt;If you need &lt;em&gt;everything&lt;/em&gt;, CMS also publishes the full NPPES data dissemination file (monthly, plus weekly updates). It's free and complete, and it's a very large CSV with hundreds of columns. For targeted lists, the API is easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Query the API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://npiregistry.cms.hhs.gov/api/?version=2.1&amp;amp;taxonomy_description=Dentist&amp;amp;city=San Diego&amp;amp;state=CA&amp;amp;limit=200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;limit&lt;/code&gt; is at most 200, and you page with &lt;code&gt;skip&lt;/code&gt;. Here's a pager that also keeps only providers whose &lt;em&gt;primary&lt;/em&gt; taxonomy is a dentist one. It ran as-is on 25 Oct 2026:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;npi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://npiregistry.cms.hhs.gov/api/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;skip&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="c1"&gt;# skip is capped at 1000 -&amp;gt; max 1,200 per query
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
        &lt;span class="n"&gt;skip&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;npi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;taxonomy_description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dentist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;San Diego&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;primary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;primary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;desc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;taxonomies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;with a dentist taxonomy as primary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1200
1193 with a dentist taxonomy as primary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exactly 1,200 is suspicious, and it should be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every query stops at 1,200 results.&lt;/strong&gt; &lt;code&gt;skip&lt;/code&gt; is capped at 1000, and a page holds 200. Worse, it fails silently: I asked for &lt;code&gt;skip=1000&lt;/code&gt;, &lt;code&gt;skip=1200&lt;/code&gt; and &lt;code&gt;skip=2000&lt;/code&gt; on a California dentist search, and all three returned the same page. If you just keep paging, you collect duplicates and think you're done. To get a whole city or state, split the search (by city, or by ZIP prefix, and for a state by both) until each piece is under 1,200, then dedupe on NPI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specialty matches &lt;em&gt;any&lt;/em&gt; taxonomy.&lt;/strong&gt; A provider with Dentist as a secondary taxonomy still matches. Above, 7 of 1,200 didn't have a dentist taxonomy as primary. For strict lists, check the &lt;code&gt;primary&lt;/code&gt; flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two addresses.&lt;/strong&gt; Each record has a &lt;code&gt;MAILING&lt;/code&gt; and a &lt;code&gt;LOCATION&lt;/code&gt; address, and in the record I checked the mailing address came first in the list. Use &lt;code&gt;address_purpose&lt;/code&gt;, not position. City and state searches can match either one, so a "San Diego" search can include providers who only &lt;em&gt;mail&lt;/em&gt; from there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"New practices" isn't a query parameter.&lt;/strong&gt; The API can't filter on the NPI issue date (&lt;code&gt;enumeration_date&lt;/code&gt;). To find newly enumerated providers, you pull and filter client-side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-reported data.&lt;/strong&gt; Phones and fax numbers are what providers entered, and some records haven't been updated in years. &lt;code&gt;last_updated&lt;/code&gt; tells you how stale a record is.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Or use the packaged version
&lt;/h2&gt;

&lt;p&gt;I packaged this as an Apify Actor, &lt;a href="https://apify.com/dedicated_crest/npi-healthcare-provider-leads" rel="noopener noreferrer"&gt;NPI Registry Scraper – NPPES Healthcare Provider Leads&lt;/a&gt;. The main thing it adds is &lt;strong&gt;auto-splitting&lt;/strong&gt;: large searches are split by state and ZIP prefix, so you can pull a whole state past the 1,200 cap. When it splits by state, it keeps providers whose &lt;em&gt;practice&lt;/em&gt; location is in that state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run-ready link:&lt;/strong&gt; &lt;a href="https://console.apify.com/actors/AeZTbhypXKAXC4S18/input" rel="noopener noreferrer"&gt;open it in Apify Console with an example input already filled in&lt;/a&gt; (&lt;code&gt;{"specialties": ["Dentist"], "states": ["CA"], "cities": ["San Diego, CA"], "maxItems": 50}&lt;/code&gt;). Click Start, or change the input first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"specialties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Dentist"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"states"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CA"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"San Diego, CA"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other inputs: individual vs organization, ZIPs, names, a list of NPIs to verify, and filters for &lt;strong&gt;NPI issued after&lt;/strong&gt;, record updated after, only with phone, and primary specialty must match. Output is deduplicated, with formatted phones and ZIPs and separate practice and mailing addresses.&lt;/p&gt;

&lt;p&gt;That input ran on 18 Oct 2026 in about 18 seconds and returned 50 providers. Some rows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;NPI&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Primary specialty&lt;/th&gt;
&lt;th&gt;NPI issued&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1326171653&lt;/td&gt;
&lt;td&gt;Dr. WILLIAM JOSEPH DI ZINNO, DMD&lt;/td&gt;
&lt;td&gt;Individual&lt;/td&gt;
&lt;td&gt;Dentist, General Practice&lt;/td&gt;
&lt;td&gt;2007-03-14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1699214478&lt;/td&gt;
&lt;td&gt;GLENN C. ASHMORE DDS, INC.&lt;/td&gt;
&lt;td&gt;Organization&lt;/td&gt;
&lt;td&gt;Dentist&lt;/td&gt;
&lt;td&gt;2017-02-14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1730639188&lt;/td&gt;
&lt;td&gt;A.S. KIM DDS, INC.&lt;/td&gt;
&lt;td&gt;Organization&lt;/td&gt;
&lt;td&gt;Dentist, General Practice&lt;/td&gt;
&lt;td&gt;2016-10-10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1487251294&lt;/td&gt;
&lt;td&gt;Dr. ZEKI ABDULAALI, DDS&lt;/td&gt;
&lt;td&gt;Individual&lt;/td&gt;
&lt;td&gt;Dentist, General Practice&lt;/td&gt;
&lt;td&gt;2020-10-05&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Organization rows are the useful ones for B2B. For &lt;code&gt;A.S. KIM DDS, INC.&lt;/code&gt;, the row has the authorized official (a DDS, title "CEO") with a phone, the practice phone, license number, other names (DBA "VILLAGE HILLCREST DENTAL"), and the count of additional practice locations.&lt;/p&gt;

&lt;p&gt;A bigger real example from my testing: all individual chiropractors in California with a phone came to 3,000 providers in about 3 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A monthly "new practices" list
&lt;/h2&gt;

&lt;p&gt;Set &lt;em&gt;NPI issued after&lt;/em&gt; (&lt;code&gt;enumeratedAfter&lt;/code&gt;) to 90 days ago, pick your specialties and states, and schedule it monthly. New clinicians and new group practices show up as they get their NPIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No emails.&lt;/strong&gt; NPPES doesn't publish them. You get practice phone and fax, the mailing address and, for organizations, the authorized official. If you need an email, you'll need the practice's website.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self-reported and sometimes stale.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NPI holders only.&lt;/strong&gt; It covers HIPAA-covered providers, not every wellness or cosmetic business.&lt;/li&gt;
&lt;li&gt;Not affiliated with CMS or HHS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;$0.001 per provider&lt;/strong&gt; ($1 per 1,000), no start fee. Duplicates and filtered-out records are free, and runs stop at your "maximum cost per run". The 50-provider test was 5 cents. The API and the full CMS file are free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Play fair
&lt;/h2&gt;

&lt;p&gt;Practice phones are business lines, but authorized officials are people. TCPA rules apply to calls and texts, CAN-SPAM to email, and some providers will rightly be annoyed by cold outreach. Be relevant and honour opt-outs.&lt;/p&gt;

&lt;p&gt;Do you work with NPPES data? I'd like to hear how you deal with stale records, so tell me in the comments.&lt;/p&gt;

</description>
      <category>healthcare</category>
      <category>opendata</category>
      <category>python</category>
      <category>api</category>
    </item>
    <item>
      <title>Cómo evitar vender inventario inexistente: actualización condicional en SQLite</title>
      <dc:creator> AppInitDev</dc:creator>
      <pubDate>Fri, 06 Nov 2026 19:38:34 +0000</pubDate>
      <link>https://dev.to/appinitdev/como-evitar-vender-inventario-inexistente-actualizacion-condicional-en-sqlite-cpi</link>
      <guid>https://dev.to/appinitdev/como-evitar-vender-inventario-inexistente-actualizacion-condicional-en-sqlite-cpi</guid>
      <description>&lt;h2&gt;
  
  
  Cómo evitar vender inventario inexistente: actualización condicional en SQLite
&lt;/h2&gt;

&lt;p&gt;Una venta debería descontar existencias solo si queda cantidad suficiente. El clásico patrón «leer stock, restar y guardar» puede fallar cuando dos operaciones se ejecutan casi a la vez.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema y el alcance del ejemplo
&lt;/h2&gt;

&lt;p&gt;La ficha de &lt;strong&gt;POSManager: TPV y Caja Ventas&lt;/strong&gt; resume su función así: «TPV y caja para comercios: controla ventas, stock y tickets desde el móvil.» POSManager describe cobros, ventas, tickets y control de stock. Para estudiar una decisión de diseño vinculada al inventario, usamos una &lt;strong&gt;actualización SQL condicionada dentro de una transacción&lt;/strong&gt;. No estamos describiendo la base de datos real del producto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo reproducir la idea
&lt;/h2&gt;

&lt;p&gt;Puedes ejecutarlo con Python 3, que incluye SQLite. Guarda el archivo como &lt;code&gt;venta.py&lt;/code&gt; y ejecútalo. La base está en memoria para que cada prueba comience desde cero.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;

&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:memory:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CREATE TABLE productos(id INTEGER PRIMARY KEY, stock INTEGER NOT NULL)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CREATE TABLE ventas(producto INTEGER, cantidad INTEGER)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO productos VALUES(1, 3)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;vender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;producto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cantidad&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cantidad&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cantidad inválida&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# transacción y rollback automático ante error
&lt;/span&gt;        &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE productos SET stock=stock-? WHERE id=? AND stock&amp;gt;=?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cantidad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;producto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cantidad&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rowcount&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stock insuficiente o producto inexistente&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO ventas VALUES(?, ?)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;producto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cantidad&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nf"&gt;vender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stock restante:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT stock FROM productos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;vender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Venta rechazada:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Qué demuestra el código
&lt;/h2&gt;

&lt;p&gt;La condición &lt;code&gt;stock &amp;gt;= ?&lt;/code&gt; se verifica en la misma instrucción que descuenta inventario. Si ninguna fila fue modificada, la venta no se inserta. La transacción relaciona los dos efectos: movimiento de stock y registro de venta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pruebas recomendadas
&lt;/h2&gt;

&lt;p&gt;Deberías obtener stock &lt;code&gt;1&lt;/code&gt; tras la primera venta y rechazo de la segunda. Prueba vender &lt;code&gt;0&lt;/code&gt; unidades y un identificador desconocido. Comprueba que la tabla de ventas contiene únicamente una fila.&lt;/p&gt;

&lt;h2&gt;
  
  
  Límites que conviene reconocer
&lt;/h2&gt;

&lt;p&gt;La prueba no modela devoluciones, cobros, sincronización offline, precios, impuestos ni múltiples dispositivos con inventarios divergentes. En un sistema distribuido la transacción local no resuelve por sí sola conflictos entre terminales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué aprendemos
&lt;/h2&gt;

&lt;p&gt;El stock es una &lt;strong&gt;invariante de negocio&lt;/strong&gt;; conviene protegerla en la operación de escritura, no solo mediante validaciones en la interfaz.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contexto y enlaces del autor
&lt;/h2&gt;

&lt;p&gt;El tema se relaciona con las funciones declaradas públicamente de &lt;strong&gt;POSManager: TPV y Caja Ventas&lt;/strong&gt;, una aplicación Android de AppInitDev. &lt;strong&gt;El código anterior es un ejemplo independiente&lt;/strong&gt;: no se ha inspeccionado el repositorio privado ni se afirma que sea la implementación interna de la app. La referencia aparece al final para ofrecer contexto, no como requisito para completar el tutorial.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Play:&lt;/strong&gt; &lt;a href="https://play.google.com/store/apps/details?id=com.appinitdev.posmanager" rel="noopener noreferrer"&gt;POSManager: TPV y Caja Ventas&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Página de la aplicación:&lt;/strong&gt; &lt;a href="https://posmanager.appinitdev.com/" rel="noopener noreferrer"&gt;https://posmanager.appinitdev.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sitio del desarrollador:&lt;/strong&gt; &lt;a href="https://appinitdev.com/" rel="noopener noreferrer"&gt;AppInitDev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxu1ysf9isrs9szokz62d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxu1ysf9isrs9szokz62d.png" alt="APPS" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Referencia técnica
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.sqlite.org/lang_transaction.html" rel="noopener noreferrer"&gt;SQLite: transacciones&lt;/a&gt;&lt;/p&gt;

</description>
      <category>sqlite</category>
      <category>python</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Alternative Storage Providers — Signed User Files and Retention Recovery</title>
      <dc:creator>MordecaiNilsson7582</dc:creator>
      <pubDate>Fri, 06 Nov 2026 19:38:14 +0000</pubDate>
      <link>https://dev.to/mordecainilsson7582/alternative-storage-providers-signed-user-files-and-retention-recovery-2gma</link>
      <guid>https://dev.to/mordecainilsson7582/alternative-storage-providers-signed-user-files-and-retention-recovery-2gma</guid>
      <description>&lt;p&gt;Short answer: choose the recovery contract before comparing storage rates. For a B2B SaaS product storing user files that must remain available until an explicit deletion deadline, Backblaze B2 is the easiest place to run a free-tier experiment, Wasabi is a storage-focused option without a free tier, and AWS S3 is the broad ecosystem baseline. The right choice depends on what happens after a timeout, a rate limit, or a worker crash.&lt;/p&gt;

&lt;p&gt;The implementation I would ship starts with one invariant: each document has a stable application ID, and every write attempt uses it. The database owns the document state and deletion deadline. Object storage owns bytes. A worker reconciles the two, uses private access, and returns a presigned URL when a user needs to download a document.&lt;/p&gt;

&lt;p&gt;For a team considering an alternative platform, Infrai is a narrower fit: one key and one bill can cover storage beside other backend services, while its public discovery surface describes request schemas and runnable examples. I would validate the storage contract before committing a signed-document workflow to it: &lt;a href="https://docs.infrai.cc/en/guides/storage/answers/store-user-uploaded-documents-%63%68%65%61%70%65%73%74-backend-option-d/" rel="noopener noreferrer"&gt;https://docs.infrai.cc/en/guides/storage/answers/store-user-uploaded-documents-%63%68%65%61%70%65%73%74-backend-option-d/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Retries are part of the data model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure path is part of the storage decision
&lt;/h2&gt;

&lt;p&gt;A client can disconnect after the service has accepted the final bytes. A rate limit can arrive after a multipart session has created state. A worker can time out while the object exists but the metadata row still says &lt;code&gt;uploading&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I start with the application identity rather than the provider's object key. The key can contain a version or a random suffix, but the database decides which object is current. That gives retries a destination without asking a provider to infer intent, and it prevents a second attempt from silently replacing a signed file.&lt;/p&gt;

&lt;p&gt;The deletion deadline belongs in the database and worker queue. A lifecycle rule is a useful second line of defense, not the only legal or product record. The shortest lifecycle supported by this storage surface is 1 day, and unfinished multipart fragments do not have an automatic cleanup rule, so abandoned sessions need an explicit worker path.&lt;/p&gt;

&lt;p&gt;Here is the small policy test I would run before connecting a provider adapter. The fake client makes this runnable as-is. Its stable idempotency key, status-aware retry, and &lt;code&gt;Retry-After&lt;/code&gt; handling are the parts worth carrying into production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UploadResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;object_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RetryableUploadError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FakeObjectStore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;object_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RetryableUploadError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;UploadResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;object_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;put_with_recovery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;object_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signed-document:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;object_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;RetryableUploadError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unreachable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeObjectStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;put_with_recovery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signed/contract-1842.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;example bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contract-1842&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;429&lt;/code&gt; branch is only half the point. Every attempt carries the same identity. A real adapter must surface non-2xx responses, honor &lt;code&gt;Retry-After&lt;/code&gt;, and distinguish a timeout from a confirmed rejection. Ignoring the response body is how a useful upload becomes an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which alternative storage providers offer a free tier for storing user files safely?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Useful starting point&lt;/th&gt;
&lt;th&gt;Main boundary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backblaze B2&lt;/td&gt;
&lt;td&gt;Free-tier experiments and S3-compatible migration&lt;/td&gt;
&lt;td&gt;Validate access-request and bandwidth behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wasabi&lt;/td&gt;
&lt;td&gt;Predictable storage-focused operation&lt;/td&gt;
&lt;td&gt;No free tier for experimentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS S3&lt;/td&gt;
&lt;td&gt;Broad ecosystem and a familiar control case&lt;/td&gt;
&lt;td&gt;Calculate storage, bandwidth, and operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;One key and one bill across backend services&lt;/td&gt;
&lt;td&gt;Private signed storage when limits fit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Backblaze B2 and Wasabi are the strongest cost-conscious alternatives here, with different experiments in mind. B2 is useful for a free-tier trial and offers competitive pricing on additional storage and access requests. Wasabi has a low flat-rate storage model, but the lack of a free tier makes experimentation a paid exercise. Both provide S3-compatible APIs, which reduces migration work, but compatibility does not make their retry, deletion, and bandwidth behavior identical.&lt;/p&gt;

&lt;p&gt;AWS S3 is the baseline I would keep in the comparison. It is the control case for compatibility, surrounding tooling, and organizational familiarity. Its bill still needs to be calculated across storage, bandwidth, and operations; that calculation matters especially for a file-sharing product with large downloads.&lt;/p&gt;

&lt;p&gt;Test all three with the same 10 GB fixture set, retry schedule, and download pattern. Record completion, recovery after a dropped connection, rate-limit behavior, deletion timing, and the application work required to reconcile an object with its database row. This is a test plan, not a claimed benchmark.&lt;/p&gt;

&lt;p&gt;Infrai belongs in a narrower part of the decision. Its positioning is one key and one bill across backend services, so a team that already uses several backend capabilities does not have to gather credentials across a dozen provider dashboards or reconcile a pile of invoices. The second advantage is operationally relevant here: the API is plain REST, and its public discovery surface exposes request schemas and runnable examples; documented capabilities have examples in 10 languages. A Python service can inspect the contract and send HTTP without adding a storage SDK, while another service can use the same platform convention.&lt;/p&gt;

&lt;p&gt;My recommendation is explicit: try Infrai for a workflow that values one backend key and bill plus a self-describing REST contract, using private signed-document storage only when its storage boundaries fit. Choose B2 when a low-friction trial is the deciding constraint, Wasabi when its storage-focused model fits the workload, and S3 when ecosystem breadth is the priority.&lt;/p&gt;

&lt;p&gt;The limitation is a selection criterion. The storage surface has no public or public-read ACL, so it does not fit permanent public links, image hosting, or static-site hosting. It has no object versioning or object lock, so it is not the sole control for immutable, finance-grade retention. There is no &lt;code&gt;If-Match&lt;/code&gt; conditional write, so strict concurrent updates need queue or database coordination. Browser-direct uploads cannot be configured through an independent CORS route, so that transfer boundary needs separate handling. A storage specialist is the better choice for those cases.&lt;/p&gt;

&lt;p&gt;The database decides.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should the recovery and deletion contract be tested?
&lt;/h2&gt;

&lt;p&gt;I use a short failure matrix. Kill the connection during a large upload and verify resume or a safe new attempt under the same document identity. Force a &lt;code&gt;429&lt;/code&gt; and verify exponential backoff rather than a tight loop. Crash the worker after storage succeeds but before the database transaction finishes; a reconciler should make one deliberate decision. Run deletion twice and make the second pass harmless.&lt;/p&gt;

&lt;p&gt;Inspect what can actually be queried. Listing supports prefix filtering, but server-side metadata search is not available. Keep tenant ownership, signer, document state, and deletion deadline queryable in the database rather than assuming object metadata can be searched later.&lt;/p&gt;

&lt;p&gt;For multipart work, keep an upload record with the provider upload ID, document ID, expected size, checksum policy, and state transitions. Clean up abandoned sessions through a worker. When a presigned URL is returned, do not send the service authorization header to that URL; the URL is the transfer authorization.&lt;/p&gt;

&lt;p&gt;The checklist belongs beside the adapter: stable document identity, private access, database-owned deadlines, status-aware retries, idempotent consumers, reconciliation, and a provider-specific large-file test. Add an audit event for each state transition. Then measure bandwidth and operations with representative sharing traffic.&lt;/p&gt;

&lt;p&gt;For the private write, the adapter can keep the verified route and transport rules in one small function. It uses an environment variable, an explicit method, a stable idempotency key, and a complete URL. The live request schema should still be checked before choosing payload fields.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.error&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;URLError&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlopen&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;put_private_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/storage/object/put/documents/signed%2Fcontract-1842.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;# Copyable request shape: curl -X PUT 'https://api.infrai.cc/v1/storage/object/put/documents/signed%2Fcontract-1842.pdf' -H 'Authorization: Bearer $INFRAI_API_KEY' -H 'Idempotency-Key: signed-document:contract-1842' -H 'Content-Type: application/octet-stream' --data-binary @signed/contract-1842.pdf
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signed-document:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/octet-stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;storage returned HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;
            &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;URLError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nf"&gt;put_private_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signed/contract-1842.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contract-1842&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request is deliberately private. A download path should ask the service for a presigned URL, then use that URL as the transfer authorization; it should not attach the Infrai bearer token to the returned URL. The application still decides who may access the document and when the database state allows deletion.&lt;/p&gt;

&lt;p&gt;One more failure is easy to miss. Suppose the final part arrives, the storage response is delayed, and the worker retries while the first request is still being processed. Without a stable key, the database can point at one object while a second object consumes space, or a late response can overwrite the state written by the retry. With a stable document ID, the reconciler has something concrete to compare: the expected object key, the recorded provider request, the byte count, and the deletion deadline.&lt;/p&gt;

&lt;p&gt;That does not remove every provider-specific decision. It makes the decision visible, testable, and boring to operate after a network failure, which is exactly what a retention workflow needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;OWASP, File Upload Cheat Sheet: &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html" rel="noopener noreferrer"&gt;https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS, Amazon S3 User Guide: &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Backblaze, B2 Cloud Storage: &lt;a href="https://www.backblaze.com/cloud-storage" rel="noopener noreferrer"&gt;https://www.backblaze.com/cloud-storage&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Wasabi, Wasabi Hot Cloud Storage: &lt;a href="https://wasabi.com/cloud-storage-pricing/" rel="noopener noreferrer"&gt;https://wasabi.com/cloud-storage-pricing/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Infrai, Storage bucket discovery: &lt;a href="https://api.infrai.cc/v1/discovery/storage.bucket.create" rel="noopener noreferrer"&gt;https://api.infrai.cc/v1/discovery/storage.bucket.create&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>storage</category>
      <category>python</category>
      <category>saas</category>
      <category>reliability</category>
    </item>
    <item>
      <title>Nunca uses float para dinero: un presupuesto reproducible con Decimal</title>
      <dc:creator> AppInitDev</dc:creator>
      <pubDate>Fri, 06 Nov 2026 19:33:38 +0000</pubDate>
      <link>https://dev.to/appinitdev/nunca-uses-float-para-dinero-un-presupuesto-reproducible-con-decimal-5afg</link>
      <guid>https://dev.to/appinitdev/nunca-uses-float-para-dinero-un-presupuesto-reproducible-con-decimal-5afg</guid>
      <description>&lt;h2&gt;
  
  
  Nunca uses float para dinero: un presupuesto reproducible con Decimal
&lt;/h2&gt;

&lt;p&gt;Los errores de redondeo parecen pequeños hasta que se acumulan en reportes, presupuestos y saldos. El modelo numérico importa incluso en una herramienta personal sin conexiones bancarias.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema y el alcance del ejemplo
&lt;/h2&gt;

&lt;p&gt;La ficha de &lt;strong&gt;Vault Wallet: Control Gastos&lt;/strong&gt; resume su función así: «Gestor de gastos y planificador de presupuesto para tus finanzas personales.» Vault Wallet anuncia registro de gastos, presupuestos y manejo de varias monedas. El ejemplo toma una decisión que se puede evaluar sin conocer el código privado de la app: &lt;strong&gt;usar decimales explícitos y no sumar divisas diferentes&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo reproducir la idea
&lt;/h2&gt;

&lt;p&gt;Necesitas Python 3; pega este código en &lt;code&gt;presupuesto.py&lt;/code&gt; y ejecútalo con &lt;code&gt;python presupuesto.py&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;

&lt;span class="n"&gt;movimientos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categoria&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comida&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;moneda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MXN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;importe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;129.90&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categoria&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comida&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;moneda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MXN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;importe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;70.10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categoria&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transporte&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;moneda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MXN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;importe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;35.50&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categoria&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;servicios&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;moneda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;USD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;importe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9.99&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;limites&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comida&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MXN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;250.00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="n"&gt;totales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;movimientos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;clave&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categoria&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;moneda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;totales&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;clave&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;totales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clave&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;importe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;clave&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gastado&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;totales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
    &lt;span class="n"&gt;limite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;limites&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clave&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clave&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gastado=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;gastado&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restante=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;limite&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gastado&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;limite&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sin límite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Qué demuestra el código
&lt;/h2&gt;

&lt;p&gt;El total en comida es exactamente &lt;code&gt;200.00 MXN&lt;/code&gt;. Si usamos números binarios de punto flotante pueden aparecer representaciones intermedias como &lt;code&gt;0.30000000000000004&lt;/code&gt;. Agrupar por &lt;code&gt;(categoría, moneda)&lt;/code&gt; evita que un total en pesos incorpore dólares silenciosamente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pruebas recomendadas
&lt;/h2&gt;

&lt;p&gt;Cambia el segundo gasto a &lt;code&gt;120.10&lt;/code&gt;: el presupuesto queda en negativo y puede convertirse en alerta. Agrega otra operación en USD: la suma se conserva separada. Prueba también importes negativos para devoluciones, definiendo explícitamente cómo deben tratarse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Límites que conviene reconocer
&lt;/h2&gt;

&lt;p&gt;No hay persistencia, impuestos, tasas de cambio, sincronización ni reglas fiscales. En producción hay que fijar políticas de redondeo, moneda principal, fechas, devoluciones y versiones de tasas antes de generar reportes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué aprendemos
&lt;/h2&gt;

&lt;p&gt;No todo se arregla con una pantalla de presupuesto. La precisión monetaria y la semántica de cada operación forman parte del modelo de datos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contexto y enlaces del autor
&lt;/h2&gt;

&lt;p&gt;El tema se relaciona con las funciones declaradas públicamente de &lt;strong&gt;Vault Wallet: Control Gastos&lt;/strong&gt;, una aplicación Android de AppInitDev. &lt;strong&gt;El código anterior es un ejemplo independiente&lt;/strong&gt;: no se ha inspeccionado el repositorio privado ni se afirma que sea la implementación interna de la app. La referencia aparece al final para ofrecer contexto, no como requisito para completar el tutorial.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Play:&lt;/strong&gt; &lt;a href="https://play.google.com/store/apps/details?id=com.appinitdev.wallet" rel="noopener noreferrer"&gt;Vault Wallet: Control Gastos&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Página de la aplicación:&lt;/strong&gt; &lt;a href="https://wallet.appinitdev.com/" rel="noopener noreferrer"&gt;https://wallet.appinitdev.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sitio del desarrollador:&lt;/strong&gt; &lt;a href="https://appinitdev.com/" rel="noopener noreferrer"&gt;AppInitDev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu10knqe3zahnv8gg09ql.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu10knqe3zahnv8gg09ql.png" alt="APPS" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Referencia técnica
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.python.org/3/library/decimal.html" rel="noopener noreferrer"&gt;Python: &lt;code&gt;decimal&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>fintech</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Jak mierzyć widoczność marki w AI Search? Powtarzalny pomiar krok po kroku</title>
      <dc:creator>FunkyMEDIA</dc:creator>
      <pubDate>Fri, 06 Nov 2026 19:26:12 +0000</pubDate>
      <link>https://dev.to/funkymedia/jak-mierzyc-widocznosc-marki-w-ai-search-powtarzalny-pomiar-krok-po-kroku-4kjg</link>
      <guid>https://dev.to/funkymedia/jak-mierzyc-widocznosc-marki-w-ai-search-powtarzalny-pomiar-krok-po-kroku-4kjg</guid>
      <description>&lt;p&gt;&lt;strong&gt;Jak sprawdzić, czy AI wymienia moją markę?&lt;/strong&gt; Przygotuj stały zestaw pytań klientów, zapisuj odpowiedzi wraz z datą i warunkami testu, a następnie sprawdzaj obecność oraz rolę marki. Udział odpowiedzi z nazwą firmy licz wyłącznie wśród poprawnie zebranych odpowiedzi. Taki wynik opisuje badaną próbkę, a nie całą widoczność marki w internecie.&lt;/p&gt;

&lt;p&gt;W tym poradniku zbudujemy prosty dziennik obserwacji AI Search i policzymy pierwszy wskaźnik w Pythonie. FunkyMEDIA będzie nazwą szukanej marki w przykładzie. Wszystkie odpowiedzi demonstracyjne są syntetyczne: nie pochodzą z rzeczywistych wyszukiwarek, nie opisują klientów i nie stanowią wyników badań firmy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Najpierw ustal, co naprawdę chcesz zmierzyć
&lt;/h2&gt;

&lt;p&gt;Pytanie „czy AI nas zna?” jest zbyt szerokie, aby dało się na nie odpowiedzieć jednym wynikiem. System może poprawnie opisać firmę, kiedy podasz jej nazwę, ale nie wymienić jej w odpowiedzi na ogólne pytanie o dostawcę. Może też wymienić nazwę, przypisując jej niewłaściwą usługę.&lt;/p&gt;

&lt;p&gt;Dlatego w naszym przykładzie oddzielimy cztery obserwacje:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;obecność nazwy marki;&lt;/li&gt;
&lt;li&gt;rolę wzmianki: przykład, opis, rekomendacja lub ostrzeżenie;&lt;/li&gt;
&lt;li&gt;poprawność informacji o firmie;&lt;/li&gt;
&lt;li&gt;widoczne źródła, na których oparto odpowiedź.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To odrębne pola. Samo wystąpienie nazwy nie potwierdza rekomendacji. Link do strony nie dowodzi, że źródło wspiera każde zdanie odpowiedzi. Rekomendacja nie oznacza natomiast, że odbiorca odwiedził witrynę albo został klientem.&lt;/p&gt;

&lt;p&gt;Na początek zmierzymy tylko obecność marki. Dopiero gdy potrafimy odtworzyć ten pomiar, warto dodawać bardziej złożone oceny.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ułóż pytania wokół potrzeb użytkownika
&lt;/h2&gt;

&lt;p&gt;Zamiast wielokrotnie wpisywać nazwę firmy, przygotuj pytania opisujące rzeczywiste zadania odbiorcy. Dla usług związanych z AI Search mogą to być:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;„Jak sprawdzić, czy moja firma pojawia się w odpowiedziach AI?”;&lt;/li&gt;
&lt;li&gt;„Jak porównać dostawców monitoringu wzmianek o marce?”;&lt;/li&gt;
&lt;li&gt;„Jakie dane powinien zawierać raport widoczności w AI?”;&lt;/li&gt;
&lt;li&gt;„Jak ocenić, czy rekomendacja dostawcy jest poparta źródłami?”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To propozycje scenariuszy, a nie dane o liczbie wyszukiwań. Ich priorytet można później zweryfikować na podstawie rozmów z klientami, dostępnych danych wyszukiwania i pytań trafiających do działu obsługi.&lt;/p&gt;

&lt;p&gt;Każdemu pytaniu przypisz trwały identyfikator oraz kategorię intencji. Zapisz również, czy zawiera nazwę marki. Pytania z nazwą firmy i pytania ogólne raportuj osobno: odpowiadają na różne potrzeby badawcze.&lt;/p&gt;

&lt;p&gt;Przy kolejnej rundzie zachowaj podstawowy panel. Nowe pytania dodaj do osobnej grupy eksploracyjnej. Jeśli zmienisz jednocześnie treść pytań i stronę internetową, późniejszą różnicę trudniej będzie zinterpretować.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zapisuj jedną obserwację jako jeden rekord
&lt;/h2&gt;

&lt;p&gt;Praktycznym formatem startowym jest JSONL: każda linia pliku zawiera osobny obiekt JSON. Taki dziennik można przeglądać ręcznie, analizować w Pythonie i później przenieść do bazy danych.&lt;/p&gt;

&lt;p&gt;Minimalny rekord może wyglądać tak:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"observation_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"demo-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"panel_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"q-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jak mierzyc obecnosc marki w AI?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"engine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"synthetic-demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"collected_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-06T19:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"market"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"session"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"new"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"answer_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FunkyMEDIA jest tu przykladowa nazwa marki."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source_urls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mention_role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"annotation_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v1"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pole „engine” powinno identyfikować faktycznie użyty produkt lub interfejs. Jeśli nie znasz wersji modelu, zapisz brak tej informacji zamiast ją zgadywać. W odpowiednich polach zanotuj również dostępność wyszukiwania, stan zalogowania i istotne ustawienia.&lt;/p&gt;

&lt;p&gt;Pusta lista źródeł oznacza tu, że nie zapisano żadnych widocznych adresów. Nie jest dowodem, że system nie korzystał z żadnej informacji. Dziennik ma oddzielać to, co obserwowalne, od przypuszczeń o działaniu modelu.&lt;/p&gt;

&lt;h2&gt;
  
  
  Oddziel błędy techniczne od odpowiedzi bez marki
&lt;/h2&gt;

&lt;p&gt;Załóżmy, że wykonano sześć prób. Pięć zakończyło się zebraniem odpowiedzi, a jedna błędem technicznym. W dwóch poprawnych odpowiedziach wystąpiła marka.&lt;/p&gt;

&lt;p&gt;Wskaźnik obecności wśród poprawnych odpowiedzi wynosi wtedy 2/5, czyli 40%. Osobno raportujemy błąd w jednej z sześciu prób.&lt;/p&gt;

&lt;p&gt;Nie należy po cichu traktować błędu jak odpowiedzi bez wzmianki. Nie zaobserwowaliśmy bowiem odpowiedzi, którą można ocenić. Jednocześnie błędów nie wolno ukrywać: jeśli częściej dotyczą konkretnego silnika lub grupy pytań, uzyskana próbka może być niepełna w istotny sposób.&lt;/p&gt;

&lt;p&gt;W tym przykładzie odpowiedź typu „nie mam wystarczających informacji” jest poprawnie zebranym wynikiem i pozostaje w mianowniku. To decyzja metodologiczna, którą trzeba zapisać przed liczeniem. Możesz stosować dodatkowe kategorie, ale nie zmieniaj ich między rundami bez oznaczenia nowej wersji.&lt;/p&gt;

&lt;h2&gt;
  
  
  Policz prosty wskaźnik w Pythonie
&lt;/h2&gt;

&lt;p&gt;Poniższy przykład korzysta wyłącznie z biblioteki standardowej. Normalizuje zapis Unicode i wielkość liter, a następnie szuka całego tokenu nazwy marki.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NFKC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;casefold&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;contains_brand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?&amp;lt;!\w)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?!\w)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;mentions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;contains_brand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attempts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid_answers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;errors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answers_with_brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mentions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;presence_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mentions&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FunkyMEDIA jako przyklad.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Opis FUNKYMEDIA.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Inny dostawca.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Brak wystarczajacych informacji.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SuperFunkyMEDIA nie jest ta sama nazwa.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FunkyMEDIA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid_answers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answers_with_brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;presence_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;contains_brand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SuperFunkyMEDIA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FunkyMEDIA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FunkyMEDIA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;presence_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oczekiwany wynik obejmuje sześć prób, pięć poprawnych odpowiedzi, jeden błąd i dwie odpowiedzi z marką. Dla pustej próbki otrzymamy &lt;code&gt;None&lt;/code&gt;, a nie zero procent. Brak obserwacji nie jest zaobserwowanym brakiem widoczności.&lt;/p&gt;

&lt;p&gt;Dokumentacje &lt;a href="https://docs.python.org/3/library/unicodedata.html" rel="noopener noreferrer"&gt;normalizacji Unicode&lt;/a&gt; i &lt;a href="https://docs.python.org/3/library/re.html" rel="noopener noreferrer"&gt;wyrażeń regularnych&lt;/a&gt; opisują użyte mechanizmy. Sama reguła dopasowania jest jednak elementem naszego przykładu, a nie uniwersalnym standardem rozpoznawania firm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nie pomyl wykrycia nazwy z identyfikacją firmy
&lt;/h2&gt;

&lt;p&gt;Prosty matcher rozwiązuje tylko część problemu. Może poprawnie wykryć nazwę, lecz nie ustali, czy chodzi o właściwą organizację. Dwie firmy mogą mieć podobne nazwy; odpowiedź może też zawierać cytat lub adres domeny.&lt;/p&gt;

&lt;p&gt;Dlatego przygotuj rejestr zatwierdzonych aliasów. Przy każdym zapisz, skąd pochodzi i czy może być niejednoznaczny. Nie dodawaj automatycznie wszystkich wariantów zwróconych przez model.&lt;/p&gt;

&lt;p&gt;W trudniejszych przypadkach sprawdzaj kontekst: domenę, lokalizację, produkt albo usługę. Niepewną wzmiankę skieruj do ręcznej oceny. Wynik „do weryfikacji” jest bardziej użyteczny niż fałszywa pewność.&lt;/p&gt;

&lt;p&gt;W raporcie warto zachować osobno automatyczne dopasowanie i ostateczną adnotację. Dzięki temu zmiana algorytmu nie usuwa historii wcześniejszych decyzji.&lt;/p&gt;

&lt;h2&gt;
  
  
  Powtarzaj test w porównywalnych warunkach
&lt;/h2&gt;

&lt;p&gt;Jedna odpowiedź jest pojedynczą obserwacją. Jeśli chcesz ocenić zmienność, zaplanuj kilka powtórzeń i zapisuj każde z nich. Nie wybieraj wyłącznie najlepszej odpowiedzi.&lt;/p&gt;

&lt;p&gt;Zachowaj ten sam panel, liczbę prób oraz możliwie porównywalne warunki sesji. Oddziel wyniki różnych silników i języków. Jeżeli interfejs lub sposób zbierania danych się zmieni, zaznacz to w raporcie.&lt;/p&gt;

&lt;p&gt;Powtórzenia tego samego pytania nie stają się automatycznie niezależnymi obserwacjami. Przy bardziej zaawansowanej analizie nie traktuj stu niemal identycznych prób jak stu niezależnych potrzeb użytkowników.&lt;/p&gt;

&lt;p&gt;Prowadź także dziennik zmian strony: co poprawiono, kiedy i dlaczego. Wzrost liczby wzmianek po zmianie treści może być interesujący, ale sam porządek czasowy nie dowodzi przyczynowości. W tym samym okresie mogły zmienić się inne źródła i sam produkt AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Raportuj wynik razem z jego zakresem
&lt;/h2&gt;

&lt;p&gt;Użyteczny komunikat brzmi: „W syntetycznym przykładzie marka wystąpiła w 2 z 5 poprawnych odpowiedzi; jedna z 6 prób zakończyła się błędem”. Nie brzmi: „Marka ma 40% rynku AI”.&lt;/p&gt;

&lt;p&gt;Do rzeczywistego raportu dołącz panel pytań, okres pomiaru, silniki, liczbę powtórzeń, zasady kwalifikacji oraz sposób identyfikacji marki. Pokaż również odpowiedzi bez marek i przypadki niejednoznaczne.&lt;/p&gt;

&lt;p&gt;Jeśli mierzysz Google AI Overviews, osobno zapisuj, czy taki element w ogóle się pojawił. Brak AI Overview to inna obserwacja niż obecny AI Overview bez wskazanej firmy. &lt;a href="https://developers.google.com/search/docs/appearance/ai-features" rel="noopener noreferrer"&gt;Dokumentacja Google dotycząca funkcji AI&lt;/a&gt; podkreśla znaczenie standardowych praktyk SEO; nie opisuje specjalnej optymalizacji gwarantującej obecność.&lt;/p&gt;

&lt;p&gt;Dalszym krokiem może być ocena poprawności informacji, źródeł i roli wzmianki. Każdy z tych etapów wymaga własnych reguł, przykładów granicznych i możliwości sprawdzenia decyzji.&lt;/p&gt;

&lt;h2&gt;
  
  
  Informacja o serii i sposobie przygotowania
&lt;/h2&gt;

&lt;p&gt;Materiał przygotowano dla serii edukacyjnej FunkyMEDIA. Kontekst firmowego podejścia opisuje &lt;a href="https://funkymedia.pl/methodology/" rel="noopener noreferrer"&gt;metodologia AI Search i Brand Mentions FunkyMEDIA&lt;/a&gt;. To źródło własne firmy, a nie niezależne potwierdzenie skuteczności przedstawionego przykładu.&lt;/p&gt;

&lt;p&gt;Profile własne wydawcy: &lt;a href="https://www.linkedin.com/company/28656569/" rel="noopener noreferrer"&gt;FunkyMEDIA na LinkedIn&lt;/a&gt; i &lt;a href="https://www.youtube.com/channel/UC4UF1quILrasL6AKR_irGKA" rel="noopener noreferrer"&gt;FunkyMEDIA na YouTube&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ujawnienie użycia AI:&lt;/strong&gt; tekst i przykłady zostały wygenerowane z użyciem AI. Dane demonstracyjne są syntetyczne. Artykuł nie przedstawia osobistych doświadczeń autora ani wyników klientów.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>analytics</category>
      <category>python</category>
      <category>seo</category>
    </item>
    <item>
      <title>Compartir archivos por Wi-Fi local: un prototipo y sus riesgos de seguridad"</title>
      <dc:creator> AppInitDev</dc:creator>
      <pubDate>Fri, 06 Nov 2026 19:24:19 +0000</pubDate>
      <link>https://dev.to/appinitdev/compartir-archivos-por-wi-fi-local-un-prototipo-y-sus-riesgos-de-seguridad-5im</link>
      <guid>https://dev.to/appinitdev/compartir-archivos-por-wi-fi-local-un-prototipo-y-sus-riesgos-de-seguridad-5im</guid>
      <description>&lt;h2&gt;
  
  
  Compartir archivos por Wi-Fi local: un prototipo y sus riesgos de seguridad
&lt;/h2&gt;

&lt;p&gt;Cuando el móvil y el ordenador están en una misma red, una transferencia no necesita pasar por la nube. Sin embargo, «funciona en mi Wi-Fi» no significa «es seguro»: descubrimiento, permisos, autenticación y transporte son problemas distintos.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema y el alcance del ejemplo
&lt;/h2&gt;

&lt;p&gt;La ficha de &lt;strong&gt;Nexus Local: Gestor Web Wi-Fi&lt;/strong&gt; resume su función así: «Gestiona archivos y duplica tu pantalla desde el navegador mediante Wi-Fi local.» La ficha pública de Nexus Local describe acceso a archivos y pantalla desde el navegador por Wi-Fi local. Para estudiar una sola pieza de ese problema, construiremos &lt;strong&gt;un servidor HTTP de solo descarga&lt;/strong&gt; con Python. Este prototipo NO reproduce el servidor, el cifrado ni la arquitectura de la app; sirve para observar el alcance real de una dirección de red.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo reproducir la idea
&lt;/h2&gt;

&lt;p&gt;Necesitas Python 3.10+, dos equipos en una red Wi-Fi de confianza y una carpeta de prueba llamada &lt;code&gt;compartir&lt;/code&gt; con un archivo no confidencial. Guarda el código como &lt;code&gt;servidor.py&lt;/code&gt; junto a esa carpeta.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;http.server&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SimpleHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ThreadingHTTPServer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;folder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;compartir&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_dir&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Crea la carpeta compartir antes de iniciar&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SimpleHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ThreadingHTTPServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Servidor de PRUEBA en el puerto 8000; Ctrl+C para salir&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;KeyboardInterrupt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;server_close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Qué demuestra el código
&lt;/h2&gt;

&lt;p&gt;El servidor atiende solicitudes en todas las interfaces (&lt;code&gt;0.0.0.0&lt;/code&gt;), y &lt;code&gt;SimpleHTTPRequestHandler&lt;/code&gt; expone el contenido de la carpeta seleccionada. No hay credenciales, control de acceso por persona ni HTTPS. Eso lo vuelve útil para un laboratorio, pero &lt;strong&gt;inapropiado para archivos privados&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pruebas recomendadas
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Ejecuta &lt;code&gt;python servidor.py&lt;/code&gt;. 2. Averigua la IP local del ordenador, por ejemplo con &lt;code&gt;ipconfig&lt;/code&gt; o &lt;code&gt;ip addr&lt;/code&gt;. 3. Desde el otro dispositivo, abre &lt;code&gt;http://IP_LOCAL:8000/&lt;/code&gt;. 4. Descarga un archivo de prueba. 5. Apaga el servidor y comprueba que la dirección deja de responder. Si falla, revisa el aislamiento entre clientes, cortafuegos y red invitada.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Límites que conviene reconocer
&lt;/h2&gt;

&lt;p&gt;No implementa subida, descubrimiento automático, TLS, contraseñas ni acceso seguro en redes públicas. Una versión real necesitaría autenticación por sesión, confirmación explícita del usuario, HTTPS correctamente configurado, límites de tamaño, validación de rutas y controles contra accesos no autorizados.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué aprendemos
&lt;/h2&gt;

&lt;p&gt;La LAN reduce dependencias externas, pero &lt;strong&gt;no reemplaza un modelo de amenazas&lt;/strong&gt;. La pregunta de diseño es qué dispositivo puede conectarse, a qué archivos y durante cuánto tiempo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contexto y enlaces del autor
&lt;/h2&gt;

&lt;p&gt;El tema se relaciona con las funciones declaradas públicamente de &lt;strong&gt;Nexus Local: Gestor Web Wi-Fi&lt;/strong&gt;, una aplicación Android de AppInitDev. &lt;strong&gt;El código anterior es un ejemplo independiente&lt;/strong&gt;: no se ha inspeccionado el repositorio privado ni se afirma que sea la implementación interna de la app. La referencia aparece al final para ofrecer contexto, no como requisito para completar el tutorial.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Play:&lt;/strong&gt; &lt;a href="https://play.google.com/store/apps/details?id=com.appinitdev.nexusLocal" rel="noopener noreferrer"&gt;Nexus Local: Gestor Web Wi-Fi&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Página de la aplicación:&lt;/strong&gt; &lt;a href="https://nexuslocal.appinitdev.com/" rel="noopener noreferrer"&gt;https://nexuslocal.appinitdev.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sitio del desarrollador:&lt;/strong&gt; &lt;a href="https://appinitdev.com/" rel="noopener noreferrer"&gt;AppInitDev&lt;/a&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8219obmj94nx38e3f727.png" alt="APPS" width="800" height="391"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Referencia técnica
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.python.org/3/library/http.server.html" rel="noopener noreferrer"&gt;Documentación de &lt;code&gt;http.server&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>networking</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Como aprendi Apache Spark — Parte 8: RDD Lineage e tolerância a falhas — como o Spark se recupera</title>
      <dc:creator>renanpyd</dc:creator>
      <pubDate>Fri, 06 Nov 2026 19:23:43 +0000</pubDate>
      <link>https://dev.to/renanpyd/como-aprendi-apache-spark-parte-8-rdd-lineage-e-tolerancia-a-falhas-como-o-spark-se-recupera-4c2i</link>
      <guid>https://dev.to/renanpyd/como-aprendi-apache-spark-parte-8-rdd-lineage-e-tolerancia-a-falhas-como-o-spark-se-recupera-4c2i</guid>
      <description>&lt;p&gt;Nos primeiros artigos desta série, começamos com uma das abstrações que ajudaram a definir o Apache Spark:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RDD — Resilient Distributed Dataset.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Falamos bastante sobre:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Distributed Dataset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vimos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Partitions
Tasks
Executors
Shuffle
Memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mas existe uma palavra no nome que merece um capítulo próprio:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resilient.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Por que um RDD é resiliente?&lt;/p&gt;

&lt;p&gt;A resposta está em uma das ideias mais elegantes da arquitetura original do Spark:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lineage.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Em vez de depender apenas da replicação de cada resultado intermediário, Spark mantém informações sobre &lt;strong&gt;como os dados foram produzidos&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Se parte deles for perdida, em muitos casos ela pode ser reconstruída.&lt;/p&gt;

&lt;p&gt;Essa ideia conecta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Immutability
    ↓
Transformations
    ↓
Lineage
    ↓
DAG
    ↓
Recomputation
    ↓
Fault Tolerance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;E muda a forma como enxergamos falhas em processamento distribuído.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Falhas fazem parte de sistemas distribuídos
&lt;/h2&gt;

&lt;p&gt;Imagine um cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             DRIVER
                │
      ┌─────────┼─────────┐
      ▼         ▼         ▼
  Executor 1 Executor 2 Executor 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora imagine que estamos processando:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 TB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;durante horas.&lt;/p&gt;

&lt;p&gt;Nesse período, alguma coisa pode acontecer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Executor failure
Machine failure
Process crash
Network problem
Disk problem
Container termination
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Em um sistema distribuído, a pergunta não deveria ser apenas:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;E se alguma coisa falhar?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Uma pergunta melhor é:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Como o sistema continua quando alguma coisa falhar?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Essa diferença é fundamental.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Um problema simples
&lt;/h2&gt;

&lt;p&gt;Imagine um dataset dividido em quatro partições:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RDD A

P1
P2
P3
P4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Distribuídas assim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Executor 1
├── P1
└── P2

Executor 2
├── P3
└── P4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora o Executor 2 desaparece.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Executor 1
├── P1
└── P2

Executor 2
└── X
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perdemos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P3
P4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O que fazer?&lt;/p&gt;

&lt;p&gt;Uma possibilidade seria manter cópias de tudo.&lt;/p&gt;

&lt;p&gt;Outra possibilidade é saber &lt;strong&gt;como reconstruir o que foi perdido&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Essa segunda ideia é central no modelo de RDDs.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. RDDs são imutáveis
&lt;/h2&gt;

&lt;p&gt;Uma característica importante dos RDDs é:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;RDDs são imutáveis.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Quando fazemos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rdd2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rdd1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;não estamos modificando &lt;code&gt;rdd1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Estamos descrevendo outro RDD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RDD1
 │
 │ map(x * 2)
 ▼
RDD2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depois:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rdd3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rdd2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;temos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RDD1
 │
 │ map
 ▼
RDD2
 │
 │ filter
 ▼
RDD3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada transformação cria uma nova abstração derivada da anterior.&lt;/p&gt;

&lt;p&gt;Essa propriedade ajuda a tornar a linhagem previsível.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. O que é Lineage?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Lineage&lt;/strong&gt; representa a sequência de dependências e transformações utilizada para produzir um dataset.&lt;/p&gt;

&lt;p&gt;Considere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rdd1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;textFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/data/events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rdd2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rdd1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rdd3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rdd2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;purchase&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rdd4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rdd3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rdd5&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rdd4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduceByKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conceitualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;textFile
   ↓
 RDD1
   ↓
  map
   ↓
 RDD2
   ↓
 filter
   ↓
 RDD3
   ↓
  map
   ↓
 RDD4
   ↓
reduceByKey
   ↓
 RDD5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essa cadeia descreve:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Como RDD5 foi derivado dos dados originais.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Isso é lineage.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Lineage não significa guardar todas as cópias
&lt;/h2&gt;

&lt;p&gt;Esse ponto é importante.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RDD1
 ↓
RDD2
 ↓
RDD3
 ↓
RDD4
 ↓
RDD5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Não precisamos interpretar isso como:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Memory

RDD1 complete
RDD2 complete
RDD3 complete
RDD4 complete
RDD5 complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lineage não significa manter automaticamente todas as versões materializadas.&lt;/p&gt;

&lt;p&gt;O que Spark preserva é a informação necessária sobre as dependências entre os datasets.&lt;/p&gt;

&lt;p&gt;Podemos pensar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Data
+
Transformation Graph
=
Ability to recompute
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essa diferença é essencial.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Lazy Evaluation e Lineage
&lt;/h2&gt;

&lt;p&gt;Agora podemos conectar outro conceito da série.&lt;/p&gt;

&lt;p&gt;Quando escrevemos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rdd2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rdd1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="n"&gt;rdd3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rdd2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="n"&gt;rdd4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rdd3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spark não precisa executar imediatamente cada transformação.&lt;/p&gt;

&lt;p&gt;Ele constrói relações entre os RDDs.&lt;/p&gt;

&lt;p&gt;Conceitualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rdd1
  │
 map
  │
rdd2
  │
filter
  │
rdd3
  │
 map
  │
rdd4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quando uma Action aparece:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rdd4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;temos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lineage
   ↓
Action
   ↓
Job
   ↓
Stages
   ↓
Tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lazy evaluation e lineage estão profundamente conectados.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. O DAG
&lt;/h2&gt;

&lt;p&gt;Essa cadeia de dependências não precisa ser apenas linear.&lt;/p&gt;

&lt;p&gt;Podemos ter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;          RDD A
         /     \
        /       \
       ▼         ▼
    RDD B      RDD C
       \         /
        \       /
         ▼     ▼
          RDD D
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isso forma um:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Directed Acyclic Graph — DAG&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ou:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Grafo Direcionado Acíclico
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Direcionado porque existem relações de dependência.&lt;/p&gt;

&lt;p&gt;Acíclico porque a computação não forma ciclos arbitrários de dependência entre esses datasets.&lt;/p&gt;

&lt;p&gt;Conceitualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source
  ↓
Transform
  ↓
Transform ─────┐
  │            │
  ▼            ▼
Branch A    Branch B
  │            │
  └──────┬─────┘
         ▼
       Result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O DAG representa a estrutura da computação.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. DAG não é exatamente a mesma coisa que Stage
&lt;/h2&gt;

&lt;p&gt;Essa distinção é importante.&lt;/p&gt;

&lt;p&gt;Podemos ter uma sequência de transformações:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;map
 ↓
filter
 ↓
map
 ↓
reduceByKey
 ↓
map
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mas isso não significa necessariamente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5 transformations
=
5 stages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Como vimos anteriormente, dependências narrow podem ser agrupadas em um mesmo Stage.&lt;/p&gt;

&lt;p&gt;Shuffle frequentemente cria uma fronteira.&lt;/p&gt;

&lt;p&gt;Então podemos ter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STAGE 1

map
 ↓
filter
 ↓
map
 ↓

====== SHUFFLE ======

STAGE 2

reduce
 ↓
map
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Portanto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lineage / DAG
      ↓
Stage boundaries
      ↓
Tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;são conceitos relacionados, mas não idênticos.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Narrow Dependencies
&lt;/h2&gt;

&lt;p&gt;Vamos revisitar um conceito importante.&lt;/p&gt;

&lt;p&gt;Considere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rdd2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rdd1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Podemos ter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RDD1 P1 ───► RDD2 P1
RDD1 P2 ───► RDD2 P2
RDD1 P3 ───► RDD2 P3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada partição filha depende de uma quantidade limitada de partições anteriores.&lt;/p&gt;

&lt;p&gt;Isso caracteriza uma dependência estreita:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Narrow Dependency.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Agora imagine que perdemos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RDD2 P2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spark pode precisar reconstruir apenas o caminho relevante:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RDD1 P2
   ↓
 map
   ↓
RDD2 P2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essa granularidade é extremamente importante para tolerância a falhas.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Recomputation
&lt;/h2&gt;

&lt;p&gt;Aqui chegamos ao coração da resiliência.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source

P1 P2 P3 P4
 │  │  │  │
 ▼  ▼  ▼  ▼

Map

P1' P2' P3' P4'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P3'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;é perdida.&lt;/p&gt;

&lt;p&gt;Spark conhece sua dependência:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source P3
    ↓
   map
    ↓
  P3'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Então pode reconstruí-la.&lt;/p&gt;

&lt;p&gt;Não precisa necessariamente recalcular:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P1'
P2'
P4'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A ideia é:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Recompute what was lost from the lineage.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Essa é uma das razões para a palavra:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resilient.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Resiliência sem replicar tudo
&lt;/h2&gt;

&lt;p&gt;Uma abordagem possível para tolerância a falhas seria:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Resultado intermediário
       ↓
Copy 1
Copy 2
Copy 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mas isso possui custos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Storage
Network
Replication
Coordination
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RDDs introduziram uma ideia poderosa para datasets derivados deterministicamente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Don't replicate every intermediate result
              ↓
Keep lineage
              ↓
Recompute lost partitions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isso não elimina a necessidade de persistência ou replicação em todos os contextos.&lt;/p&gt;

&lt;p&gt;Mas cria um mecanismo elegante para recuperar dados derivados.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. Um exemplo mais completo
&lt;/h2&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;textFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/data/logs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parse_line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;by_service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;by_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduceByKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Temos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TEXT FILE
    ↓
   raw
    ↓
  parse
    ↓
 parsed
    ↓
 filter
    ↓
 errors
    ↓
   map
    ↓
by_service
    ↓
reduceByKey
    ↓
 counts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora imagine uma partição de &lt;code&gt;errors&lt;/code&gt; sendo perdida.&lt;/p&gt;

&lt;p&gt;Spark conhece:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;textFile
   ↓
 parse
   ↓
 filter
   ↓
errors partition
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Então pode reconstruir aquela partição a partir da origem e das transformações necessárias.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. E depois de um Shuffle?
&lt;/h2&gt;

&lt;p&gt;Agora a situação fica mais interessante.&lt;/p&gt;

&lt;p&gt;Considere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;by_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduceByKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Como vimos na Parte 4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Map-side work
      ↓
Shuffle Write
      ↓
Network
      ↓
Shuffle Read
      ↓
Reduce-side work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Temos uma wide dependency.&lt;/p&gt;

&lt;p&gt;Agora uma partição posterior pode depender de dados produzidos por várias partições anteriores.&lt;/p&gt;

&lt;p&gt;Conceitualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P1 ───┐
P2 ───┼──► Shuffle ───► R1
P3 ───┤
P4 ───┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se determinado resultado for perdido, a recomputação pode ser mais cara porque envolve dependências mais amplas.&lt;/p&gt;

&lt;p&gt;Isso mostra que:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nem todo lineage possui o mesmo custo de recuperação.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  14. Narrow vs. Wide também importa para Fault Tolerance
&lt;/h2&gt;

&lt;p&gt;Compare:&lt;/p&gt;

&lt;h3&gt;
  
  
  Narrow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P1 ───► P1'
P2 ───► P2'
P3 ───► P3'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perdeu &lt;code&gt;P2'&lt;/code&gt;?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Recompute P2
     ↓
     P2'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora:&lt;/p&gt;

&lt;h3&gt;
  
  
  Wide
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P1 ───┐
P2 ───┼──► P'
P3 ───┤
P4 ───┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A dependência é mais ampla.&lt;/p&gt;

&lt;p&gt;Isso afeta:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;custo de recomputação;&lt;/li&gt;
&lt;li&gt;organização dos Stages;&lt;/li&gt;
&lt;li&gt;shuffle;&lt;/li&gt;
&lt;li&gt;recuperação.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mais uma vez, shuffle aparece como uma fronteira importante da arquitetura.&lt;/p&gt;




&lt;h2&gt;
  
  
  15. Então por que usar Cache?
&lt;/h2&gt;

&lt;p&gt;Se Spark consegue recomputar tudo, poderíamos perguntar:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Por que utilizar cache?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Porque recomputação também custa.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source
 ↓
Expensive Transformation A
 ↓
Expensive Transformation B
 ↓
Dataset X
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora utilizamos &lt;code&gt;Dataset X&lt;/code&gt; cinco vezes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             Dataset X
            /    |    \
           ▼     ▼     ▼
         Job A Job B Job C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sem persistência, determinadas partes da linhagem podem precisar ser recomputadas.&lt;/p&gt;

&lt;p&gt;Se &lt;code&gt;Dataset X&lt;/code&gt; for caro de produzir, podemos decidir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;dataset_x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora trocamos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Repeated Computation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;por:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esse é um trade-off.&lt;/p&gt;




&lt;h2&gt;
  
  
  16. Cache vs. Recomputation
&lt;/h2&gt;

&lt;p&gt;Podemos pensar assim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 Dataset
                    │
          ┌─────────┴─────────┐
          ▼                   ▼
      Recompute             Cache
          │                   │
          ▼                   ▼
     More CPU             More Memory
     More Time            Storage Cost
          │                   │
          └─────────┬─────────┘
                    ▼
                 Trade-off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A pergunta não é:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cache é bom?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A pergunta é:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;O custo de manter esse dataset é menor do que o custo esperado de recomputá-lo?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Essa é uma pergunta muito mais útil.&lt;/p&gt;




&lt;h2&gt;
  
  
  17. Persistência e tolerância a falhas
&lt;/h2&gt;

&lt;p&gt;Agora existe uma nuance importante.&lt;/p&gt;

&lt;p&gt;Se persistimos um RDD e uma partição persistida é perdida devido à falha de um Executor, o lineage ainda pode permitir sua recomputação.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RDD X cached

Executor 1
├── P1
└── P2

Executor 2
├── P3
└── P4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Executor 2 falha:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P3 X
P4 X
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spark ainda conhece:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lineage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;e pode reconstruir essas partições quando necessário.&lt;/p&gt;

&lt;p&gt;Então:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;e:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lineage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;não são mecanismos concorrentes.&lt;/p&gt;

&lt;p&gt;Eles se complementam.&lt;/p&gt;




&lt;h2&gt;
  
  
  18. &lt;code&gt;persist()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Como vimos na Parte 7, podemos usar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rdd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;para controlar estratégias de persistência.&lt;/p&gt;

&lt;p&gt;Conceitualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Persistence
    │
    ├── Memory
    ├── Disk
    ├── Serialized forms
    └── Replication options
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A escolha depende do workload.&lt;/p&gt;

&lt;p&gt;Por exemplo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dataset pequeno
+
reutilizado frequentemente
+
caro de recomputar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pode justificar uma estratégia diferente de:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dataset enorme
+
utilizado uma vez
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Persistência deve responder a uma necessidade real.&lt;/p&gt;




&lt;h2&gt;
  
  
  19. Mas Lineage pode crescer
&lt;/h2&gt;

&lt;p&gt;Agora imagine um pipeline muito longo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RDD1
 ↓
RDD2
 ↓
RDD3
 ↓
RDD4
 ↓
RDD5
 ↓
RDD6
 ↓
...
 ↓
RDD100
 ↓
...
 ↓
RDD500
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Temos uma longa cadeia de dependências.&lt;/p&gt;

&lt;p&gt;Em determinados workloads iterativos ou pipelines extensos, essa linhagem pode se tornar indesejavelmente longa ou cara para recuperação.&lt;/p&gt;

&lt;p&gt;Queremos então, em alguns cenários, interromper essa dependência histórica.&lt;/p&gt;

&lt;p&gt;É aqui que aparece outro conceito:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoint.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  20. O que é Checkpoint?
&lt;/h2&gt;

&lt;p&gt;A ideia do checkpoint é materializar o estado de um dataset em armazenamento confiável e cortar sua dependência da linhagem anterior para fins de recuperação.&lt;/p&gt;

&lt;p&gt;Antes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source
 ↓
A
 ↓
B
 ↓
C
 ↓
D
 ↓
E
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se &lt;code&gt;E&lt;/code&gt; precisar ser reconstruído:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source → A → B → C → D → E
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora imagine um checkpoint em &lt;code&gt;C&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source
 ↓
A
 ↓
B
 ↓
C
 ↓
CHECKPOINT
 ↓
D
 ↓
E
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A recuperação posterior pode partir do checkpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CHECKPOINT
    ↓
    D
    ↓
    E
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isso trunca a linhagem anterior.&lt;/p&gt;




&lt;h2&gt;
  
  
  21. Configurando Checkpoint em RDD
&lt;/h2&gt;

&lt;p&gt;Em um contexto de RDD, podemos definir um diretório de checkpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCheckpointDir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/path/to/reliable/checkpoints&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depois:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rdd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quando o RDD for materializado por uma Action, o checkpoint poderá ser efetivamente produzido.&lt;/p&gt;

&lt;p&gt;Por exemplo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rdd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;rdd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conceitualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;checkpoint()
    ↓
mark RDD
    ↓
Action
    ↓
compute
    ↓
write checkpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assim como cache, existe interação com lazy evaluation.&lt;/p&gt;




&lt;h2&gt;
  
  
  22. Checkpoint não é Cache
&lt;/h2&gt;

&lt;p&gt;Essa distinção é fundamental.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache
&lt;/h3&gt;

&lt;p&gt;Objetivo principal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reuse
 ↓
Performance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Checkpoint
&lt;/h3&gt;

&lt;p&gt;Objetivo principal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Materialize reliable state
        ↓
Truncate lineage
        ↓
Recovery / lineage management
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simplificando:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CACHE
"Evite recomputar algo que vou reutilizar."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;versus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CHECKPOINT
"Crie um novo ponto materializado e corte a dependência da linhagem anterior."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Os dois podem aparecer juntos.&lt;/p&gt;

&lt;p&gt;Mas resolvem problemas diferentes.&lt;/p&gt;




&lt;h2&gt;
  
  
  23. Cache não corta Lineage
&lt;/h2&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A
 ↓
B
 ↓
C
 ↓
D
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fazemos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ainda existe conceitualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A
 ↓
B
 ↓
C
 ↓
D [cached]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O lineage continua conhecido.&lt;/p&gt;

&lt;p&gt;Se a partição persistida for perdida, Spark pode usar essa linhagem para reconstruí-la.&lt;/p&gt;

&lt;p&gt;Agora com checkpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A
 ↓
B
 ↓
C
 ↓
D
 ↓
CHECKPOINT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;a linhagem anterior pode ser truncada depois que o checkpoint é materializado corretamente.&lt;/p&gt;

&lt;p&gt;Essa é uma diferença arquitetural importante.&lt;/p&gt;




&lt;h2&gt;
  
  
  24. Checkpoint também tem custo
&lt;/h2&gt;

&lt;p&gt;Checkpoint não é gratuito.&lt;/p&gt;

&lt;p&gt;Precisamos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Compute
   ↓
Write
   ↓
Reliable Storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isso envolve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I/O;&lt;/li&gt;
&lt;li&gt;armazenamento;&lt;/li&gt;
&lt;li&gt;tempo;&lt;/li&gt;
&lt;li&gt;gerenciamento.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Portanto, não queremos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;checkpoint everything
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Queremos checkpoint quando existe uma razão arquitetural.&lt;/p&gt;

&lt;p&gt;Por exemplo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lineage muito longo;&lt;/li&gt;
&lt;li&gt;processamento iterativo;&lt;/li&gt;
&lt;li&gt;necessidade de ponto confiável;&lt;/li&gt;
&lt;li&gt;custo elevado de reconstrução.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  25. Um pipeline iterativo
&lt;/h2&gt;

&lt;p&gt;Imagine um algoritmo que executa repetidamente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Iteration 1
    ↓
Iteration 2
    ↓
Iteration 3
    ↓
Iteration 4
    ↓
...
    ↓
Iteration 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se cada iteração depende da anterior, podemos formar uma linhagem extensa.&lt;/p&gt;

&lt;p&gt;Conceitualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initial
 ↓
I1
 ↓
I2
 ↓
I3
 ↓
...
 ↓
I100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Em determinadas situações, podemos estabelecer checkpoints intermediários:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initial
 ↓
I1
 ↓
...
 ↓
I20
 ↓
CHECKPOINT
 ↓
I21
 ↓
...
 ↓
I40
 ↓
CHECKPOINT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assim reduzimos a profundidade necessária para recuperação futura.&lt;/p&gt;




&lt;h2&gt;
  
  
  26. Falha de Executor não significa falha da aplicação
&lt;/h2&gt;

&lt;p&gt;Agora podemos enxergar resiliência de forma melhor.&lt;/p&gt;

&lt;p&gt;Temos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Executor 1
Executor 2
Executor 3
Executor 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Um desaparece:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Executor 1
Executor 2
Executor 3 X
Executor 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isso não implica necessariamente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APPLICATION FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spark pode reagendar trabalho e reconstruir dados necessários quando a situação permite.&lt;/p&gt;

&lt;p&gt;Conceitualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Executor failure
      ↓
Lost Tasks / Data
      ↓
Scheduler
      ↓
Re-execution
      ↓
Lineage
      ↓
Recompute
      ↓
Continue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essa capacidade é central em computação distribuída.&lt;/p&gt;




&lt;h2&gt;
  
  
  27. Task Failure
&lt;/h2&gt;

&lt;p&gt;Também podemos ter falha em uma Task específica.&lt;/p&gt;

&lt;p&gt;Por exemplo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stage 10

Task 1 ✓
Task 2 ✓
Task 3 X
Task 4 ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spark pode tentar executar novamente a Task.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Task 3
  ↓
Retry
  ↓
Task 3 ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Talvez em outro Executor.&lt;/p&gt;

&lt;p&gt;Isso ajuda com falhas transitórias.&lt;/p&gt;

&lt;p&gt;Mas existe uma distinção importante.&lt;/p&gt;




&lt;h2&gt;
  
  
  28. Falha transitória vs. falha determinística
&lt;/h2&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Task failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;porque uma máquina teve um problema temporário.&lt;/p&gt;

&lt;p&gt;Retry pode funcionar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attempt 1 → FAIL
Attempt 2 → SUCCESS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora imagine código:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;e temos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x = 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Task falha.&lt;/p&gt;

&lt;p&gt;Spark tenta novamente.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attempt 1 → divide by zero
Attempt 2 → divide by zero
Attempt 3 → divide by zero
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Não importa quantas máquinas utilizarmos.&lt;/p&gt;

&lt;p&gt;O problema está nos dados ou no código.&lt;/p&gt;

&lt;p&gt;Isso nos ensina:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Fault tolerance não corrige lógica determinística incorreta.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  29. Recomputation também não corrige dados ruins
&lt;/h2&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;corrupted input
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A partição falha.&lt;/p&gt;

&lt;p&gt;Spark tenta novamente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read
 ↓
fail
 ↓
retry
 ↓
read same corrupted data
 ↓
fail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lineage ajuda a recuperar computação perdida.&lt;/p&gt;

&lt;p&gt;Não transforma entrada inválida em entrada válida.&lt;/p&gt;

&lt;p&gt;Resiliência tem limites.&lt;/p&gt;




&lt;h2&gt;
  
  
  30. O Driver continua sendo especial
&lt;/h2&gt;

&lt;p&gt;Até agora falamos muito sobre Executors.&lt;/p&gt;

&lt;p&gt;Mas existe um componente central:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DRIVER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O Driver coordena a aplicação.&lt;/p&gt;

&lt;p&gt;Se um Executor falha, Spark pode frequentemente reagendar trabalho.&lt;/p&gt;

&lt;p&gt;A falha do Driver possui implicações diferentes porque ali está o processo que coordena a aplicação.&lt;/p&gt;

&lt;p&gt;A capacidade de recuperar o Driver depende também:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;do modo de implantação;&lt;/li&gt;
&lt;li&gt;do cluster manager;&lt;/li&gt;
&lt;li&gt;da configuração;&lt;/li&gt;
&lt;li&gt;da plataforma utilizada;&lt;/li&gt;
&lt;li&gt;dos mecanismos externos de supervisão/restart.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Portanto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Executor fault tolerance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;não deve ser confundida automaticamente com:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Driver high availability
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;São problemas relacionados, mas diferentes.&lt;/p&gt;




&lt;h2&gt;
  
  
  31. Lineage não substitui durabilidade da fonte
&lt;/h2&gt;

&lt;p&gt;Imagine que nosso RDD começa em:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/data/events
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;e depois:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source
 ↓
Map
 ↓
Filter
 ↓
RDD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se perdemos uma partição intermediária, podemos tentar reconstruí-la a partir da fonte.&lt;/p&gt;

&lt;p&gt;Mas e se a fonte original desaparecer?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source X
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora lineage sabe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;como
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;reconstruir.&lt;/p&gt;

&lt;p&gt;Mas não possui necessariamente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;de onde
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;obter os dados originais.&lt;/p&gt;

&lt;p&gt;Portanto:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lineage não substitui armazenamento durável dos dados de origem.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Essa distinção é muito importante em arquitetura de dados.&lt;/p&gt;




&lt;h2&gt;
  
  
  32. Fault tolerance não é Backup
&lt;/h2&gt;

&lt;p&gt;Outro erro conceitual seria pensar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Spark é fault tolerant
        ↓
não preciso de backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Não.&lt;/p&gt;

&lt;p&gt;São problemas diferentes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fault tolerance
&lt;/h3&gt;

&lt;p&gt;Ajuda o sistema a continuar ou recuperar processamento diante de determinadas falhas.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backup
&lt;/h3&gt;

&lt;p&gt;Protege dados contra cenários como:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exclusão;&lt;/li&gt;
&lt;li&gt;corrupção;&lt;/li&gt;
&lt;li&gt;desastre;&lt;/li&gt;
&lt;li&gt;erro operacional;&lt;/li&gt;
&lt;li&gt;perda da fonte.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Podemos ter um engine extremamente resiliente processando uma fonte que foi permanentemente destruída.&lt;/p&gt;

&lt;p&gt;O engine não recria magicamente dados que deixaram de existir.&lt;/p&gt;




&lt;h2&gt;
  
  
  33. Lineage e DataFrames
&lt;/h2&gt;

&lt;p&gt;Até agora usei RDDs porque lineage é um conceito especialmente natural para explicar a arquitetura original do Spark.&lt;/p&gt;

&lt;p&gt;Mas DataFrames também participam de uma computação baseada em dependências e planos.&lt;/p&gt;

&lt;p&gt;Quando escrevemos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;resultado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;vendas&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ano = 2026&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;estado&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;continuamos tendo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source
 ↓
Transformations
 ↓
Dependencies
 ↓
Stages
 ↓
Tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A diferença é que agora também existe a camada estruturada que vimos na Parte 6:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DataFrame
 ↓
Logical Plan
 ↓
Catalyst
 ↓
Physical Plan
 ↓
Distributed Execution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Os fundamentos não desapareceram.&lt;/p&gt;

&lt;p&gt;Eles ganharam novas abstrações.&lt;/p&gt;




&lt;h2&gt;
  
  
  34. Lineage vs. Query Plan
&lt;/h2&gt;

&lt;p&gt;Também não devemos tratar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lineage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;e:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query Plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;como exatamente a mesma coisa.&lt;/p&gt;

&lt;p&gt;Uma forma útil de pensar é:&lt;/p&gt;

&lt;h3&gt;
  
  
  Lineage
&lt;/h3&gt;

&lt;p&gt;Relaciona datasets e dependências necessárias para derivação/recomputação.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query Plan
&lt;/h3&gt;

&lt;p&gt;Representa operações lógicas e físicas usadas pelo engine estruturado para planejar e executar uma consulta.&lt;/p&gt;

&lt;p&gt;Eles se encontram na execução distribuída, mas são conceitos distintos.&lt;/p&gt;




&lt;h2&gt;
  
  
  35. DAG Scheduler
&lt;/h2&gt;

&lt;p&gt;Agora podemos encaixar outra peça da arquitetura.&lt;/p&gt;

&lt;p&gt;Spark possui componentes responsáveis por transformar a computação em trabalho executável.&lt;/p&gt;

&lt;p&gt;Uma visão conceitual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    ↓
Action
    ↓
Job
    ↓
DAG Scheduler
    ↓
Stages
    ↓
Task Sets
    ↓
Executors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O DAG Scheduler consegue analisar dependências e identificar fronteiras de Stage, especialmente ao redor de shuffles.&lt;/p&gt;

&lt;p&gt;Podemos pensar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Narrow
 ↓
same Stage where possible

Wide
 ↓
Shuffle
 ↓
new Stage boundary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isso conecta lineage diretamente ao modelo de execução que estudamos na Parte 2.&lt;/p&gt;




&lt;h2&gt;
  
  
  36. Task Scheduling
&lt;/h2&gt;

&lt;p&gt;Depois que o trabalho é dividido em Stages e Tasks, precisamos executar essas Tasks nos Executors.&lt;/p&gt;

&lt;p&gt;Conceitualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DAG Scheduler
     ↓
Stages
     ↓
Task Scheduler
     ↓
Executors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora entram preocupações como:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recursos disponíveis;&lt;/li&gt;
&lt;li&gt;localização;&lt;/li&gt;
&lt;li&gt;execução;&lt;/li&gt;
&lt;li&gt;retries;&lt;/li&gt;
&lt;li&gt;status das Tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;O Spark não é apenas uma biblioteca de transformação.&lt;/p&gt;

&lt;p&gt;Existe um sistema de scheduling distribuído por trás da API.&lt;/p&gt;




&lt;h2&gt;
  
  
  37. Data Locality
&lt;/h2&gt;

&lt;p&gt;Existe ainda uma ideia importante.&lt;/p&gt;

&lt;p&gt;Se os dados estão próximos de determinado Executor ou nó, pode ser vantajoso executar o processamento próximo deles.&lt;/p&gt;

&lt;p&gt;Em vez de:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Move 100 GB of data
      ↓
to computation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;podemos preferir, quando a arquitetura permite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Move computation
      ↓
to data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essa filosofia foi extremamente importante no ecossistema de Big Data.&lt;/p&gt;

&lt;p&gt;No Spark, scheduling pode considerar locality dependendo da fonte e do ambiente.&lt;/p&gt;

&lt;p&gt;Novamente encontramos o princípio:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Mover computação costuma ser mais barato do que mover grandes volumes de dados.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  38. Falhas e Shuffle
&lt;/h2&gt;

&lt;p&gt;Shuffle torna a recuperação mais interessante.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stage 1
  ↓
Shuffle Write
  ↓
Stage 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Os outputs intermediários de shuffle são importantes para o Stage seguinte.&lt;/p&gt;

&lt;p&gt;Se dados intermediários necessários forem perdidos devido a falhas, Spark pode precisar recomputar partes anteriores.&lt;/p&gt;

&lt;p&gt;Conceitualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lost Shuffle Data
       ↓
Need previous output
       ↓
Recompute upstream tasks
       ↓
Regenerate shuffle data
       ↓
Continue downstream work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora entendemos por que lineage continua relevante mesmo depois de falarmos sobre shuffle.&lt;/p&gt;




&lt;h2&gt;
  
  
  39. Recomputation tem custo
&lt;/h2&gt;

&lt;p&gt;Fault tolerance não significa:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;failure = free
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2-hour computation
      ↓
lost intermediate data
      ↓
recompute
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O sistema pode conseguir se recuperar.&lt;/p&gt;

&lt;p&gt;Mas isso custa:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU;&lt;/li&gt;
&lt;li&gt;tempo;&lt;/li&gt;
&lt;li&gt;rede;&lt;/li&gt;
&lt;li&gt;I/O;&lt;/li&gt;
&lt;li&gt;dinheiro.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Então existe outra pergunta arquitetural:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Qual é o custo aceitável de recomputação?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;É aqui que persistência e checkpoint podem entrar.&lt;/p&gt;




&lt;h2&gt;
  
  
  40. Cache, Checkpoint ou Recompute?
&lt;/h2&gt;

&lt;p&gt;Podemos montar um modelo mental:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dataset intermediário
        │
        ▼
Será reutilizado?
   │           │
  sim         não
   │           │
   ▼           ▼
Cache?     Recompute?
   │
   ▼
É caro de reconstruir
e lineage é problemático?
   │
   ▼
Checkpoint?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Não é uma árvore de decisão absoluta.&lt;/p&gt;

&lt;p&gt;É apenas uma forma de pensar.&lt;/p&gt;

&lt;p&gt;A escolha depende de:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cost of computation
Cost of storage
Reuse
Failure probability
Lineage length
Recovery requirements
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  41. Um exemplo prático
&lt;/h2&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/data/events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_date &amp;gt;= &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2026-01-01&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essa transformação é usada por vários outputs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;por_cliente&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;por_evento&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;receita&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Temos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  RAW
                   │
                   ▼
             Filter + Select
                   │
                   ▼
                  BASE
             ┌─────┼─────┐
             ▼     ▼     ▼
         Customer Event Revenue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se &lt;code&gt;base&lt;/code&gt; for caro de produzir e reutilizado várias vezes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pode fazer sentido.&lt;/p&gt;

&lt;p&gt;A decisão nasce do DAG.&lt;/p&gt;

&lt;p&gt;Não de uma regra genérica de que:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cache = performance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  42. Um exemplo de Checkpoint
&lt;/h2&gt;

&lt;p&gt;Agora imagine um processamento iterativo ou uma cadeia extremamente extensa.&lt;/p&gt;

&lt;p&gt;Em RDD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rdd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;textFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/data/source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rdd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;expensive_transformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rdd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A linhagem pode crescer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source
 ↓
T1
 ↓
T2
 ↓
T3
 ↓
...
 ↓
T100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Podemos decidir estabelecer checkpoints em pontos apropriados.&lt;/p&gt;

&lt;p&gt;Conceitualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source
 ↓
T1
 ↓
...
 ↓
T25
 ↓
CHECKPOINT
 ↓
T26
 ↓
...
 ↓
T50
 ↓
CHECKPOINT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assim controlamos o tamanho da cadeia de recuperação.&lt;/p&gt;




&lt;h2&gt;
  
  
  43. &lt;code&gt;localCheckpoint()&lt;/code&gt; merece cuidado
&lt;/h2&gt;

&lt;p&gt;Spark também possui mecanismos de checkpoint local em determinados contextos.&lt;/p&gt;

&lt;p&gt;Mas existe uma diferença conceitual importante:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reliable checkpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;busca armazenamento confiável para quebrar a linhagem mantendo capacidade adequada de recuperação.&lt;/p&gt;

&lt;p&gt;Já um checkpoint baseado apenas em armazenamento local dos Executors não oferece necessariamente a mesma garantia de tolerância a falhas.&lt;/p&gt;

&lt;p&gt;Portanto:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Não devemos tratar todo tipo de checkpoint como equivalente em resiliência.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A escolha depende do objetivo.&lt;/p&gt;




&lt;h2&gt;
  
  
  44. Resiliência também depende do storage
&lt;/h2&gt;

&lt;p&gt;Se o checkpoint precisa servir como ponto confiável de recuperação, precisamos pensar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Onde ele está armazenado?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Um diretório local efêmero possui características muito diferentes de armazenamento distribuído e durável.&lt;/p&gt;

&lt;p&gt;Então a arquitetura completa é:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Spark
   +
Lineage
   +
Checkpoint strategy
   +
Reliable storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fault tolerance não acontece isoladamente.&lt;/p&gt;




&lt;h2&gt;
  
  
  45. O que acontece quando uma máquina desaparece?
&lt;/h2&gt;

&lt;p&gt;Agora podemos responder de forma muito melhor.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Executor 3
    ↓
FAIL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spark pode identificar trabalho perdido.&lt;/p&gt;

&lt;p&gt;Depois:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lost partition
      ↓
Find lineage
      ↓
Find required ancestors
      ↓
Schedule recomputation
      ↓
Execute elsewhere
      ↓
Rebuild data
      ↓
Continue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essa sequência explica o:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resilient&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;de:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resilient Distributed Dataset.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  46. O que eu olho quando existem muitas recomputações?
&lt;/h2&gt;

&lt;p&gt;Se uma aplicação está refazendo trabalho demais, quero investigar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Executors estão instáveis?

Existe perda de containers?

Existe pressão de memória?

Existe GC excessivo?

Existe problema de infraestrutura?

Existe shuffle data sendo perdida?

A linhagem é cara?

Falta persistência em um ponto estratégico?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ou seja, recomputation é um mecanismo de recuperação.&lt;/p&gt;

&lt;p&gt;Mas recomputation excessiva também pode ser sintoma.&lt;/p&gt;




&lt;h2&gt;
  
  
  47. Resiliência não significa ignorar infraestrutura
&lt;/h2&gt;

&lt;p&gt;Podemos ter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Executor failure
 ↓
Retry
 ↓
Executor failure
 ↓
Retry
 ↓
Executor failure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Em algum momento a pergunta deixa de ser:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Spark consegue tentar novamente?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;e passa a ser:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Por que meus Executors continuam morrendo?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Talvez exista:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OOM
Disk failure
Container eviction
Network instability
Bad node
Data skew
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fault tolerance deve absorver falhas ocasionais.&lt;/p&gt;

&lt;p&gt;Não deve ser usada para esconder um cluster permanentemente instável.&lt;/p&gt;




&lt;h2&gt;
  
  
  48. Observabilidade novamente
&lt;/h2&gt;

&lt;p&gt;No troubleshooting, quero relacionar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    ↓
Jobs
    ↓
Stages
    ↓
Tasks
    ↓
Executors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;com:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failures
Retries
Recomputation
Shuffle
Memory
GC
Spill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se Tasks estão sendo repetidas, preciso saber por quê.&lt;/p&gt;

&lt;p&gt;Se Stages estão sendo refeitos, preciso saber por quê.&lt;/p&gt;

&lt;p&gt;Se Executors desaparecem, preciso saber por quê.&lt;/p&gt;

&lt;p&gt;Resiliência é muito mais útil quando acompanhada de observabilidade.&lt;/p&gt;




&lt;h2&gt;
  
  
  49. Um modelo mental de Fault Tolerance
&lt;/h2&gt;

&lt;p&gt;Minha forma de enxergar ficou assim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                   DATA SOURCE
                       │
                       ▼
                 Transformations
                       │
                       ▼
                    Lineage
                       │
                       ▼
                      DAG
                       │
                       ▼
                    Stages
                       │
                       ▼
                     Tasks
                       │
                       ▼
                   Executors
                       │
                       ▼
                    Failure?
                   /         \
                 não          sim
                 │             │
                 ▼             ▼
             Continue      What was lost?
                               │
                               ▼
                           Lineage
                               │
                               ▼
                         Recompute
                               │
                               ▼
                           Continue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora acrescente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;quando queremos evitar recomputação frequente.&lt;/p&gt;

&lt;p&gt;E:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Checkpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;quando queremos estabelecer um novo ponto materializado e truncar uma linhagem problemática.&lt;/p&gt;




&lt;h2&gt;
  
  
  50. A principal lição deste capítulo
&lt;/h2&gt;

&lt;p&gt;A resiliência do Spark não vem de uma única configuração.&lt;/p&gt;

&lt;p&gt;Ela emerge de várias ideias trabalhando juntas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Immutable Datasets
        +
Transformations
        +
Dependencies
        +
Lineage
        +
DAG
        +
Task Scheduling
        +
Retries
        +
Recomputation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;E, quando necessário:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cache / Persist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ou:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Checkpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essa arquitetura permite que uma falha parcial não precise significar perda completa da computação.&lt;/p&gt;




&lt;h2&gt;
  
  
  51. O significado de RDD ficou muito mais claro
&lt;/h2&gt;

&lt;p&gt;No começo da série:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RDD
=
Resilient Distributed Dataset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;podia parecer apenas um nome.&lt;/p&gt;

&lt;p&gt;Agora conseguimos decompor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DISTRIBUTED
     ↓
Partitions across cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATASET
     ↓
Collection of data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;e:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RESILIENT
     ↓
Lineage
     ↓
Recomputation
     ↓
Fault recovery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essa é uma das ideias fundamentais da arquitetura original do Spark.&lt;/p&gt;




&lt;h2&gt;
  
  
  52. O que essa ideia me ensinou além do Spark
&lt;/h2&gt;

&lt;p&gt;A lição mais interessante vai além da tecnologia.&lt;/p&gt;

&lt;p&gt;Em sistemas distribuídos, não podemos construir arquitetura assumindo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Everything will always work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Precisamos assumir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Machines fail
Processes fail
Networks fail
Tasks fail
Containers disappear
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Então a arquitetura precisa responder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What happens next?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essa pergunta vale para:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;processamento de dados;&lt;/li&gt;
&lt;li&gt;microsserviços;&lt;/li&gt;
&lt;li&gt;bancos distribuídos;&lt;/li&gt;
&lt;li&gt;streaming;&lt;/li&gt;
&lt;li&gt;Kubernetes;&lt;/li&gt;
&lt;li&gt;cloud;&lt;/li&gt;
&lt;li&gt;Machine Learning distribuído.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tolerância a falhas não é um recurso opcional adicionado no final.&lt;/p&gt;

&lt;p&gt;É uma propriedade arquitetural.&lt;/p&gt;




&lt;h2&gt;
  
  
  53. Como penso hoje
&lt;/h2&gt;

&lt;p&gt;Quando vejo uma transformação Spark:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;resultado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dados&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduceByKey&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;não penso apenas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input
 ↓
transform
 ↓
output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Penso:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Where is the source?
        ↓
What are the dependencies?
        ↓
Where are the shuffle boundaries?
        ↓
What is the lineage?
        ↓
What happens if a partition disappears?
        ↓
How expensive is recomputation?
        ↓
Should something be persisted?
        ↓
Is checkpointing justified?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essa mudança de mentalidade é muito mais importante do que memorizar APIs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Próximo artigo
&lt;/h2&gt;

&lt;p&gt;Agora temos uma base sólida sobre:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RDD
 ↓
Partitions
 ↓
Transformations
 ↓
Lineage
 ↓
DAG
 ↓
Stages
 ↓
Tasks
 ↓
Executors
 ↓
Fault Tolerance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mas existe um tema do material original que merece ser revisitado com bastante cuidado:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;variáveis compartilhadas.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Em uma aplicação comum, podemos imaginar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;contador&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;e esperar que exista apenas um contador.&lt;/p&gt;

&lt;p&gt;Mas em Spark temos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Driver
  +
Executor 1
  +
Executor 2
  +
Executor 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Então surge uma pergunta:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;O que significa "variável compartilhada" em uma aplicação distribuída?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No próximo artigo vamos explorar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;closures;&lt;/li&gt;
&lt;li&gt;serialização de funções;&lt;/li&gt;
&lt;li&gt;variáveis capturadas;&lt;/li&gt;
&lt;li&gt;Driver vs. Executors;&lt;/li&gt;
&lt;li&gt;por que alterar uma variável dentro de uma Task não funciona como muitos esperam;&lt;/li&gt;
&lt;li&gt;Broadcast Variables;&lt;/li&gt;
&lt;li&gt;Accumulators;&lt;/li&gt;
&lt;li&gt;quando utilizar cada mecanismo;&lt;/li&gt;
&lt;li&gt;armadilhas de side effects;&lt;/li&gt;
&lt;li&gt;e como pensar em estado em computação distribuída.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vamos sair disto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;counter = 0

for item:
    counter += 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;para isto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              DRIVER
                 │
          serialized closure
        ┌────────┼────────┐
        ▼        ▼        ▼
     Executor Executor Executor
        │        │        │
      Task     Task      Task
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;E entender por que código aparentemente simples muda completamente quando atravessa máquinas.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;← Parte 7:&lt;/strong&gt; Memória — Executors, Driver, Cache, Spill e OutOfMemory&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Próximo:&lt;/strong&gt; Parte 9 — Variáveis compartilhadas no Apache Spark: Broadcast, Accumulators e Closures&lt;/p&gt;

</description>
      <category>spark</category>
      <category>python</category>
      <category>dataengineering</category>
      <category>bigdata</category>
    </item>
  </channel>
</rss>
