wenmo8 发布的文章

再过半年就40岁了,人到中年的我,正在经历着职业生涯中最大的一次坎坷,我失业了。24年的3月份,我上了公司的裁员名单,经过一个月的拉扯,在4月初,我收拾收拾东西,离开了公司,正式进入了失业大军。距离现在已经半年多了,在这半年里,我有很多的尝试,也有很多的感受,就和大家聊一聊吧。

离职谈判

最近两年公司一直在裁员,基本上半年就要走一拨。最开始是和外包终止合作,然后是各种裁员,甚至技术的高管也裁了四五个人,同事都说最后都要走向被裁的道路,所以公司主动离职的人很少。这次自己被裁也在意料之中,不过真到被裁的时候,心里也不是滋味儿。刚开始人力找我谈的时候,说只有N+1的补偿,未休年假折换成工资,其他的就没有了。我想,公司也太能算计了,要在3月底之前把我干掉,这样就不用给我年终奖了。这里要说明一下,公司去年刚上市,所以一个完整财年是4月份到次年3月份,所以年终奖的计算也要截至到3月底。公司发布的年终考核明确说了,考核周期结束前离职是没有年终奖的。公司要在3月份之前干掉我,明显是想省下年终奖的钱。这我是绝对不能同意的,要拖也要拖过3月份,我也明确和人力说了,要不公司强制辞退我,要不过了3月份再说。公司的人力也不是很强硬,和我说公司没有强制辞退的意思,这不是和您在协商吗?我说,要是协商的话,这个条件我不能同意。说完我就离开会议室,回到工位继续工作了。
这里和大家普及个知识吧:大家平时说的N+1都是__协商离职__,协商的意思大家都懂吧,就是双方商量,双方都同意。所以在和人力谈的时候,对自己不利的条件要坚决不同意。另外一种就是强制辞退了,这种情况最好办,去劳动仲裁拿2N就可以了。
之后,公司又找我谈了两三次,我都坚持要过了3月份,拿了年终奖再走。在这期间,我也在收集一些证据,以便在劳动仲裁时使用。甚至我都已经在劳动仲裁网上提交了资料,保存为草稿了,如果公司和我玩硬的,我就直接点提交,立刻走劳动仲裁。最后公司也没有为难我,答应了我的要求。也是,4月初,我离开了公司,回家了。

在这家公司工作了3年多,公司里的同事都是985211毕业的,虽然他们的学历都很高,但是感觉书生气太重,无论是刚毕业工作两三年的,还是工作了10多年的,还是没有摆脱身上的学生气,我在他们中间显得格格不入。既然从学校毕业了,开始混社会了,就要用社会人的规则,身上那股学生气该去掉就去掉吧。很多大厂里,都用“某某同学”称呼对方,即使被裁了,也用“毕业”,这就是这些大厂要保留大家的学生气、书生气,这样便于管理,如果大家都是社会人,混社会的,那这些大厂的管理成本要大幅提高了。哈哈,这可能也是中国教育的悲哀吧,想想100年前,那群有学问有知识的年轻人,哪个不是一身的阳刚之气。

社会工作者

在离职前的一段时间里,我在晚上没事儿刷着某音,经常刷到考社会工作者证的视频,说考下社会工作者证,到社区上班,工作稳定,朝九晚五不加班,平时上班也没啥事,还不耽误接孩子上下学,考试很简单,都是选择题。这确实让我很心动,虽然钱不多,但是够吃够喝了,事少离家近,还能接孩子上下学,于是我就报名了。真正报完名,已经是4月中旬了,考试是在6月中旬,还有两个月的时间,要考3门课。在这两个月的时间里,我整天在网上看课程的视频,刷题。真正学习起来,发现这个考试并不容易,这也是一个国家级的考试,据说通过率只有7%,难度确实不小,并不像某音里说的那样。就这样被套路了一回,最后我也是只过了两门,剩下的那门只能来年再考了。

这两个月的时间,应该是我失业后最失落了一段时间,这段时间整天刷boss,投简历,基本上都是已读不回。北京的JAVA行情是要崩了吗?记得口罩前还不是这个样子,boss都是主动联系你的。而现在你主动投,人家都懒得搭理你,这是大环境的原因吗?还是自己的原因?干了这么多年,自己在技术上,管理上都是挺不错的。难道是年龄的原因?哎,不管什么原因吧,用辩证法分析,就是内因和外因。继续投简历继续找工作,慢慢碰吧。

尝试接私活

社工考试失利后,我想在今年找工作比较困难的情况下,是不是可以尝试做一些其他的事情。自认为技术能力不错的我,在几个比较大的接单平台注册了账号,尝试接单。不久平台就给我推了一个单子,让我自己去聊,说需求很笼统。于是我和对方联系了一下,发现对方离我住的地方不远,我们就约好了线下聊,线下见面后,我才了解到,对方要做的是一套部队用的系统,目前只有一个系统要求,具体的需求没有,只有一些目录菜单。我和对方说,您还是细化需求吧,这个现在进入不了开发阶段,太笼统了。对方说现在还在招标阶段,让我帮他搞标书,画架构图。我也和对方说了,标书中技术部分我可以搞,其他部分我就搞不了了。我回去给对方画了个架构图,对方还是比较满意的。过了几天,对方给我打电话,说要我陪他去出差,说是去部队,和人家谈投标的事,明天就走。这也太突然了,今天打电话,明天就走,而且也没有说车票和住宿的问题,我拒绝了对方,之后就没有再联系过。这种单子确实没法做,还在投标阶段,没有成果物,也没有办法和对方谈钱,我本来也是想着和对方保持着合作的关系,钱不钱的先放一边,可对方又让我出差,而且没有谈费用,这让我接受不了,虽然拒绝了出差,但是我想如果对方继续联系我,我还是会合作的,但是对方也一直没有联系我,这个单子就终止了。

没多久,平台又给我推了一个单子,这个单子的需求要明确一些,就是两个系统之间导数据。这个需求比较简单,和对方视频联系了一下,聊的也很好,我感觉这个的单子应该能够拿下。可是随后几天和对方深入了解需求,发现两套系统的字段完全对不上,我把问题反馈给了对方,甚至把两套系统的字段列了出来,让对方找对应关系,对方也意识到了问题,说是和第三方系统的人去聊。接下来就是漫长的等待,期间也和对方聊这个项目还要不要做,对方说要做,可是后面就没什么进展了。这个项目也黄了。

我自己属于比较保守的,在接单平台并没有交会员费,既然没有交费,人家也不会把那些需求明确,上手就能干的单子给你。现在的接单平台真的是两头赚,一边赚需求方的,另一边赚开发者的。有的平台接单前要交3W多的费用,我还没有接到单,还没有挣到钱,先要交平台3W多,这风险太大了。还是朋友推荐的靠谱,可是我没有这方面的资源。这两个单子没接成,后面也没有再尝试。

玩电商

接私活失败后,就到了7月中旬,孩子放暑假了,于是带着家人去呼伦贝尔完了一圈,历时半个月吧。回来后想着总要干点事吧,正在发愁之际,某音的一波推流让我搞起了失败的电商。经常刷到一个人在家做电商的视频,视频中介绍做无货源,轻松月入过万。先和大家说说什么是无货源电商吧,就是你开个网店,不用先进货,而是去某某88上找你要卖的货,比如你想卖鞋子,就去某某88上去找鞋子,然后通过铺货软件把商品详情的内容搬运到你的网店里,你的网店里就会有同样的产品了,然后你再设置好价格,就可以商家去卖了,用户下单好,订单也会通过软件自动同步给某某88的供应商,你在中间就赚差价。很轻松是不是,我也觉得是,而且某音里都是教你怎么做网店的视频,不去做就怕错过这一波机会呀。于是乎兴致勃勃的注册了营业执照,开通了网店,按照视频的说法,新店没有流量没有订单,要用货损的方式起店,货损就是亏钱卖,找个几块钱的产商品,每单亏一两块钱,这样很快就会有订单,我也是按照这种方式去做的,但是这里的坑也是很深的。因为某某88的商品质量问题,俗话说得好,便宜没好货,好货不便宜,为了出单,我找的都是七八块钱的商品,用货损出单,开始确实出单很快,很快店铺出了体验分,但是随之而来的是退货,商品质量不行,用户退货很多,这就是网上购物的缺点吧,看不见货,其实就是在卖图片,谁的图片拍的好,谁的单子就多,当用户收到货后,发现质量不行马上就会退货。我在某某88上看到的野餐垫不错,就铺到了我的店铺,图片看着是挺不错的,价格也不贵,我就交我媳妇买了一个,结果送到家后,打开一看,和一张塑料布没有什么区别,这简直就是骗子,我赶紧就把这个野餐垫下架了。哎,想出单就要便宜,但是便宜没好货,这里边还是有很多门道的。群里有个店主卖的是大裤衩,结果自己买了一个,到家打开一看,直接就给扔了。

上面说的都是货的问题,如果打算卖的好,还真是要好好选选自己卖的商品,在网上看不到实物,不知道质量如何,你卖了差品那就是你的问题。再一个就是“割韭菜”的问题,网店没开几天,你的电话就会被打爆,一群做代运营的天天骚扰你,代运营就是他们帮你经营店铺,店铺上他们的垃圾货,通过你的店铺卖出去,然后他们再抽佣,等于赚了两份钱,你店铺的流量和单量就上去了,但是你的后面你的退货率很高,你真正想卖的东西也是卖不动的。这种代运营的,我根本没有理他们,他们割不到我的韭菜。其实仔细想想就好,他们那么会经营店铺,为什么不自己开店铺卖货呢?归根结底还是现在电商不好做,看看网上那些教人做电商的,那么好做,你自己闷声做发大财就行了,干嘛还要叫别人呢?明显就是割韭菜。

我的店铺基本已经死了,没什么流量。瞎搞吧,铺货软件花了35,货损差不多20多吧,基本上是亏了五六十块吧,和别人比起来我这就不叫赔。

何去何从

半年多的时间,基本上就是瞎搞,各个方向也有所尝试,没有找到出路。期间也有过几个面试,基本都是小公司,而且还没有成功,面试的感觉也不错,就是不想要我,也没办法,慢慢碰吧。继续投简历,有面试积极准备面试。我想后续我还是回归的技术上来吧,毕竟干了10多年了,但是怎么通过技术去变现,还没有想好,慢慢去尝试吧。这一段时间,应该是我职业生涯甚至是我人生中最困难的一段时间,慢慢熬吧~~

概述

Hystrix 为 微服务架构提供了一整套服务隔离、服务熔断和服务降级的解决方案。它是熔断器的一种实现,主要用于解决微服务架构的高可用及服务雪崩等问题

Hystrix 的特性如下:

  • 服务熔断:Hystrix 熔断器就像家中的安全阀一样,一旦某个服务不可用,熔断器就会直接切断该链路上的请求,避免大量的无效请求影响系统稳定,并且熔断器有自我检测和恢复的功能,在服务状态恢复正常后会自动关闭
  • 服务降级:Hystrix 通过 falback 实现服务降级,在需要进行服务降级的类中定义一个 falback 方法,当请求的远程服务出现异常时,可以直接使用 fallback 方法返回异常信息,而不调用远程服务
  • 依赖隔离:Hyslrix 通过线程池和信号量两种方式实现服务之间的依赖隔离,这样即使其中一个服务出现异常,资源迟迟不能释放,也不会影响其他业务线程的正常运行
    • 线程池的隔离策略:Hystrix 线程池的资源隔离为每个依赖的服务都分配一个线程池,每个线程池都处理特定的服务,多个服务之间的线程资源互不影响,以达到资源隔离的目标。当某个依赖服务异常时,只会阻塞这个依赖服务的线程资源,不影响其他依赖服务
    • 信号量的隔离策略:Hystrix 信号量的隔离策略是为每个依赣的服务都分配一个信号量(原子计数器),当请求某个依赖服务时,先判断该服务的信号量值是否超过最大值。如果超过,则直接丢弃并返回错误提示,如果不超过,则在处理请求前执行“信号量+1”的操作,在请求返回后执行“信号量-1”的操作
  • 请求缓存:Hystrix 按照请求参数把请求结果缓存起来,当后面有相同的请求时不会再走完整的调用链流程,而是把上次缓存的结果直接返回,以达到服务快速响应和性能优化的目的。同时,缓存可作为服务降级的数据源,当远程服务不可用时,直接返回缓存数据,对于消费者来说,只是可能获取了过期的数据,这样就优雅地处理了系统异常
  • 请求合井:当微服务需要调用多个远程服务做结果的汇总时,需要使用请求合并。Hystrix 采用异步消息订阅的方式进行请求合并,当应用程序需要请求多个接口时,采用异步调用的方式提交请求,然后订阅返回值,应用程序的业务可以接着执行其他任务而不用阻塞等待,当所有请求都返回时,应用程序会得到一个通知,取出返回值合并即可


Hystrix 服务降级流程

  1. 当有服务请求时,首先会根据注解创建一个 HystrixCommand 指令对象,该对象设置了服务调用失败的场景和调用失败后服务降级的业务逻辑方法
  2. 熔斯器判断状态,当熔断器处于开路状态时,直接调用服务降级的业务逻辑方法返回调用失败的反馈信息
  3. 当熔断器处于半开路或者闭路状态时,服务会进行线程池和信号量等检查如果有可用资源,有则调用正常业务逻辑。如果调用正常业务逻辑成功,则返回成功后的息,如果失败,则调用降级的业务逻辑,进行服务降级
  4. 当熔断器处于半开路或者闭路状态时,如果在当前服务线程池和信号量中无可用资源,则执行服务降级的业务逻辑,返回调用失败的信息
  5. 当熔断器处于半开路状态并且本次服务执行失败时,熔断器会进入开路状态
  6. 当正常业务逻辑处理超时或者出现错误时,HystrixCommand 会执行服务降缓的业务逐辑,返回调用失败的信息
  7. 线程池和信号量的资源检查及正常业务逻辑会将自己的状态和调用结果反馈给监控,监控将服务状态反馈给熔断器,以便熔断器判断熔断状态


Hystrix 应用

Hystrix 的使用主要分为服务熔断、服务降级和服务监控三个方面

在 pom.xml 文件中引入 Hystrix 依赖,其中,spring-cloud-slarter-netflix-hystrix 和 hystrix-javanica 为 Hystrix 服务熔断所需的依赖,spring-cloud-netflix-hystrix-dashboard 为 Hystrix 服务监控所需的依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-eureka</artifactId>
  <version>1.4.6.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
  <version>1.4.6.RELEASE</version>
</dependency>
<dependency>
  <groupId>com.netflix.hystrix</groupId>
  <artifactId>hystrix-javanica</artifactId>
  <version>RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-netflix-hystrix-dashboard</artifactId>
  <version>1.4.6.RELEASE</version>
</dependency>

通过 @EnableHystrix 注解开启对服务熔断的支持,通过 @EnableHystrixDashboard 注解开启对服务监控的支持。注意,Hystrix 一般和服务发现配合使用,这里使 @EnableEurekaClient 开启了对服务发现客户端的支持

@SpringBootApplication
@EnableEurekaClient
@EnableHystrix
@EnableHystrixDashboard
public class HystrixServiceApplication {

  public static void main(String[] args) {
    SpringApplication.run(HystrixServiceApplication.class, args);
  }

  @Bean
  public IRule ribbonRule() {
    return new RandomRule();
  }

  @Bean
  @LoadBalanced
  public RestTemplate restTemplate() {
    return new RestTemplate();
  }
}

配置 application.properties 文件

#服务名
spring.application.name=hystrix
#服务的端口
server.port=9005
#注册中心的地址
eureka.client.serviceUrl.defaultZone=http://localhost:9001/eureka/
eureka.client.registry-fetch-interval-seconds=30

服务熔断和降级,定义了一个远程调用的方法 hystrixHandler(),并通过 @HystrixCommand(fallbackMethod="exceptionHandler") 在方法上定义了一个服务降级的命令,当远程方法调用失败时,Hystrix 会自动调用 fallbackMethod 来完成服务熔断和降级,这里会调用 exceptionHandler 方法

@Autowired
private RestTemplate restTemplate;

//定义服务降级命令
@HystrixCommand(fallbackMethod = "exceptionHandler")
@RequestMapping(value = "/service/hystrix", method = RequestMethod.GET)
public String hystrixHandler() {
  return restTemplate.getForEntity("http://EUREKA-CLIENT/serviceProducer", String.class).getBody();
}

public String exceptionHandler() {
  return "提供者服务挂了";
}


异步处理

上节中的远程调用请求必须等到网络请求返回结果后,才会执行后面的代码,即阻塞运行。而在实际使用过程中,应用程序常常希望使用非阻塞 IO 来更优雅地实现功能.Hyslrix 为非阻塞 IO 提供了两种实现方式,分别是表示将来式的 Future 和表示回调式的 Callable

1. Future

定义 HystrixCommand

public class CommandFuture extends HystrixCommand<String> {

  private String name;

  private RestTemplate restTemplate;

  public CommandFuture(String name, RestTemplate restTemplate) {
    super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
                //1:通过 HystrixCommandKey 工厂定义依赖的名称
                .andCommandKey(HystrixComnandKey.Factory.asKey("HelloWorld"))
                //2:通过 HystrixThreadPoolKey 工厂定义线程池的名称
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("HelloWorldPool")));
    this.name = name;
    this.restTemplate = restTemplate;
  }

  //3:定义远程调用的方法体
  @Override
  protected String run() {
    String result = restTemplate.getForEntity("http://EUREKA-CLIENT/serviceProducer", String.class).getBody();
    return result;
  }

  //4:服务降级的处理逻辑
  @Override
  protected String getFallback() {
    return "远程服务异常";
  }
}

以上代码通过继承 HystrixCommand 定义了一个 CommandFuture 来实现异步请求,其中,正常业务执行的逻辑在覆写的 run() 方法体中被执行,服务降级的方法在 getFallback() 中被执行。需要注意的是,这里使用 andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld")) 实现了使用 HystrixCommandKey 工厂定义依赖的名称,每个 CommandKey 都代表一个依赖抽象,相同的依赖要使用相同的 CommandKey 名称。依赖隔离的本质敦是对相同
CommandKey 的依赖进行离,使用 andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("HelloWorldPool")) 实现了基于 HystrixThreadPoolKey 工厂定义线程池的名称。当对同一业务的依赖进行资源隔离时,使用 CommandGroup 进行区分,但是当对同一依赖进行不同的远程调用时(例如,一个是 Redis 服务,一个是 HTTP 服务),则可以使用 HystrixThreadPoolKey 进行隔离区分

使用 HystrixCommand

@RequeatMapping(value = "/service/hystrix/future", method = RequestMethod.GET)
public String hystrixFutureHandler() throws ExecutionException, InterruptedException {
  //定义基于Future的异步调用,请求会以队列的形式在线程池中被执行
  Future<String> future = new CommandFuture("future", restTemplate).queue();
  return future.get();
}

2. Callable

预定义一个回调任务,Callable 在发出请求后,主线程继续执行,在请求执行完成并返回结果后,Callable 会自动调用回调任务

定义 HystrixObservableCommand

public class CommandObservable extends HystrixObservabCommand<String> {

  private String name;
  private RestTemplate restTemplate;

  public CommandObservable(String nane, RestTemplate restTemplate) {
    
    super(HystrixConmandGroupKey.Factory.asKey("ExampleGroup"));
    this.nane = name;
    this.restTemplate = restTemplate;
  }

  //基于观察者模式的请求发布
  @Override
  protected Observable<String> construct () {
    return Observable.create(new Observable.OnSubscribe<String>() {
      @Override
      public void call(Subscriber<? super String> subscriber) {
        try {
          //执行远程过程调用
          if(!subscriber.isUnsubscribed()) {
            String result = restTemplate.getForEntity("http://EUREKA-CLIENT/serviceProducer", String.class).getBody();
            //将调用结果传递下去
            subscriber.onNext(result):
            subscriber.onCompleted();
          }
        } catch(Exception e) {
          e.printStackTrace();
          subscriber.onError(e);
        }
      }
    }
  } 

  //服务降级的处理逻辑
  @Override
  protected Observable<String> resumeWithFallback() {
    return Observable.create(new Observable.OnSubscribe<String>() {
      @Override
      public void call(Subscriber<? super String> subscriber) {
        try {
          if(!subscriber.isUnsubscribed()) {
            subscriber.onNext("远程服务异常”);
            subscriber.onCompleted();
        } catch (Exception e) {
            subscriber.onError(e);        
        }
      }
    }
  }
}

以上代码定义了名为 CommandObservable 的类,该类继承自 HystrixObservableCommand 接口,并通过覆写 HystrixObservableCommand 接口中的 construct() 实现观察者模式。具体实现为通过 Obsenvable.create() 创建并返回一个 Observable
对象,在创建对象时,通过 new Observable.OnSubscribe () 实现消息的监听和处理。其中,call 方法用于消息的接收和业务的处理,在消息处理完成后通过 subscriber.onNext(result) 将调用结果传递下去,当所有任务都执行完成时通过 subscriber.onCompleted() 将总体执行结果发布出去。resumeWithFallback 方法是服务降级的处理逻辑,当服务出现异常时,通过 subscriber.onNext("远程服务异常") 进行服务熔断和异常消息的发布,实现服务降级处理

使用 HystrixObservableCommand

public String hystrixCallableHandler() throws ExecutionException, InterruptedException {

  List<String> list = new ArrayList<>();

  //定义基于消息订间的异步调用,请求结果会以事件的方式通知
  Observable<String> observable = new CommandObservable("observer", restTemplate).observe();
  //基于观察者模式的清求结果订阅
  observable.subscribe(new Observer<String>() {
    //onCompleted方法在所有请求完成后执行@
    @Override
    public void onCompleted() {
      System.out.println("所有请求已经完成...”);
    }
    @Override
    public void onError(Throwable throwable) {
      throwable.printStackTrace();
    }
    //订阅调用事件,请求结果汇聚的地方,用集合将返回的结果收集起来
    @Override
    public void onNext(String s) {
      System.out.printin("结果来了...");
      list.add(s);
      return list.toString();
    }
  }
}

以上代码通过 new CommandObservable("observer", restTemplate).observe() 定义了一个实现服务发布的命令。通过调用 abservabe.subscribe() 来实现基于观察者模式的请求结果订阅,其中,订阅的数据结果在 onNext() 中被通知,总体调用结果在 onCompleted() 中被通知。服务处理异常结果在 onError() 中被通知


Hystrix Dashboard

HystrixDashboard 主要用于实时监控 Hystrix 的各项运行指标。通过 HystrixDashboard 可以查询 Hystrix 的实时信息,用于快速定位和发现问题。Hystrix Dashboard 的使用简单、方便,首先在 pom.xml 文件中加入 spring-cloud-netfix-hystrix-dashboard 依赖,然后使用 @EnableHystrixDashboard 注解开启 Dashboard 功能即可。在服务启动后,在浏览器地址栏中输人
http://127.0.0.1:9005/hystrix
,就可以看到监控界面

lxml官方入门教程(The lxml.etree Tutorial)翻译

说明:

  • 首次发表日期:2024-09-05
  • 官方教程链接:
    https://lxml.de/tutorial.html
  • 使用KIMI和豆包机翻
  • 水平有限,如有错误请不吝指出

这是一个关于使用lxml.etree处理XML的教程。它简要概述了ElementTree API的主要概念,以及一些简单的增强功能,这些功能可以让您作为程序员的生活更轻松。

有关API的完整参考,请查看
生成的API文档

导入lxml.etree的常见方式如下:

from lxml import etree

如果你的代码仅使用ElementTree API,并且不依赖于
lxml.etree
任何的特有功能,您还可以使用以下导入链来回退到Python标准库中的ElementTree:

try:
    from lxml import etree
    print("running with lxml.etree")
except ImportError:
    import xml.etree.ElementTree as etree
    print("running with Python's xml.etree.ElementTree")

为了帮助编写可移植代码,本教程在示例中明确指出了所呈现API的哪一部分是lxml.etree对原始ElementTree API的扩展。

The Element class

元素(Element)是ElementTree API的主要容器对象。大部分XML树功能都是通过这个类访问的。元素(Elements)可以通过
Element
factory轻松创建:

root = etree.Element("root")

元素的XML标签名称可以通过
tag
属性访问:

print(root.tag)

元素在XML树结构中组织。要创建子元素并将它们添加到父元素,您可以使用
append()
方法:

root.append(etree.Element("child"))

然而,这种情况非常常见,因此有一个更简短且效率更高的方法来实现这一点:
SubElement
工厂。它接受与
Element
工厂相同的参数,但另外需要将父元素作为第一个参数:

child2 = etree.SubElement(root, "child2")
child3 = etree.SubElement(root, "child3")

要确认这确实是XML,您可以序列化您创建的树:

etree.tostring(root)
b'<root><child1/><child2/><child3/></root>'

我们将创建一个小型辅助函数,为我们美观地打印XML:

def prettyprint(element, **kwargs):
    xml = etree.tostring(element, pretty_print=True, **kwargs)
    print(xml.decode(), end='')
prettyprint(root)
<root>
  <child1/>
  <child2/>
  <child3/>
</root>

Elements are lists

为了便于直接访问这些子元素,元素尽可能地模仿了普通Python列表的行为:

>>> child = root[0]
>>> print(child.tag)
child1

>>> print(len(root))
3

>>> root.index(root[1])  # lxml.etree only!
1

>>> children = list(root)

>>> for child in root:
...     print(child.tag)
child1
child2
child3

>>> root.insert(0, etree.Element("child0"))
>>> start = root[:1]
>>> end   = root[-1:]

>>> print(start[0].tag)
child0
>>> print(end[0].tag)
child3

在ElementTree 1.3和lxml 2.0之前,您还可以检查元素的真值,以查看它是否有子元素,即查看子元素列表是否为空:

if root:   # this no longer works!
    print("The root element has children")

这种做法不再被支持,因为人们倾向于期望“某物”(something)evaluates为
True
,并期望元素(Elements)是“某物”,无论它们是否有子元素。因此,许多用户发现,任何元素在像上面的if语句中评估为False是令人惊讶的。相反,使用
len(element)
,这既更明确,也更少出错。

print(etree.iselement(root)) # test if it's some kind of Element
True
if len(root):  # test if it has children
    print("The root element has children")

在另一个重要的场景下,lxml中元素(从2.0及以后版本)的行为与列表(lists)的行为以及原始ElementTree(1.3版本之前或Python 2.7/3.2之前)的行为有所不同:

for child in root:
    print(child.tag)
child0
child1
child2
child3
root[0] = root[-1] # this moves the element in lxml.etree!
for child in root:
    print(child.tag)
child3
child1
child2

在这个例子中,最后一个元素被
移动
到了一个不同的位置,而不是被复制,也就是说,当它被放到一个不同的位置时,它会自动从之前的位置被移除。在列表中,对象可以同时出现在多个位置,上述赋值操作只会将项目引用复制到第一个位置,因此两者包含完全相同的项目:

>>> l = [0, 1, 2, 3]
>>> l[0] = l[-1]
>>> l
[3, 1, 2, 3]

请注意,在原始的ElementTree中,单个元素对象可以位于任何数量的树中的任何位置,这允许进行与列表相同的复制操作。明显的不足是,对这样的元素进行的修改将应用于它在树中出现的所有位置,这可能是也可能不是预期的。

备注
:在lxml中,上述赋值操作会移动元素,与lists和原始的ElementTree中不同。

这种差异的好处是,在lxml.etree中的一个元素总是恰好有一个父元素,这可以通过getparent()方法查询。这在原始的ElementTree中是不支持的。

root is root[0].getparent() # lxml.etree only!

如果您想将元素复制到lxml.etree中的不同位置,请考虑使用Python标准库中的copy模块创建一个独立的深拷贝:

from copy import deepcopy

element = etree.Element("neu")
element.append(deepcopy(root[1]))
print(element[0].tag)
# child1
print([c.tag for c in root])
# ['child3', 'child1', 'child2']

元素的兄弟(或邻居)作为下一个和上一个元素进行访问:

root[0] is root[1].preprevious()  # lxml.etree only!
# True
root[1] is root[0].getnext() # lxml.etree only!

Elements carry attributes as a dict

XML元素支持属性(attributes)。您可以直接在Element工厂中创建它们:

root = etree.Element("root", interesting="totally")
etree.tostring(root)
# b'<root interesting="totally"/>'

属性只是无序的
名称-值
对,因此通过元素的类似字典的接口处理它们非常方便:

print(root.get("interesting"))
# totally
print(root.get("hello"))
# None
root.set("hello", "Huhu")
print(root.get("hello"))
# Huhu
etree.tostring(root)
# b'<root interesting="totally" hello="Huhu"/>'
sorted(root.keys())
# ['hello', 'interesting']
for name, value in sorted(root.items()):
    print('%s = %r' % (name, value))
# hello = 'Huhu'
# interesting = 'totally'

在您想要进行项目查找或有其他原因需要获取一个“真实”的类似字典的对象的情况下,例如为了传递它,您可以使用
attrib
属性:

>>> attributes = root.attrib

>>> print(attributes["interesting"])
totally
>>> print(attributes.get("no-such-attribute"))
None

>>> attributes["hello"] = "Guten Tag"
>>> print(attributes["hello"])
Guten Tag
>>> print(root.get("hello"))
Guten Tag

请注意,attrib是一个由元素本身支持(backed)的类似字典的对象。这意味着对元素的任何更改都会反映在attrib中,反之亦然。这也意味着只要XML树有一个元素的attrib在使用中,XML树就会在内存中保持活动状态。要获取一个不依赖于XML树的属性的独立快照,将其复制到一个字典中:

d = dict(root.attrib)
sorted(d.items())
# ('hello', 'Guten Tag'), ('interesting', 'totally')]

Elements contain text

元素可以包含文本:

root = etree.Element("root")
root.text = "TEXT"

print(root.text)
# TEXT

etree.tostring(root)
# b'<root>TEXT</root>'

在许多XML文档(以数据为中心的文档)中,这是唯一可以找到文本的地方。它被树层次结构最底层的一个叶子标签封装。

然而,如果XML用于标记文本文档,如(X)HTML,文本也可以出现在不同元素之间,就在树的中间:

<html><body>Hello<br/>World</body></html>

在这里,
<br/>
标签被文本包围。这通常被称为文档样式或混合内容XML。元素通过它们的tail属性支持这一点。它包含直接跟随元素的文本,直到XML树中的下一个元素:

>>> html = etree.Element("html")
>>> body = etree.SubElement(html, "body")
>>> body.text = "TEXT"

>>> etree.tostring(html)
b'<html><body>TEXT</body></html>'

>>> br = etree.SubElement(body, "br")
>>> etree.tostring(html)
b'<html><body>TEXT<br/></body></html>'

>>> br.tail = "TAIL"
>>> etree.tostring(html)
b'<html><body>TEXT<br/>TAIL</body></html>'

.text

.tail
这两个属性足以表示XML文档中的任何文本内容。这样,ElementTree API 除了 “Element” 类之外不需要任何特殊的文本节点,这些节点往往经常会常造成阻碍(正如你可能从传统 DOM API 中了解到的那样)。

然而,也有一些情况下,尾随(tail)文本也会碍事。例如,当您序列化树中的一个元素时,您并不总是希望其尾随文本出现在结果中(尽管您仍然希望包含其子元素的尾部文本)。为此,
tostring()
函数接受关键字参数
with_tail

>>> etree.tostring(br)
b'<br/>TAIL'
>>> etree.tostring(br, with_tail=False) # lxml.etree only!
b'<br/>'

如果你只想读取文本,即不包含任何中间标签,你必须以正确的顺序递归地连接所有的文本和尾部属性。同样,“
tostring()
” 函数可以提供帮助,这次使用 “
method
” 关键字。

>>> etree.tostring(html, method="text")
b'TEXTTAIL'

Using XPath to find text

提取树文本内容的另一种方式是XPath,它还允许您将单独的文本块提取到一个列表中:

>>> print(html.xpath("string()")) # lxml.etree only!
TEXTTAIL
>>> print(html.xpath("//text()")) # lxml.etree only!
['TEXT', 'TAIL']

如果您想更频繁地使用这个功能,您可以将其封装在一个函数中:

>>> build_text_list = etree.XPath("//text()") # lxml.etree only!
>>> print(build_text_list(html))
['TEXT', 'TAIL']

请注意,XPath返回的字符串结果是一个特殊的“智能”对象,它了解其来源。您可以通过其
getparent()
方法询问它来自哪里,就像您对元素所做的那样:

>>> texts = build_text_list(html)
>>> print(texts[0])
TEXT
>>> parent = texts[0].getparent()
>>> print(parent.tag)
body

>>> print(texts[1])
TAIL
>>> print(texts[1].getparent().tag)
br

您还可以找出它是普通文本内容还是尾随文本:

>>> print(texts[0].is_text)
True
>>> print(texts[1].is_text)
False
>>> print(texts[1].is_tail)
True

虽然这对text()函数的结果有效,但lxml不会告诉您由XPath函数string()或concat()构造的字符串值的来源:

>>> stringify = etree.XPath("string()")
>>> print(stringify(html))
TEXTTAIL
>>> print(stringify(html).getparent())
None

Tree iteration

对于上述这样的问题,当你想要递归地遍历树并对其元素进行一些操作时,树迭代(tree iteration)是一个非常方便的解决方案。元素(Elements)为此提供了一个树迭代器。它按照文档顺序生成元素,即与将树序列化为XML时其标签出现的顺序一致。

>>> root = etree.Element("root")
>>> etree.SubElement(root, "child").text = "Child 1"
>>> etree.SubElement(root, "child").text = "Child 2"
>>> etree.SubElement(root, "another").text = "Child 3"

>>> prettyprint(root)
<root>
  <child>Child 1</child>
  <child>Child 2</child>
  <another>Child 3</another>
</root>

>>> for element in root.iter():
...     print(f"{element.tag} - {element.text}")
root - None
child - Child 1
child - Child 2
another - Child 3

如果您知道您只对单个标签感兴趣,可以将标签名称传递给
iter()
,让它为您过滤。从lxml 3.0开始,您还可以传递多个标签,在迭代期间拦截多个标签。

>>> for element in root.iter("child"):
...     print(f"{element.tag} - {element.text}")
child - Child 1
child - Child 2

>>> for element in root.iter("another", "child"):
...     print(f"{element.tag} - {element.text}")
child - Child 1
child - Child 2
another - Child 3

默认情况下,迭代会生成树中的所有节点,包括ProcessingInstructions、Comments和Entity实例。如果您想确保只返回Element对象,可以将Element工厂作为标签参数传递:

>>> root.append(etree.Entity("#234"))
>>> root.append(etree.Comment("some comment"))

>>> for element in root.iter():
...     if isinstance(element.tag, str):
...         print(f"{element.tag} - {element.text}")
...     else:
...         print(f"SPECIAL: {element} - {element.text}")
root - None
child - Child 1
child - Child 2
another - Child 3
SPECIAL: &#234; - &#234;
SPECIAL: <!--some comment--> - some comment

>>> for element in root.iter(tag=etree.Element):
...     print(f"{element.tag} - {element.text}")
root - None
child - Child 1
child - Child 2
another - Child 3

>>> for element in root.iter(tag=etree.Entity):
...     print(element.text)
&#234;

请注意,传递通配符“
*
”作为标签名也将生成所有的
Element
节点(并且只有元素节点)。

for element in root.iter(tag="*"):
    if isinstance(element.tag, str):
        print(f"element.tag - {element.text}")
    else:
        print(f"SPECIAL: {element} - {element.text}")
element.tag - None
element.tag - Child 1
element.tag - Child 2
element.tag - Child 3


lxml.etree
中,
elements
为树中的所有方向提供了进一步的迭代器:子节点(
iterchildren()
)、父节点(或者更确切地说是祖先节点)(
iterancestors()
)和兄弟节点(
itersiblings()
)。

Serialisation

序列化通常使用
tostring()
函数返回字符串,或者使用
ElementTree.write()
方法写入文件、类文件对象(file-like object)或URL(通过FTP PUT或HTTP POST)。这两个调用都接受相同的关键字参数,如
pretty_print
用于格式化输出,或者
encoding
用于选择除纯ASCII之外的特定输出编码:

>>> root = etree.XML('<root><a><b/></a></root>')

>>> etree.tostring(root)
b'<root><a><b/></a></root>'

>>> xml_string = etree.tostring(root, xml_declaration=True)
>>> print(xml_string.decode(), end='')
<?xml version='1.0' encoding='ASCII'?>
<root><a><b/></a></root>

>>> latin1_bytesstring = etree.tostring(root, encoding='iso8859-1')
>>> print(latin1_bytesstring.decode('iso8859-1'), end='')
<?xml version='1.0' encoding='iso8859-1'?>
<root><a><b/></a></root>

>>> print(etree.tostring(root, pretty_print=True).decode(), end='')
<root>
  <a>
    <b/>
  </a>
</root>

请注意,美观打印(
pretty_print
)会在末尾附加一个新行。因此,我们在这里使用
end=''
选项,以防止
print()
函数添加另一个换行符。

为了在序列化之前对美观打印(
pretty_print
)进行更细粒度的控制,您可以使用
indent()
函数(在lxml 4.5中添加)在树中添加空白缩进:

>>> root = etree.XML('<root><a><b/>\n</a></root>')
>>> print(etree.tostring(root).decode())
<root><a><b/>
</a></root>

>>> etree.indent(root)
>>> print(etree.tostring(root).decode())
<root>
  <a>
    <b/>
  </a>
</root>

>>> root.text
'\n  '
>>> root[0].text
'\n    '

>>> etree.indent(root, space="    ")
>>> print(etree.tostring(root).decode())
<root>
    <a>
        <b/>
    </a>
</root>

>>> etree.indent(root, space="\t")
>>> etree.tostring(root)
b'<root>\n\t<a>\n\t\t<b/>\n\t</a>\n</root>'

在lxml 2.0及更高版本以及xml.etree中,序列化函数不仅可以进行XML序列化。您可以通过传递method关键字来序列化为HTML或提取文本内容:

>>> root = etree.XML(
...    '<html><head/><body><p>Hello<br/>World</p></body></html>')

>>> etree.tostring(root)  # default: method = 'xml'
b'<html><head/><body><p>Hello<br/>World</p></body></html>'

>>> etree.tostring(root, method='xml')  # same as above
b'<html><head/><body><p>Hello<br/>World</p></body></html>'

>>> etree.tostring(root, method='html')
b'<html><head></head><body><p>Hello<br>World</p></body></html>'

>>> prettyprint(root, method='html')
<html>
<head></head>
<body><p>Hello<br>World</p></body>
</html>

>>> etree.tostring(root, method='text')
b'HelloWorld'

与XML序列化一样,纯文本序列化的默认编码是ASCII:

>>> br = next(root.iter('br'))  # get first result of iteration
>>> br.tail = 'Wörld'

>>> etree.tostring(root, method='text')  # doctest: +ELLIPSIS
Traceback (most recent call last):
  ...
UnicodeEncodeError: 'ascii' codec can't encode character '\xf6' ...

>>> etree.tostring(root, method='text', encoding="UTF-8")
b'HelloW\xc3\xb6rld'

在这里,将序列化目标设为Python文本字符串(text string)而不是字节字符串(byte string)可能会很方便。只需将'unicode'作为编码传递:

>>> etree.tostring(root, encoding='unicode', method='text')
'HelloWörld'
>>> etree.tostring(root, encoding='unicode')
'<html><head/><body><p>Hello<br/>Wörld</p></body></html>'

W3C有一篇关于Unicode字符集和字符编码的好文章:
https://www.w3.org/International/tutorials/tutorial-char-enc/

The ElementTree class

ElementTree
主要是一个围绕具有根节点的树的文档包装器。它提供了一些用于序列化和一般文档处理的方法。

root = etree.XML('''<?xml version="1.0"?>
<!DOCTYPE root SYSTEM "test" [ <!ENTITY tasty "parsnips"> ]>
<root>
    <a>&tasty;</a>
</root>
''')
tree = etree.ElementTree(root)
print(tree.docinfo.xml_version)
1.0
print(tree.docinfo.doctype)
<!DOCTYPE root SYSTEM "test">
tree.docinfo.public_id = '-//W3C//DTD XHTML 1.0 Transitional//EN'
tree.docinfo.system_url = 'file://local.dtd'
print(tree.docinfo.doctype)
<!DOCTYPE root PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "file://local.dtd">

当您调用
parse()
函数解析文件或类文件对象(file-like object)时(见下文的解析部分),您得到的也是一个ElementTree。

一个重要的不同之处在于,
ElementTree
类序列化为一个完整的文档,而不是单个
Element
。这包括顶级(top-level)处理指令和注释,以及文档中的DOCTYPE和其他DTD内容:

>>> prettyprint(tree)  # lxml 1.3.4 and later
<!DOCTYPE root PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "file://local.dtd" [
<!ENTITY tasty "parsnips">
]>
<root>
  <a>parsnips</a>
</root>

在原始的xml.etree.ElementTree实现以及直到1.3.3版本的lxml中,输出看起来与仅序列化根元素时相同:

>>> prettyprint(tree.getroot())
<root>
  <a>parsnips</a>
</root>

在lxml 1.3.4中,这种序列化行为发生了变化。以前,树在没有DTD内容的情况下被序列化,这使得lxml在输入输出循环中丢失了DTD信息。

Parsing from strings and files

lxml.etree支持以多种方式解析XML,并且可以从所有重要的源解析,即字符串、文件、URL(http/ftp)和类文件对象(file-like object)。主要的解析函数是
fromstring()

parse()
,都以源作为第一个参数调用。默认情况下,它们使用标准解析器,但您总是可以作为第二个参数传递不同的解析器。

The fromstring() function

fromstring()
函数是解析字符串的最简单方法:

>>> some_xml_data = "<root>data</root>"

>>> root = etree.fromstring(some_xml_data)
>>> print(root.tag)
root
>>> etree.tostring(root)
b'<root>data</root>'
print(type(root))
# <class 'lxml.etree._Element'>

The XML() function

XML()
函数的行为类似于
fromstring()
函数,但通常用于将XML字面量直接写入源代码:

>>> root = etree.XML("<root>data</root>")
>>> print(root.tag)
root
>>> etree.tostring(root)
b'<root>data</root>'
print(type(root))
# <class 'lxml.etree._Element'>

还有一个相应的函数
HTML()
用于HTML字面量。

>>> root = etree.HTML("<p>data</p>")
>>> etree.tostring(root)
b'<html><body><p>data</p></body></html>'
print(type(root))
# <class 'lxml.etree._Element'>

The parse() function

parse()
函数用于从文件和类文件对象(file-like object)解析。

作为此类类文件对象的一个例子,以下代码使用BytesIO类从字符串而不是外部文件中读取。然而,在现实生活中,您显然会避免这样做,而是使用像上面提到的
fromstring()
这样的字符串解析函数。

>>> from io import BytesIO
>>> some_file_or_file_like_object = BytesIO(b"<root>data</root>")

>>> tree = etree.parse(some_file_or_file_like_object)

>>> etree.tostring(tree)
b'<root>data</root>'

请注意,
parse()
返回一个
ElementTree
对象,而不是像字符串解析函数那样的
Element
对象:

print(type(tree))
# <class 'lxml.etree._ElementTree'>
>>> root = tree.getroot()
>>> print(root.tag)
root
>>> etree.tostring(root)
b'<root>data</root>'

这种差异背后的原因是parse()从文件返回一个完整的文档,而字符串解析函数通常用于解析XML片段。

parse()
函数支持以下任何来源:

  • 一个打开的文件对象(确保以二进制模式打开)
  • 一个具有
    .read(byte_count)
    方法的类文件对象,每次调用都返回一个字节字符串
  • 一个文件名字符串
  • 一个HTTP或FTP URL字符串

请注意,传递文件名或URL通常比传递打开的文件或类文件对象更快。然而,libxml2中的HTTP/FTP客户端相当简单,因此像HTTP认证这样的事情需要一个专门的URL请求库,例如urllib2或requests。这些库通常提供一个类文件对象作为结果,您可以在响应流式传输时从中解析。

Parser objects

默认情况下,lxml.etree使用具有默认设置的标准解析器。如果您想配置解析器,可以创建一个新实例:

parser = etree.XMLParser(remove_blank_text=True)  # lxml.etree only!

这创建了一个解析器,在解析时删除标签之间的空白文本,这可以减少树的大小,并避免在您知道空白内容对您的数据没有意义时出现悬挂的尾随文本。例如:

>>> root = etree.XML("<root>  <a/>   <b>  </b>     </root>", parser)

>>> etree.tostring(root)
b'<root><a/><b>  </b></root>'

请注意,
<b>
标签内的空白内容没有被移除,因为叶元素中的内容往往是数据内容(即使是空白的)。您可以通过遍历树来轻松地移除它:

for element in root.iter("*"):
    if element.text is not None and not element.text.strip():
        element.text = None

etree.tostring(root)
b'<root><a/><b/></root>'

请参阅
help(etree.XMLParser)
以了解有关可用解析器选项的信息。

help(etree.XMLParser)

Incremental parsing

lxml.etree提供了两种逐步增量解析的方法。一种是通过类文件对象,它反复调用read()方法。这最好用在数据来自像urllib这样的源或任何其他类文件对象(可以按请求提供数据)的地方。请注意,在这种情况下,解析器会阻塞并等待数据变得可用:

class DataSource:
    data = [ b"<roo", b"t><", b"a/", b"><", b"/root>" ]
    def read(self, requested_size):
        try:
            return self.data.pop(0)
        except IndexError:
            return b''

tree = etree.parse(DataSource())

etree.tostring(tree)
b'<root><a/></root>'

第二种方法是通过parser提供的feed(data)和close()方法:

parser = etree.XMLParser()

parser.feed("<roo")
parser.feed("t><")
parser.feed("a/")
parser.feed("><")
parser.feed("/root>")

root = parser.close()

etree.tostring(root)
b'<root><a/></root>'

在这里,你可以在任何时候中断解析过程,并在稍后通过再次调用
feed()
方法继续进行解析。这在你想要避免对解析器的阻塞调用时非常有用,例如在像 Twisted 这样的框架中,或者每当数据缓慢地或以块的形式到来,并且你在等待下一块数据时想要做其他事情的时候。

在调用close()方法(或解析器引发异常)之后,您可以通过再次调用其feed()方法来重用解析器:

parser.feed("<root/>")
root = parser.close()
etree.tostring(root)
b'<root/>'

Event-driven parsing

有时,您从文档中所需的只是树内部深处的一小部分,因此将整个树解析到内存中、遍历它然后丢弃它可能会有太多的开销。lxml.etree通过两种事件驱动的解析器接口支持这种用例,一种在构建树时生成解析器事件(
iterparse
),另一种根本不构建树,而是以类似SAX的方式在目标对象上调用反馈方法。

这里有一个简单的
iterparse()
示例:

some_file_like = BytesIO(b"<root><a>data</a></root>")

for event, element in etree.iterparse(some_file_like):
    print(f"{event}, {element.tag:>4}, {element.text}")
end,    a, data
end, root, None

默认情况下,iterparse()只在解析完一个元素时才生成一个事件,但您可以通过events关键字参数控制这一点:

some_file_like = BytesIO(b"<root><a>data</a></root>")

for event, element in etree.iterparse(some_file_like,
                                      events=("start", "end")):
    print(f"{event:>5}, {element.tag:>4}, {element.text}")
start, root, None
start,    a, data
  end,    a, data
  end, root, None

请注意,在接收
start
事件时,元素的文本、尾随文本和子元素不一定已经存在。只有
end
事件保证了元素已经被完全解析。

它还允许您使用
.clear()
方法或修改元素的内容以节省内存。因此,如果您解析了一个大的树,并且您希望保持内存使用量小,您应该清理不再需要的树的部分。
.clear()
方法的
keep_tail=True
参数确保当前元素后面的(尾随)文本内容不会被触动。强烈不建议修改解析器可能尚未完全读取的任何内容。

some_file_like = BytesIO(b"<root><a><b>data</b></a><a><b/></a></root>")

for event, element in etree.iterparse(some_file_like):
    if element.tag == 'b':
        print(element.text)
    elif element.tag == 'a':
        print("** cleaning up the subtree")
        element.clear(keep_tail=True)
data
** cleaning up the subtree
None
** cleaning up the subtree

iterparse()的一个非常重要的用例是解析大型生成的XML文件,例如数据库转储(database dumps)。最常见的情况是,这些XML格式只有一个主要的数据项元素直接挂在根节点下,并且这个元素会重复数千次。在这种情况下,最佳实践是让
lxml.etree
进行树的构建,并且只拦截这一个元素,使用正常的树API进行数据提取。

xml_file = BytesIO(b'''
<root>
  <a><b>ABC</b><c>abc</c></a>
  <a><b>MORE DATA</b><c>more data</c></a>
  <a><b>XYZ</b><c>xyz</c></a>
</root>''')

for _, element in etree.iterparse(xml_file, tag='a'):
    print('%s -- %s' % (element.findtext('b'), element[1].text))
    element.clear(keep_tail=True)
ABC -- abc
MORE DATA -- more data
XYZ -- xyz

如果出于某种原因,根本不希望构建树,可以使用lxml.etree的目标解析器接口(target parser interface)。它通过调用目标对象的方法创建类似SAX的事件。通过实现这些方法中的一些或全部,您可以控制生成哪些事件:

class ParserTarget:
    events = []
    close_count = 0
    def start(self, tag, attrib):
        self.events.append(('start', tag, attrib))
    def close(self):
        events, self.events = self.events, []
        self.close_count += 1
        return events

parser_target = ParserTarget()

parser = etree.XMLParser(target=parser_target)
events = etree.fromstring('<root test="true"/>', parser)

print(parser_target.close_count)
1
event: start - tag: root
 * test = true

您可以随心所欲地重用解析器及其目标,因此您应该确保
.close()
方法确实将目标重置为可用状态(即使在出错的情况下也是如此!)。

>>> events = etree.fromstring('<root test="true"/>', parser)
>>> print(parser_target.close_count)
2
>>> events = etree.fromstring('<root test="true"/>', parser)
>>> print(parser_target.close_count)
3
>>> events = etree.fromstring('<root test="true"/>', parser)
>>> print(parser_target.close_count)
4

>>> for event in events:
...     print(f'event: {event[0]} - tag: {event[1]}')
...     for attr, value in event[2].items():
...         print(f' * {attr} = {value}')
event: start - tag: root
 * test = true

Namespaces

只要有可能,ElementTree API 都避免使用
命名空间前缀
,而是使用真实的命名空间(URI):

>>> xhtml = etree.Element("{http://www.w3.org/1999/xhtml}html")
>>> body = etree.SubElement(xhtml, "{http://www.w3.org/1999/xhtml}body")
>>> body.text = "Hello World"

>>> prettyprint(xhtml)
<html:html xmlns:html="http://www.w3.org/1999/xhtml">
  <html:body>Hello World</html:body>
</html:html>

ElementTree使用的表示法最初由James Clark提出。它的主要优点是为标签提供了一个通用限定名称(universally qualified name),无论文档中可能已经使用或定义的任何前缀。通过将前缀的间接性(indirection of prefixes)移开,它使命名空间感知的代码更加清晰,更容易正确处理。

正如您从示例中看到的,前缀只在序列化结果时变得重要。然而,由于命名空间名称较长,上述代码看起来有些冗长。而且,一遍又一遍地重新键入或复制字符串容易出错。因此,通常的做法是将命名空间URI存储在全局变量中。为了调整(adapt)用于序列化的命名空间前缀,你也可以将一个映射传递给
Element
工厂函数,例如定义默认命名空间:

>>> XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml"
>>> XHTML = "{%s}" % XHTML_NAMESPACE

>>> NSMAP = {None : XHTML_NAMESPACE} # the default namespace (no prefix)

>>> xhtml = etree.Element(XHTML + "html", nsmap=NSMAP) # lxml only!
>>> body = etree.SubElement(xhtml, XHTML + "body")
>>> body.text = "Hello World"

>>> prettyprint(xhtml)
<html xmlns="http://www.w3.org/1999/xhtml">
  <body>Hello World</body>
</html>

你也可以使用
QName
辅助类来构建或拆分限定的标签名称(qualified tag names)。

>>> tag = etree.QName('http://www.w3.org/1999/xhtml', 'html')
>>> print(tag.localname)
html
>>> print(tag.namespace)
http://www.w3.org/1999/xhtml
>>> print(tag.text)
{http://www.w3.org/1999/xhtml}html

>>> tag = etree.QName('{http://www.w3.org/1999/xhtml}html')
>>> print(tag.localname)
html
>>> print(tag.namespace)
http://www.w3.org/1999/xhtml

>>> root = etree.Element('{http://www.w3.org/1999/xhtml}html')
>>> tag = etree.QName(root)
>>> print(tag.localname)
html

>>> tag = etree.QName(root, 'script')
>>> print(tag.text)
{http://www.w3.org/1999/xhtml}script
>>> tag = etree.QName('{http://www.w3.org/1999/xhtml}html', 'script')
>>> print(tag.text)
{http://www.w3.org/1999/xhtml}script

lxml.etree允许您通过
.nsmap
属性查找为节点定义的当前命名空间:

>>> xhtml.nsmap
{None: 'http://www.w3.org/1999/xhtml'}

请注意,这包括在元素的上下文中已知的所有前缀,而不仅仅是它自己定义的那些。

root = etree.Element('root', nsmap={'a': 'http://a.b/c'})
child = etree.SubElement(root, 'child',
                         nsmap={'b': 'http://b.c/d'})
print(root.nsmap)
{'a': '[http://a.b/c](http://a.b/c)'}
len(root.nsmap)
# 1
print(child.nsmap)
{'b': '[http://b.c/d](http://b.c/d)', 'a': '[http://a.b/c](http://a.b/c)'}
len(child.nsmap)
child.nsmap['a']
# 'http://a.b/c'
child.nsmap['b']
# 'http://b.c/d'

因此,修改返回的字典对Element(元素)没有任何有意义的影响。对它的任何更改都会被忽略。

属性(attributes)上的命名空间工作方式类似,但自2.3版本起,lxml.etree将确保属性使用带有前缀的命名空间声明。这是因为XML命名空间规范(
第6.2节
)认为未加前缀的属性名称不处于任何命名空间中,因此即使它们出现在命名空间元素中,它们在序列化-解析循环中可能会丢失其命名空间。

body.set(XHTML + "bgcolor", "#CCFFAA")
prettyprint(xhtml)
<html xmlns="http://www.w3.org/1999/xhtml">
  <body xmlns:html="http://www.w3.org/1999/xhtml" html:bgcolor="#CCFFAA">Hello World</body>
</html>
# XML命名空间规范认为未加前缀的属性名称不处于任何命名空间中,所以返回None
print(body.get("bgcolor"))
None
# 使用加上前缀的属性名称
body.get(XHTML + "bgcolor")
'#CCFFAA'

您还可以使用完全限定的名称(fully qualified names)来使用XPath:

# 先回顾一下xhtml的内容
print(etree.tostring(xhtml).decode())
<html xmlns="http://www.w3.org/1999/xhtml"><body xmlns:html="http://www.w3.org/1999/xhtml" html:bgcolor="#CCFFAA" bgcolor="#CCFFAA">Hello World</body></html>
>>> find_xhtml_body = etree.ETXPath(      # lxml only !
...     "//{%s}body" % XHTML_NAMESPACE)
>>> results = find_xhtml_body(xhtml)

>>> print(results[0].tag)
{http://www.w3.org/1999/xhtml}body

为了方便,您可以在lxml.etree的所有迭代器中使用"
*
"通配符,无论是对于标签名称还是命名空间:

>>> for el in xhtml.iter('*'): print(el.tag)   # any element
{http://www.w3.org/1999/xhtml}html
{http://www.w3.org/1999/xhtml}body

>>> for el in xhtml.iter('{http://www.w3.org/1999/xhtml}*'): print(el.tag)
{http://www.w3.org/1999/xhtml}html
{http://www.w3.org/1999/xhtml}body

>>> for el in xhtml.iter('{*}body'): print(el.tag)
{http://www.w3.org/1999/xhtml}body

要查找没有命名空间的元素,请使用纯标签名称,或明确提供空的命名空间:

>>> [ el.tag for el in xhtml.iter('{http://www.w3.org/1999/xhtml}body') ]
['{http://www.w3.org/1999/xhtml}body']
>>> [ el.tag for el in xhtml.iter('body') ]
[]
>>> [ el.tag for el in xhtml.iter('{}body') ]
[]
>>> [ el.tag for el in xhtml.iter('{}*') ]
[]

The E-factory

E-factory提供了一种简单紧凑的语法,用于生成XML和HTML:

from lxml.builder import E

def CLASS(*args):    # class is a reserved word in Python
    return {"class":' '.join(args)}

html = page = (
    E.html(
        E.head(
            E.title("This is a sample document")
        ),
        E.body(
            E.h1("Hello!", CLASS("title")),
            E.p("This is a paragraph with ", E.b("bold"), " text in it!"),
            E.p("This is another paragraph, with a", "\n      ",
                E.a("link", href="http://www.python.org"), "."),
            E.p("Here are some reserved characters: <spam&egg>."),
            etree.XML("<p>And finally an embedded XHTML fragment.</p>"),
        )
    )
)

prettyprint(page)
<html>
  <head>
    <title>This is a sample document</title>
  </head>
  <body>
    <h1 class="title">Hello!</h1>
    <p>This is a paragraph with <b>bold</b> text in it!</p>
    <p>This is another paragraph, with a
      <a href="http://www.python.org">link</a>.</p>
    <p>Here are some reserved characters: &lt;spam&amp;egg&gt;.</p>
    <p>And finally an embedded XHTML fragment.</p>
  </body>
</html>

基于属性访问的元素创建使得为XML 语言构建一种简单的词汇表变得容易。

from lxml.builder import ElementMaker  # lxml only!

E = ElementMaker(namespace="http://my.de/fault/namespace", nsmap={'p': "http://my.de/fault/namespace"})

DOC = E.doc
TITLE = E.title
SECTION = E.section
PAR = E.par

my_doc = DOC(
    TITLE("The dog and the hog"),
    SECTION(
        TITLE("The dog"),
        PAR("Once upon a time, ..."),
        PAR("And then ...")
    ),
    SECTION(
        TITLE("The hog"),
        PAR("Sooner or later ...")
    )
)

prettyprint(my_doc)
<p:doc xmlns:p="http://my.de/fault/namespace">
  <p:title>The dog and the hog</p:title>
  <p:section>
    <p:title>The dog</p:title>
    <p:par>Once upon a time, ...</p:par>
    <p:par>And then ...</p:par>
  </p:section>
  <p:section>
    <p:title>The hog</p:title>
    <p:par>Sooner or later ...</p:par>
  </p:section>
</p:doc>

一个这样的例子是模块lxml.html.builder,它为HTML提供了一个词汇表。

当处理多个命名空间时,最佳实践是为每个命名空间URI定义一个ElementMaker。再次注意,上述示例如何在命名常量中预定义了标签构建器(tag builders)。这使得将一个命名空间的所有标签声明放入一个Python模块,并从那里导入/使用标签名称常量变得容易。这避免了诸如拼写错误或意外遗漏命名空间之类的陷阱。

ElementPath

ElementTree库附带了一个简单的类似XPath的路径语言,称为ElementPath。主要区别在于您可以在ElementPath表达式中使用{namespace}tag表示法。然而,高级特性如值比较和函数是不可用的。

除了
完整的XPath实现
,lxml.etree以与ElementTree相同的方式支持ElementPath语言,甚至使用(几乎)相同的实现。API在这里提供了四种方法,您可以在Elements和ElementTrees上找到这些方法:

  • iterfind()
    遍历所有匹配路径表达式(path expression)的元素。
  • findall()
    返回匹配元素的列表。
  • find()
    高效地仅返回第一个匹配项。
  • findtext()
    返回第一个匹配项的
    .text
    内容。

这里有一些示例:

root = etree.XML("<root><a x='123'>aText<b/><c/><b/></a></root>")

查找元素的子元素:

>>> print(root.find("b"))
None
>>> print(root.find("a").tag)
a

在树中查找元素:

>>> print(root.find(".//b").tag)
b
>>> [ b.tag for b in root.iterfind(".//b") ]
['b', 'b']

查找具有特定属性的元素:

>>> print(root.findall(".//a[@x]")[0].tag)
a
>>> print(root.findall(".//a[@y]"))
[]


lxml
3.4 版本中,有一个新的辅助函数用于为一个
Element
生成结构化的
ElementPath
表达式。

>>> tree = etree.ElementTree(root)
>>> a = root[0]
>>> print(tree.getelementpath(a[0]))
a/b[1]
>>> print(tree.getelementpath(a[1]))
a/c
>>> print(tree.getelementpath(a[2]))
a/b[2]
>>> tree.find(tree.getelementpath(a[2])) == a[2]
True

只要树未被修改,这个路径表达式就代表给定元素的标识符,可以稍后在相同树中使用find()找到它。与XPath相比,ElementPath表达式的优势在于即使对于使用命名空间的文档,它们也是自包含的。

.iter()
方法是一个特例,它仅通过名称在树中查找特定标签,而不是基于路径。这意味着在成功的情况下,以下命令是等效的:

>>> print(root.find(".//b").tag)
b
>>> print(next(root.iterfind(".//b")).tag)
b
>>> print(next(root.iter("b")).tag)
b

本篇是 Python 系列教程第 13 篇,更多内容敬请访问我的 Python 合集

Python 装饰器是一种强大的工具,用于修改或增强函数或方法的行为,而无需更改其源代码。装饰器本质上是一个接收函数作为参数的函数,并返回一个新的函数。装饰器的用途包括日志记录、性能测试、事务处理、缓存、权限校验等

1 基本语法

装饰器的基本语法是在函数定义之前使用
@
符号,紧跟着装饰器的名字。例如:

# 定义一个装饰器,参数为被装饰的方法
def my_decorator(func):
    def wrapper():
        print("方法运行前")
        func()
        print("方法运行后")

    return wrapper

# 用“@”使用装饰器
@my_decorator
def say_hello():
    print("Hello!")

say_hello()

这段代码会输出:

方法运行前
Hello!
方法运行后

2 参数传递

如果被装饰的函数需要参数,装饰器也需要相应地处理这些参数:

def my_decorator(func):
    def wrapper(name):
        print("方法运行前")
        func(name)
        print("方法运行后")

    return wrapper

@my_decorator
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")

输出:

方法运行前
Hello, Alice!
方法运行后

参数可以用可变参数,比较灵活,如下:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("方法运行前")
        func(*args, **kwargs)
        print("方法运行后")

    return wrapper

@my_decorator
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")

3 使用多个装饰器

你可以为同一个函数使用多个装饰器:

def decorator_A(func):
    print("enter A")
    def wrapper():
        print("Before A")
        func()
        print("After A")
    print("out A")
    return wrapper
 
def decorator_B(func):
    print("enter B")
    def wrapper():
        print("Before B")
        func()
        print("After B")
    print("out B")
    return wrapper
 
@decorator_A
@decorator_B
def my_function():
    print("Inside my_function")
 
# 执行被装饰器装饰的函数
my_function()

输出:

enter B
out B
enter A
out A
Before A
Before B
Inside my_function
After B
After A

注意打印结果的顺序。

为了方便表达我们先把靠近被修饰方法的装饰器叫内层装饰器,如示例中的
@decorator_B
,不靠近的叫外层装饰器,如示例中的
@decorator_A

在闭包
wrapper
外面的代码是内层装饰器先执行,在闭包
wrapper
内部的代码执行顺序复杂一些:
①外层装饰器先执行
func()
前面的代码->②内层装饰器执行
func()
前面的代码->③执行
func()
->④内层装饰器执行
func()
后面的代码->⑤外层装饰器执行
func()
后面的代码。

4 给装饰器传参

装饰器本身可以接受参数,可以根据传入的不同参数来改变装饰器的行为。

前面的例子都是没有参数的装饰器,如果我们想要给装饰器传参该怎么办呢?于是我们就思考一下,什么东东可以接收参数呢,答案是函数。bingo!Python也是这样设计的,我们只需要在装饰器外面包裹一层函数,就可以把参数传递给函数进而传递给装饰器了。

可以这样定义装饰器:

def repeat(num_times):
    def decorator_repeat(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator_repeat

@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")

输出:

Hello, Alice!
Hello, Alice!
Hello, Alice!

这样就定义了一个根据传入装饰器的数值执行指定次数函数的装饰器。

5 类作为装饰器

5.1
__call__
方法

装饰器不仅仅可以是方法,也可以是类。这就不得不介绍一个特殊的方法
__call__

Python的类只要实现了
__call__
这个特殊方法,类的实例对象就可以像函数一样被调用,因为当尝试把对象写成方法调用的写法时(名称+()),Python 解释器会查找该对象的
__call__
方法并调用它。

下面来看一个简单的例子,演示
__call__
的使用:

class Counter:
    def __init__(self):
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"方法被调用了 {self.count} 次")

counter = Counter()

# 模拟调用
counter()
counter()
counter()

打印:

方法被调用了 1 次
方法被调用了 2 次
方法被调用了 3 次

5.2 类作为装饰器

类作为装饰器的一个主要优势是可以方便地维护状态,因为类可以有实例变量。

理解了
__call__
之后,我们可以想到类作为装饰器的原理是在类里实现了
__call__
方法,使得装饰器的代码可以被执行。

下面我们定义一个记录函数调用次数的装饰器:

class CallCounter:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.func.__name__} 被调用了 {self.count} 次")
        return self.func(*args, **kwargs)

@CallCounter
def say_hello(name):
    print(f"Hello, {name}!")

# 调用被装饰的函数
say_hello("Alice")
say_hello("Bob")
say_hello("Charlie")

# 输出
# say_hello 被调用了 1 次
# Hello, Alice!
# say_hello 被调用了 2 次
# Hello, Bob!
# say_hello 被调用了 3 次
# Hello, Charlie!

代码解释:

  1. CallCounter
    类有一个构造函数
    __init__
    ,它必须接受一个函数作为参数。
  2. 类实现了
    __call__
    方法,这使得其实例可以像函数一样被调用。

  3. __call__
    方法中,每次调用被装饰的函数时,都会增加计数器
    count
    的值,并打印出函数被调用的次数。
  4. 最后,
    __call__
    方法调用了原始函数
    self.func
    并返回结果。

我们在定义SQLAlchemy对象模型的关系的时候,用到了relationship 来标识关系,其中 lazy 的参数有多种不同的加载策略,本篇随笔介绍它们之间的关系,以及在异步处理中的一些代码案例。

1、在 SQLAlchemy 中定义关系

在 SQLAlchemy 中,
relationship()
函数用于定义表之间的关系(如
one-to-many

many-to-one

many-to-many
等)。它支持许多参数来控制如何加载和处理关联的数据。以下是一些常用的
relationship()
参数及其说明:

1.
lazy

  • 作用
    : 控制如何加载关联数据。
  • 可选值
    :
    • 'select'
      : 延迟加载。访问关系属性时,发送一个独立的查询来获取关联数据(默认值)。
    • 'selectin'
      : 使用
      IN
      查询批量加载关联对象,避免 n+1 查询问题。
    • 'joined'
      : 使用
      JOIN
      直接在主查询中加载关联数据。
    • 'subquery'
      : 使用子查询来批量加载关联对象。
    • 'immediate'
      : 在加载主对象后,立即加载关联对象。
    • 'dynamic'
      : 仅适用于
      one-to-many
      ,返回一个查询对象,可以进一步过滤或操作关联数据。
  • 详细说明

在SQLAlchemy中,
lazy
是一个定义ORM关系如何加载的参数,主要用于控制关联关系(如
one-to-many

many-to-one
等)在访问时的加载方式。

1)
lazy='select'
(默认)

  • 说明
    : 这是最常见的方式,使用"延迟加载"策略。当访问关联属性时,SQLAlchemy会发送一条新的SQL查询来加载相关数据。
  • 优点
    : 避免了不必要的查询,节省资源。
  • 缺点
    : 当你访问多个关联对象时,可能会导致"n+1查询问题",即每次访问关联数据时都会发出新的SQL查询。

2)
lazy='selectin'

  • 说明
    : 类似于
    lazy='select'
    ,但通过
    IN
    语句批量查询相关对象。SQLAlchemy会在一次查询中批量获取多个对象的关联数据,而不是为每个对象单独查询。
  • 优点
    : 解决了"n+1查询问题",效率高于
    select
  • 缺点
    : 适用于可以通过
    IN
    语句高效查询的场景,但如果结果集非常大,可能会影响性能。

3)
lazy='joined'

  • 说明
    : 在主查询时,使用
    JOIN
    语句直接加载关联对象。这意味着关联对象在查询时就会被立即加载,不需要额外的查询。
  • 优点
    : 避免了多个SQL查询,适合在同一查询中需要大量关联数据的场景。
  • 缺点
    : 如果
    JOIN
    的表数据较多,可能会导致查询结果变得复杂且性能下降。

4)
lazy='immediate'

  • 说明
    : 在加载主对象时,立即加载所有关联对象。与
    select
    类似,但是在主对象加载后,马上发送查询请求加载关联对象。
  • 优点
    : 保证在对象加载后立刻有完整的数据。
  • 缺点
    : 对每个关联的对象仍然会发送单独的查询,可能造成"n+1查询问题"。

5)
lazy='subquery'

  • 说明
    : 使用子查询来加载关联对象。SQLAlchemy会在查询主对象时生成一个子查询,以批量加载相关对象。
  • 优点
    : 避免了"n+1查询问题",适合处理大型数据集。
  • 缺点
    : 子查询可能会导致查询效率降低,特别是在复杂的查询场景中。

6)
lazy='dynamic'

  • 说明
    : 仅适用于
    one-to-many
    关系,返回一个查询对象,而不是实际的结果集。你可以通过调用查询对象来进一步过滤或操作关联对象。
  • 优点
    : 非常灵活,可以根据需要随时查询关联对象。
  • 缺点
    : 不能使用通常的方式访问关联属性,必须通过查询进一步获取数据。

2.
backref

  • 作用
    : 定义反向引用,允许从关联表访问当前表。

  • 用法
    : 通过
    backref
    ,可以在关联的表中自动生成一个反向关系,避免手动定义双向关系。

  • 示例
    :

classParent(Base):__tablename__ = 'parent'id= Column(Integer, primary_key=True)
children
= relationship("Child", backref="parent")classChild(Base):__tablename__ = 'child'id= Column(Integer, primary_key=True)
parent_id
= Column(Integer, ForeignKey('parent.id'))

3.
back_populates

  • 作用
    : 手动定义双向关系时,使用
    back_populates
    来明确地表示两个表之间的相互关系。

  • 示例
    :

classParent(Base):__tablename__ = 'parent'id= Column(Integer, primary_key=True)
children
= relationship("Child", back_populates="parent")classChild(Base):__tablename__ = 'child'id= Column(Integer, primary_key=True)
parent_id
= Column(Integer, ForeignKey('parent.id'))
parent
= relationship("Parent", back_populates="children")

4.
cascade

  • 作用
    : 定义级联操作,决定在父对象上进行操作时,是否自动对关联的子对象进行相应操作。

  • 常见值
    :


    • 'save-update'
      : 当父对象被保存或更新时,子对象也会被保存或更新。
    • 'delete'
      : 当父对象被删除时,子对象也会被删除。
    • 'delete-orphan'
      : 当子对象失去与父对象的关联时,子对象将被删除。
    • 'all'
      : 包含所有级联操作。
  • 示例
    :

children = relationship("Child", cascade="all, delete-orphan")

5.
uselist

  • 作用
    : 控制关联属性是否返回一个列表。适用于
    one-to-one

    one-to-many
    关系。

  • 用法
    :


    • True
      : 返回一个列表(适用于
      one-to-many
      ,默认值)。
    • False
      : 返回单个对象(适用于
      one-to-one
      )。
  • 示例
    :

parent = relationship("Parent", uselist=False)  #one-to-one 关系

6.
order_by

  • 作用
    : 定义关联对象的排序方式。

  • 示例
    :

children = relationship("Child", order_by="Child.name")

7.
foreign_keys

  • 作用
    : 显式指定哪些列是用于定义关联关系的外键,适用于存在多个外键的场景。

  • 示例
    :

parent = relationship("Parent", foreign_keys="[Child.parent_id]")

8.
primaryjoin

  • 作用
    : 明确定义关联关系的连接条件,通常在 SQLAlchemy 无法自动推断时使用。

  • 示例
    :

parent = relationship("Parent", primaryjoin="Parent.id == Child.parent_id")

9.
secondary

  • 作用
    : 定义多对多(
    many-to-many
    )关系时,指定关联的中间表。

  • 示例
    :

classAssociation(Base):__tablename__ = 'association'parent_id= Column(Integer, ForeignKey('parent.id'))
child_id
= Column(Integer, ForeignKey('child.id'))

children
= relationship("Child", secondary="association")

10.
secondaryjoin

  • 作用
    : 定义
    secondary
    表中的关联条件,通常用于复杂的多对多关系。

  • 示例
    :

children = relationship("Child", secondary="association", 
secondaryjoin
="Child.id == Association.child_id")

11.
viewonly

  • 作用
    : 定义只读的关系,不允许通过此关系修改数据。

  • 示例
    :

children = relationship("Child", viewonly=True)

12.
passive_deletes

  • 作用
    : 控制删除时的行为。如果设置为
    True
    ,SQLAlchemy 不会主动删除关联对象,而是依赖数据库的级联删除。

  • 示例
    :

children = relationship("Child", passive_deletes=True)

这些参数可以根据具体的业务需求和场景进行调整,以优化查询和数据管理策略。

2、用户角色表的关系分析

在实际业务中,机构和用户是多对多的关系的,我们以机构表定义来进行分析它们的关系信息。

如机构表的模型定义大致如下。

classOu(Base):"""机构(部门)信息-表模型"""
    __tablename__ = "t_acl_ou"id= Column(Integer, primary_key=True, comment="主键", autoincrement=True)
pid
= Column(Integer, ForeignKey("t_acl_ou.id"), comment="父级机构ID", default="-1")
handno
= Column(String, comment="机构编码")
name
= Column(String, comment="机构名称")#定义 parent 关系 parent =relationship("Ou", remote_side=[id], back_populates="children", lazy="immediate")#定义 children 关系 children = relationship("Ou", back_populates="parent", lazy="immediate")#定义 users 关系 users =relationship("User", secondary="t_acl_ou_user", back_populates="ous", lazy="select")

我们可以看到其中加载的多对多关系是采用lazy=select的方式的。

当你使用
await session.get(Ou, ou_id)
来获取一个
Ou
对象后,访问其关系属性(如
ou.users
)时,可能会遇到异步相关的问题。原因是,SQLAlchemy 的异步会话需要使用
selectinload
或其他异步加载选项来确保在异步环境中正确地加载关联数据。

在默认的
lazy='select'
关系中,加载关系对象会触发一个同步查询,而这与异步会话不兼容,导致错误。为了解决这个问题,你需要确保关系的加载是通过异步的方式进行的。

解决方法:

1. 使用
selectinload
进行预加载

在查询时,显式地通过
selectinload
来加载关联的
users
关系:

from sqlalchemy.orm importselectinload

ou
= await session.get(Ou, ou_id, options=[selectinload(Ou.users)])#现在你可以访问 ou.users,关系对象已经被异步加载 print(ou.users)

2. 使用
lazy='selectin'
或其他异步兼容的加载策略

你还可以在定义模型的关联关系时,将
lazy='selectin'
设置为默认的加载方式,这样当访问关联属性时,SQLAlchemy 会自动使用异步兼容的加载机制:

classOu(Base):__tablename__ = 'ou'id= Column(Integer, primary_key=True)
users
= relationship("User", lazy='selectin') #使用 selectin 异步加载 ou=await session.get(Ou, ou_id)print(ou.users) #关联对象可以正常异步访问

总结:

  • 在异步环境中访问关系对象时,如果使用了同步的
    lazy='select'
    ,会导致异步不兼容问题。
  • 解决方案是通过查询时使用
    selectinload
    或将关系的
    lazy
    属性设置为异步兼容的选项,如
    selectin

因此,如果机构和用户的关系信息,我们可以通过selectload关系实现加载,也可以考虑使用中间表的关系进行获取,如下代码所示:获取指定用户的关联的机构列表.

    async def get_ous_by_user(self, db: AsyncSession, user_id: str) ->list[int]:"""获取指定用户的关联的机构列表"""
        #方式一,子查询方式
        stmt = select(User).options(selectinload(User.ous)).where(User.id ==user_id)
result
=await db.execute(stmt)
user
=result.scalars().first()
ous
= user.ous if user else[]#方式二,关联表方式 #stmt = ( #select(Ou) #.join(user_ou, User.id == user_ou.c.user_id) #.where(user_ou.c.user_id == user_id) #) #result = await db.execute(stmt) #ous = result.scalars().all() ouids= [ou.id for ou inous]return ouids

上面两种方式是等效的,一个是通过orm关系进行获取关系集合,一个是通过中间表的关系检索主表数据集合。

通过中间表,我们也可以很方便的添加角色的关系,如下面是为角色添加用户,也就是在中间表进行处理即可。

    async def add_user(self, db: AsyncSession, role_id: int, user_id: int) ->bool:"""添加角色-用户关联"""stmt=select(user_role).where(
and_(
user_role.c.role_id
==role_id,
user_role.c.user_id
==user_id,
)
)
if not(await db.execute(stmt)).scalars().first():
await db.execute(
user_role.insert().values(role_id
=role_id, user_id=user_id)
)
await db.commit()
returnTruereturn False

当然。如果我们不用这种中间表的处理方式,也是可以使用常规多对多关系进行添加处理,不过需要对数据进行多一些检索,也许性能会差一些。

    async def add_user(self, db: AsyncSession, ou_id: int, user_id: int) ->bool:"""给机构添加用户"""
        #可以使用下面方式,也可以使用中间表方式处理
        #先判断用户是否存在
        user =await db.get(User, user_id)if notuser:returnFalse#再判断机构是否存在
        result =await db.execute(
select(Ou).options(selectinload(Ou.users)).filter_by(id
=ou_id)
)
#await db.get(Ou, ou_id) #这种方式不能获得users,因为配置为selectin #await db.get(Ou, ou_id, options=[selectinload(Ou.users)]) # 这种方式可以获得users ou =result.scalars().first()if notou:returnFalse#再判断用户是否已经存在于机构中 if user inou.users:returnFalse#加入机构 ou.users.append(user)
await db.commit()
return True